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