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