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