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