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