SlidingDrawerDemo.java:
package org.lee.android; import org.lee.android.ExpoInterpolator.EasingType; import org.lee.android.ExpoInterpolator.ExpoInterpolator; import org.lee.android.widget.Panel; import org.lee.android.widget.Panel.OnPanelListener; import android.app.Activity; import android.os.Bundle; import android.util.Log; public class SlidingDrawerDemo extends Activity implements OnPanelListener{ @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); super.setContentView(R.layout.sildingdrawer); Panel panel; panel = (Panel) findViewById(R.id.topPanel); panel.setOnPanelListener(this); panel.setInterpolator(new ExpoInterpolator(EasingType.OUT)); } public void onPanelClosed(Panel panel) { String panelName = getResources().getResourceEntryName(panel.getId()); Log.d("TestPanels", "Panel [" + panelName + "] closed"); } public void onPanelOpened(Panel panel) { String panelName = getResources().getResourceEntryName(panel.getId()); Log.d("TestPanels", "Panel [" + panelName + "] opened"); } }
sildingdrawer.xml:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:panel="http://schemas.android.com/apk/res/org.lee.android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#ffffff" android:orientation="vertical" > <RelativeLayout android:id="@+id/listHeader" android:layout_width="fill_parent" android:layout_height="48dp" android:layout_alignParentTop="true" android:background="#F7BAD6" > <ImageView android:id="@+id/header_back" android:layout_width="12dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_marginLeft="12px" android:background="@drawable/header_back" /> <TextView android:id="@+id/shop_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="15px" android:layout_toRightOf="@id/header_back" android:text="标题" android:textColor="#ffffff" android:textSize="28px" /> <ImageView android:id="@+id/three_rect" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="12dp" android:background="@drawable/three_rect" /> </RelativeLayout> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/listHeader" android:orientation="vertical" > <!-- tab标签栏 --> <LinearLayout android:id="@+id/header" android:layout_width="fill_parent" android:layout_height="40dp" android:background="#FFFFFF" > <TextView android:id="@+id/text1" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡1" android:textColor="#000000" android:textSize="22.0dip" /> <View android:layout_width="2dip" android:layout_height="30dip" android:layout_gravity="center_vertical" android:background="#D7D7D7" /> <TextView android:id="@+id/text2" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡2" android:textColor="#000000" android:textSize="22.0dip" /> <View android:layout_width="2dip" android:layout_height="30dip" android:layout_gravity="center_vertical" android:background="#D7D7D7" /> <TextView android:id="@+id/text3" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡3" android:textColor="#000000" android:textSize="22.0dip" /> <View android:layout_width="2dip" android:layout_height="30dip" android:layout_gravity="center_vertical" android:background="#D7D7D7" /> <TextView android:id="@+id/text4" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡4" android:textColor="#000000" android:textSize="22.0dip" /> <!-- 最右侧分类标识 --> <FrameLayout android:layout_width="50dp" android:layout_height="fill_parent" android:layout_marginRight="5dp" > </FrameLayout> </LinearLayout> <TextView android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_below="@id/header" android:background="#ff6600" android:text="和哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈和" android:textColor="#ffffff" android:textSize="18sp" android:textStyle="bold" /> <org.lee.android.widget.Panel android:id="@+id/topPanel" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="top|right" android:paddingBottom="4dip" panel:animationDuration="1200" panel:content="@+id/panelContent" panel:handle="@+id/panelHandle" panel:linearFlying="true" panel:position="top" > <TextView android:id="@+id/panelHandle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:background="@drawable/up" android:gravity="center" android:padding="5dp" android:text="分类" android:textColor="#ffffff" android:textSize="15sp" /> <LinearLayout android:id="@+id/panelContent" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > <CheckBox android:layout_width="fill_parent" android:layout_height="60dip" android:background="#688" android:gravity="center" android:text="top check box" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#323299" android:gravity="center" android:padding="4dip" android:text="Bounce\nInterpolator" android:textColor="#eee" android:textSize="16dip" android:textStyle="bold" /> </LinearLayout> </org.lee.android.widget.Panel> </RelativeLayout> </RelativeLayout>
EasingType.java:
package org.lee.android.ExpoInterpolator; public class EasingType { public static final int IN = 0; public static final int OUT = 1; public static final int INOUT = 2; }
ExpoInterpolator.java:
package org.lee.android.ExpoInterpolator; /* * * Open source under the BSD License. * * Copyright © 2001 Robert Penner * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * Neither the name of the author nor the names of contributors may be used to endorse * or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * */ import android.view.animation.Interpolator; public class ExpoInterpolator implements Interpolator { private int type; public ExpoInterpolator(int type) { this.type = type; } public float getInterpolation(float t) { if (type == EasingType.IN) { return in(t); } else if (type == EasingType.OUT) { return out(t); } else if (type == EasingType.INOUT) { return inout(t); } return 0; } private float in(float t) { return (float) ((t==0) ? 0 : Math.pow(2, 10 * (t - 1))); } private float out(float t) { return (float) ((t>=1) ? 1 : (-Math.pow(2, -10 * t) + 1)); } private float inout(float t) { if (t == 0) { return 0; } if (t >= 1) { return 1; } t *= 2; if (t < 1) { return (float) (0.5f * Math.pow(2, 10 * (t - 1))); } else { return (float) (0.5f * (-Math.pow(2, -10 * --t) + 2)); } } }
Panel.java:
package org.lee.android.widget; import org.lee.android.R; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.TranslateAnimation; import android.widget.FrameLayout; import android.widget.LinearLayout; public class Panel extends LinearLayout { private static final String TAG = "Panel"; /** * Callback invoked when the panel is opened/closed. */ public static interface OnPanelListener { /** * Invoked when the panel becomes fully closed. */ public void onPanelClosed(Panel panel); /** * Invoked when the panel becomes fully opened. */ public void onPanelOpened(Panel panel); } private boolean mIsShrinking; private int mPosition; private int mDuration; private boolean mLinearFlying; private int mHandleId; private int mContentId; private View mHandle; private View mContent; private Drawable mOpenedHandle; private Drawable mClosedHandle; private float mTrackX; private float mTrackY; private float mVelocity; private OnPanelListener panelListener; public static final int TOP = 0; public static final int BOTTOM = 1; public static final int LEFT = 2; public static final int RIGHT = 3; private enum State { ABOUT_TO_ANIMATE, ANIMATING, READY, TRACKING, FLYING, }; private State mState; private Interpolator mInterpolator; private GestureDetector mGestureDetector; private int mContentHeight; private int mContentWidth; private int mOrientation; private float mWeight; private PanelOnGestureListener mGestureListener; private boolean mBringToFront; public Panel(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Panel); mDuration = a.getInteger(R.styleable.Panel_animationDuration, 750); // duration defaults to 750 ms mPosition = a.getInteger(R.styleable.Panel_position, BOTTOM); // position defaults to BOTTOM mLinearFlying = a.getBoolean(R.styleable.Panel_linearFlying, false); // linearFlying defaults to false mWeight = a.getFraction(R.styleable.Panel_weight, 0, 1, 0.0f); // weight defaults to 0.0 if (mWeight < 0 || mWeight > 1) { mWeight = 0.0f; Log.w(TAG, a.getPositionDescription() + ": weight must be > 0 and <= 1"); } mOpenedHandle = a.getDrawable(R.styleable.Panel_openedHandle); mClosedHandle = a.getDrawable(R.styleable.Panel_closedHandle); RuntimeException e = null; mHandleId = a.getResourceId(R.styleable.Panel_handle, 0); if (mHandleId == 0) { e = new IllegalArgumentException(a.getPositionDescription() + ": The handle attribute is required and must refer to a valid child."); } mContentId = a.getResourceId(R.styleable.Panel_content, 0); if (mContentId == 0) { e = new IllegalArgumentException(a.getPositionDescription() + ": The content attribute is required and must refer to a valid child."); } a.recycle(); if (e != null) { throw e; } mOrientation = (mPosition == TOP || mPosition == BOTTOM)? VERTICAL : HORIZONTAL; setOrientation(mOrientation); mState = State.READY; mGestureListener = new PanelOnGestureListener(); mGestureDetector = new GestureDetector(mGestureListener); mGestureDetector.setIsLongpressEnabled(false); // i DON'T really know why i need this... setBaselineAligned(false); } /** * Sets the listener that receives a notification when the panel becomes open/close. * * @param onPanelListener The listener to be notified when the panel is opened/closed. */ public void setOnPanelListener(OnPanelListener onPanelListener) { panelListener = onPanelListener; } /** * Gets Panel's mHandle * * @return Panel's mHandle */ public View getHandle() { return mHandle; } /** * Gets Panel's mContent * * @return Panel's mContent */ public View getContent() { return mContent; } /** * Sets the acceleration curve for panel's animation. * * @param i The interpolator which defines the acceleration curve */ public void setInterpolator(Interpolator i) { mInterpolator = i; } /** * Set the opened state of Panel. * * @param open True if Panel is to be opened, false if Panel is to be closed. * @param animate True if use animation, false otherwise. * * @return True if operation was performed, false otherwise. * */ public boolean setOpen(boolean open, boolean animate) { if (mState == State.READY && isOpen() ^ open) { mIsShrinking = !open; if (animate) { mState = State.ABOUT_TO_ANIMATE; if (!mIsShrinking) { // this could make flicker so we test mState in dispatchDraw() // to see if is equal to ABOUT_TO_ANIMATE mContent.setVisibility(VISIBLE); } post(startAnimation); } else { mContent.setVisibility(open? VISIBLE : GONE); postProcess(); } return true; } return false; } /** * Returns the opened status for Panel. * * @return True if Panel is opened, false otherwise. */ public boolean isOpen() { return mContent.getVisibility() == VISIBLE; } @Override protected void onFinishInflate() { super.onFinishInflate(); mHandle = findViewById(mHandleId); if (mHandle == null) { String name = getResources().getResourceEntryName(mHandleId); throw new RuntimeException("Your Panel must have a child View whose id attribute is 'R.id." + name + "'"); } mHandle.setOnTouchListener(touchListener); mHandle.setOnClickListener(clickListener); mContent = findViewById(mContentId); if (mContent == null) { String name = getResources().getResourceEntryName(mHandleId); throw new RuntimeException("Your Panel must have a child View whose id attribute is 'R.id." + name + "'"); } // reposition children removeView(mHandle); removeView(mContent); if (mPosition == TOP || mPosition == LEFT) { addView(mContent); addView(mHandle); } else { addView(mHandle); addView(mContent); } if (mClosedHandle != null) { mHandle.setBackgroundDrawable(mClosedHandle); } mContent.setClickable(true); mContent.setVisibility(GONE); if (mWeight > 0) { ViewGroup.LayoutParams params = mContent.getLayoutParams(); if (mOrientation == VERTICAL) { params.height = ViewGroup.LayoutParams.FILL_PARENT; } else { params.width = ViewGroup.LayoutParams.FILL_PARENT; } mContent.setLayoutParams(params); } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); ViewParent parent = getParent(); if (parent != null && parent instanceof FrameLayout) { mBringToFront = true; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mWeight > 0 && mContent.getVisibility() == VISIBLE) { View parent = (View) getParent(); if (parent != null) { if (mOrientation == VERTICAL) { heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (parent.getHeight() * mWeight), MeasureSpec.EXACTLY); } else { widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (parent.getWidth() * mWeight), MeasureSpec.EXACTLY); } } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mContentWidth = mContent.getWidth(); mContentHeight = mContent.getHeight(); } @Override protected void dispatchDraw(Canvas canvas) { // String name = getResources().getResourceEntryName(getId()); // Log.d(TAG, name + " ispatchDraw " + mState); // this is why 'mState' was added: // avoid flicker before animation start if (mState == State.ABOUT_TO_ANIMATE && !mIsShrinking) { int delta = mOrientation == VERTICAL? mContentHeight : mContentWidth; if (mPosition == LEFT || mPosition == TOP) { delta = -delta; } if (mOrientation == VERTICAL) { canvas.translate(0, delta); } else { canvas.translate(delta, 0); } } if (mState == State.TRACKING || mState == State.FLYING) { canvas.translate(mTrackX, mTrackY); } super.dispatchDraw(canvas); } private float ensureRange(float v, int min, int max) { v = Math.max(v, min); v = Math.min(v, max); return v; } OnTouchListener touchListener = new OnTouchListener() { float touchX, touchY; public boolean onTouch(View v, MotionEvent event) { if (mState == State.ANIMATING) { // we are animating return false; } int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { if (mBringToFront) { bringToFront(); } touchX = event.getX(); touchY = event.getY(); } if (!mGestureDetector.onTouchEvent(event)) { if (action == MotionEvent.ACTION_UP) { // tup up after scrolling int size = (int) (Math.abs(touchX - event.getX()) + Math .abs(touchY - event.getY())); if (size == mContentWidth || size == mContentHeight) { mState = State.ABOUT_TO_ANIMATE; //Log.e("size", String.valueOf(size)); //Log.e(String.valueOf(mContentWidth),String.valueOf(mContentHeight)); } post(startAnimation); } } return false; } }; OnClickListener clickListener = new OnClickListener() { public void onClick(View v) { if (mBringToFront) { bringToFront(); } if (initChange()) { post(startAnimation); } } }; public boolean initChange() { if (mState != State.READY) { // we are animating or just about to animate return false; } mState = State.ABOUT_TO_ANIMATE; mIsShrinking = mContent.getVisibility() == VISIBLE; if (!mIsShrinking) { // this could make flicker so we test mState in dispatchDraw() // to see if is equal to ABOUT_TO_ANIMATE mContent.setVisibility(VISIBLE); } return true; } Runnable startAnimation = new Runnable() { public void run() { // this is why we post this Runnable couple of lines above: // now its save to use mContent.getHeight() && mContent.getWidth() TranslateAnimation animation; int fromXDelta = 0, toXDelta = 0, fromYDelta = 0, toYDelta = 0; if (mState == State.FLYING) { mIsShrinking = (mPosition == TOP || mPosition == LEFT) ^ (mVelocity > 0); } int calculatedDuration; if (mOrientation == VERTICAL) { int height = mContentHeight; if (!mIsShrinking) { fromYDelta = mPosition == TOP? -height : height; } else { toYDelta = mPosition == TOP? -height : height; } if (mState == State.TRACKING) { if (Math.abs(mTrackY - fromYDelta) < Math.abs(mTrackY - toYDelta)) { mIsShrinking = !mIsShrinking; toYDelta = fromYDelta; } fromYDelta = (int) mTrackY; } else if (mState == State.FLYING) { fromYDelta = (int) mTrackY; } // for FLYING events we calculate animation duration based on flying velocity // also for very high velocity make sure duration >= 20 ms if (mState == State.FLYING && mLinearFlying) { calculatedDuration = (int) (1000 * Math.abs((toYDelta - fromYDelta) / mVelocity)); calculatedDuration = Math.max(calculatedDuration, 20); } else { calculatedDuration = mDuration * Math.abs(toYDelta - fromYDelta) / mContentHeight; } } else { int width = mContentWidth; if (!mIsShrinking) { fromXDelta = mPosition == LEFT? -width : width; } else { toXDelta = mPosition == LEFT? -width : width; } if (mState == State.TRACKING) { if (Math.abs(mTrackX - fromXDelta) < Math.abs(mTrackX - toXDelta)) { mIsShrinking = !mIsShrinking; toXDelta = fromXDelta; } fromXDelta = (int) mTrackX; } else if (mState == State.FLYING) { fromXDelta = (int) mTrackX; } // for FLYING events we calculate animation duration based on flying velocity // also for very high velocity make sure duration >= 20 ms if (mState == State.FLYING && mLinearFlying) { calculatedDuration = (int) (1000 * Math.abs((toXDelta - fromXDelta) / mVelocity)); calculatedDuration = Math.max(calculatedDuration, 20); } else { calculatedDuration = mDuration * Math.abs(toXDelta - fromXDelta) / mContentWidth; } } mTrackX = mTrackY = 0; if (calculatedDuration == 0) { mState = State.READY; if (mIsShrinking) { mContent.setVisibility(GONE); } postProcess(); return; } animation = new TranslateAnimation(fromXDelta, toXDelta, fromYDelta, toYDelta); animation.setDuration(calculatedDuration); animation.setAnimationListener(animationListener); if (mState == State.FLYING && mLinearFlying) { animation.setInterpolator(new LinearInterpolator()); } else if (mInterpolator != null) { animation.setInterpolator(mInterpolator); } startAnimation(animation); } }; private AnimationListener animationListener = new AnimationListener() { public void onAnimationEnd(Animation animation) { mState = State.READY; if (mIsShrinking) { mContent.setVisibility(GONE); } postProcess(); } public void onAnimationRepeat(Animation animation) { } public void onAnimationStart(Animation animation) { mState = State.ANIMATING; } }; private void postProcess() { if (mIsShrinking && mClosedHandle != null) { mHandle.setBackgroundDrawable(mClosedHandle); } else if (!mIsShrinking && mOpenedHandle != null) { mHandle.setBackgroundDrawable(mOpenedHandle); } // invoke listener if any if (panelListener != null) { if (mIsShrinking) { panelListener.onPanelClosed(Panel.this); } else { panelListener.onPanelOpened(Panel.this); } } } class PanelOnGestureListener implements OnGestureListener { float scrollY; float scrollX; public void setScroll(int initScrollX, int initScrollY) { scrollX = initScrollX; scrollY = initScrollY; } public boolean onDown(MotionEvent e) { scrollX = scrollY = 0; initChange(); return true; } public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mState = State.FLYING; mVelocity = mOrientation == VERTICAL? velocityY : velocityX; post(startAnimation); return true; } public void onLongPress(MotionEvent e) { // not used } public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { mState = State.TRACKING; float tmpY = 0, tmpX = 0; if (mOrientation == VERTICAL) { scrollY -= distanceY; if (mPosition == TOP) { tmpY = ensureRange(scrollY, -mContentHeight, 0); } else { tmpY = ensureRange(scrollY, 0, mContentHeight); } } else { scrollX -= distanceX; if (mPosition == LEFT) { tmpX = ensureRange(scrollX, -mContentWidth, 0); } else { tmpX = ensureRange(scrollX, 0, mContentWidth); } } if (tmpX != mTrackX || tmpY != mTrackY) { mTrackX = tmpX; mTrackY = tmpY; invalidate(); } return true; } public void onShowPress(MotionEvent e) { // not used } public boolean onSingleTapUp(MotionEvent e) { // not used return false; } } }
attrs.xml:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="Panel"> <!-- Defines panel animation duration in ms. --> <attr name="animationDuration" format="integer" /> <!-- Defines panel position on the screen. --> <attr name="position"> <!-- Panel placed at top of the screen. --> <enum name="top" value="0" /> <!-- Panel placed at bottom of the screen. --> <enum name="bottom" value="1" /> <!-- Panel placed at left of the screen. --> <enum name="left" value="2" /> <!-- Panel placed at right of the screen. --> <enum name="right" value="3" /> </attr> <!-- Identifier for the child that represents the panel's handle. --> <attr name="handle" format="reference" /> <!-- Identifier for the child that represents the panel's content. --> <attr name="content" format="reference" /> <!-- Defines if flying gesture forces linear interpolator in animation. --> <attr name="linearFlying" format="boolean" /> <!-- Defines size relative to parent (must be in form: nn%p). --> <attr name="weight" format="fraction" /> <!-- Defines opened handle (drawable/color). --> <attr name="openedHandle" format="reference|color" /> <!-- Defines closed handle (drawable/color). --> <attr name="closedHandle" format="reference|color" /> </declare-styleable> </resources>