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