Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts.widget;
     18 
     19 import android.content.Context;
     20 import android.graphics.Canvas;
     21 import android.graphics.Rect;
     22 import android.graphics.RectF;
     23 import android.util.AttributeSet;
     24 import android.view.MotionEvent;
     25 import android.view.View;
     26 import android.view.ViewGroup;
     27 import android.widget.AbsListView;
     28 import android.widget.AbsListView.OnScrollListener;
     29 import android.widget.AdapterView;
     30 import android.widget.AdapterView.OnItemSelectedListener;
     31 import android.widget.ListAdapter;
     32 
     33 /**
     34  * A ListView that maintains a header pinned at the top of the list. The
     35  * pinned header can be pushed up and dissolved as needed.
     36  */
     37 public class PinnedHeaderListView extends AutoScrollListView
     38         implements OnScrollListener, OnItemSelectedListener {
     39 
     40     /**
     41      * Adapter interface.  The list adapter must implement this interface.
     42      */
     43     public interface PinnedHeaderAdapter {
     44 
     45         /**
     46          * Returns the overall number of pinned headers, visible or not.
     47          */
     48         int getPinnedHeaderCount();
     49 
     50         /**
     51          * Creates or updates the pinned header view.
     52          */
     53         View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent);
     54 
     55         /**
     56          * Configures the pinned headers to match the visible list items. The
     57          * adapter should call {@link PinnedHeaderListView#setHeaderPinnedAtTop},
     58          * {@link PinnedHeaderListView#setHeaderPinnedAtBottom},
     59          * {@link PinnedHeaderListView#setFadingHeader} or
     60          * {@link PinnedHeaderListView#setHeaderInvisible}, for each header that
     61          * needs to change its position or visibility.
     62          */
     63         void configurePinnedHeaders(PinnedHeaderListView listView);
     64 
     65         /**
     66          * Returns the list position to scroll to if the pinned header is touched.
     67          * Return -1 if the list does not need to be scrolled.
     68          */
     69         int getScrollPositionForHeader(int viewIndex);
     70     }
     71 
     72     private static final int MAX_ALPHA = 255;
     73     private static final int TOP = 0;
     74     private static final int BOTTOM = 1;
     75     private static final int FADING = 2;
     76 
     77     private static final int DEFAULT_ANIMATION_DURATION = 20;
     78 
     79     private static final class PinnedHeader {
     80         View view;
     81         boolean visible;
     82         int y;
     83         int height;
     84         int alpha;
     85         int state;
     86 
     87         boolean animating;
     88         boolean targetVisible;
     89         int sourceY;
     90         int targetY;
     91         long targetTime;
     92     }
     93 
     94     private PinnedHeaderAdapter mAdapter;
     95     private int mSize;
     96     private PinnedHeader[] mHeaders;
     97     private RectF mBounds = new RectF();
     98     private Rect mClipRect = new Rect();
     99     private OnScrollListener mOnScrollListener;
    100     private OnItemSelectedListener mOnItemSelectedListener;
    101     private int mScrollState;
    102 
    103     private int mAnimationDuration = DEFAULT_ANIMATION_DURATION;
    104     private boolean mAnimating;
    105     private long mAnimationTargetTime;
    106     private int mHeaderPaddingLeft;
    107     private int mHeaderWidth;
    108 
    109     public PinnedHeaderListView(Context context) {
    110         this(context, null, com.android.internal.R.attr.listViewStyle);
    111     }
    112 
    113     public PinnedHeaderListView(Context context, AttributeSet attrs) {
    114         this(context, attrs, com.android.internal.R.attr.listViewStyle);
    115     }
    116 
    117     public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
    118         super(context, attrs, defStyle);
    119         super.setOnScrollListener(this);
    120         super.setOnItemSelectedListener(this);
    121     }
    122 
    123     @Override
    124     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    125         super.onLayout(changed, l, t, r, b);
    126         mHeaderPaddingLeft = getPaddingLeft();
    127         mHeaderWidth = r - l - mHeaderPaddingLeft - getPaddingRight();
    128     }
    129 
    130     public void setPinnedHeaderAnimationDuration(int duration) {
    131         mAnimationDuration = duration;
    132     }
    133 
    134     @Override
    135     public void setAdapter(ListAdapter adapter) {
    136         mAdapter = (PinnedHeaderAdapter)adapter;
    137         super.setAdapter(adapter);
    138     }
    139 
    140     @Override
    141     public void setOnScrollListener(OnScrollListener onScrollListener) {
    142         mOnScrollListener = onScrollListener;
    143         super.setOnScrollListener(this);
    144     }
    145 
    146     @Override
    147     public void setOnItemSelectedListener(OnItemSelectedListener listener) {
    148         mOnItemSelectedListener = listener;
    149         super.setOnItemSelectedListener(this);
    150     }
    151 
    152     @Override
    153     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    154             int totalItemCount) {
    155         if (mAdapter != null) {
    156             int count = mAdapter.getPinnedHeaderCount();
    157             if (count != mSize) {
    158                 mSize = count;
    159                 if (mHeaders == null) {
    160                     mHeaders = new PinnedHeader[mSize];
    161                 } else if (mHeaders.length < mSize) {
    162                     PinnedHeader[] headers = mHeaders;
    163                     mHeaders = new PinnedHeader[mSize];
    164                     System.arraycopy(headers, 0, mHeaders, 0, headers.length);
    165                 }
    166             }
    167 
    168             for (int i = 0; i < mSize; i++) {
    169                 if (mHeaders[i] == null) {
    170                     mHeaders[i] = new PinnedHeader();
    171                 }
    172                 mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this);
    173             }
    174 
    175             mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration;
    176             mAdapter.configurePinnedHeaders(this);
    177             invalidateIfAnimating();
    178 
    179         }
    180         if (mOnScrollListener != null) {
    181             mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount);
    182         }
    183     }
    184 
    185     @Override
    186     protected float getTopFadingEdgeStrength() {
    187         // Disable vertical fading at the top when the pinned header is present
    188         return mSize > 0 ? 0 : super.getTopFadingEdgeStrength();
    189     }
    190 
    191     @Override
    192     public void onScrollStateChanged(AbsListView view, int scrollState) {
    193         mScrollState = scrollState;
    194         if (mOnScrollListener != null) {
    195             mOnScrollListener.onScrollStateChanged(this, scrollState);
    196         }
    197     }
    198 
    199     /**
    200      * Ensures that the selected item is positioned below the top-pinned headers
    201      * and above the bottom-pinned ones.
    202      */
    203     @Override
    204     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
    205         int height = getHeight();
    206 
    207         int windowTop = 0;
    208         int windowBottom = height;
    209 
    210         for (int i = 0; i < mSize; i++) {
    211             PinnedHeader header = mHeaders[i];
    212             if (header.visible) {
    213                 if (header.state == TOP) {
    214                     windowTop = header.y + header.height;
    215                 } else if (header.state == BOTTOM) {
    216                     windowBottom = header.y;
    217                     break;
    218                 }
    219             }
    220         }
    221 
    222         View selectedView = getSelectedView();
    223         if (selectedView != null) {
    224             if (selectedView.getTop() < windowTop) {
    225                 setSelectionFromTop(position, windowTop);
    226             } else if (selectedView.getBottom() > windowBottom) {
    227                 setSelectionFromTop(position, windowBottom - selectedView.getHeight());
    228             }
    229         }
    230 
    231         if (mOnItemSelectedListener != null) {
    232             mOnItemSelectedListener.onItemSelected(parent, view, position, id);
    233         }
    234     }
    235 
    236     @Override
    237     public void onNothingSelected(AdapterView<?> parent) {
    238         if (mOnItemSelectedListener != null) {
    239             mOnItemSelectedListener.onNothingSelected(parent);
    240         }
    241     }
    242 
    243     public int getPinnedHeaderHeight(int viewIndex) {
    244         ensurePinnedHeaderLayout(viewIndex);
    245         return mHeaders[viewIndex].view.getHeight();
    246     }
    247 
    248     /**
    249      * Set header to be pinned at the top.
    250      *
    251      * @param viewIndex index of the header view
    252      * @param y is position of the header in pixels.
    253      * @param animate true if the transition to the new coordinate should be animated
    254      */
    255     public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) {
    256         ensurePinnedHeaderLayout(viewIndex);
    257         PinnedHeader header = mHeaders[viewIndex];
    258         header.visible = true;
    259         header.y = y;
    260         header.state = TOP;
    261 
    262         // TODO perhaps we should animate at the top as well
    263         header.animating = false;
    264     }
    265 
    266     /**
    267      * Set header to be pinned at the bottom.
    268      *
    269      * @param viewIndex index of the header view
    270      * @param y is position of the header in pixels.
    271      * @param animate true if the transition to the new coordinate should be animated
    272      */
    273     public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) {
    274         ensurePinnedHeaderLayout(viewIndex);
    275         PinnedHeader header = mHeaders[viewIndex];
    276         header.state = BOTTOM;
    277         if (header.animating) {
    278             header.targetTime = mAnimationTargetTime;
    279             header.sourceY = header.y;
    280             header.targetY = y;
    281         } else if (animate && (header.y != y || !header.visible)) {
    282             if (header.visible) {
    283                 header.sourceY = header.y;
    284             } else {
    285                 header.visible = true;
    286                 header.sourceY = y + header.height;
    287             }
    288             header.animating = true;
    289             header.targetVisible = true;
    290             header.targetTime = mAnimationTargetTime;
    291             header.targetY = y;
    292         } else {
    293             header.visible = true;
    294             header.y = y;
    295         }
    296     }
    297 
    298     /**
    299      * Set header to be pinned at the top of the first visible item.
    300      *
    301      * @param viewIndex index of the header view
    302      * @param position is position of the header in pixels.
    303      */
    304     public void setFadingHeader(int viewIndex, int position, boolean fade) {
    305         ensurePinnedHeaderLayout(viewIndex);
    306 
    307         View child = getChildAt(position - getFirstVisiblePosition());
    308         if (child == null) return;
    309 
    310         PinnedHeader header = mHeaders[viewIndex];
    311         header.visible = true;
    312         header.state = FADING;
    313         header.alpha = MAX_ALPHA;
    314         header.animating = false;
    315 
    316         int top = getTotalTopPinnedHeaderHeight();
    317         header.y = top;
    318         if (fade) {
    319             int bottom = child.getBottom() - top;
    320             int headerHeight = header.height;
    321             if (bottom < headerHeight) {
    322                 int portion = bottom - headerHeight;
    323                 header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight;
    324                 header.y = top + portion;
    325             }
    326         }
    327     }
    328 
    329     /**
    330      * Makes header invisible.
    331      *
    332      * @param viewIndex index of the header view
    333      * @param animate true if the transition to the new coordinate should be animated
    334      */
    335     public void setHeaderInvisible(int viewIndex, boolean animate) {
    336         PinnedHeader header = mHeaders[viewIndex];
    337         if (header.visible && (animate || header.animating) && header.state == BOTTOM) {
    338             header.sourceY = header.y;
    339             if (!header.animating) {
    340                 header.visible = true;
    341                 header.targetY = getBottom() + header.height;
    342             }
    343             header.animating = true;
    344             header.targetTime = mAnimationTargetTime;
    345             header.targetVisible = false;
    346         } else {
    347             header.visible = false;
    348         }
    349     }
    350 
    351     private void ensurePinnedHeaderLayout(int viewIndex) {
    352         View view = mHeaders[viewIndex].view;
    353         if (view.isLayoutRequested()) {
    354             int widthSpec = MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY);
    355             int heightSpec;
    356             ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
    357             if (layoutParams != null && layoutParams.height > 0) {
    358                 heightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
    359             } else {
    360                 heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
    361             }
    362             view.measure(widthSpec, heightSpec);
    363             int height = view.getMeasuredHeight();
    364             mHeaders[viewIndex].height = height;
    365             view.layout(0, 0, mHeaderWidth, height);
    366         }
    367     }
    368 
    369     /**
    370      * Returns the sum of heights of headers pinned to the top.
    371      */
    372     public int getTotalTopPinnedHeaderHeight() {
    373         for (int i = mSize; --i >= 0;) {
    374             PinnedHeader header = mHeaders[i];
    375             if (header.visible && header.state == TOP) {
    376                 return header.y + header.height;
    377             }
    378         }
    379         return 0;
    380     }
    381 
    382     /**
    383      * Returns the list item position at the specified y coordinate.
    384      */
    385     public int getPositionAt(int y) {
    386         do {
    387             int position = pointToPosition(getPaddingLeft() + 1, y);
    388             if (position != -1) {
    389                 return position;
    390             }
    391             // If position == -1, we must have hit a separator. Let's examine
    392             // a nearby pixel
    393             y--;
    394         } while (y > 0);
    395         return 0;
    396     }
    397 
    398     @Override
    399     public boolean onInterceptTouchEvent(MotionEvent ev) {
    400         if (mScrollState == SCROLL_STATE_IDLE) {
    401             final int y = (int)ev.getY();
    402             for (int i = mSize; --i >= 0;) {
    403                 PinnedHeader header = mHeaders[i];
    404                 if (header.visible && header.y <= y && header.y + header.height > y) {
    405                     if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    406                         return smoothScrollToPartition(i);
    407                     } else {
    408                         return true;
    409                     }
    410                 }
    411             }
    412         }
    413 
    414         return super.onInterceptTouchEvent(ev);
    415     }
    416 
    417     private boolean smoothScrollToPartition(int partition) {
    418         final int position = mAdapter.getScrollPositionForHeader(partition);
    419         if (position == -1) {
    420             return false;
    421         }
    422 
    423         int offset = 0;
    424         for (int i = 0; i < partition; i++) {
    425             PinnedHeader header = mHeaders[i];
    426             if (header.visible) {
    427                 offset += header.height;
    428             }
    429         }
    430 
    431         smoothScrollToPositionFromTop(position + getHeaderViewsCount(), offset);
    432         return true;
    433     }
    434 
    435     private void invalidateIfAnimating() {
    436         mAnimating = false;
    437         for (int i = 0; i < mSize; i++) {
    438             if (mHeaders[i].animating) {
    439                 mAnimating = true;
    440                 invalidate();
    441                 return;
    442             }
    443         }
    444     }
    445 
    446     @Override
    447     protected void dispatchDraw(Canvas canvas) {
    448         long currentTime = mAnimating ? System.currentTimeMillis() : 0;
    449 
    450         int top = 0;
    451         int bottom = getBottom();
    452         boolean hasVisibleHeaders = false;
    453         for (int i = 0; i < mSize; i++) {
    454             PinnedHeader header = mHeaders[i];
    455             if (header.visible) {
    456                 hasVisibleHeaders = true;
    457                 if (header.state == BOTTOM && header.y < bottom) {
    458                     bottom = header.y;
    459                 } else if (header.state == TOP || header.state == FADING) {
    460                     int newTop = header.y + header.height;
    461                     if (newTop > top) {
    462                         top = newTop;
    463                     }
    464                 }
    465             }
    466         }
    467 
    468         if (hasVisibleHeaders) {
    469             canvas.save();
    470             mClipRect.set(0, top, getWidth(), bottom);
    471             canvas.clipRect(mClipRect);
    472         }
    473 
    474         super.dispatchDraw(canvas);
    475 
    476         if (hasVisibleHeaders) {
    477             canvas.restore();
    478 
    479             // First draw top headers, then the bottom ones to handle the Z axis correctly
    480             for (int i = mSize; --i >= 0;) {
    481                 PinnedHeader header = mHeaders[i];
    482                 if (header.visible && (header.state == TOP || header.state == FADING)) {
    483                     drawHeader(canvas, header, currentTime);
    484                 }
    485             }
    486 
    487             for (int i = 0; i < mSize; i++) {
    488                 PinnedHeader header = mHeaders[i];
    489                 if (header.visible && header.state == BOTTOM) {
    490                     drawHeader(canvas, header, currentTime);
    491                 }
    492             }
    493         }
    494 
    495         invalidateIfAnimating();
    496     }
    497 
    498     private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) {
    499         if (header.animating) {
    500             int timeLeft = (int)(header.targetTime - currentTime);
    501             if (timeLeft <= 0) {
    502                 header.y = header.targetY;
    503                 header.visible = header.targetVisible;
    504                 header.animating = false;
    505             } else {
    506                 header.y = header.targetY + (header.sourceY - header.targetY) * timeLeft
    507                         / mAnimationDuration;
    508             }
    509         }
    510         if (header.visible) {
    511             View view = header.view;
    512             int saveCount = canvas.save();
    513             canvas.translate(mHeaderPaddingLeft, header.y);
    514             if (header.state == FADING) {
    515                 mBounds.set(0, 0, mHeaderWidth, view.getHeight());
    516                 canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG);
    517             }
    518             view.draw(canvas);
    519             canvas.restoreToCount(saveCount);
    520         }
    521     }
    522 }
    523