Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2008 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 package android.widget;
     18 
     19 import android.R;
     20 import android.content.Context;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Bitmap;
     23 import android.graphics.Canvas;
     24 import android.graphics.Rect;
     25 import android.os.Handler;
     26 import android.os.Message;
     27 import android.os.SystemClock;
     28 import android.util.AttributeSet;
     29 import android.view.MotionEvent;
     30 import android.view.SoundEffectConstants;
     31 import android.view.VelocityTracker;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.view.accessibility.AccessibilityEvent;
     35 
     36 /**
     37  * SlidingDrawer hides content out of the screen and allows the user to drag a handle
     38  * to bring the content on screen. SlidingDrawer can be used vertically or horizontally.
     39  *
     40  * A special widget composed of two children views: the handle, that the users drags,
     41  * and the content, attached to the handle and dragged with it.
     42  *
     43  * SlidingDrawer should be used as an overlay inside layouts. This means SlidingDrawer
     44  * should only be used inside of a FrameLayout or a RelativeLayout for instance. The
     45  * size of the SlidingDrawer defines how much space the content will occupy once slid
     46  * out so SlidingDrawer should usually use match_parent for both its dimensions.
     47  *
     48  * Inside an XML layout, SlidingDrawer must define the id of the handle and of the
     49  * content:
     50  *
     51  * <pre class="prettyprint">
     52  * &lt;SlidingDrawer
     53  *     android:id="@+id/drawer"
     54  *     android:layout_width="match_parent"
     55  *     android:layout_height="match_parent"
     56  *
     57  *     android:handle="@+id/handle"
     58  *     android:content="@+id/content"&gt;
     59  *
     60  *     &lt;ImageView
     61  *         android:id="@id/handle"
     62  *         android:layout_width="88dip"
     63  *         android:layout_height="44dip" /&gt;
     64  *
     65  *     &lt;GridView
     66  *         android:id="@id/content"
     67  *         android:layout_width="match_parent"
     68  *         android:layout_height="match_parent" /&gt;
     69  *
     70  * &lt;/SlidingDrawer&gt;
     71  * </pre>
     72  *
     73  * @attr ref android.R.styleable#SlidingDrawer_content
     74  * @attr ref android.R.styleable#SlidingDrawer_handle
     75  * @attr ref android.R.styleable#SlidingDrawer_topOffset
     76  * @attr ref android.R.styleable#SlidingDrawer_bottomOffset
     77  * @attr ref android.R.styleable#SlidingDrawer_orientation
     78  * @attr ref android.R.styleable#SlidingDrawer_allowSingleTap
     79  * @attr ref android.R.styleable#SlidingDrawer_animateOnClick
     80  */
     81 public class SlidingDrawer extends ViewGroup {
     82     public static final int ORIENTATION_HORIZONTAL = 0;
     83     public static final int ORIENTATION_VERTICAL = 1;
     84 
     85     private static final int TAP_THRESHOLD = 6;
     86     private static final float MAXIMUM_TAP_VELOCITY = 100.0f;
     87     private static final float MAXIMUM_MINOR_VELOCITY = 150.0f;
     88     private static final float MAXIMUM_MAJOR_VELOCITY = 200.0f;
     89     private static final float MAXIMUM_ACCELERATION = 2000.0f;
     90     private static final int VELOCITY_UNITS = 1000;
     91     private static final int MSG_ANIMATE = 1000;
     92     private static final int ANIMATION_FRAME_DURATION = 1000 / 60;
     93 
     94     private static final int EXPANDED_FULL_OPEN = -10001;
     95     private static final int COLLAPSED_FULL_CLOSED = -10002;
     96 
     97     private final int mHandleId;
     98     private final int mContentId;
     99 
    100     private View mHandle;
    101     private View mContent;
    102 
    103     private final Rect mFrame = new Rect();
    104     private final Rect mInvalidate = new Rect();
    105     private boolean mTracking;
    106     private boolean mLocked;
    107 
    108     private VelocityTracker mVelocityTracker;
    109 
    110     private boolean mVertical;
    111     private boolean mExpanded;
    112     private int mBottomOffset;
    113     private int mTopOffset;
    114     private int mHandleHeight;
    115     private int mHandleWidth;
    116 
    117     private OnDrawerOpenListener mOnDrawerOpenListener;
    118     private OnDrawerCloseListener mOnDrawerCloseListener;
    119     private OnDrawerScrollListener mOnDrawerScrollListener;
    120 
    121     private final Handler mHandler = new SlidingHandler();
    122     private float mAnimatedAcceleration;
    123     private float mAnimatedVelocity;
    124     private float mAnimationPosition;
    125     private long mAnimationLastTime;
    126     private long mCurrentAnimationTime;
    127     private int mTouchDelta;
    128     private boolean mAnimating;
    129     private boolean mAllowSingleTap;
    130     private boolean mAnimateOnClick;
    131 
    132     private final int mTapThreshold;
    133     private final int mMaximumTapVelocity;
    134     private final int mMaximumMinorVelocity;
    135     private final int mMaximumMajorVelocity;
    136     private final int mMaximumAcceleration;
    137     private final int mVelocityUnits;
    138 
    139     /**
    140      * Callback invoked when the drawer is opened.
    141      */
    142     public static interface OnDrawerOpenListener {
    143         /**
    144          * Invoked when the drawer becomes fully open.
    145          */
    146         public void onDrawerOpened();
    147     }
    148 
    149     /**
    150      * Callback invoked when the drawer is closed.
    151      */
    152     public static interface OnDrawerCloseListener {
    153         /**
    154          * Invoked when the drawer becomes fully closed.
    155          */
    156         public void onDrawerClosed();
    157     }
    158 
    159     /**
    160      * Callback invoked when the drawer is scrolled.
    161      */
    162     public static interface OnDrawerScrollListener {
    163         /**
    164          * Invoked when the user starts dragging/flinging the drawer's handle.
    165          */
    166         public void onScrollStarted();
    167 
    168         /**
    169          * Invoked when the user stops dragging/flinging the drawer's handle.
    170          */
    171         public void onScrollEnded();
    172     }
    173 
    174     /**
    175      * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
    176      *
    177      * @param context The application's environment.
    178      * @param attrs The attributes defined in XML.
    179      */
    180     public SlidingDrawer(Context context, AttributeSet attrs) {
    181         this(context, attrs, 0);
    182     }
    183 
    184     /**
    185      * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
    186      *
    187      * @param context The application's environment.
    188      * @param attrs The attributes defined in XML.
    189      * @param defStyle The style to apply to this widget.
    190      */
    191     public SlidingDrawer(Context context, AttributeSet attrs, int defStyle) {
    192         super(context, attrs, defStyle);
    193         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingDrawer, defStyle, 0);
    194 
    195         int orientation = a.getInt(R.styleable.SlidingDrawer_orientation, ORIENTATION_VERTICAL);
    196         mVertical = orientation == ORIENTATION_VERTICAL;
    197         mBottomOffset = (int) a.getDimension(R.styleable.SlidingDrawer_bottomOffset, 0.0f);
    198         mTopOffset = (int) a.getDimension(R.styleable.SlidingDrawer_topOffset, 0.0f);
    199         mAllowSingleTap = a.getBoolean(R.styleable.SlidingDrawer_allowSingleTap, true);
    200         mAnimateOnClick = a.getBoolean(R.styleable.SlidingDrawer_animateOnClick, true);
    201 
    202         int handleId = a.getResourceId(R.styleable.SlidingDrawer_handle, 0);
    203         if (handleId == 0) {
    204             throw new IllegalArgumentException("The handle attribute is required and must refer "
    205                     + "to a valid child.");
    206         }
    207 
    208         int contentId = a.getResourceId(R.styleable.SlidingDrawer_content, 0);
    209         if (contentId == 0) {
    210             throw new IllegalArgumentException("The content attribute is required and must refer "
    211                     + "to a valid child.");
    212         }
    213 
    214         if (handleId == contentId) {
    215             throw new IllegalArgumentException("The content and handle attributes must refer "
    216                     + "to different children.");
    217         }
    218 
    219         mHandleId = handleId;
    220         mContentId = contentId;
    221 
    222         final float density = getResources().getDisplayMetrics().density;
    223         mTapThreshold = (int) (TAP_THRESHOLD * density + 0.5f);
    224         mMaximumTapVelocity = (int) (MAXIMUM_TAP_VELOCITY * density + 0.5f);
    225         mMaximumMinorVelocity = (int) (MAXIMUM_MINOR_VELOCITY * density + 0.5f);
    226         mMaximumMajorVelocity = (int) (MAXIMUM_MAJOR_VELOCITY * density + 0.5f);
    227         mMaximumAcceleration = (int) (MAXIMUM_ACCELERATION * density + 0.5f);
    228         mVelocityUnits = (int) (VELOCITY_UNITS * density + 0.5f);
    229 
    230         a.recycle();
    231 
    232         setAlwaysDrawnWithCacheEnabled(false);
    233     }
    234 
    235     @Override
    236     protected void onFinishInflate() {
    237         mHandle = findViewById(mHandleId);
    238         if (mHandle == null) {
    239             throw new IllegalArgumentException("The handle attribute is must refer to an"
    240                     + " existing child.");
    241         }
    242         mHandle.setOnClickListener(new DrawerToggler());
    243 
    244         mContent = findViewById(mContentId);
    245         if (mContent == null) {
    246             throw new IllegalArgumentException("The content attribute is must refer to an"
    247                     + " existing child.");
    248         }
    249         mContent.setVisibility(View.GONE);
    250     }
    251 
    252     @Override
    253     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    254         int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    255         int widthSpecSize =  MeasureSpec.getSize(widthMeasureSpec);
    256 
    257         int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    258         int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
    259 
    260         if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
    261             throw new RuntimeException("SlidingDrawer cannot have UNSPECIFIED dimensions");
    262         }
    263 
    264         final View handle = mHandle;
    265         measureChild(handle, widthMeasureSpec, heightMeasureSpec);
    266 
    267         if (mVertical) {
    268             int height = heightSpecSize - handle.getMeasuredHeight() - mTopOffset;
    269             mContent.measure(MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.EXACTLY),
    270                     MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
    271         } else {
    272             int width = widthSpecSize - handle.getMeasuredWidth() - mTopOffset;
    273             mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
    274                     MeasureSpec.makeMeasureSpec(heightSpecSize, MeasureSpec.EXACTLY));
    275         }
    276 
    277         setMeasuredDimension(widthSpecSize, heightSpecSize);
    278     }
    279 
    280     @Override
    281     protected void dispatchDraw(Canvas canvas) {
    282         final long drawingTime = getDrawingTime();
    283         final View handle = mHandle;
    284         final boolean isVertical = mVertical;
    285 
    286         drawChild(canvas, handle, drawingTime);
    287 
    288         if (mTracking || mAnimating) {
    289             final Bitmap cache = mContent.getDrawingCache();
    290             if (cache != null) {
    291                 if (isVertical) {
    292                     canvas.drawBitmap(cache, 0, handle.getBottom(), null);
    293                 } else {
    294                     canvas.drawBitmap(cache, handle.getRight(), 0, null);
    295                 }
    296             } else {
    297                 canvas.save();
    298                 canvas.translate(isVertical ? 0 : handle.getLeft() - mTopOffset,
    299                         isVertical ? handle.getTop() - mTopOffset : 0);
    300                 drawChild(canvas, mContent, drawingTime);
    301                 canvas.restore();
    302             }
    303         } else if (mExpanded) {
    304             drawChild(canvas, mContent, drawingTime);
    305         }
    306     }
    307 
    308     @Override
    309     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    310         if (mTracking) {
    311             return;
    312         }
    313 
    314         final int width = r - l;
    315         final int height = b - t;
    316 
    317         final View handle = mHandle;
    318 
    319         int childWidth = handle.getMeasuredWidth();
    320         int childHeight = handle.getMeasuredHeight();
    321 
    322         int childLeft;
    323         int childTop;
    324 
    325         final View content = mContent;
    326 
    327         if (mVertical) {
    328             childLeft = (width - childWidth) / 2;
    329             childTop = mExpanded ? mTopOffset : height - childHeight + mBottomOffset;
    330 
    331             content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(),
    332                     mTopOffset + childHeight + content.getMeasuredHeight());
    333         } else {
    334             childLeft = mExpanded ? mTopOffset : width - childWidth + mBottomOffset;
    335             childTop = (height - childHeight) / 2;
    336 
    337             content.layout(mTopOffset + childWidth, 0,
    338                     mTopOffset + childWidth + content.getMeasuredWidth(),
    339                     content.getMeasuredHeight());
    340         }
    341 
    342         handle.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
    343         mHandleHeight = handle.getHeight();
    344         mHandleWidth = handle.getWidth();
    345     }
    346 
    347     @Override
    348     public boolean onInterceptTouchEvent(MotionEvent event) {
    349         if (mLocked) {
    350             return false;
    351         }
    352 
    353         final int action = event.getAction();
    354 
    355         float x = event.getX();
    356         float y = event.getY();
    357 
    358         final Rect frame = mFrame;
    359         final View handle = mHandle;
    360 
    361         handle.getHitRect(frame);
    362         if (!mTracking && !frame.contains((int) x, (int) y)) {
    363             return false;
    364         }
    365 
    366         if (action == MotionEvent.ACTION_DOWN) {
    367             mTracking = true;
    368 
    369             handle.setPressed(true);
    370             // Must be called before prepareTracking()
    371             prepareContent();
    372 
    373             // Must be called after prepareContent()
    374             if (mOnDrawerScrollListener != null) {
    375                 mOnDrawerScrollListener.onScrollStarted();
    376             }
    377 
    378             if (mVertical) {
    379                 final int top = mHandle.getTop();
    380                 mTouchDelta = (int) y - top;
    381                 prepareTracking(top);
    382             } else {
    383                 final int left = mHandle.getLeft();
    384                 mTouchDelta = (int) x - left;
    385                 prepareTracking(left);
    386             }
    387             mVelocityTracker.addMovement(event);
    388         }
    389 
    390         return true;
    391     }
    392 
    393     @Override
    394     public boolean onTouchEvent(MotionEvent event) {
    395         if (mLocked) {
    396             return true;
    397         }
    398 
    399         if (mTracking) {
    400             mVelocityTracker.addMovement(event);
    401             final int action = event.getAction();
    402             switch (action) {
    403                 case MotionEvent.ACTION_MOVE:
    404                     moveHandle((int) (mVertical ? event.getY() : event.getX()) - mTouchDelta);
    405                     break;
    406                 case MotionEvent.ACTION_UP:
    407                 case MotionEvent.ACTION_CANCEL: {
    408                     final VelocityTracker velocityTracker = mVelocityTracker;
    409                     velocityTracker.computeCurrentVelocity(mVelocityUnits);
    410 
    411                     float yVelocity = velocityTracker.getYVelocity();
    412                     float xVelocity = velocityTracker.getXVelocity();
    413                     boolean negative;
    414 
    415                     final boolean vertical = mVertical;
    416                     if (vertical) {
    417                         negative = yVelocity < 0;
    418                         if (xVelocity < 0) {
    419                             xVelocity = -xVelocity;
    420                         }
    421                         if (xVelocity > mMaximumMinorVelocity) {
    422                             xVelocity = mMaximumMinorVelocity;
    423                         }
    424                     } else {
    425                         negative = xVelocity < 0;
    426                         if (yVelocity < 0) {
    427                             yVelocity = -yVelocity;
    428                         }
    429                         if (yVelocity > mMaximumMinorVelocity) {
    430                             yVelocity = mMaximumMinorVelocity;
    431                         }
    432                     }
    433 
    434                     float velocity = (float) Math.hypot(xVelocity, yVelocity);
    435                     if (negative) {
    436                         velocity = -velocity;
    437                     }
    438 
    439                     final int top = mHandle.getTop();
    440                     final int left = mHandle.getLeft();
    441 
    442                     if (Math.abs(velocity) < mMaximumTapVelocity) {
    443                         if (vertical ? (mExpanded && top < mTapThreshold + mTopOffset) ||
    444                                 (!mExpanded && top > mBottomOffset + mBottom - mTop -
    445                                         mHandleHeight - mTapThreshold) :
    446                                 (mExpanded && left < mTapThreshold + mTopOffset) ||
    447                                 (!mExpanded && left > mBottomOffset + mRight - mLeft -
    448                                         mHandleWidth - mTapThreshold)) {
    449 
    450                             if (mAllowSingleTap) {
    451                                 playSoundEffect(SoundEffectConstants.CLICK);
    452 
    453                                 if (mExpanded) {
    454                                     animateClose(vertical ? top : left);
    455                                 } else {
    456                                     animateOpen(vertical ? top : left);
    457                                 }
    458                             } else {
    459                                 performFling(vertical ? top : left, velocity, false);
    460                             }
    461 
    462                         } else {
    463                             performFling(vertical ? top : left, velocity, false);
    464                         }
    465                     } else {
    466                         performFling(vertical ? top : left, velocity, false);
    467                     }
    468                 }
    469                 break;
    470             }
    471         }
    472 
    473         return mTracking || mAnimating || super.onTouchEvent(event);
    474     }
    475 
    476     private void animateClose(int position) {
    477         prepareTracking(position);
    478         performFling(position, mMaximumAcceleration, true);
    479     }
    480 
    481     private void animateOpen(int position) {
    482         prepareTracking(position);
    483         performFling(position, -mMaximumAcceleration, true);
    484     }
    485 
    486     private void performFling(int position, float velocity, boolean always) {
    487         mAnimationPosition = position;
    488         mAnimatedVelocity = velocity;
    489 
    490         if (mExpanded) {
    491             if (always || (velocity > mMaximumMajorVelocity ||
    492                     (position > mTopOffset + (mVertical ? mHandleHeight : mHandleWidth) &&
    493                             velocity > -mMaximumMajorVelocity))) {
    494                 // We are expanded, but they didn't move sufficiently to cause
    495                 // us to retract.  Animate back to the expanded position.
    496                 mAnimatedAcceleration = mMaximumAcceleration;
    497                 if (velocity < 0) {
    498                     mAnimatedVelocity = 0;
    499                 }
    500             } else {
    501                 // We are expanded and are now going to animate away.
    502                 mAnimatedAcceleration = -mMaximumAcceleration;
    503                 if (velocity > 0) {
    504                     mAnimatedVelocity = 0;
    505                 }
    506             }
    507         } else {
    508             if (!always && (velocity > mMaximumMajorVelocity ||
    509                     (position > (mVertical ? getHeight() : getWidth()) / 2 &&
    510                             velocity > -mMaximumMajorVelocity))) {
    511                 // We are collapsed, and they moved enough to allow us to expand.
    512                 mAnimatedAcceleration = mMaximumAcceleration;
    513                 if (velocity < 0) {
    514                     mAnimatedVelocity = 0;
    515                 }
    516             } else {
    517                 // We are collapsed, but they didn't move sufficiently to cause
    518                 // us to retract.  Animate back to the collapsed position.
    519                 mAnimatedAcceleration = -mMaximumAcceleration;
    520                 if (velocity > 0) {
    521                     mAnimatedVelocity = 0;
    522                 }
    523             }
    524         }
    525 
    526         long now = SystemClock.uptimeMillis();
    527         mAnimationLastTime = now;
    528         mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
    529         mAnimating = true;
    530         mHandler.removeMessages(MSG_ANIMATE);
    531         mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
    532         stopTracking();
    533     }
    534 
    535     private void prepareTracking(int position) {
    536         mTracking = true;
    537         mVelocityTracker = VelocityTracker.obtain();
    538         boolean opening = !mExpanded;
    539         if (opening) {
    540             mAnimatedAcceleration = mMaximumAcceleration;
    541             mAnimatedVelocity = mMaximumMajorVelocity;
    542             mAnimationPosition = mBottomOffset +
    543                     (mVertical ? getHeight() - mHandleHeight : getWidth() - mHandleWidth);
    544             moveHandle((int) mAnimationPosition);
    545             mAnimating = true;
    546             mHandler.removeMessages(MSG_ANIMATE);
    547             long now = SystemClock.uptimeMillis();
    548             mAnimationLastTime = now;
    549             mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
    550             mAnimating = true;
    551         } else {
    552             if (mAnimating) {
    553                 mAnimating = false;
    554                 mHandler.removeMessages(MSG_ANIMATE);
    555             }
    556             moveHandle(position);
    557         }
    558     }
    559 
    560     private void moveHandle(int position) {
    561         final View handle = mHandle;
    562 
    563         if (mVertical) {
    564             if (position == EXPANDED_FULL_OPEN) {
    565                 handle.offsetTopAndBottom(mTopOffset - handle.getTop());
    566                 invalidate();
    567             } else if (position == COLLAPSED_FULL_CLOSED) {
    568                 handle.offsetTopAndBottom(mBottomOffset + mBottom - mTop -
    569                         mHandleHeight - handle.getTop());
    570                 invalidate();
    571             } else {
    572                 final int top = handle.getTop();
    573                 int deltaY = position - top;
    574                 if (position < mTopOffset) {
    575                     deltaY = mTopOffset - top;
    576                 } else if (deltaY > mBottomOffset + mBottom - mTop - mHandleHeight - top) {
    577                     deltaY = mBottomOffset + mBottom - mTop - mHandleHeight - top;
    578                 }
    579                 handle.offsetTopAndBottom(deltaY);
    580 
    581                 final Rect frame = mFrame;
    582                 final Rect region = mInvalidate;
    583 
    584                 handle.getHitRect(frame);
    585                 region.set(frame);
    586 
    587                 region.union(frame.left, frame.top - deltaY, frame.right, frame.bottom - deltaY);
    588                 region.union(0, frame.bottom - deltaY, getWidth(),
    589                         frame.bottom - deltaY + mContent.getHeight());
    590 
    591                 invalidate(region);
    592             }
    593         } else {
    594             if (position == EXPANDED_FULL_OPEN) {
    595                 handle.offsetLeftAndRight(mTopOffset - handle.getLeft());
    596                 invalidate();
    597             } else if (position == COLLAPSED_FULL_CLOSED) {
    598                 handle.offsetLeftAndRight(mBottomOffset + mRight - mLeft -
    599                         mHandleWidth - handle.getLeft());
    600                 invalidate();
    601             } else {
    602                 final int left = handle.getLeft();
    603                 int deltaX = position - left;
    604                 if (position < mTopOffset) {
    605                     deltaX = mTopOffset - left;
    606                 } else if (deltaX > mBottomOffset + mRight - mLeft - mHandleWidth - left) {
    607                     deltaX = mBottomOffset + mRight - mLeft - mHandleWidth - left;
    608                 }
    609                 handle.offsetLeftAndRight(deltaX);
    610 
    611                 final Rect frame = mFrame;
    612                 final Rect region = mInvalidate;
    613 
    614                 handle.getHitRect(frame);
    615                 region.set(frame);
    616 
    617                 region.union(frame.left - deltaX, frame.top, frame.right - deltaX, frame.bottom);
    618                 region.union(frame.right - deltaX, 0,
    619                         frame.right - deltaX + mContent.getWidth(), getHeight());
    620 
    621                 invalidate(region);
    622             }
    623         }
    624     }
    625 
    626     private void prepareContent() {
    627         if (mAnimating) {
    628             return;
    629         }
    630 
    631         // Something changed in the content, we need to honor the layout request
    632         // before creating the cached bitmap
    633         final View content = mContent;
    634         if (content.isLayoutRequested()) {
    635             if (mVertical) {
    636                 final int childHeight = mHandleHeight;
    637                 int height = mBottom - mTop - childHeight - mTopOffset;
    638                 content.measure(MeasureSpec.makeMeasureSpec(mRight - mLeft, MeasureSpec.EXACTLY),
    639                         MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
    640                 content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(),
    641                         mTopOffset + childHeight + content.getMeasuredHeight());
    642             } else {
    643                 final int childWidth = mHandle.getWidth();
    644                 int width = mRight - mLeft - childWidth - mTopOffset;
    645                 content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
    646                         MeasureSpec.makeMeasureSpec(mBottom - mTop, MeasureSpec.EXACTLY));
    647                 content.layout(childWidth + mTopOffset, 0,
    648                         mTopOffset + childWidth + content.getMeasuredWidth(),
    649                         content.getMeasuredHeight());
    650             }
    651         }
    652         // Try only once... we should really loop but it's not a big deal
    653         // if the draw was cancelled, it will only be temporary anyway
    654         content.getViewTreeObserver().dispatchOnPreDraw();
    655         content.buildDrawingCache();
    656 
    657         content.setVisibility(View.GONE);
    658     }
    659 
    660     private void stopTracking() {
    661         mHandle.setPressed(false);
    662         mTracking = false;
    663 
    664         if (mOnDrawerScrollListener != null) {
    665             mOnDrawerScrollListener.onScrollEnded();
    666         }
    667 
    668         if (mVelocityTracker != null) {
    669             mVelocityTracker.recycle();
    670             mVelocityTracker = null;
    671         }
    672     }
    673 
    674     private void doAnimation() {
    675         if (mAnimating) {
    676             incrementAnimation();
    677             if (mAnimationPosition >= mBottomOffset + (mVertical ? getHeight() : getWidth()) - 1) {
    678                 mAnimating = false;
    679                 closeDrawer();
    680             } else if (mAnimationPosition < mTopOffset) {
    681                 mAnimating = false;
    682                 openDrawer();
    683             } else {
    684                 moveHandle((int) mAnimationPosition);
    685                 mCurrentAnimationTime += ANIMATION_FRAME_DURATION;
    686                 mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE),
    687                         mCurrentAnimationTime);
    688             }
    689         }
    690     }
    691 
    692     private void incrementAnimation() {
    693         long now = SystemClock.uptimeMillis();
    694         float t = (now - mAnimationLastTime) / 1000.0f;                   // ms -> s
    695         final float position = mAnimationPosition;
    696         final float v = mAnimatedVelocity;                                // px/s
    697         final float a = mAnimatedAcceleration;                            // px/s/s
    698         mAnimationPosition = position + (v * t) + (0.5f * a * t * t);     // px
    699         mAnimatedVelocity = v + (a * t);                                  // px/s
    700         mAnimationLastTime = now;                                         // ms
    701     }
    702 
    703     /**
    704      * Toggles the drawer open and close. Takes effect immediately.
    705      *
    706      * @see #open()
    707      * @see #close()
    708      * @see #animateClose()
    709      * @see #animateOpen()
    710      * @see #animateToggle()
    711      */
    712     public void toggle() {
    713         if (!mExpanded) {
    714             openDrawer();
    715         } else {
    716             closeDrawer();
    717         }
    718         invalidate();
    719         requestLayout();
    720     }
    721 
    722     /**
    723      * Toggles the drawer open and close with an animation.
    724      *
    725      * @see #open()
    726      * @see #close()
    727      * @see #animateClose()
    728      * @see #animateOpen()
    729      * @see #toggle()
    730      */
    731     public void animateToggle() {
    732         if (!mExpanded) {
    733             animateOpen();
    734         } else {
    735             animateClose();
    736         }
    737     }
    738 
    739     /**
    740      * Opens the drawer immediately.
    741      *
    742      * @see #toggle()
    743      * @see #close()
    744      * @see #animateOpen()
    745      */
    746     public void open() {
    747         openDrawer();
    748         invalidate();
    749         requestLayout();
    750 
    751         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
    752     }
    753 
    754     /**
    755      * Closes the drawer immediately.
    756      *
    757      * @see #toggle()
    758      * @see #open()
    759      * @see #animateClose()
    760      */
    761     public void close() {
    762         closeDrawer();
    763         invalidate();
    764         requestLayout();
    765     }
    766 
    767     /**
    768      * Closes the drawer with an animation.
    769      *
    770      * @see #close()
    771      * @see #open()
    772      * @see #animateOpen()
    773      * @see #animateToggle()
    774      * @see #toggle()
    775      */
    776     public void animateClose() {
    777         prepareContent();
    778         final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
    779         if (scrollListener != null) {
    780             scrollListener.onScrollStarted();
    781         }
    782         animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft());
    783 
    784         if (scrollListener != null) {
    785             scrollListener.onScrollEnded();
    786         }
    787     }
    788 
    789     /**
    790      * Opens the drawer with an animation.
    791      *
    792      * @see #close()
    793      * @see #open()
    794      * @see #animateClose()
    795      * @see #animateToggle()
    796      * @see #toggle()
    797      */
    798     public void animateOpen() {
    799         prepareContent();
    800         final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
    801         if (scrollListener != null) {
    802             scrollListener.onScrollStarted();
    803         }
    804         animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft());
    805 
    806         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
    807 
    808         if (scrollListener != null) {
    809             scrollListener.onScrollEnded();
    810         }
    811     }
    812 
    813     private void closeDrawer() {
    814         moveHandle(COLLAPSED_FULL_CLOSED);
    815         mContent.setVisibility(View.GONE);
    816         mContent.destroyDrawingCache();
    817 
    818         if (!mExpanded) {
    819             return;
    820         }
    821 
    822         mExpanded = false;
    823         if (mOnDrawerCloseListener != null) {
    824             mOnDrawerCloseListener.onDrawerClosed();
    825         }
    826     }
    827 
    828     private void openDrawer() {
    829         moveHandle(EXPANDED_FULL_OPEN);
    830         mContent.setVisibility(View.VISIBLE);
    831 
    832         if (mExpanded) {
    833             return;
    834         }
    835 
    836         mExpanded = true;
    837 
    838         if (mOnDrawerOpenListener != null) {
    839             mOnDrawerOpenListener.onDrawerOpened();
    840         }
    841     }
    842 
    843     /**
    844      * Sets the listener that receives a notification when the drawer becomes open.
    845      *
    846      * @param onDrawerOpenListener The listener to be notified when the drawer is opened.
    847      */
    848     public void setOnDrawerOpenListener(OnDrawerOpenListener onDrawerOpenListener) {
    849         mOnDrawerOpenListener = onDrawerOpenListener;
    850     }
    851 
    852     /**
    853      * Sets the listener that receives a notification when the drawer becomes close.
    854      *
    855      * @param onDrawerCloseListener The listener to be notified when the drawer is closed.
    856      */
    857     public void setOnDrawerCloseListener(OnDrawerCloseListener onDrawerCloseListener) {
    858         mOnDrawerCloseListener = onDrawerCloseListener;
    859     }
    860 
    861     /**
    862      * Sets the listener that receives a notification when the drawer starts or ends
    863      * a scroll. A fling is considered as a scroll. A fling will also trigger a
    864      * drawer opened or drawer closed event.
    865      *
    866      * @param onDrawerScrollListener The listener to be notified when scrolling
    867      *        starts or stops.
    868      */
    869     public void setOnDrawerScrollListener(OnDrawerScrollListener onDrawerScrollListener) {
    870         mOnDrawerScrollListener = onDrawerScrollListener;
    871     }
    872 
    873     /**
    874      * Returns the handle of the drawer.
    875      *
    876      * @return The View reprenseting the handle of the drawer, identified by
    877      *         the "handle" id in XML.
    878      */
    879     public View getHandle() {
    880         return mHandle;
    881     }
    882 
    883     /**
    884      * Returns the content of the drawer.
    885      *
    886      * @return The View reprenseting the content of the drawer, identified by
    887      *         the "content" id in XML.
    888      */
    889     public View getContent() {
    890         return mContent;
    891     }
    892 
    893     /**
    894      * Unlocks the SlidingDrawer so that touch events are processed.
    895      *
    896      * @see #lock()
    897      */
    898     public void unlock() {
    899         mLocked = false;
    900     }
    901 
    902     /**
    903      * Locks the SlidingDrawer so that touch events are ignores.
    904      *
    905      * @see #unlock()
    906      */
    907     public void lock() {
    908         mLocked = true;
    909     }
    910 
    911     /**
    912      * Indicates whether the drawer is currently fully opened.
    913      *
    914      * @return True if the drawer is opened, false otherwise.
    915      */
    916     public boolean isOpened() {
    917         return mExpanded;
    918     }
    919 
    920     /**
    921      * Indicates whether the drawer is scrolling or flinging.
    922      *
    923      * @return True if the drawer is scroller or flinging, false otherwise.
    924      */
    925     public boolean isMoving() {
    926         return mTracking || mAnimating;
    927     }
    928 
    929     private class DrawerToggler implements OnClickListener {
    930         public void onClick(View v) {
    931             if (mLocked) {
    932                 return;
    933             }
    934             // mAllowSingleTap isn't relevant here; you're *always*
    935             // allowed to open/close the drawer by clicking with the
    936             // trackball.
    937 
    938             if (mAnimateOnClick) {
    939                 animateToggle();
    940             } else {
    941                 toggle();
    942             }
    943         }
    944     }
    945 
    946     private class SlidingHandler extends Handler {
    947         public void handleMessage(Message m) {
    948             switch (m.what) {
    949                 case MSG_ANIMATE:
    950                     doAnimation();
    951                     break;
    952             }
    953         }
    954     }
    955 }
    956