• 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.intentresolver.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 androidx.recyclerview.widget.RecyclerView;
49 
50 import com.android.intentresolver.R;
51 import com.android.internal.logging.MetricsLogger;
52 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
53 
54 public class ResolverDrawerLayout extends ViewGroup {
55     private static final String TAG = "ResolverDrawerLayout";
56     private MetricsLogger mMetricsLogger;
57 
58     /**
59      * Max width of the whole drawer layout
60      */
61     private final int mMaxWidth;
62 
63     /**
64      * Max total visible height of views not marked always-show when in the closed/initial state
65      */
66     private int mMaxCollapsedHeight;
67 
68     /**
69      * Max total visible height of views not marked always-show when in the closed/initial state
70      * when a default option is present
71      */
72     private int mMaxCollapsedHeightSmall;
73 
74     /**
75      * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or
76      * inferred by {@code mMaxCollapsedHeight}.
77      */
78     private final boolean mIsMaxCollapsedHeightSmallExplicit;
79 
80     private boolean mSmallCollapsed;
81 
82     /**
83      * Move views down from the top by this much in px
84      */
85     private float mCollapseOffset;
86 
87     /**
88       * Track fractions of pixels from drag calculations. Without this, the view offsets get
89       * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
90       */
91     private float mDragRemainder = 0.0f;
92     private int mHeightUsed;
93     private int mCollapsibleHeight;
94     private int mAlwaysShowHeight;
95 
96     /**
97      * The height in pixels of reserved space added to the top of the collapsed UI;
98      * e.g. chooser targets
99      */
100     private int mCollapsibleHeightReserved;
101 
102     private int mTopOffset;
103     private boolean mShowAtTop;
104     @IdRes
105     private int mIgnoreOffsetTopLimitViewId = ID_NULL;
106 
107     private boolean mIsDragging;
108     private boolean mOpenOnClick;
109     private boolean mOpenOnLayout;
110     private boolean mDismissOnScrollerFinished;
111     private final int mTouchSlop;
112     private final float mMinFlingVelocity;
113     private final OverScroller mScroller;
114     private final VelocityTracker mVelocityTracker;
115 
116     private Drawable mScrollIndicatorDrawable;
117 
118     private OnDismissedListener mOnDismissedListener;
119     private RunOnDismissedListener mRunOnDismissedListener;
120     private OnCollapsedChangedListener mOnCollapsedChangedListener;
121 
122     private boolean mDismissLocked;
123 
124     private float mInitialTouchX;
125     private float mInitialTouchY;
126     private float mLastTouchY;
127     private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
128 
129     private final Rect mTempRect = new Rect();
130 
131     private AbsListView mNestedListChild;
132     private RecyclerView mNestedRecyclerChild;
133 
134     private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
135             new ViewTreeObserver.OnTouchModeChangeListener() {
136                 @Override
137                 public void onTouchModeChanged(boolean isInTouchMode) {
138                     if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
139                         smoothScrollTo(0, 0);
140                     }
141                 }
142             };
143 
ResolverDrawerLayout(Context context)144     public ResolverDrawerLayout(Context context) {
145         this(context, null);
146     }
147 
ResolverDrawerLayout(Context context, AttributeSet attrs)148     public ResolverDrawerLayout(Context context, AttributeSet attrs) {
149         this(context, attrs, 0);
150     }
151 
ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr)152     public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
153         super(context, attrs, defStyleAttr);
154 
155         final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout,
156                 defStyleAttr, 0);
157         mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_android_maxWidth, -1);
158         mMaxCollapsedHeight = a.getDimensionPixelSize(
159                 R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
160         mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
161                 R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
162                 mMaxCollapsedHeight);
163         mIsMaxCollapsedHeightSmallExplicit =
164                 a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
165         mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
166         if (a.hasValue(R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit)) {
167             mIgnoreOffsetTopLimitViewId = a.getResourceId(
168                     R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);
169         }
170         a.recycle();
171 
172         mScrollIndicatorDrawable = mContext.getDrawable(
173                 com.android.internal.R.drawable.scroll_indicator_material);
174 
175         mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context,
176                 android.R.interpolator.decelerate_quint));
177         mVelocityTracker = VelocityTracker.obtain();
178 
179         final ViewConfiguration vc = ViewConfiguration.get(context);
180         mTouchSlop = vc.getScaledTouchSlop();
181         mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
182 
183         setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
184     }
185 
186     /**
187      * Dynamically set the max collapsed height. Note this also updates the small collapsed
188      * height if it wasn't specified explicitly.
189      */
setMaxCollapsedHeight(int heightInPixels)190     public void setMaxCollapsedHeight(int heightInPixels) {
191         if (heightInPixels == mMaxCollapsedHeight) {
192             return;
193         }
194         mMaxCollapsedHeight = heightInPixels;
195         if (!mIsMaxCollapsedHeightSmallExplicit) {
196             mMaxCollapsedHeightSmall = mMaxCollapsedHeight;
197         }
198         requestLayout();
199     }
200 
setSmallCollapsed(boolean smallCollapsed)201     public void setSmallCollapsed(boolean smallCollapsed) {
202         if (mSmallCollapsed != smallCollapsed) {
203             mSmallCollapsed = smallCollapsed;
204             requestLayout();
205         }
206     }
207 
isSmallCollapsed()208     public boolean isSmallCollapsed() {
209         return mSmallCollapsed;
210     }
211 
isCollapsed()212     public boolean isCollapsed() {
213         return mCollapseOffset > 0;
214     }
215 
setShowAtTop(boolean showOnTop)216     public void setShowAtTop(boolean showOnTop) {
217         if (mShowAtTop != showOnTop) {
218             mShowAtTop = showOnTop;
219             requestLayout();
220         }
221     }
222 
getShowAtTop()223     public boolean getShowAtTop() {
224         return mShowAtTop;
225     }
226 
setCollapsed(boolean collapsed)227     public void setCollapsed(boolean collapsed) {
228         if (!isLaidOut()) {
229             mOpenOnLayout = !collapsed;
230         } else {
231             smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
232         }
233     }
234 
setCollapsibleHeightReserved(int heightPixels)235     public void setCollapsibleHeightReserved(int heightPixels) {
236         final int oldReserved = mCollapsibleHeightReserved;
237         mCollapsibleHeightReserved = heightPixels;
238         if (oldReserved != mCollapsibleHeightReserved) {
239             requestLayout();
240         }
241 
242         final int dReserved = mCollapsibleHeightReserved - oldReserved;
243         if (dReserved != 0 && mIsDragging) {
244             mLastTouchY -= dReserved;
245         }
246 
247         final int oldCollapsibleHeight = updateCollapsibleHeight();
248         if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
249             return;
250         }
251 
252         invalidate();
253     }
254 
setDismissLocked(boolean locked)255     public void setDismissLocked(boolean locked) {
256         mDismissLocked = locked;
257     }
258 
isMoving()259     private boolean isMoving() {
260         return mIsDragging || !mScroller.isFinished();
261     }
262 
isDragging()263     private boolean isDragging() {
264         return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
265     }
266 
updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed)267     private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
268         if (oldCollapsibleHeight == mCollapsibleHeight) {
269             return false;
270         }
271 
272         if (getShowAtTop()) {
273             // Keep the drawer fully open.
274             setCollapseOffset(0);
275             return false;
276         }
277 
278         if (isLaidOut()) {
279             final boolean isCollapsedOld = mCollapseOffset != 0;
280             if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
281                     && mCollapseOffset == oldCollapsibleHeight)) {
282                 // Stay closed even at the new height.
283                 setCollapseOffset(mCollapsibleHeight);
284             } else {
285                 setCollapseOffset(Math.min(mCollapseOffset, mCollapsibleHeight));
286             }
287             final boolean isCollapsedNew = mCollapseOffset != 0;
288             if (isCollapsedOld != isCollapsedNew) {
289                 onCollapsedChanged(isCollapsedNew);
290             }
291         } else {
292             // Start out collapsed at first unless we restored state for otherwise
293             setCollapseOffset(mOpenOnLayout ? 0 : mCollapsibleHeight);
294         }
295         return true;
296     }
297 
setCollapseOffset(float collapseOffset)298     private void setCollapseOffset(float collapseOffset) {
299         if (mCollapseOffset != collapseOffset) {
300             mCollapseOffset = collapseOffset;
301             requestLayout();
302         }
303     }
304 
getMaxCollapsedHeight()305     private int getMaxCollapsedHeight() {
306         return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
307                 + mCollapsibleHeightReserved;
308     }
309 
setOnDismissedListener(OnDismissedListener listener)310     public void setOnDismissedListener(OnDismissedListener listener) {
311         mOnDismissedListener = listener;
312     }
313 
isDismissable()314     private boolean isDismissable() {
315         return mOnDismissedListener != null && !mDismissLocked;
316     }
317 
setOnCollapsedChangedListener(OnCollapsedChangedListener listener)318     public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
319         mOnCollapsedChangedListener = listener;
320     }
321 
322     @Override
onInterceptTouchEvent(MotionEvent ev)323     public boolean onInterceptTouchEvent(MotionEvent ev) {
324         final int action = ev.getActionMasked();
325 
326         if (action == MotionEvent.ACTION_DOWN) {
327             mVelocityTracker.clear();
328         }
329 
330         mVelocityTracker.addMovement(ev);
331 
332         switch (action) {
333             case MotionEvent.ACTION_DOWN: {
334                 final float x = ev.getX();
335                 final float y = ev.getY();
336                 mInitialTouchX = x;
337                 mInitialTouchY = mLastTouchY = y;
338                 mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
339             }
340             break;
341 
342             case MotionEvent.ACTION_MOVE: {
343                 final float x = ev.getX();
344                 final float y = ev.getY();
345                 final float dy = y - mInitialTouchY;
346                 if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null &&
347                         (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
348                     mActivePointerId = ev.getPointerId(0);
349                     mIsDragging = true;
350                     mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
351                             Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
352                 }
353             }
354             break;
355 
356             case MotionEvent.ACTION_POINTER_UP: {
357                 onSecondaryPointerUp(ev);
358             }
359             break;
360 
361             case MotionEvent.ACTION_CANCEL:
362             case MotionEvent.ACTION_UP: {
363                 resetTouch();
364             }
365             break;
366         }
367 
368         if (mIsDragging) {
369             abortAnimation();
370         }
371         return mIsDragging || mOpenOnClick;
372     }
373 
isNestedListChildScrolled()374     private boolean isNestedListChildScrolled() {
375         return  mNestedListChild != null
376                 && mNestedListChild.getChildCount() > 0
377                 && (mNestedListChild.getFirstVisiblePosition() > 0
378                         || mNestedListChild.getChildAt(0).getTop() < 0);
379     }
380 
isNestedRecyclerChildScrolled()381     private boolean isNestedRecyclerChildScrolled() {
382         if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) {
383             final RecyclerView.ViewHolder vh =
384                     mNestedRecyclerChild.findViewHolderForAdapterPosition(0);
385             return vh == null || vh.itemView.getTop() < 0;
386         }
387         return false;
388     }
389 
390     @Override
onTouchEvent(MotionEvent ev)391     public boolean onTouchEvent(MotionEvent ev) {
392         final int action = ev.getActionMasked();
393 
394         mVelocityTracker.addMovement(ev);
395 
396         boolean handled = false;
397         switch (action) {
398             case MotionEvent.ACTION_DOWN: {
399                 final float x = ev.getX();
400                 final float y = ev.getY();
401                 mInitialTouchX = x;
402                 mInitialTouchY = mLastTouchY = y;
403                 mActivePointerId = ev.getPointerId(0);
404                 final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
405                 handled = isDismissable() || mCollapsibleHeight > 0;
406                 mIsDragging = hitView && handled;
407                 abortAnimation();
408             }
409             break;
410 
411             case MotionEvent.ACTION_MOVE: {
412                 int index = ev.findPointerIndex(mActivePointerId);
413                 if (index < 0) {
414                     Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
415                     index = 0;
416                     mActivePointerId = ev.getPointerId(0);
417                     mInitialTouchX = ev.getX();
418                     mInitialTouchY = mLastTouchY = ev.getY();
419                 }
420                 final float x = ev.getX(index);
421                 final float y = ev.getY(index);
422                 if (!mIsDragging) {
423                     final float dy = y - mInitialTouchY;
424                     if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
425                         handled = mIsDragging = true;
426                         mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
427                                 Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
428                     }
429                 }
430                 if (mIsDragging) {
431                     final float dy = y - mLastTouchY;
432                     if (dy > 0 && isNestedListChildScrolled()) {
433                         mNestedListChild.smoothScrollBy((int) -dy, 0);
434                     } else if (dy > 0 && isNestedRecyclerChildScrolled()) {
435                         mNestedRecyclerChild.scrollBy(0, (int) -dy);
436                     } else {
437                         performDrag(dy);
438                     }
439                 }
440                 mLastTouchY = y;
441             }
442             break;
443 
444             case MotionEvent.ACTION_POINTER_DOWN: {
445                 final int pointerIndex = ev.getActionIndex();
446                 mActivePointerId = ev.getPointerId(pointerIndex);
447                 mInitialTouchX = ev.getX(pointerIndex);
448                 mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
449             }
450             break;
451 
452             case MotionEvent.ACTION_POINTER_UP: {
453                 onSecondaryPointerUp(ev);
454             }
455             break;
456 
457             case MotionEvent.ACTION_UP: {
458                 final boolean wasDragging = mIsDragging;
459                 mIsDragging = false;
460                 if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null &&
461                         findChildUnder(ev.getX(), ev.getY()) == null) {
462                     if (isDismissable()) {
463                         dispatchOnDismissed();
464                         resetTouch();
465                         return true;
466                     }
467                 }
468                 if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop &&
469                         Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
470                     smoothScrollTo(0, 0);
471                     return true;
472                 }
473                 mVelocityTracker.computeCurrentVelocity(1000);
474                 final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
475                 if (Math.abs(yvel) > mMinFlingVelocity) {
476                     if (getShowAtTop()) {
477                         if (isDismissable() && yvel < 0) {
478                             abortAnimation();
479                             dismiss();
480                         } else {
481                             smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
482                         }
483                     } else {
484                         if (isDismissable()
485                                 && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
486                             smoothScrollTo(mHeightUsed, yvel);
487                             mDismissOnScrollerFinished = true;
488                         } else {
489                             scrollNestedScrollableChildBackToTop();
490                             smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
491                         }
492                     }
493                 }else {
494                     smoothScrollTo(
495                             mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
496                 }
497                 resetTouch();
498             }
499             break;
500 
501             case MotionEvent.ACTION_CANCEL: {
502                 if (mIsDragging) {
503                     smoothScrollTo(
504                             mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
505                 }
506                 resetTouch();
507                 return true;
508             }
509         }
510 
511         return handled;
512     }
513 
514     /**
515      * Scroll nested scrollable child back to top if it has been scrolled.
516      */
517     public void scrollNestedScrollableChildBackToTop() {
518         if (isNestedListChildScrolled()) {
519             mNestedListChild.smoothScrollToPosition(0);
520         } else if (isNestedRecyclerChildScrolled()) {
521             mNestedRecyclerChild.smoothScrollToPosition(0);
522         }
523     }
524 
525     private void onSecondaryPointerUp(MotionEvent ev) {
526         final int pointerIndex = ev.getActionIndex();
527         final int pointerId = ev.getPointerId(pointerIndex);
528         if (pointerId == mActivePointerId) {
529             // This was our active pointer going up. Choose a new
530             // active pointer and adjust accordingly.
531             final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
532             mInitialTouchX = ev.getX(newPointerIndex);
533             mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
534             mActivePointerId = ev.getPointerId(newPointerIndex);
535         }
536     }
537 
538     private void resetTouch() {
539         mActivePointerId = MotionEvent.INVALID_POINTER_ID;
540         mIsDragging = false;
541         mOpenOnClick = false;
542         mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
543         mVelocityTracker.clear();
544     }
545 
546     private void dismiss() {
547         mRunOnDismissedListener = new RunOnDismissedListener();
548         post(mRunOnDismissedListener);
549     }
550 
551     @Override
552     public void computeScroll() {
553         super.computeScroll();
554         if (mScroller.computeScrollOffset()) {
555             final boolean keepGoing = !mScroller.isFinished();
556             performDrag(mScroller.getCurrY() - mCollapseOffset);
557             if (keepGoing) {
558                 postInvalidateOnAnimation();
559             } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
560                 dismiss();
561             }
562         }
563     }
564 
565     private void abortAnimation() {
566         mScroller.abortAnimation();
567         mRunOnDismissedListener = null;
568         mDismissOnScrollerFinished = false;
569     }
570 
571     private float performDrag(float dy) {
572         if (getShowAtTop()) {
573             return 0;
574         }
575 
576         final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, mHeightUsed));
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         // TODO: find a more suitable way to fix it.
845         //  RecyclerView started reporting `consumed` as true whenever a scrolling is enabled,
846         //  previously the value was based whether the fling can be performed in given direction
847         //  i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop method is a
848         //  workaround that restores the legacy functionality.
849         boolean shouldConsume = (Math.abs(velocityY) > mMinFlingVelocity)
850                 && (!consumed || (velocityY < 0 && isRecyclerViewAtTheTop(target)));
851         if (shouldConsume) {
852             if (getShowAtTop()) {
853                 if (isDismissable() && velocityY > 0) {
854                     abortAnimation();
855                     dismiss();
856                 } else {
857                     smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
858                 }
859             } else {
860                 if (isDismissable()
861                         && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
862                     smoothScrollTo(mHeightUsed, velocityY);
863                     mDismissOnScrollerFinished = true;
864                 } else {
865                     smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
866                 }
867             }
868             return true;
869         }
870         return false;
871     }
872 
873     private static boolean isRecyclerViewAtTheTop(View target) {
874         // TODO: there's a very similar functionality in #isNestedRecyclerChildScrolled(),
875         //  consolidate the two.
876         if (!(target instanceof RecyclerView)) {
877             return false;
878         }
879         RecyclerView recyclerView = (RecyclerView) target;
880         if (recyclerView.getChildCount() == 0) {
881             return true;
882         }
883         View firstChild = recyclerView.getChildAt(0);
884         return recyclerView.getChildAdapterPosition(firstChild) == 0
885                 && firstChild.getTop() >= recyclerView.getPaddingTop();
886     }
887 
888     private boolean performAccessibilityActionCommon(int action) {
889         switch (action) {
890             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
891             case AccessibilityNodeInfo.ACTION_EXPAND:
892             case com.android.internal.R.id.accessibilityActionScrollDown:
893                 if (mCollapseOffset != 0) {
894                     smoothScrollTo(0, 0);
895                     return true;
896                 }
897                 break;
898             case AccessibilityNodeInfo.ACTION_COLLAPSE:
899                 if (mCollapseOffset < mCollapsibleHeight) {
900                     smoothScrollTo(mCollapsibleHeight, 0);
901                     return true;
902                 }
903                 break;
904             case AccessibilityNodeInfo.ACTION_DISMISS:
905                 if ((mCollapseOffset < mHeightUsed) && isDismissable()) {
906                     smoothScrollTo(mHeightUsed, 0);
907                     mDismissOnScrollerFinished = true;
908                     return true;
909                 }
910                 break;
911         }
912 
913         return false;
914     }
915 
916     @Override
917     public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
918         if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
919             return true;
920         }
921 
922         return performAccessibilityActionCommon(action);
923     }
924 
925     @Override
926     public CharSequence getAccessibilityClassName() {
927         // Since we support scrolling, make this ViewGroup look like a
928         // ScrollView. This is kind of a hack until we have support for
929         // specifying auto-scroll behavior.
930         return android.widget.ScrollView.class.getName();
931     }
932 
933     @Override
934     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
935         super.onInitializeAccessibilityNodeInfoInternal(info);
936 
937         if (isEnabled()) {
938             if (mCollapseOffset != 0) {
939                 info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
940                 info.addAction(AccessibilityAction.ACTION_EXPAND);
941                 info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
942                 info.setScrollable(true);
943             }
944             if ((mCollapseOffset < mHeightUsed)
945                     && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) {
946                 info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
947                 info.setScrollable(true);
948             }
949             if (mCollapseOffset < mCollapsibleHeight) {
950                 info.addAction(AccessibilityAction.ACTION_COLLAPSE);
951             }
952             if (mCollapseOffset < mHeightUsed && isDismissable()) {
953                 info.addAction(AccessibilityAction.ACTION_DISMISS);
954             }
955         }
956 
957         // This view should never get accessibility focus, but it's interactive
958         // via nested scrolling, so we can't hide it completely.
959         info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
960     }
961 
962     @Override
963     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
964         if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
965             // This view should never get accessibility focus.
966             return false;
967         }
968 
969         if (super.performAccessibilityActionInternal(action, arguments)) {
970             return true;
971         }
972 
973         return performAccessibilityActionCommon(action);
974     }
975 
976     @Override
977     public void onDrawForeground(Canvas canvas) {
978         if (mScrollIndicatorDrawable != null) {
979             mScrollIndicatorDrawable.draw(canvas);
980         }
981 
982         super.onDrawForeground(canvas);
983     }
984 
985     @Override
986     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
987         final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
988         int widthSize = sourceWidth;
989         final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
990 
991         // Single-use layout; just ignore the mode and use available space.
992         // Clamp to maxWidth.
993         if (mMaxWidth >= 0) {
994             widthSize = Math.min(widthSize, mMaxWidth + getPaddingLeft() + getPaddingRight());
995         }
996 
997         final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
998         final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
999 
1000         // Currently we allot more height than is really needed so that the entirety of the
1001         // sheet may be pulled up.
1002         // TODO: Restrict the height here to be the right value.
1003         int heightUsed = 0;
1004 
1005         // Measure always-show children first.
1006         final int childCount = getChildCount();
1007         for (int i = 0; i < childCount; i++) {
1008             final View child = getChildAt(i);
1009             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1010             if (lp.alwaysShow && child.getVisibility() != GONE) {
1011                 if (lp.maxHeight != -1) {
1012                     final int remainingHeight = heightSize - heightUsed;
1013                     measureChildWithMargins(child, widthSpec, 0,
1014                             MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
1015                             lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
1016                 } else {
1017                     measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
1018                 }
1019                 heightUsed += child.getMeasuredHeight();
1020             }
1021         }
1022 
1023         mAlwaysShowHeight = heightUsed;
1024 
1025         // And now the rest.
1026         for (int i = 0; i < childCount; i++) {
1027             final View child = getChildAt(i);
1028 
1029             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1030             if (!lp.alwaysShow && child.getVisibility() != GONE) {
1031                 if (lp.maxHeight != -1) {
1032                     final int remainingHeight = heightSize - heightUsed;
1033                     measureChildWithMargins(child, widthSpec, 0,
1034                             MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
1035                             lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
1036                 } else {
1037                     measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
1038                 }
1039                 heightUsed += child.getMeasuredHeight();
1040             }
1041         }
1042 
1043         mHeightUsed = heightUsed;
1044         int oldCollapsibleHeight = updateCollapsibleHeight();
1045         updateCollapseOffset(oldCollapsibleHeight, !isDragging());
1046 
1047         if (getShowAtTop()) {
1048             mTopOffset = 0;
1049         } else {
1050             mTopOffset = Math.max(0, heightSize - mHeightUsed) + (int) mCollapseOffset;
1051         }
1052 
1053         setMeasuredDimension(sourceWidth, heightSize);
1054     }
1055 
1056     private int updateCollapsibleHeight() {
1057         final int oldCollapsibleHeight = mCollapsibleHeight;
1058         mCollapsibleHeight = Math.max(0, mHeightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
1059         return oldCollapsibleHeight;
1060     }
1061 
1062     /**
1063       * @return The space reserved by views with 'alwaysShow=true'
1064       */
1065     public int getAlwaysShowHeight() {
1066         return mAlwaysShowHeight;
1067     }
1068 
1069     @Override
1070     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1071         final int width = getWidth();
1072 
1073         View indicatorHost = null;
1074 
1075         int ypos = mTopOffset;
1076         final int leftEdge = getPaddingLeft();
1077         final int rightEdge = width - getPaddingRight();
1078         final int widthAvailable = rightEdge - leftEdge;
1079 
1080         boolean isIgnoreOffsetLimitSet = false;
1081         int ignoreOffsetLimit = 0;
1082         final int childCount = getChildCount();
1083         for (int i = 0; i < childCount; i++) {
1084             final View child = getChildAt(i);
1085             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1086             if (lp.hasNestedScrollIndicator) {
1087                 indicatorHost = child;
1088             }
1089 
1090             if (child.getVisibility() == GONE) {
1091                 continue;
1092             }
1093 
1094             if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) {
1095                 if (mIgnoreOffsetTopLimitViewId == child.getId()) {
1096                     ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
1097                     isIgnoreOffsetLimitSet = true;
1098                 }
1099             }
1100 
1101             int top = ypos + lp.topMargin;
1102             if (lp.ignoreOffset) {
1103                 if (!isDragging()) {
1104                     lp.mFixedTop = (int) (top - mCollapseOffset);
1105                 }
1106                 if (isIgnoreOffsetLimitSet) {
1107                     top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset));
1108                     ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin;
1109                 } else {
1110                     top -= mCollapseOffset;
1111                 }
1112             }
1113             final int bottom = top + child.getMeasuredHeight();
1114 
1115             final int childWidth = child.getMeasuredWidth();
1116             final int left = leftEdge + (widthAvailable - childWidth) / 2;
1117             final int right = left + childWidth;
1118 
1119             child.layout(left, top, right, bottom);
1120 
1121             ypos = bottom + lp.bottomMargin;
1122         }
1123 
1124         if (mScrollIndicatorDrawable != null) {
1125             if (indicatorHost != null) {
1126                 final int left = indicatorHost.getLeft();
1127                 final int right = indicatorHost.getRight();
1128                 final int bottom = indicatorHost.getTop();
1129                 final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
1130                 mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
1131                 setWillNotDraw(!isCollapsed());
1132             } else {
1133                 mScrollIndicatorDrawable = null;
1134                 setWillNotDraw(true);
1135             }
1136         }
1137     }
1138 
1139     @Override
1140     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
1141         return new LayoutParams(getContext(), attrs);
1142     }
1143 
1144     @Override
1145     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
1146         if (p instanceof LayoutParams) {
1147             return new LayoutParams((LayoutParams) p);
1148         } else if (p instanceof MarginLayoutParams) {
1149             return new LayoutParams((MarginLayoutParams) p);
1150         }
1151         return new LayoutParams(p);
1152     }
1153 
1154     @Override
1155     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
1156         return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
1157     }
1158 
1159     @Override
1160     protected Parcelable onSaveInstanceState() {
1161         final SavedState ss = new SavedState(super.onSaveInstanceState());
1162         ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
1163         ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved;
1164         return ss;
1165     }
1166 
1167     @Override
1168     protected void onRestoreInstanceState(Parcelable state) {
1169         final SavedState ss = (SavedState) state;
1170         super.onRestoreInstanceState(ss.getSuperState());
1171         mOpenOnLayout = ss.open;
1172         mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
1173     }
1174 
1175     private View findIgnoreOffsetLimitView() {
1176         if (mIgnoreOffsetTopLimitViewId == ID_NULL) {
1177             return null;
1178         }
1179         View v = findViewById(mIgnoreOffsetTopLimitViewId);
1180         if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) {
1181             return v;
1182         }
1183         return null;
1184     }
1185 
1186     public static class LayoutParams extends MarginLayoutParams {
1187         public boolean alwaysShow;
1188         public boolean ignoreOffset;
1189         public boolean hasNestedScrollIndicator;
1190         public int maxHeight;
1191         int mFixedTop;
1192 
1193         public LayoutParams(Context c, AttributeSet attrs) {
1194             super(c, attrs);
1195 
1196             final TypedArray a = c.obtainStyledAttributes(attrs,
1197                     R.styleable.ResolverDrawerLayout_LayoutParams);
1198             alwaysShow = a.getBoolean(
1199                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow,
1200                     false);
1201             ignoreOffset = a.getBoolean(
1202                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset,
1203                     false);
1204             hasNestedScrollIndicator = a.getBoolean(
1205                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator,
1206                     false);
1207             maxHeight = a.getDimensionPixelSize(
1208                     R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1);
1209             a.recycle();
1210         }
1211 
1212         public LayoutParams(int width, int height) {
1213             super(width, height);
1214         }
1215 
1216         public LayoutParams(LayoutParams source) {
1217             super(source);
1218             this.alwaysShow = source.alwaysShow;
1219             this.ignoreOffset = source.ignoreOffset;
1220             this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
1221             this.maxHeight = source.maxHeight;
1222         }
1223 
1224         public LayoutParams(MarginLayoutParams source) {
1225             super(source);
1226         }
1227 
1228         public LayoutParams(ViewGroup.LayoutParams source) {
1229             super(source);
1230         }
1231     }
1232 
1233     static class SavedState extends BaseSavedState {
1234         boolean open;
1235         private int mCollapsibleHeightReserved;
1236 
1237         SavedState(Parcelable superState) {
1238             super(superState);
1239         }
1240 
1241         private SavedState(Parcel in) {
1242             super(in);
1243             open = in.readInt() != 0;
1244             mCollapsibleHeightReserved = in.readInt();
1245         }
1246 
1247         @Override
1248         public void writeToParcel(Parcel out, int flags) {
1249             super.writeToParcel(out, flags);
1250             out.writeInt(open ? 1 : 0);
1251             out.writeInt(mCollapsibleHeightReserved);
1252         }
1253 
1254         public static final Parcelable.Creator<SavedState> CREATOR =
1255                 new Parcelable.Creator<SavedState>() {
1256             @Override
1257             public SavedState createFromParcel(Parcel in) {
1258                 return new SavedState(in);
1259             }
1260 
1261             @Override
1262             public SavedState[] newArray(int size) {
1263                 return new SavedState[size];
1264             }
1265         };
1266     }
1267 
1268     /**
1269      * Listener for sheet dismissed events.
1270      */
1271     public interface OnDismissedListener {
1272         /**
1273          * Callback when the sheet is dismissed by the user.
1274          */
1275         void onDismissed();
1276     }
1277 
1278     /**
1279      * Listener for sheet collapsed / expanded events.
1280      */
1281     public interface OnCollapsedChangedListener {
1282         /**
1283          * Callback when the sheet is either fully expanded or collapsed.
1284          * @param isCollapsed true when collapsed, false when expanded.
1285          */
1286         void onCollapsedChanged(boolean isCollapsed);
1287     }
1288 
1289     private class RunOnDismissedListener implements Runnable {
1290         @Override
1291         public void run() {
1292             dispatchOnDismissed();
1293         }
1294     }
1295 
1296     private MetricsLogger getMetricsLogger() {
1297         if (mMetricsLogger == null) {
1298             mMetricsLogger = new MetricsLogger();
1299         }
1300         return mMetricsLogger;
1301     }
1302 }
1303