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