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