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