Home | History | Annotate | Download | only in widget
      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 
     18 package com.android.internal.widget;
     19 
     20 import com.android.internal.R;
     21 
     22 import android.content.Context;
     23 import android.content.res.TypedArray;
     24 import android.graphics.Canvas;
     25 import android.graphics.Color;
     26 import android.graphics.Rect;
     27 import android.graphics.drawable.ColorDrawable;
     28 import android.graphics.drawable.Drawable;
     29 import android.os.Bundle;
     30 import android.os.Parcel;
     31 import android.os.Parcelable;
     32 import android.util.AttributeSet;
     33 import android.util.Log;
     34 import android.view.MotionEvent;
     35 import android.view.VelocityTracker;
     36 import android.view.View;
     37 import android.view.ViewConfiguration;
     38 import android.view.ViewGroup;
     39 import android.view.ViewParent;
     40 import android.view.ViewTreeObserver;
     41 import android.view.accessibility.AccessibilityEvent;
     42 import android.view.accessibility.AccessibilityNodeInfo;
     43 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
     44 import android.view.animation.AnimationUtils;
     45 import android.widget.AbsListView;
     46 import android.widget.OverScroller;
     47 
     48 public 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