SlidingDrawerpublic class SlidingDrawer extends android.view.ViewGroup SlidingDrawer hides content out of the screen and allows the user to drag a handle
to bring the content on screen. SlidingDrawer can be used vertically or horizontally.
A special widget composed of two children views: the handle, that the users drags,
and the content, attached to the handle and dragged with it.
SlidingDrawer should be used as an overlay inside layouts. This means SlidingDrawer
should only be used inside of a FrameLayout or a RelativeLayout for instance. The
size of the SlidingDrawer defines how much space the content will occupy once slid
out so SlidingDrawer should usually use match_parent for both its dimensions.
Inside an XML layout, SlidingDrawer must define the id of the handle and of the
content:
<SlidingDrawer
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:handle="@+id/handle"
android:content="@+id/content">
<ImageView
android:id="@id/handle"
android:layout_width="88dip"
android:layout_height="44dip" />
<GridView
android:id="@id/content"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</SlidingDrawer>
|
Fields Summary |
---|
public static final int | ORIENTATION_HORIZONTAL | public static final int | ORIENTATION_VERTICAL | private static final int | TAP_THRESHOLD | private static final float | MAXIMUM_TAP_VELOCITY | private static final float | MAXIMUM_MINOR_VELOCITY | private static final float | MAXIMUM_MAJOR_VELOCITY | private static final float | MAXIMUM_ACCELERATION | private static final int | VELOCITY_UNITS | private static final int | MSG_ANIMATE | private static final int | ANIMATION_FRAME_DURATION | private static final int | EXPANDED_FULL_OPEN | private static final int | COLLAPSED_FULL_CLOSED | private final int | mHandleId | private final int | mContentId | private android.view.View | mHandle | private android.view.View | mContent | private final android.graphics.Rect | mFrame | private final android.graphics.Rect | mInvalidate | private boolean | mTracking | private boolean | mLocked | private android.view.VelocityTracker | mVelocityTracker | private boolean | mVertical | private boolean | mExpanded | private int | mBottomOffset | private int | mTopOffset | private int | mHandleHeight | private int | mHandleWidth | private OnDrawerOpenListener | mOnDrawerOpenListener | private OnDrawerCloseListener | mOnDrawerCloseListener | private OnDrawerScrollListener | mOnDrawerScrollListener | private final android.os.Handler | mHandler | private float | mAnimatedAcceleration | private float | mAnimatedVelocity | private float | mAnimationPosition | private long | mAnimationLastTime | private long | mCurrentAnimationTime | private int | mTouchDelta | private boolean | mAnimating | private boolean | mAllowSingleTap | private boolean | mAnimateOnClick | private final int | mTapThreshold | private final int | mMaximumTapVelocity | private final int | mMaximumMinorVelocity | private final int | mMaximumMajorVelocity | private final int | mMaximumAcceleration | private final int | mVelocityUnits |
Constructors Summary |
---|
public SlidingDrawer(android.content.Context context, android.util.AttributeSet attrs)Creates a new SlidingDrawer from a specified set of attributes defined in XML.
this(context, attrs, 0);
| public SlidingDrawer(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr)Creates a new SlidingDrawer from a specified set of attributes defined in XML.
this(context, attrs, defStyleAttr, 0);
| public SlidingDrawer(android.content.Context context, android.util.AttributeSet attrs, int defStyleAttr, int defStyleRes)Creates a new SlidingDrawer from a specified set of attributes defined in XML.
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.SlidingDrawer, defStyleAttr, defStyleRes);
int orientation = a.getInt(R.styleable.SlidingDrawer_orientation, ORIENTATION_VERTICAL);
mVertical = orientation == ORIENTATION_VERTICAL;
mBottomOffset = (int) a.getDimension(R.styleable.SlidingDrawer_bottomOffset, 0.0f);
mTopOffset = (int) a.getDimension(R.styleable.SlidingDrawer_topOffset, 0.0f);
mAllowSingleTap = a.getBoolean(R.styleable.SlidingDrawer_allowSingleTap, true);
mAnimateOnClick = a.getBoolean(R.styleable.SlidingDrawer_animateOnClick, true);
int handleId = a.getResourceId(R.styleable.SlidingDrawer_handle, 0);
if (handleId == 0) {
throw new IllegalArgumentException("The handle attribute is required and must refer "
+ "to a valid child.");
}
int contentId = a.getResourceId(R.styleable.SlidingDrawer_content, 0);
if (contentId == 0) {
throw new IllegalArgumentException("The content attribute is required and must refer "
+ "to a valid child.");
}
if (handleId == contentId) {
throw new IllegalArgumentException("The content and handle attributes must refer "
+ "to different children.");
}
mHandleId = handleId;
mContentId = contentId;
final float density = getResources().getDisplayMetrics().density;
mTapThreshold = (int) (TAP_THRESHOLD * density + 0.5f);
mMaximumTapVelocity = (int) (MAXIMUM_TAP_VELOCITY * density + 0.5f);
mMaximumMinorVelocity = (int) (MAXIMUM_MINOR_VELOCITY * density + 0.5f);
mMaximumMajorVelocity = (int) (MAXIMUM_MAJOR_VELOCITY * density + 0.5f);
mMaximumAcceleration = (int) (MAXIMUM_ACCELERATION * density + 0.5f);
mVelocityUnits = (int) (VELOCITY_UNITS * density + 0.5f);
a.recycle();
setAlwaysDrawnWithCacheEnabled(false);
|
Methods Summary |
---|
private void | animateClose(int position)
prepareTracking(position);
performFling(position, mMaximumAcceleration, true);
| public void | animateClose()Closes the drawer with an animation.
prepareContent();
final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
if (scrollListener != null) {
scrollListener.onScrollStarted();
}
animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft());
if (scrollListener != null) {
scrollListener.onScrollEnded();
}
| private void | animateOpen(int position)
prepareTracking(position);
performFling(position, -mMaximumAcceleration, true);
| public void | animateOpen()Opens the drawer with an animation.
prepareContent();
final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
if (scrollListener != null) {
scrollListener.onScrollStarted();
}
animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft());
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
if (scrollListener != null) {
scrollListener.onScrollEnded();
}
| public void | animateToggle()Toggles the drawer open and close with an animation.
if (!mExpanded) {
animateOpen();
} else {
animateClose();
}
| public void | close()Closes the drawer immediately.
closeDrawer();
invalidate();
requestLayout();
| private void | closeDrawer()
moveHandle(COLLAPSED_FULL_CLOSED);
mContent.setVisibility(View.GONE);
mContent.destroyDrawingCache();
if (!mExpanded) {
return;
}
mExpanded = false;
if (mOnDrawerCloseListener != null) {
mOnDrawerCloseListener.onDrawerClosed();
}
| protected void | dispatchDraw(android.graphics.Canvas canvas)
final long drawingTime = getDrawingTime();
final View handle = mHandle;
final boolean isVertical = mVertical;
drawChild(canvas, handle, drawingTime);
if (mTracking || mAnimating) {
final Bitmap cache = mContent.getDrawingCache();
if (cache != null) {
if (isVertical) {
canvas.drawBitmap(cache, 0, handle.getBottom(), null);
} else {
canvas.drawBitmap(cache, handle.getRight(), 0, null);
}
} else {
canvas.save();
canvas.translate(isVertical ? 0 : handle.getLeft() - mTopOffset,
isVertical ? handle.getTop() - mTopOffset : 0);
drawChild(canvas, mContent, drawingTime);
canvas.restore();
}
} else if (mExpanded) {
drawChild(canvas, mContent, drawingTime);
}
| private void | doAnimation()
if (mAnimating) {
incrementAnimation();
if (mAnimationPosition >= mBottomOffset + (mVertical ? getHeight() : getWidth()) - 1) {
mAnimating = false;
closeDrawer();
} else if (mAnimationPosition < mTopOffset) {
mAnimating = false;
openDrawer();
} else {
moveHandle((int) mAnimationPosition);
mCurrentAnimationTime += ANIMATION_FRAME_DURATION;
mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE),
mCurrentAnimationTime);
}
}
| public android.view.View | getContent()Returns the content of the drawer.
return mContent;
| public android.view.View | getHandle()Returns the handle of the drawer.
return mHandle;
| private void | incrementAnimation()
long now = SystemClock.uptimeMillis();
float t = (now - mAnimationLastTime) / 1000.0f; // ms -> s
final float position = mAnimationPosition;
final float v = mAnimatedVelocity; // px/s
final float a = mAnimatedAcceleration; // px/s/s
mAnimationPosition = position + (v * t) + (0.5f * a * t * t); // px
mAnimatedVelocity = v + (a * t); // px/s
mAnimationLastTime = now; // ms
| public boolean | isMoving()Indicates whether the drawer is scrolling or flinging.
return mTracking || mAnimating;
| public boolean | isOpened()Indicates whether the drawer is currently fully opened.
return mExpanded;
| public void | lock()Locks the SlidingDrawer so that touch events are ignores.
mLocked = true;
| private void | moveHandle(int position)
final View handle = mHandle;
if (mVertical) {
if (position == EXPANDED_FULL_OPEN) {
handle.offsetTopAndBottom(mTopOffset - handle.getTop());
invalidate();
} else if (position == COLLAPSED_FULL_CLOSED) {
handle.offsetTopAndBottom(mBottomOffset + mBottom - mTop -
mHandleHeight - handle.getTop());
invalidate();
} else {
final int top = handle.getTop();
int deltaY = position - top;
if (position < mTopOffset) {
deltaY = mTopOffset - top;
} else if (deltaY > mBottomOffset + mBottom - mTop - mHandleHeight - top) {
deltaY = mBottomOffset + mBottom - mTop - mHandleHeight - top;
}
handle.offsetTopAndBottom(deltaY);
final Rect frame = mFrame;
final Rect region = mInvalidate;
handle.getHitRect(frame);
region.set(frame);
region.union(frame.left, frame.top - deltaY, frame.right, frame.bottom - deltaY);
region.union(0, frame.bottom - deltaY, getWidth(),
frame.bottom - deltaY + mContent.getHeight());
invalidate(region);
}
} else {
if (position == EXPANDED_FULL_OPEN) {
handle.offsetLeftAndRight(mTopOffset - handle.getLeft());
invalidate();
} else if (position == COLLAPSED_FULL_CLOSED) {
handle.offsetLeftAndRight(mBottomOffset + mRight - mLeft -
mHandleWidth - handle.getLeft());
invalidate();
} else {
final int left = handle.getLeft();
int deltaX = position - left;
if (position < mTopOffset) {
deltaX = mTopOffset - left;
} else if (deltaX > mBottomOffset + mRight - mLeft - mHandleWidth - left) {
deltaX = mBottomOffset + mRight - mLeft - mHandleWidth - left;
}
handle.offsetLeftAndRight(deltaX);
final Rect frame = mFrame;
final Rect region = mInvalidate;
handle.getHitRect(frame);
region.set(frame);
region.union(frame.left - deltaX, frame.top, frame.right - deltaX, frame.bottom);
region.union(frame.right - deltaX, 0,
frame.right - deltaX + mContent.getWidth(), getHeight());
invalidate(region);
}
}
| protected void | onFinishInflate()
mHandle = findViewById(mHandleId);
if (mHandle == null) {
throw new IllegalArgumentException("The handle attribute is must refer to an"
+ " existing child.");
}
mHandle.setOnClickListener(new DrawerToggler());
mContent = findViewById(mContentId);
if (mContent == null) {
throw new IllegalArgumentException("The content attribute is must refer to an"
+ " existing child.");
}
mContent.setVisibility(View.GONE);
| public void | onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent event)
super.onInitializeAccessibilityEvent(event);
event.setClassName(SlidingDrawer.class.getName());
| public void | onInitializeAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo info)
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(SlidingDrawer.class.getName());
| public boolean | onInterceptTouchEvent(android.view.MotionEvent event)
if (mLocked) {
return false;
}
final int action = event.getAction();
float x = event.getX();
float y = event.getY();
final Rect frame = mFrame;
final View handle = mHandle;
handle.getHitRect(frame);
if (!mTracking && !frame.contains((int) x, (int) y)) {
return false;
}
if (action == MotionEvent.ACTION_DOWN) {
mTracking = true;
handle.setPressed(true);
// Must be called before prepareTracking()
prepareContent();
// Must be called after prepareContent()
if (mOnDrawerScrollListener != null) {
mOnDrawerScrollListener.onScrollStarted();
}
if (mVertical) {
final int top = mHandle.getTop();
mTouchDelta = (int) y - top;
prepareTracking(top);
} else {
final int left = mHandle.getLeft();
mTouchDelta = (int) x - left;
prepareTracking(left);
}
mVelocityTracker.addMovement(event);
}
return true;
| protected void | onLayout(boolean changed, int l, int t, int r, int b)
if (mTracking) {
return;
}
final int width = r - l;
final int height = b - t;
final View handle = mHandle;
int childWidth = handle.getMeasuredWidth();
int childHeight = handle.getMeasuredHeight();
int childLeft;
int childTop;
final View content = mContent;
if (mVertical) {
childLeft = (width - childWidth) / 2;
childTop = mExpanded ? mTopOffset : height - childHeight + mBottomOffset;
content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(),
mTopOffset + childHeight + content.getMeasuredHeight());
} else {
childLeft = mExpanded ? mTopOffset : width - childWidth + mBottomOffset;
childTop = (height - childHeight) / 2;
content.layout(mTopOffset + childWidth, 0,
mTopOffset + childWidth + content.getMeasuredWidth(),
content.getMeasuredHeight());
}
handle.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
mHandleHeight = handle.getHeight();
mHandleWidth = handle.getWidth();
| protected void | onMeasure(int widthMeasureSpec, int heightMeasureSpec)
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
throw new RuntimeException("SlidingDrawer cannot have UNSPECIFIED dimensions");
}
final View handle = mHandle;
measureChild(handle, widthMeasureSpec, heightMeasureSpec);
if (mVertical) {
int height = heightSpecSize - handle.getMeasuredHeight() - mTopOffset;
mContent.measure(MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
} else {
int width = widthSpecSize - handle.getMeasuredWidth() - mTopOffset;
mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(heightSpecSize, MeasureSpec.EXACTLY));
}
setMeasuredDimension(widthSpecSize, heightSpecSize);
| public boolean | onTouchEvent(android.view.MotionEvent event)
if (mLocked) {
return true;
}
if (mTracking) {
mVelocityTracker.addMovement(event);
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_MOVE:
moveHandle((int) (mVertical ? event.getY() : event.getX()) - mTouchDelta);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(mVelocityUnits);
float yVelocity = velocityTracker.getYVelocity();
float xVelocity = velocityTracker.getXVelocity();
boolean negative;
final boolean vertical = mVertical;
if (vertical) {
negative = yVelocity < 0;
if (xVelocity < 0) {
xVelocity = -xVelocity;
}
if (xVelocity > mMaximumMinorVelocity) {
xVelocity = mMaximumMinorVelocity;
}
} else {
negative = xVelocity < 0;
if (yVelocity < 0) {
yVelocity = -yVelocity;
}
if (yVelocity > mMaximumMinorVelocity) {
yVelocity = mMaximumMinorVelocity;
}
}
float velocity = (float) Math.hypot(xVelocity, yVelocity);
if (negative) {
velocity = -velocity;
}
final int top = mHandle.getTop();
final int left = mHandle.getLeft();
if (Math.abs(velocity) < mMaximumTapVelocity) {
if (vertical ? (mExpanded && top < mTapThreshold + mTopOffset) ||
(!mExpanded && top > mBottomOffset + mBottom - mTop -
mHandleHeight - mTapThreshold) :
(mExpanded && left < mTapThreshold + mTopOffset) ||
(!mExpanded && left > mBottomOffset + mRight - mLeft -
mHandleWidth - mTapThreshold)) {
if (mAllowSingleTap) {
playSoundEffect(SoundEffectConstants.CLICK);
if (mExpanded) {
animateClose(vertical ? top : left);
} else {
animateOpen(vertical ? top : left);
}
} else {
performFling(vertical ? top : left, velocity, false);
}
} else {
performFling(vertical ? top : left, velocity, false);
}
} else {
performFling(vertical ? top : left, velocity, false);
}
}
break;
}
}
return mTracking || mAnimating || super.onTouchEvent(event);
| public void | open()Opens the drawer immediately.
openDrawer();
invalidate();
requestLayout();
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
| private void | openDrawer()
moveHandle(EXPANDED_FULL_OPEN);
mContent.setVisibility(View.VISIBLE);
if (mExpanded) {
return;
}
mExpanded = true;
if (mOnDrawerOpenListener != null) {
mOnDrawerOpenListener.onDrawerOpened();
}
| private void | performFling(int position, float velocity, boolean always)
mAnimationPosition = position;
mAnimatedVelocity = velocity;
if (mExpanded) {
if (always || (velocity > mMaximumMajorVelocity ||
(position > mTopOffset + (mVertical ? mHandleHeight : mHandleWidth) &&
velocity > -mMaximumMajorVelocity))) {
// We are expanded, but they didn't move sufficiently to cause
// us to retract. Animate back to the expanded position.
mAnimatedAcceleration = mMaximumAcceleration;
if (velocity < 0) {
mAnimatedVelocity = 0;
}
} else {
// We are expanded and are now going to animate away.
mAnimatedAcceleration = -mMaximumAcceleration;
if (velocity > 0) {
mAnimatedVelocity = 0;
}
}
} else {
if (!always && (velocity > mMaximumMajorVelocity ||
(position > (mVertical ? getHeight() : getWidth()) / 2 &&
velocity > -mMaximumMajorVelocity))) {
// We are collapsed, and they moved enough to allow us to expand.
mAnimatedAcceleration = mMaximumAcceleration;
if (velocity < 0) {
mAnimatedVelocity = 0;
}
} else {
// We are collapsed, but they didn't move sufficiently to cause
// us to retract. Animate back to the collapsed position.
mAnimatedAcceleration = -mMaximumAcceleration;
if (velocity > 0) {
mAnimatedVelocity = 0;
}
}
}
long now = SystemClock.uptimeMillis();
mAnimationLastTime = now;
mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
mAnimating = true;
mHandler.removeMessages(MSG_ANIMATE);
mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_ANIMATE), mCurrentAnimationTime);
stopTracking();
| private void | prepareContent()
if (mAnimating) {
return;
}
// Something changed in the content, we need to honor the layout request
// before creating the cached bitmap
final View content = mContent;
if (content.isLayoutRequested()) {
if (mVertical) {
final int childHeight = mHandleHeight;
int height = mBottom - mTop - childHeight - mTopOffset;
content.measure(MeasureSpec.makeMeasureSpec(mRight - mLeft, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(),
mTopOffset + childHeight + content.getMeasuredHeight());
} else {
final int childWidth = mHandle.getWidth();
int width = mRight - mLeft - childWidth - mTopOffset;
content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mBottom - mTop, MeasureSpec.EXACTLY));
content.layout(childWidth + mTopOffset, 0,
mTopOffset + childWidth + content.getMeasuredWidth(),
content.getMeasuredHeight());
}
}
// Try only once... we should really loop but it's not a big deal
// if the draw was cancelled, it will only be temporary anyway
content.getViewTreeObserver().dispatchOnPreDraw();
if (!content.isHardwareAccelerated()) content.buildDrawingCache();
content.setVisibility(View.GONE);
| private void | prepareTracking(int position)
mTracking = true;
mVelocityTracker = VelocityTracker.obtain();
boolean opening = !mExpanded;
if (opening) {
mAnimatedAcceleration = mMaximumAcceleration;
mAnimatedVelocity = mMaximumMajorVelocity;
mAnimationPosition = mBottomOffset +
(mVertical ? getHeight() - mHandleHeight : getWidth() - mHandleWidth);
moveHandle((int) mAnimationPosition);
mAnimating = true;
mHandler.removeMessages(MSG_ANIMATE);
long now = SystemClock.uptimeMillis();
mAnimationLastTime = now;
mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
mAnimating = true;
} else {
if (mAnimating) {
mAnimating = false;
mHandler.removeMessages(MSG_ANIMATE);
}
moveHandle(position);
}
| public void | setOnDrawerCloseListener(android.widget.SlidingDrawer$OnDrawerCloseListener onDrawerCloseListener)Sets the listener that receives a notification when the drawer becomes close.
mOnDrawerCloseListener = onDrawerCloseListener;
| public void | setOnDrawerOpenListener(android.widget.SlidingDrawer$OnDrawerOpenListener onDrawerOpenListener)Sets the listener that receives a notification when the drawer becomes open.
mOnDrawerOpenListener = onDrawerOpenListener;
| public void | setOnDrawerScrollListener(android.widget.SlidingDrawer$OnDrawerScrollListener onDrawerScrollListener)Sets the listener that receives a notification when the drawer starts or ends
a scroll. A fling is considered as a scroll. A fling will also trigger a
drawer opened or drawer closed event.
mOnDrawerScrollListener = onDrawerScrollListener;
| private void | stopTracking()
mHandle.setPressed(false);
mTracking = false;
if (mOnDrawerScrollListener != null) {
mOnDrawerScrollListener.onScrollEnded();
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
| public void | toggle()Toggles the drawer open and close. Takes effect immediately.
if (!mExpanded) {
openDrawer();
} else {
closeDrawer();
}
invalidate();
requestLayout();
| public void | unlock()Unlocks the SlidingDrawer so that touch events are processed.
mLocked = false;
|
|