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