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