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