blob: 01895c7cf7998ff6495e70e5b69f67521777bbda [file] [log] [blame]
Chiao Cheng89437e82012-11-01 13:41:51 -07001/*
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
Gary Mai69c182a2016-12-05 13:07:03 -080017package com.android.contacts.list;
Chiao Cheng89437e82012-11-01 13:41:51 -070018
19import android.content.Context;
20import android.graphics.Canvas;
Chiao Cheng89437e82012-11-01 13:41:51 -070021import android.graphics.RectF;
22import android.util.AttributeSet;
23import android.view.MotionEvent;
24import android.view.View;
25import android.view.ViewGroup;
26import android.widget.AbsListView;
27import android.widget.AbsListView.OnScrollListener;
28import android.widget.AdapterView;
29import android.widget.AdapterView.OnItemSelectedListener;
30import android.widget.ListAdapter;
Wenyi Wangd5403fa2016-07-20 17:16:38 -070031import android.widget.TextView;
Chiao Cheng89437e82012-11-01 13:41:51 -070032
Gary Mai69c182a2016-12-05 13:07:03 -080033import com.android.contacts.util.ViewUtil;
Yorke Lee50a89a52013-11-04 14:44:30 -080034
Chiao Cheng89437e82012-11-01 13:41:51 -070035/**
36 * A ListView that maintains a header pinned at the top of the list. The
37 * pinned header can be pushed up and dissolved as needed.
38 */
39public class PinnedHeaderListView extends AutoScrollListView
40 implements OnScrollListener, OnItemSelectedListener {
41
42 /**
43 * Adapter interface. The list adapter must implement this interface.
44 */
45 public interface PinnedHeaderAdapter {
46
47 /**
48 * Returns the overall number of pinned headers, visible or not.
49 */
50 int getPinnedHeaderCount();
51
52 /**
53 * Creates or updates the pinned header view.
54 */
55 View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent);
56
57 /**
58 * Configures the pinned headers to match the visible list items. The
59 * adapter should call {@link PinnedHeaderListView#setHeaderPinnedAtTop},
60 * {@link PinnedHeaderListView#setHeaderPinnedAtBottom},
61 * {@link PinnedHeaderListView#setFadingHeader} or
62 * {@link PinnedHeaderListView#setHeaderInvisible}, for each header that
63 * needs to change its position or visibility.
64 */
65 void configurePinnedHeaders(PinnedHeaderListView listView);
66
67 /**
68 * Returns the list position to scroll to if the pinned header is touched.
69 * Return -1 if the list does not need to be scrolled.
70 */
71 int getScrollPositionForHeader(int viewIndex);
72 }
73
74 private static final int MAX_ALPHA = 255;
75 private static final int TOP = 0;
76 private static final int BOTTOM = 1;
77 private static final int FADING = 2;
78
79 private static final int DEFAULT_ANIMATION_DURATION = 20;
80
Yorke Leefbae8372013-10-01 09:17:08 -070081 private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 100;
82
Chiao Cheng89437e82012-11-01 13:41:51 -070083 private static final class PinnedHeader {
84 View view;
85 boolean visible;
86 int y;
87 int height;
88 int alpha;
89 int state;
90
91 boolean animating;
92 boolean targetVisible;
93 int sourceY;
94 int targetY;
95 long targetTime;
96 }
97
98 private PinnedHeaderAdapter mAdapter;
99 private int mSize;
100 private PinnedHeader[] mHeaders;
101 private RectF mBounds = new RectF();
Chiao Cheng89437e82012-11-01 13:41:51 -0700102 private OnScrollListener mOnScrollListener;
103 private OnItemSelectedListener mOnItemSelectedListener;
104 private int mScrollState;
105
Yorke Leefbae8372013-10-01 09:17:08 -0700106 private boolean mScrollToSectionOnHeaderTouch = false;
107 private boolean mHeaderTouched = false;
108
Chiao Cheng89437e82012-11-01 13:41:51 -0700109 private int mAnimationDuration = DEFAULT_ANIMATION_DURATION;
110 private boolean mAnimating;
111 private long mAnimationTargetTime;
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700112 private int mHeaderPaddingStart;
Chiao Cheng89437e82012-11-01 13:41:51 -0700113 private int mHeaderWidth;
114
115 public PinnedHeaderListView(Context context) {
Yorke Lee9f2769d2013-10-16 10:07:24 -0700116 this(context, null, android.R.attr.listViewStyle);
Chiao Cheng89437e82012-11-01 13:41:51 -0700117 }
118
119 public PinnedHeaderListView(Context context, AttributeSet attrs) {
Yorke Lee9f2769d2013-10-16 10:07:24 -0700120 this(context, attrs, android.R.attr.listViewStyle);
Chiao Cheng89437e82012-11-01 13:41:51 -0700121 }
122
123 public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
124 super(context, attrs, defStyle);
125 super.setOnScrollListener(this);
126 super.setOnItemSelectedListener(this);
127 }
128
129 @Override
130 protected void onLayout(boolean changed, int l, int t, int r, int b) {
131 super.onLayout(changed, l, t, r, b);
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700132 mHeaderPaddingStart = getPaddingStart();
133 mHeaderWidth = r - l - mHeaderPaddingStart - getPaddingEnd();
Chiao Cheng89437e82012-11-01 13:41:51 -0700134 }
135
Chiao Cheng89437e82012-11-01 13:41:51 -0700136 @Override
137 public void setAdapter(ListAdapter adapter) {
138 mAdapter = (PinnedHeaderAdapter)adapter;
139 super.setAdapter(adapter);
140 }
141
142 @Override
143 public void setOnScrollListener(OnScrollListener onScrollListener) {
144 mOnScrollListener = onScrollListener;
145 super.setOnScrollListener(this);
146 }
147
148 @Override
149 public void setOnItemSelectedListener(OnItemSelectedListener listener) {
150 mOnItemSelectedListener = listener;
151 super.setOnItemSelectedListener(this);
152 }
153
Yorke Leefbae8372013-10-01 09:17:08 -0700154 public void setScrollToSectionOnHeaderTouch(boolean value) {
155 mScrollToSectionOnHeaderTouch = value;
156 }
157
Chiao Cheng89437e82012-11-01 13:41:51 -0700158 @Override
159 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
160 int totalItemCount) {
161 if (mAdapter != null) {
162 int count = mAdapter.getPinnedHeaderCount();
163 if (count != mSize) {
164 mSize = count;
165 if (mHeaders == null) {
166 mHeaders = new PinnedHeader[mSize];
167 } else if (mHeaders.length < mSize) {
168 PinnedHeader[] headers = mHeaders;
169 mHeaders = new PinnedHeader[mSize];
170 System.arraycopy(headers, 0, mHeaders, 0, headers.length);
171 }
172 }
173
174 for (int i = 0; i < mSize; i++) {
175 if (mHeaders[i] == null) {
176 mHeaders[i] = new PinnedHeader();
177 }
178 mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this);
179 }
180
181 mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration;
182 mAdapter.configurePinnedHeaders(this);
183 invalidateIfAnimating();
Chiao Cheng89437e82012-11-01 13:41:51 -0700184 }
185 if (mOnScrollListener != null) {
186 mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount);
187 }
188 }
189
190 @Override
191 protected float getTopFadingEdgeStrength() {
192 // Disable vertical fading at the top when the pinned header is present
193 return mSize > 0 ? 0 : super.getTopFadingEdgeStrength();
194 }
195
196 @Override
197 public void onScrollStateChanged(AbsListView view, int scrollState) {
198 mScrollState = scrollState;
199 if (mOnScrollListener != null) {
200 mOnScrollListener.onScrollStateChanged(this, scrollState);
201 }
202 }
203
204 /**
205 * Ensures that the selected item is positioned below the top-pinned headers
206 * and above the bottom-pinned ones.
207 */
208 @Override
209 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
210 int height = getHeight();
211
212 int windowTop = 0;
213 int windowBottom = height;
214
215 for (int i = 0; i < mSize; i++) {
216 PinnedHeader header = mHeaders[i];
217 if (header.visible) {
218 if (header.state == TOP) {
219 windowTop = header.y + header.height;
220 } else if (header.state == BOTTOM) {
221 windowBottom = header.y;
222 break;
223 }
224 }
225 }
226
227 View selectedView = getSelectedView();
228 if (selectedView != null) {
229 if (selectedView.getTop() < windowTop) {
230 setSelectionFromTop(position, windowTop);
231 } else if (selectedView.getBottom() > windowBottom) {
232 setSelectionFromTop(position, windowBottom - selectedView.getHeight());
233 }
234 }
235
236 if (mOnItemSelectedListener != null) {
237 mOnItemSelectedListener.onItemSelected(parent, view, position, id);
238 }
239 }
240
241 @Override
242 public void onNothingSelected(AdapterView<?> parent) {
243 if (mOnItemSelectedListener != null) {
244 mOnItemSelectedListener.onNothingSelected(parent);
245 }
246 }
247
248 public int getPinnedHeaderHeight(int viewIndex) {
249 ensurePinnedHeaderLayout(viewIndex);
250 return mHeaders[viewIndex].view.getHeight();
251 }
252
253 /**
254 * Set header to be pinned at the top.
255 *
256 * @param viewIndex index of the header view
257 * @param y is position of the header in pixels.
258 * @param animate true if the transition to the new coordinate should be animated
259 */
260 public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) {
261 ensurePinnedHeaderLayout(viewIndex);
262 PinnedHeader header = mHeaders[viewIndex];
263 header.visible = true;
264 header.y = y;
265 header.state = TOP;
266
267 // TODO perhaps we should animate at the top as well
268 header.animating = false;
269 }
270
271 /**
272 * Set header to be pinned at the bottom.
273 *
274 * @param viewIndex index of the header view
275 * @param y is position of the header in pixels.
276 * @param animate true if the transition to the new coordinate should be animated
277 */
278 public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) {
279 ensurePinnedHeaderLayout(viewIndex);
280 PinnedHeader header = mHeaders[viewIndex];
281 header.state = BOTTOM;
282 if (header.animating) {
283 header.targetTime = mAnimationTargetTime;
284 header.sourceY = header.y;
285 header.targetY = y;
286 } else if (animate && (header.y != y || !header.visible)) {
287 if (header.visible) {
288 header.sourceY = header.y;
289 } else {
290 header.visible = true;
291 header.sourceY = y + header.height;
292 }
293 header.animating = true;
294 header.targetVisible = true;
295 header.targetTime = mAnimationTargetTime;
296 header.targetY = y;
297 } else {
298 header.visible = true;
299 header.y = y;
300 }
301 }
302
303 /**
304 * Set header to be pinned at the top of the first visible item.
305 *
306 * @param viewIndex index of the header view
307 * @param position is position of the header in pixels.
308 */
309 public void setFadingHeader(int viewIndex, int position, boolean fade) {
310 ensurePinnedHeaderLayout(viewIndex);
311
312 View child = getChildAt(position - getFirstVisiblePosition());
313 if (child == null) return;
314
315 PinnedHeader header = mHeaders[viewIndex];
Wenyi Wangd5403fa2016-07-20 17:16:38 -0700316 // Hide header when it's a star.
317 // TODO: try showing the view even when it's a star;
318 // if we have to hide the star view, then try hiding it in some higher layer.
John Shaoda188712016-08-19 14:57:11 -0700319 header.visible = !((TextView) header.view).getText().toString().isEmpty();
Chiao Cheng89437e82012-11-01 13:41:51 -0700320 header.state = FADING;
321 header.alpha = MAX_ALPHA;
322 header.animating = false;
323
324 int top = getTotalTopPinnedHeaderHeight();
325 header.y = top;
326 if (fade) {
327 int bottom = child.getBottom() - top;
328 int headerHeight = header.height;
329 if (bottom < headerHeight) {
330 int portion = bottom - headerHeight;
331 header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight;
332 header.y = top + portion;
333 }
334 }
335 }
336
337 /**
338 * Makes header invisible.
339 *
340 * @param viewIndex index of the header view
341 * @param animate true if the transition to the new coordinate should be animated
342 */
343 public void setHeaderInvisible(int viewIndex, boolean animate) {
344 PinnedHeader header = mHeaders[viewIndex];
345 if (header.visible && (animate || header.animating) && header.state == BOTTOM) {
346 header.sourceY = header.y;
347 if (!header.animating) {
348 header.visible = true;
349 header.targetY = getBottom() + header.height;
350 }
351 header.animating = true;
352 header.targetTime = mAnimationTargetTime;
353 header.targetVisible = false;
354 } else {
355 header.visible = false;
356 }
357 }
358
359 private void ensurePinnedHeaderLayout(int viewIndex) {
360 View view = mHeaders[viewIndex].view;
361 if (view.isLayoutRequested()) {
Chiao Cheng89437e82012-11-01 13:41:51 -0700362 ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
Andrew Lee020ba622014-04-25 15:26:35 -0700363 int widthSpec;
364 int heightSpec;
365
366 if (layoutParams != null && layoutParams.width > 0) {
367 widthSpec = View.MeasureSpec
368 .makeMeasureSpec(layoutParams.width, View.MeasureSpec.EXACTLY);
369 } else {
370 widthSpec = View.MeasureSpec
371 .makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY);
372 }
373
Chiao Cheng89437e82012-11-01 13:41:51 -0700374 if (layoutParams != null && layoutParams.height > 0) {
375 heightSpec = View.MeasureSpec
376 .makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY);
377 } else {
378 heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
379 }
380 view.measure(widthSpec, heightSpec);
381 int height = view.getMeasuredHeight();
382 mHeaders[viewIndex].height = height;
Andrew Lee020ba622014-04-25 15:26:35 -0700383 view.layout(0, 0, view.getMeasuredWidth(), height);
Chiao Cheng89437e82012-11-01 13:41:51 -0700384 }
385 }
386
387 /**
388 * Returns the sum of heights of headers pinned to the top.
389 */
390 public int getTotalTopPinnedHeaderHeight() {
391 for (int i = mSize; --i >= 0;) {
392 PinnedHeader header = mHeaders[i];
393 if (header.visible && header.state == TOP) {
394 return header.y + header.height;
395 }
396 }
397 return 0;
398 }
399
400 /**
401 * Returns the list item position at the specified y coordinate.
402 */
403 public int getPositionAt(int y) {
404 do {
405 int position = pointToPosition(getPaddingLeft() + 1, y);
406 if (position != -1) {
407 return position;
408 }
409 // If position == -1, we must have hit a separator. Let's examine
410 // a nearby pixel
411 y--;
412 } while (y > 0);
413 return 0;
414 }
415
416 @Override
417 public boolean onInterceptTouchEvent(MotionEvent ev) {
Yorke Leefbae8372013-10-01 09:17:08 -0700418 mHeaderTouched = false;
Yorke Lee528258e2013-09-20 15:21:23 -0700419 if (super.onInterceptTouchEvent(ev)) {
420 return true;
421 }
422
Chiao Cheng89437e82012-11-01 13:41:51 -0700423 if (mScrollState == SCROLL_STATE_IDLE) {
424 final int y = (int)ev.getY();
Yorke Lee4bacccc2013-10-15 15:41:59 -0700425 final int x = (int)ev.getX();
Chiao Cheng89437e82012-11-01 13:41:51 -0700426 for (int i = mSize; --i >= 0;) {
427 PinnedHeader header = mHeaders[i];
John Shao9c129d52016-07-21 19:43:08 -0700428 final int padding = ViewUtil.isViewLayoutRtl(this) ?
429 getWidth() - mHeaderPaddingStart - header.view.getWidth() :
430 mHeaderPaddingStart;
Yorke Lee4bacccc2013-10-15 15:41:59 -0700431 if (header.visible && header.y <= y && header.y + header.height > y &&
Andrew Lee020ba622014-04-25 15:26:35 -0700432 x >= padding && padding + header.view.getWidth() >= x) {
Yorke Leefbae8372013-10-01 09:17:08 -0700433 mHeaderTouched = true;
434 if (mScrollToSectionOnHeaderTouch &&
435 ev.getAction() == MotionEvent.ACTION_DOWN) {
Chiao Cheng89437e82012-11-01 13:41:51 -0700436 return smoothScrollToPartition(i);
437 } else {
438 return true;
439 }
440 }
441 }
442 }
443
Yorke Lee528258e2013-09-20 15:21:23 -0700444 return false;
Chiao Cheng89437e82012-11-01 13:41:51 -0700445 }
446
Yorke Leefbae8372013-10-01 09:17:08 -0700447 @Override
448 public boolean onTouchEvent(MotionEvent ev) {
449 if (mHeaderTouched) {
450 if (ev.getAction() == MotionEvent.ACTION_UP) {
451 mHeaderTouched = false;
452 }
453 return true;
454 }
455 return super.onTouchEvent(ev);
John Shao9c129d52016-07-21 19:43:08 -0700456 }
Yorke Leefbae8372013-10-01 09:17:08 -0700457
Chiao Cheng89437e82012-11-01 13:41:51 -0700458 private boolean smoothScrollToPartition(int partition) {
Jay Shrauner04760932014-09-06 19:57:09 -0700459 if (mAdapter == null) {
460 return false;
461 }
Chiao Cheng89437e82012-11-01 13:41:51 -0700462 final int position = mAdapter.getScrollPositionForHeader(partition);
463 if (position == -1) {
464 return false;
465 }
466
467 int offset = 0;
468 for (int i = 0; i < partition; i++) {
469 PinnedHeader header = mHeaders[i];
470 if (header.visible) {
471 offset += header.height;
472 }
473 }
Yorke Leefbae8372013-10-01 09:17:08 -0700474 smoothScrollToPositionFromTop(position + getHeaderViewsCount(), offset,
475 DEFAULT_SMOOTH_SCROLL_DURATION);
Chiao Cheng89437e82012-11-01 13:41:51 -0700476 return true;
477 }
478
479 private void invalidateIfAnimating() {
480 mAnimating = false;
481 for (int i = 0; i < mSize; i++) {
482 if (mHeaders[i].animating) {
483 mAnimating = true;
484 invalidate();
485 return;
486 }
487 }
488 }
489
490 @Override
491 protected void dispatchDraw(Canvas canvas) {
492 long currentTime = mAnimating ? System.currentTimeMillis() : 0;
493
494 int top = 0;
Andrew Lee020ba622014-04-25 15:26:35 -0700495 int right = 0;
Chiao Cheng89437e82012-11-01 13:41:51 -0700496 int bottom = getBottom();
497 boolean hasVisibleHeaders = false;
498 for (int i = 0; i < mSize; i++) {
499 PinnedHeader header = mHeaders[i];
500 if (header.visible) {
501 hasVisibleHeaders = true;
502 if (header.state == BOTTOM && header.y < bottom) {
503 bottom = header.y;
504 } else if (header.state == TOP || header.state == FADING) {
505 int newTop = header.y + header.height;
506 if (newTop > top) {
507 top = newTop;
508 }
509 }
510 }
511 }
512
513 if (hasVisibleHeaders) {
514 canvas.save();
Chiao Cheng89437e82012-11-01 13:41:51 -0700515 }
516
517 super.dispatchDraw(canvas);
518
519 if (hasVisibleHeaders) {
520 canvas.restore();
521
Yorke Lee3d3262b2014-08-30 19:08:27 -0700522 // If the first item is visible and if it has a positive top that is greater than the
523 // first header's assigned y-value, use that for the first header's y value. This way,
524 // the header inherits any padding applied to the list view.
Andrew Lee741cbbf2014-06-02 14:11:27 -0700525 if (mSize > 0 && getFirstVisiblePosition() == 0) {
526 View firstChild = getChildAt(0);
527 PinnedHeader firstHeader = mHeaders[0];
528
529 if (firstHeader != null) {
Andrew Lee84e3a882014-06-23 16:28:13 -0700530 int firstHeaderTop = firstChild != null ? firstChild.getTop() : 0;
Yorke Lee3d3262b2014-08-30 19:08:27 -0700531 firstHeader.y = Math.max(firstHeader.y, firstHeaderTop);
Andrew Lee741cbbf2014-06-02 14:11:27 -0700532 }
533 }
534
Chiao Cheng89437e82012-11-01 13:41:51 -0700535 // First draw top headers, then the bottom ones to handle the Z axis correctly
536 for (int i = mSize; --i >= 0;) {
537 PinnedHeader header = mHeaders[i];
538 if (header.visible && (header.state == TOP || header.state == FADING)) {
539 drawHeader(canvas, header, currentTime);
540 }
541 }
542
543 for (int i = 0; i < mSize; i++) {
544 PinnedHeader header = mHeaders[i];
545 if (header.visible && header.state == BOTTOM) {
546 drawHeader(canvas, header, currentTime);
547 }
548 }
549 }
550
551 invalidateIfAnimating();
552 }
553
554 private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) {
555 if (header.animating) {
556 int timeLeft = (int)(header.targetTime - currentTime);
557 if (timeLeft <= 0) {
558 header.y = header.targetY;
559 header.visible = header.targetVisible;
560 header.animating = false;
561 } else {
562 header.y = header.targetY + (header.sourceY - header.targetY) * timeLeft
563 / mAnimationDuration;
564 }
565 }
566 if (header.visible) {
567 View view = header.view;
568 int saveCount = canvas.save();
Andrew Lee020ba622014-04-25 15:26:35 -0700569 int translateX = ViewUtil.isViewLayoutRtl(this) ?
Andrew Lee741cbbf2014-06-02 14:11:27 -0700570 getWidth() - mHeaderPaddingStart - view.getWidth() :
Andrew Lee020ba622014-04-25 15:26:35 -0700571 mHeaderPaddingStart;
572 canvas.translate(translateX, header.y);
Chiao Cheng89437e82012-11-01 13:41:51 -0700573 if (header.state == FADING) {
Andrew Lee741cbbf2014-06-02 14:11:27 -0700574 mBounds.set(0, 0, view.getWidth(), view.getHeight());
Chiao Cheng89437e82012-11-01 13:41:51 -0700575 canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG);
576 }
577 view.draw(canvas);
578 canvas.restoreToCount(saveCount);
579 }
580 }
Jay Shrauner04760932014-09-06 19:57:09 -0700581}