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