• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
getPinnedHeaderCount()48         int getPinnedHeaderCount();
49 
50         /**
51          * Creates or updates the pinned header view.
52          */
getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent)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          */
configurePinnedHeaders(PinnedHeaderListView listView)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          */
getScrollPositionForHeader(int viewIndex)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 
PinnedHeaderListView(Context context)109     public PinnedHeaderListView(Context context) {
110         this(context, null, com.android.internal.R.attr.listViewStyle);
111     }
112 
PinnedHeaderListView(Context context, AttributeSet attrs)113     public PinnedHeaderListView(Context context, AttributeSet attrs) {
114         this(context, attrs, com.android.internal.R.attr.listViewStyle);
115     }
116 
PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle)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
onLayout(boolean changed, int l, int t, int r, int b)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 
setPinnedHeaderAnimationDuration(int duration)130     public void setPinnedHeaderAnimationDuration(int duration) {
131         mAnimationDuration = duration;
132     }
133 
134     @Override
setAdapter(ListAdapter adapter)135     public void setAdapter(ListAdapter adapter) {
136         mAdapter = (PinnedHeaderAdapter)adapter;
137         super.setAdapter(adapter);
138     }
139 
140     @Override
setOnScrollListener(OnScrollListener onScrollListener)141     public void setOnScrollListener(OnScrollListener onScrollListener) {
142         mOnScrollListener = onScrollListener;
143         super.setOnScrollListener(this);
144     }
145 
146     @Override
setOnItemSelectedListener(OnItemSelectedListener listener)147     public void setOnItemSelectedListener(OnItemSelectedListener listener) {
148         mOnItemSelectedListener = listener;
149         super.setOnItemSelectedListener(this);
150     }
151 
152     @Override
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)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
getTopFadingEdgeStrength()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
onScrollStateChanged(AbsListView view, int scrollState)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
onItemSelected(AdapterView<?> parent, View view, int position, long id)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
onNothingSelected(AdapterView<?> parent)237     public void onNothingSelected(AdapterView<?> parent) {
238         if (mOnItemSelectedListener != null) {
239             mOnItemSelectedListener.onNothingSelected(parent);
240         }
241     }
242 
getPinnedHeaderHeight(int viewIndex)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      */
setHeaderPinnedAtTop(int viewIndex, int y, boolean animate)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      */
setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate)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      */
setFadingHeader(int viewIndex, int position, boolean fade)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      */
setHeaderInvisible(int viewIndex, boolean animate)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 
ensurePinnedHeaderLayout(int viewIndex)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      */
getTotalTopPinnedHeaderHeight()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      */
getPositionAt(int y)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
onInterceptTouchEvent(MotionEvent ev)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 
smoothScrollToPartition(int partition)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 
invalidateIfAnimating()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
dispatchDraw(Canvas canvas)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 
drawHeader(Canvas canvas, PinnedHeader header, long currentTime)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