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
18package com.android.internal.widget;
19
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.graphics.Rect;
23import android.os.Bundle;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.view.MotionEvent;
29import android.view.VelocityTracker;
30import android.view.View;
31import android.view.ViewConfiguration;
32import android.view.ViewGroup;
33import android.view.ViewParent;
34import android.view.ViewTreeObserver;
35import android.view.accessibility.AccessibilityEvent;
36import android.view.accessibility.AccessibilityNodeInfo;
37import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
38import android.view.animation.AnimationUtils;
39import android.widget.AbsListView;
40import android.widget.OverScroller;
41import com.android.internal.R;
42
43public 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
109    public ResolverDrawerLayout(Context context) {
110        this(context, null);
111    }
112
113    public ResolverDrawerLayout(Context context, AttributeSet attrs) {
114        this(context, attrs, 0);
115    }
116
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
141    public void setSmallCollapsed(boolean smallCollapsed) {
142        mSmallCollapsed = smallCollapsed;
143        requestLayout();
144    }
145
146    public boolean isSmallCollapsed() {
147        return mSmallCollapsed;
148    }
149
150    public boolean isCollapsed() {
151        return mCollapseOffset > 0;
152    }
153
154    public void setCollapsed(boolean collapsed) {
155        if (!isLaidOut()) {
156            mOpenOnLayout = collapsed;
157        } else {
158            smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
159        }
160    }
161
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
181    private boolean isMoving() {
182        return mIsDragging || !mScroller.isFinished();
183    }
184
185    private boolean isDragging() {
186        return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
187    }
188
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
215    private int getMaxCollapsedHeight() {
216        return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
217                + mCollapsibleHeightReserved;
218    }
219
220    public void setOnDismissedListener(OnDismissedListener listener) {
221        mOnDismissedListener = listener;
222    }
223
224    @Override
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
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