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