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