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