1 /*
2  * Copyright (C) 2014 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 
18 package com.android.internal.widget;
19 
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.graphics.Rect;
24 import android.graphics.drawable.Drawable;
25 import android.metrics.LogMaker;
26 import android.os.Bundle;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.util.AttributeSet;
30 import android.util.Log;
31 import android.view.MotionEvent;
32 import android.view.VelocityTracker;
33 import android.view.View;
34 import android.view.ViewConfiguration;
35 import android.view.ViewGroup;
36 import android.view.ViewParent;
37 import android.view.ViewTreeObserver;
38 import android.view.accessibility.AccessibilityEvent;
39 import android.view.accessibility.AccessibilityNodeInfo;
40 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
41 import android.view.animation.AnimationUtils;
42 import android.widget.AbsListView;
43 import android.widget.OverScroller;
44 
45 import com.android.internal.R;
46 import com.android.internal.logging.MetricsLogger;
47 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
48 
49 public class ResolverDrawerLayout extends ViewGroup {
50     private static final String TAG = "ResolverDrawerLayout";
51     private MetricsLogger mMetricsLogger;
52 
53     /**
54      * Max width of the whole drawer layout
55      */
56     private int mMaxWidth;
57 
58     /**
59      * Max total visible height of views not marked always-show when in the closed/initial state
60      */
61     private int mMaxCollapsedHeight;
62 
63     /**
64      * Max total visible height of views not marked always-show when in the closed/initial state
65      * when a default option is present
66      */
67     private int mMaxCollapsedHeightSmall;
68 
69     /**
70      * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or
71      * inferred by {@code mMaxCollapsedHeight}.
72      */
73     private final boolean mIsMaxCollapsedHeightSmallExplicit;
74 
75     private boolean mSmallCollapsed;
76 
77     /**
78      * Move views down from the top by this much in px
79      */
80     private float mCollapseOffset;
81 
82     /**
83       * Track fractions of pixels from drag calculations. Without this, the view offsets get
84       * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
85       */
86     private float mDragRemainder = 0.0f;
87     private int mCollapsibleHeight;
88     private int mUncollapsibleHeight;
89     private int mAlwaysShowHeight;
90 
91     /**
92      * The height in pixels of reserved space added to the top of the collapsed UI;
93      * e.g. chooser targets
94      */
95     private int mCollapsibleHeightReserved;
96 
97     private int mTopOffset;
98     private boolean mShowAtTop;
99 
100     private boolean mIsDragging;
101     private boolean mOpenOnClick;
102     private boolean mOpenOnLayout;
103     private boolean mDismissOnScrollerFinished;
104     private final int mTouchSlop;
105     private final float mMinFlingVelocity;
106     private final OverScroller mScroller;
107     private final VelocityTracker mVelocityTracker;
108 
109     private Drawable mScrollIndicatorDrawable;
110 
111     private OnDismissedListener mOnDismissedListener;
112     private RunOnDismissedListener mRunOnDismissedListener;
113     private OnCollapsedChangedListener mOnCollapsedChangedListener;
114 
115     private boolean mDismissLocked;
116 
117     private float mInitialTouchX;
118     private float mInitialTouchY;
119     private float mLastTouchY;
120     private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
121 
122     private final Rect mTempRect = new Rect();
123 
124     private AbsListView mNestedListChild;
125     private RecyclerView mNestedRecyclerChild;
126 
127     private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
128             new ViewTreeObserver.OnTouchModeChangeListener() {
129                 @Override
130                 public void onTouchModeChanged(boolean isInTouchMode) {
131                     if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
132                         smoothScrollTo(0, 0);
133                     }
134                 }
135             };
136 
ResolverDrawerLayout(Context context)137     public ResolverDrawerLayout(Context context) {
138         this(context, null);
139     }
140 
ResolverDrawerLayout(Context context, AttributeSet attrs)141     public ResolverDrawerLayout(Context context, AttributeSet attrs) {
142         this(context, attrs, 0);
143     }
144 
ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr)145     public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
146         super(context, attrs, defStyleAttr);
147 
148         final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout,
149                 defStyleAttr, 0);
150         mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_maxWidth, -1);
151         mMaxCollapsedHeight = a.getDimensionPixelSize(
152                 R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
153         mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
154                 R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
155                 mMaxCollapsedHeight);
156         mIsMaxCollapsedHeightSmallExplicit =
157                 a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
158         mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
159         a.recycle();
160 
161         mScrollIndicatorDrawable = mContext.getDrawable(R.drawable.scroll_indicator_material);
162 
163         mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context,
164                 android.R.interpolator.decelerate_quint));
165         mVelocityTracker = VelocityTracker.obtain();
166 
167         final ViewConfiguration vc = ViewConfiguration.get(context);
168         mTouchSlop = vc.getScaledTouchSlop();
169         mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
170 
171         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
172     }
173 
174     /**
175      * Dynamically set the max collapsed height. Note this also updates the small collapsed
176      * height if it wasn't specified explicitly.
177      */
setMaxCollapsedHeight(int heightInPixels)178     public void setMaxCollapsedHeight(int heightInPixels) {
179         if (heightInPixels == mMaxCollapsedHeight) {
180             return;
181         }
182         mMaxCollapsedHeight = heightInPixels;
183         if (!mIsMaxCollapsedHeightSmallExplicit) {
184             mMaxCollapsedHeightSmall = mMaxCollapsedHeight;
185         }
186         requestLayout();
187     }
188 
setSmallCollapsed(boolean smallCollapsed)189     public void setSmallCollapsed(boolean smallCollapsed) {
190         mSmallCollapsed = smallCollapsed;
191         requestLayout();
192     }
193 
isSmallCollapsed()194     public boolean isSmallCollapsed() {
195         return mSmallCollapsed;
196     }
197 
isCollapsed()198     public boolean isCollapsed() {
199         return mCollapseOffset > 0;
200     }
201 
setShowAtTop(boolean showOnTop)202     public void setShowAtTop(boolean showOnTop) {
203         mShowAtTop = showOnTop;
204         invalidate();
205         requestLayout();
206     }
207 
getShowAtTop()208     public boolean getShowAtTop() {
209         return mShowAtTop;
210     }
211 
setCollapsed(boolean collapsed)212     public void setCollapsed(boolean collapsed) {
213         if (!isLaidOut()) {
214             mOpenOnLayout = !collapsed;
215         } else {
216             smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
217         }
218     }
219 
setCollapsibleHeightReserved(int heightPixels)220     public void setCollapsibleHeightReserved(int heightPixels) {
221         final int oldReserved = mCollapsibleHeightReserved;
222         mCollapsibleHeightReserved = heightPixels;
223 
224         final int dReserved = mCollapsibleHeightReserved - oldReserved;
225         if (dReserved != 0 && mIsDragging) {
226             mLastTouchY -= dReserved;
227         }
228 
229         final int oldCollapsibleHeight = mCollapsibleHeight;
230         mCollapsibleHeight = Math.min(mCollapsibleHeight, getMaxCollapsedHeight());
231 
232         if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
233             return;
234         }
235 
236         invalidate();
237     }
238 
setDismissLocked(boolean locked)239     public void setDismissLocked(boolean locked) {
240         mDismissLocked = locked;
241     }
242 
isMoving()243     private boolean isMoving() {
244         return mIsDragging || !mScroller.isFinished();
245     }
246 
isDragging()247     private boolean isDragging() {
248         return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
249     }
250 
updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed)251     private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
252         if (oldCollapsibleHeight == mCollapsibleHeight) {
253             return false;
254         }
255 
256         if (getShowAtTop()) {
257             // Keep the drawer fully open.
258             mCollapseOffset = 0;
259             return false;
260         }
261 
262         if (isLaidOut()) {
263             final boolean isCollapsedOld = mCollapseOffset != 0;
264             if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
265                     && mCollapseOffset == oldCollapsibleHeight)) {
266                 // Stay closed even at the new height.
267                 mCollapseOffset = mCollapsibleHeight;
268             } else {
269                 mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight);
270             }
271             final boolean isCollapsedNew = mCollapseOffset != 0;
272             if (isCollapsedOld != isCollapsedNew) {
273                 onCollapsedChanged(isCollapsedNew);
274             }
275         } else {
276             // Start out collapsed at first unless we restored state for otherwise
277             mCollapseOffset = mOpenOnLayout ? 0 : mCollapsibleHeight;
278         }
279         return true;
280     }
281 
getMaxCollapsedHeight()282     private int getMaxCollapsedHeight() {
283         return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
284                 + mCollapsibleHeightReserved;
285     }
286 
setOnDismissedListener(OnDismissedListener listener)287     public void setOnDismissedListener(OnDismissedListener listener) {
288         mOnDismissedListener = listener;
289     }
290 
isDismissable()291     private boolean isDismissable() {
292         return mOnDismissedListener != null && !mDismissLocked;
293     }
294 
setOnCollapsedChangedListener(OnCollapsedChangedListener listener)295     public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
296         mOnCollapsedChangedListener = listener;
297     }
298 
299     @Override
onInterceptTouchEvent(MotionEvent ev)300     public boolean onInterceptTouchEvent(MotionEvent ev) {
301         final int action = ev.getActionMasked();
302 
303         if (action == MotionEvent.ACTION_DOWN) {
304             mVelocityTracker.clear();
305         }
306 
307         mVelocityTracker.addMovement(ev);
308 
309         switch (action) {
310             case MotionEvent.ACTION_DOWN: {
311                 final float x = ev.getX();
312                 final float y = ev.getY();
313                 mInitialTouchX = x;
314                 mInitialTouchY = mLastTouchY = y;
315                 mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
316             }
317             break;
318 
319             case MotionEvent.ACTION_MOVE: {
320                 final float x = ev.getX();
321                 final float y = ev.getY();
322                 final float dy = y - mInitialTouchY;
323                 if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null &&
324                         (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
325                     mActivePointerId = ev.getPointerId(0);
326                     mIsDragging = true;
327                     mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
328                             Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
329                 }
330             }
331             break;
332 
333             case MotionEvent.ACTION_POINTER_UP: {
334                 onSecondaryPointerUp(ev);
335             }
336             break;
337 
338             case MotionEvent.ACTION_CANCEL:
339             case MotionEvent.ACTION_UP: {
340                 resetTouch();
341             }
342             break;
343         }
344 
345         if (mIsDragging) {
346             abortAnimation();
347         }
348         return mIsDragging || mOpenOnClick;
349     }
350 
isNestedListChildScrolled()351     private boolean isNestedListChildScrolled() {
352         return  mNestedListChild != null
353                 && mNestedListChild.getChildCount() > 0
354                 && (mNestedListChild.getFirstVisiblePosition() > 0
355                         || mNestedListChild.getChildAt(0).getTop() < 0);
356     }
357 
isNestedRecyclerChildScrolled()358     private boolean isNestedRecyclerChildScrolled() {
359         if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) {
360             final RecyclerView.ViewHolder vh =
361                     mNestedRecyclerChild.findViewHolderForAdapterPosition(0);
362             return vh == null || vh.itemView.getTop() < 0;
363         }
364         return false;
365     }
366 
367     @Override
onTouchEvent(MotionEvent ev)368     public boolean onTouchEvent(MotionEvent ev) {
369         final int action = ev.getActionMasked();
370 
371         mVelocityTracker.addMovement(ev);
372 
373         boolean handled = false;
374         switch (action) {
375             case MotionEvent.ACTION_DOWN: {
376                 final float x = ev.getX();
377                 final float y = ev.getY();
378                 mInitialTouchX = x;
379                 mInitialTouchY = mLastTouchY = y;
380                 mActivePointerId = ev.getPointerId(0);
381                 final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
382                 handled = isDismissable() || mCollapsibleHeight > 0;
383                 mIsDragging = hitView && handled;
384                 abortAnimation();
385             }
386             break;
387 
388             case MotionEvent.ACTION_MOVE: {
389                 int index = ev.findPointerIndex(mActivePointerId);
390                 if (index < 0) {
391                     Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
392                     index = 0;
393                     mActivePointerId = ev.getPointerId(0);
394                     mInitialTouchX = ev.getX();
395                     mInitialTouchY = mLastTouchY = ev.getY();
396                 }
397                 final float x = ev.getX(index);
398                 final float y = ev.getY(index);
399                 if (!mIsDragging) {
400                     final float dy = y - mInitialTouchY;
401                     if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
402                         handled = mIsDragging = true;
403                         mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
404                                 Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
405                     }
406                 }
407                 if (mIsDragging) {
408                     final float dy = y - mLastTouchY;
409                     if (dy > 0 && isNestedListChildScrolled()) {
410                         mNestedListChild.smoothScrollBy((int) -dy, 0);
411                     } else if (dy > 0 && isNestedRecyclerChildScrolled()) {
412                         mNestedRecyclerChild.scrollBy(0, (int) -dy);
413                     } else {
414                         performDrag(dy);
415                     }
416                 }
417                 mLastTouchY = y;
418             }
419             break;
420 
421             case MotionEvent.ACTION_POINTER_DOWN: {
422                 final int pointerIndex = ev.getActionIndex();
423                 final int pointerId = ev.getPointerId(pointerIndex);
424                 mActivePointerId = pointerId;
425                 mInitialTouchX = ev.getX(pointerIndex);
426                 mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
427             }
428             break;
429 
430             case MotionEvent.ACTION_POINTER_UP: {
431                 onSecondaryPointerUp(ev);
432             }
433             break;
434 
435             case MotionEvent.ACTION_UP: {
436                 final boolean wasDragging = mIsDragging;
437                 mIsDragging = false;
438                 if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null &&
439                         findChildUnder(ev.getX(), ev.getY()) == null) {
440                     if (isDismissable()) {
441                         dispatchOnDismissed();
442                         resetTouch();
443                         return true;
444                     }
445                 }
446                 if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop &&
447                         Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
448                     smoothScrollTo(0, 0);
449                     return true;
450                 }
451                 mVelocityTracker.computeCurrentVelocity(1000);
452                 final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
453                 if (Math.abs(yvel) > mMinFlingVelocity) {
454                     if (getShowAtTop()) {
455                         if (isDismissable() && yvel < 0) {
456                             abortAnimation();
457                             dismiss();
458                         } else {
459                             smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
460                         }
461                     } else {
462                         if (isDismissable()
463                                 && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
464                             smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel);
465                             mDismissOnScrollerFinished = true;
466                         } else {
467                             scrollNestedScrollableChildBackToTop();
468                             smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
469                         }
470                     }
471                 }else {
472                     smoothScrollTo(
473                             mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
474                 }
475                 resetTouch();
476             }
477             break;
478 
479             case MotionEvent.ACTION_CANCEL: {
480                 if (mIsDragging) {
481                     smoothScrollTo(
482                             mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
483                 }
484                 resetTouch();
485                 return true;
486             }
487         }
488 
489         return handled;
490     }
491 
492     /**
493      * Scroll nested scrollable child back to top if it has been scrolled.
494      */
495     public void scrollNestedScrollableChildBackToTop() {
496         if (isNestedListChildScrolled()) {
497             mNestedListChild.smoothScrollToPosition(0);
498         } else if (isNestedRecyclerChildScrolled()) {
499             mNestedRecyclerChild.smoothScrollToPosition(0);
500         }
501     }
502 
503     private void onSecondaryPointerUp(MotionEvent ev) {
504         final int pointerIndex = ev.getActionIndex();
505         final int pointerId = ev.getPointerId(pointerIndex);
506         if (pointerId == mActivePointerId) {
507             // This was our active pointer going up. Choose a new
508             // active pointer and adjust accordingly.
509             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
510             mInitialTouchX = ev.getX(newPointerIndex);
511             mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
512             mActivePointerId = ev.getPointerId(newPointerIndex);
513         }
514     }
515 
516     private void resetTouch() {
517         mActivePointerId = MotionEvent.INVALID_POINTER_ID;
518         mIsDragging = false;
519         mOpenOnClick = false;
520         mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
521         mVelocityTracker.clear();
522     }
523 
524     private void dismiss() {
525         mRunOnDismissedListener = new RunOnDismissedListener();
526         post(mRunOnDismissedListener);
527     }
528 
529     @Override
530     public void computeScroll() {
531         super.computeScroll();
532         if (mScroller.computeScrollOffset()) {
533             final boolean keepGoing = !mScroller.isFinished();
534             performDrag(mScroller.getCurrY() - mCollapseOffset);
535             if (keepGoing) {
536                 postInvalidateOnAnimation();
537             } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
538                 dismiss();
539             }
540         }
541     }
542 
543     private void abortAnimation() {
544         mScroller.abortAnimation();
545         mRunOnDismissedListener = null;
546         mDismissOnScrollerFinished = false;
547     }
548 
549     private float performDrag(float dy) {
550         if (getShowAtTop()) {
551             return 0;
552         }
553 
554         final float newPos = Math.max(0, Math.min(mCollapseOffset + dy,
555                 mCollapsibleHeight + mUncollapsibleHeight));
556         if (newPos != mCollapseOffset) {
557             dy = newPos - mCollapseOffset;
558 
559             mDragRemainder += dy - (int) dy;
560             if (mDragRemainder >= 1.0f) {
561                 mDragRemainder -= 1.0f;
562                 dy += 1.0f;
563             } else if (mDragRemainder <= -1.0f) {
564                 mDragRemainder += 1.0f;
565                 dy -= 1.0f;
566             }
567 
568             final int childCount = getChildCount();
569             for (int i = 0; i < childCount; i++) {
570                 final View child = getChildAt(i);
571                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
572                 if (!lp.ignoreOffset) {
573                     child.offsetTopAndBottom((int) dy);
574                 }
575             }
576             final boolean isCollapsedOld = mCollapseOffset != 0;
577             mCollapseOffset = newPos;
578             mTopOffset += dy;
579             final boolean isCollapsedNew = newPos != 0;
580             if (isCollapsedOld != isCollapsedNew) {
581                 onCollapsedChanged(isCollapsedNew);
582                 getMetricsLogger().write(
583                         new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED)
584                         .setSubtype(isCollapsedNew ? 1 : 0));
585             }
586             onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy));
587             postInvalidateOnAnimation();
588             return dy;
589         }
590         return 0;
591     }
592 
593     private void onCollapsedChanged(boolean isCollapsed) {
594         notifyViewAccessibilityStateChangedIfNeeded(
595                 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
596 
597         if (mScrollIndicatorDrawable != null) {
598             setWillNotDraw(!isCollapsed);
599         }
600 
601         if (mOnCollapsedChangedListener != null) {
602             mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed);
603         }
604     }
605 
606     void dispatchOnDismissed() {
607         if (mOnDismissedListener != null) {
608             mOnDismissedListener.onDismissed();
609         }
610         if (mRunOnDismissedListener != null) {
611             removeCallbacks(mRunOnDismissedListener);
612             mRunOnDismissedListener = null;
613         }
614     }
615 
616     private void smoothScrollTo(int yOffset, float velocity) {
617         abortAnimation();
618         final int sy = (int) mCollapseOffset;
619         int dy = yOffset - sy;
620         if (dy == 0) {
621             return;
622         }
623 
624         final int height = getHeight();
625         final int halfHeight = height / 2;
626         final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height);
627         final float distance = halfHeight + halfHeight *
628                 distanceInfluenceForSnapDuration(distanceRatio);
629 
630         int duration = 0;
631         velocity = Math.abs(velocity);
632         if (velocity > 0) {
633             duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
634         } else {
635             final float pageDelta = (float) Math.abs(dy) / height;
636             duration = (int) ((pageDelta + 1) * 100);
637         }
638         duration = Math.min(duration, 300);
639 
640         mScroller.startScroll(0, sy, 0, dy, duration);
641         postInvalidateOnAnimation();
642     }
643 
644     private float distanceInfluenceForSnapDuration(float f) {
645         f -= 0.5f; // center the values about 0.
646         f *= 0.3f * Math.PI / 2.0f;
647         return (float) Math.sin(f);
648     }
649 
650     /**
651      * Note: this method doesn't take Z into account for overlapping views
652      * since it is only used in contexts where this doesn't affect the outcome.
653      */
654     private View findChildUnder(float x, float y) {
655         return findChildUnder(this, x, y);
656     }
657 
658     private static View findChildUnder(ViewGroup parent, float x, float y) {
659         final int childCount = parent.getChildCount();
660         for (int i = childCount - 1; i >= 0; i--) {
661             final View child = parent.getChildAt(i);
662             if (isChildUnder(child, x, y)) {
663                 return child;
664             }
665         }
666         return null;
667     }
668 
669     private View findListChildUnder(float x, float y) {
670         View v = findChildUnder(x, y);
671         while (v != null) {
672             x -= v.getX();
673             y -= v.getY();
674             if (v instanceof AbsListView) {
675                 // One more after this.
676                 return findChildUnder((ViewGroup) v, x, y);
677             }
678             v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null;
679         }
680         return v;
681     }
682 
683     /**
684      * This only checks clipping along the bottom edge.
685      */
686     private boolean isListChildUnderClipped(float x, float y) {
687         final View listChild = findListChildUnder(x, y);
688         return listChild != null && isDescendantClipped(listChild);
689     }
690 
691     private boolean isDescendantClipped(View child) {
692         mTempRect.set(0, 0, child.getWidth(), child.getHeight());
693         offsetDescendantRectToMyCoords(child, mTempRect);
694         View directChild;
695         if (child.getParent() == this) {
696             directChild = child;
697         } else {
698             View v = child;
699             ViewParent p = child.getParent();
700             while (p != this) {
701                 v = (View) p;
702                 p = v.getParent();
703             }
704             directChild = v;
705         }
706 
707         // ResolverDrawerLayout lays out vertically in child order;
708         // the next view and forward is what to check against.
709         int clipEdge = getHeight() - getPaddingBottom();
710         final int childCount = getChildCount();
711         for (int i = indexOfChild(directChild) + 1; i < childCount; i++) {
712             final View nextChild = getChildAt(i);
713             if (nextChild.getVisibility() == GONE) {
714                 continue;
715             }
716             clipEdge = Math.min(clipEdge, nextChild.getTop());
717         }
718         return mTempRect.bottom > clipEdge;
719     }
720 
721     private static boolean isChildUnder(View child, float x, float y) {
722         final float left = child.getX();
723         final float top = child.getY();
724         final float right = left + child.getWidth();
725         final float bottom = top + child.getHeight();
726         return x >= left && y >= top && x < right && y < bottom;
727     }
728 
729     @Override
730     public void requestChildFocus(View child, View focused) {
731         super.requestChildFocus(child, focused);
732         if (!isInTouchMode() && isDescendantClipped(focused)) {
733             smoothScrollTo(0, 0);
734         }
735     }
736 
737     @Override
738     protected void onAttachedToWindow() {
739         super.onAttachedToWindow();
740         getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener);
741     }
742 
743     @Override
744     protected void onDetachedFromWindow() {
745         super.onDetachedFromWindow();
746         getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener);
747         abortAnimation();
748     }
749 
750     @Override
751     public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
752         if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) {
753             if (target instanceof AbsListView) {
754                 mNestedListChild = (AbsListView) target;
755             }
756             if (target instanceof RecyclerView) {
757                 mNestedRecyclerChild = (RecyclerView) target;
758             }
759             return true;
760         }
761         return false;
762     }
763 
764     @Override
765     public void onNestedScrollAccepted(View child, View target, int axes) {
766         super.onNestedScrollAccepted(child, target, axes);
767     }
768 
769     @Override
770     public void onStopNestedScroll(View child) {
771         super.onStopNestedScroll(child);
772         if (mScroller.isFinished()) {
773             smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
774         }
775     }
776 
777     @Override
778     public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
779             int dxUnconsumed, int dyUnconsumed) {
780         if (dyUnconsumed < 0) {
781             performDrag(-dyUnconsumed);
782         }
783     }
784 
785     @Override
786     public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
787         if (dy > 0) {
788             consumed[1] = (int) -performDrag(-dy);
789         }
790     }
791 
792     @Override
793     public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
794         if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
795             smoothScrollTo(0, velocityY);
796             return true;
797         }
798         return false;
799     }
800 
801     @Override
802     public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
803         if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
804             if (getShowAtTop()) {
805                 if (isDismissable() && velocityY > 0) {
806                     abortAnimation();
807                     dismiss();
808                 } else {
809                     smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
810                 }
811             } else {
812                 if (isDismissable()
813                         && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
814                     smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY);
815                     mDismissOnScrollerFinished = true;
816                 } else {
817                     smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
818                 }
819             }
820             return true;
821         }
822         return false;
823     }
824 
825     private boolean performAccessibilityActionCommon(int action) {
826         switch (action) {
827             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
828             case AccessibilityNodeInfo.ACTION_EXPAND:
829             case R.id.accessibilityActionScrollDown:
830                 if (mCollapseOffset != 0) {
831                     smoothScrollTo(0, 0);
832                     return true;
833                 }
834                 break;
835             case AccessibilityNodeInfo.ACTION_COLLAPSE:
836                 if (mCollapseOffset < mCollapsibleHeight) {
837                     smoothScrollTo(mCollapsibleHeight, 0);
838                     return true;
839                 }
840                 break;
841             case AccessibilityNodeInfo.ACTION_DISMISS:
842                 if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
843                         && isDismissable()) {
844                     smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 0);
845                     mDismissOnScrollerFinished = true;
846                     return true;
847                 }
848                 break;
849         }
850 
851         return false;
852     }
853 
854     @Override
855     public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
856         if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
857             return true;
858         }
859 
860         return performAccessibilityActionCommon(action);
861     }
862 
863     @Override
864     public CharSequence getAccessibilityClassName() {
865         // Since we support scrolling, make this ViewGroup look like a
866         // ScrollView. This is kind of a hack until we have support for
867         // specifying auto-scroll behavior.
868         return android.widget.ScrollView.class.getName();
869     }
870 
871     @Override
872     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
873         super.onInitializeAccessibilityNodeInfoInternal(info);
874 
875         if (isEnabled()) {
876             if (mCollapseOffset != 0) {
877                 info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
878                 info.addAction(AccessibilityAction.ACTION_EXPAND);
879                 info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
880                 info.setScrollable(true);
881             }
882             if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
883                     && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) {
884                 info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
885                 info.setScrollable(true);
886             }
887             if (mCollapseOffset < mCollapsibleHeight) {
888                 info.addAction(AccessibilityAction.ACTION_COLLAPSE);
889             }
890             if (mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight && isDismissable()) {
891                 info.addAction(AccessibilityAction.ACTION_DISMISS);
892             }
893         }
894 
895         // This view should never get accessibility focus, but it's interactive
896         // via nested scrolling, so we can't hide it completely.
897         info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
898     }
899 
900     @Override
901     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
902         if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
903             // This view should never get accessibility focus.
904             return false;
905         }
906 
907         if (super.performAccessibilityActionInternal(action, arguments)) {
908             return true;
909         }
910 
911         return performAccessibilityActionCommon(action);
912     }
913 
914     @Override
915     public void onDrawForeground(Canvas canvas) {
916         if (mScrollIndicatorDrawable != null) {
917             mScrollIndicatorDrawable.draw(canvas);
918         }
919 
920         super.onDrawForeground(canvas);
921     }
922 
923     @Override
924     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
925         final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
926         int widthSize = sourceWidth;
927         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
928 
929         // Single-use layout; just ignore the mode and use available space.
930         // Clamp to maxWidth.
931         if (mMaxWidth >= 0) {
932             widthSize = Math.min(widthSize, mMaxWidth);
933         }
934 
935         final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
936         final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
937 
938         // Currently we allot more height than is really needed so that the entirety of the
939         // sheet may be pulled up.
940         // TODO: Restrict the height here to be the right value.
941         int heightUsed = 0;
942 
943         // Measure always-show children first.
944         final int childCount = getChildCount();
945         for (int i = 0; i < childCount; i++) {
946             final View child = getChildAt(i);
947             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
948             if (lp.alwaysShow && child.getVisibility() != GONE) {
949                 if (lp.maxHeight != -1) {
950                     final int remainingHeight = heightSize - heightUsed;
951                     measureChildWithMargins(child, widthSpec, 0,
952                             MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
953                             lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
954                 } else {
955                     measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
956                 }
957                 heightUsed += child.getMeasuredHeight();
958             }
959         }
960 
961         mAlwaysShowHeight = heightUsed;
962 
963         // And now the rest.
964         for (int i = 0; i < childCount; i++) {
965             final View child = getChildAt(i);
966 
967             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
968             if (!lp.alwaysShow && child.getVisibility() != GONE) {
969                 if (lp.maxHeight != -1) {
970                     final int remainingHeight = heightSize - heightUsed;
971                     measureChildWithMargins(child, widthSpec, 0,
972                             MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
973                             lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
974                 } else {
975                     measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
976                 }
977                 heightUsed += child.getMeasuredHeight();
978             }
979         }
980 
981         final int oldCollapsibleHeight = mCollapsibleHeight;
982         mCollapsibleHeight = Math.max(0,
983                 heightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
984         mUncollapsibleHeight = heightUsed - mCollapsibleHeight;
985 
986         updateCollapseOffset(oldCollapsibleHeight, !isDragging());
987 
988         if (getShowAtTop()) {
989             mTopOffset = 0;
990         } else {
991             mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
992         }
993 
994         setMeasuredDimension(sourceWidth, heightSize);
995     }
996 
997     /**
998       * @return The space reserved by views with 'alwaysShow=true'
999       */
1000     public int getAlwaysShowHeight() {
1001         return mAlwaysShowHeight;
1002     }
1003 
1004     @Override
1005     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1006         final int width = getWidth();
1007 
1008         View indicatorHost = null;
1009 
1010         int ypos = mTopOffset;
1011         int leftEdge = getPaddingLeft();
1012         int rightEdge = width - getPaddingRight();
1013 
1014         final int childCount = getChildCount();
1015         for (int i = 0; i < childCount; i++) {
1016             final View child = getChildAt(i);
1017             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1018             if (lp.hasNestedScrollIndicator) {
1019                 indicatorHost = child;
1020             }
1021 
1022             if (child.getVisibility() == GONE) {
1023                 continue;
1024             }
1025 
1026             int top = ypos + lp.topMargin;
1027             if (lp.ignoreOffset) {
1028                 top -= mCollapseOffset;
1029             }
1030             final int bottom = top + child.getMeasuredHeight();
1031 
1032             final int childWidth = child.getMeasuredWidth();
1033             final int widthAvailable = rightEdge - leftEdge;
1034             final int left = leftEdge + (widthAvailable - childWidth) / 2;
1035             final int right = left + childWidth;
1036 
1037             child.layout(left, top, right, bottom);
1038 
1039             ypos = bottom + lp.bottomMargin;
1040         }
1041 
1042         if (mScrollIndicatorDrawable != null) {
1043             if (indicatorHost != null) {
1044                 final int left = indicatorHost.getLeft();
1045                 final int right = indicatorHost.getRight();
1046                 final int bottom = indicatorHost.getTop();
1047                 final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
1048                 mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
1049                 setWillNotDraw(!isCollapsed());
1050             } else {
1051                 mScrollIndicatorDrawable = null;
1052                 setWillNotDraw(true);
1053             }
1054         }
1055     }
1056 
1057     @Override
1058     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
1059         return new LayoutParams(getContext(), attrs);
1060     }
1061 
1062     @Override
1063     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
1064         if (p instanceof LayoutParams) {
1065             return new LayoutParams((LayoutParams) p);
1066         } else if (p instanceof MarginLayoutParams) {
1067             return new LayoutParams((MarginLayoutParams) p);
1068         }
1069         return new LayoutParams(p);
1070     }
1071 
1072     @Override
1073     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
1074         return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
1075     }
1076 
1077     @Override
1078     protected Parcelable onSaveInstanceState() {
1079         final SavedState ss = new SavedState(super.onSaveInstanceState());
1080         ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
1081         ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved;
1082         return ss;
1083     }
1084 
1085     @Override
1086     protected void onRestoreInstanceState(Parcelable state) {
1087         final SavedState ss = (SavedState) state;
1088         super.onRestoreInstanceState(ss.getSuperState());
1089         mOpenOnLayout = ss.open;
1090         mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
1091     }
1092 
1093     public static class LayoutParams extends MarginLayoutParams {
1094         public boolean alwaysShow;
1095         public boolean ignoreOffset;
1096         public boolean hasNestedScrollIndicator;
1097         public int maxHeight;
1098 
1099         public LayoutParams(Context c, AttributeSet attrs) {
1100             super(c, attrs);
1101 
1102             final TypedArray a = c.obtainStyledAttributes(attrs,
1103                     R.styleable.ResolverDrawerLayout_LayoutParams);
1104             alwaysShow = a.getBoolean(
1105                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow,
1106                     false);
1107             ignoreOffset = a.getBoolean(
1108                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset,
1109                     false);
1110             hasNestedScrollIndicator = a.getBoolean(
1111                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator,
1112                     false);
1113             maxHeight = a.getDimensionPixelSize(
1114                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1);
1115             a.recycle();
1116         }
1117 
1118         public LayoutParams(int width, int height) {
1119             super(width, height);
1120         }
1121 
1122         public LayoutParams(LayoutParams source) {
1123             super(source);
1124             this.alwaysShow = source.alwaysShow;
1125             this.ignoreOffset = source.ignoreOffset;
1126             this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
1127             this.maxHeight = source.maxHeight;
1128         }
1129 
1130         public LayoutParams(MarginLayoutParams source) {
1131             super(source);
1132         }
1133 
1134         public LayoutParams(ViewGroup.LayoutParams source) {
1135             super(source);
1136         }
1137     }
1138 
1139     static class SavedState extends BaseSavedState {
1140         boolean open;
1141         private int mCollapsibleHeightReserved;
1142 
1143         SavedState(Parcelable superState) {
1144             super(superState);
1145         }
1146 
1147         private SavedState(Parcel in) {
1148             super(in);
1149             open = in.readInt() != 0;
1150             mCollapsibleHeightReserved = in.readInt();
1151         }
1152 
1153         @Override
1154         public void writeToParcel(Parcel out, int flags) {
1155             super.writeToParcel(out, flags);
1156             out.writeInt(open ? 1 : 0);
1157             out.writeInt(mCollapsibleHeightReserved);
1158         }
1159 
1160         public static final Parcelable.Creator<SavedState> CREATOR =
1161                 new Parcelable.Creator<SavedState>() {
1162             @Override
1163             public SavedState createFromParcel(Parcel in) {
1164                 return new SavedState(in);
1165             }
1166 
1167             @Override
1168             public SavedState[] newArray(int size) {
1169                 return new SavedState[size];
1170             }
1171         };
1172     }
1173 
1174     /**
1175      * Listener for sheet dismissed events.
1176      */
1177     public interface OnDismissedListener {
1178         /**
1179          * Callback when the sheet is dismissed by the user.
1180          */
1181         void onDismissed();
1182     }
1183 
1184     /**
1185      * Listener for sheet collapsed / expanded events.
1186      */
1187     public interface OnCollapsedChangedListener {
1188         /**
1189          * Callback when the sheet is either fully expanded or collapsed.
1190          * @param isCollapsed true when collapsed, false when expanded.
1191          */
1192         void onCollapsedChanged(boolean isCollapsed);
1193     }
1194 
1195     private class RunOnDismissedListener implements Runnable {
1196         @Override
1197         public void run() {
1198             dispatchOnDismissed();
1199         }
1200     }
1201 
1202     private MetricsLogger getMetricsLogger() {
1203         if (mMetricsLogger == null) {
1204             mMetricsLogger = new MetricsLogger();
1205         }
1206         return mMetricsLogger;
1207     }
1208 }
1209