Android抽屉/侧滑菜单:开源AndroidSideMenu

AndroidSideMenu能够让你轻而易举地创建侧滑菜单。需要注意的是,该项目自身并不提供任何创建菜单的工具,因此,开发者可以自由创建内部菜单。


核心类如下:

/*
 * Copyright dmitry.zaicew@gmail.com Dmitry Zaitsev
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.agimind.widget;

import java.util.LinkedList;
import java.util.Queue;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.Region.Op;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Transformation;
import android.widget.FrameLayout;

public class SlideHolder extends FrameLayout {

	public final static int DIRECTION_LEFT = 1;
	public final static int DIRECTION_RIGHT = -1;
	
	protected final static int MODE_READY = 0;
	protected final static int MODE_SLIDE = 1;
	protected final static int MODE_FINISHED = 2;
	
	private Bitmap mCachedBitmap;
	private Canvas mCachedCanvas;
	private Paint mCachedPaint;
	private View mMenuView;
	
	private int mMode = MODE_READY;
	private int mDirection = DIRECTION_LEFT;
	
	private int mOffset = 0;
	private int mStartOffset;
	private int mEndOffset;
	
	private boolean mEnabled = true;
	private boolean mInterceptTouch = true;
	private boolean mAlwaysOpened = false;
	private boolean mDispatchWhenOpened = false;
	
	private Queue<Runnable> mWhenReady = new LinkedList<Runnable>();
	
	private OnSlideListener mListener;
	
	public SlideHolder(Context context) {
		super(context);
		
		initView();
	}
	
	public SlideHolder(Context context, AttributeSet attrs) {
		super(context, attrs);
		
		initView();
	}
	
	public SlideHolder(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		
		initView();
	}
	
	private void initView() {
		mCachedPaint = new Paint(
					Paint.ANTI_ALIAS_FLAG
					| Paint.FILTER_BITMAP_FLAG
					| Paint.DITHER_FLAG
				);
	}
	
	@Override
	public void setEnabled(boolean enabled) {
		mEnabled = enabled;
	}
	
	@Override
	public boolean isEnabled() {
		return mEnabled;
	}
	
	/**
	 * 
	 * @param direction - direction in which SlideHolder opens. Can be: DIRECTION_LEFT, DIRECTION_RIGHT
	 */
	public void setDirection(int direction) {
		closeImmediately();
		
		mDirection = direction;
	}
	
	/**
	 * 
	 * @param allow - if false, SlideHolder won't react to swiping gestures (but still will be able to work by manually invoking mathods)
	 */
	public void setAllowInterceptTouch(boolean allow) {
		mInterceptTouch = allow;
	}
	
	public boolean isAllowedInterceptTouch() {
		return mInterceptTouch;
	}
	
	/**
	 * 
	 * @param dispatch - if true, in open state SlideHolder will dispatch touch events to main layout (in other words - it will be clickable)
	 */
	public void setDispatchTouchWhenOpened(boolean dispatch) {
		mDispatchWhenOpened = dispatch;
	}
	
	public boolean isDispatchTouchWhenOpened() {
		return mDispatchWhenOpened;
	}
	
	/**
	 * 
	 * @param opened - if true, SlideHolder will always be in opened state (which means that swiping won't work)
	 */
	public void setAlwaysOpened(boolean opened) {
		mAlwaysOpened = opened;
		
		requestLayout();
	}
	
	public int getMenuOffset() {
		return mOffset;
	}
	
	public void setOnSlideListener(OnSlideListener lis) {
		mListener = lis;
	}
	
	public boolean isOpened() {
		return mAlwaysOpened || mMode == MODE_FINISHED;
	}
	
	public void toggle(boolean immediately) {
		if(immediately) {
			toggleImmediately();
		} else {
			toggle();
		}
	}
	
	public void toggle() {
		if(isOpened()) {
			close();
		} else {
			open();
		}
	}
	
	public void toggleImmediately() {
		if(isOpened()) {
			closeImmediately();
		} else {
			openImmediately();
		}
	}
	
	public boolean open() {
		if(isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) {
			return false;
		}
		
		if(!isReadyForSlide()) {
			mWhenReady.add(new Runnable() {
				
				@Override
				public void run() {
					open();
				}
			});
			
			return true;
		}
		
		initSlideMode();
		
		Animation anim = new SlideAnimation(mOffset, mEndOffset);
		anim.setAnimationListener(mOpenListener);
		startAnimation(anim);
		
		invalidate();
		
		return true;
	}
	
	public boolean openImmediately() {
		if(isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) {
			return false;
		}
		
		if(!isReadyForSlide()) {
			mWhenReady.add(new Runnable() {
				
				@Override
				public void run() {
					openImmediately();
				}
			});
			
			return true;
		}
		
		mMenuView.setVisibility(View.VISIBLE);
		mMode = MODE_FINISHED;
		requestLayout();
		
		if(mListener != null) {
			mListener.onSlideCompleted(true);
		}
		
		return true;
	}
	
	public boolean close() {
		if(!isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) {
			return false;
		}
		
		if(!isReadyForSlide()) {
			mWhenReady.add(new Runnable() {
				
				@Override
				public void run() {
					close();
				}
			});
			
			return true;
		}
		
		initSlideMode();
		
		Animation anim = new SlideAnimation(mOffset, mEndOffset);
		anim.setAnimationListener(mCloseListener);
		startAnimation(anim);
		
		invalidate();
		
		return true;
	}
	
	public boolean closeImmediately() {
		if(!isOpened() || mAlwaysOpened || mMode == MODE_SLIDE) {
			return false;
		}
		
		if(!isReadyForSlide()) {
			mWhenReady.add(new Runnable() {
				
				@Override
				public void run() {
					closeImmediately();
				}
			});
			
			return true;
		}
		
		mMenuView.setVisibility(View.GONE);
		mMode = MODE_READY;
		requestLayout();
		
		if(mListener != null) {
			mListener.onSlideCompleted(false);
		}
		
		return true;
	}
	
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		final int parentLeft = 0;
		final int parentTop = 0;
		final int parentRight = r - l;
		final int parentBottom = b - t;
		
		View menu = getChildAt(0);
		int menuWidth = menu.getMeasuredWidth();
		
		if(mDirection == DIRECTION_LEFT) {
			menu.layout(parentLeft, parentTop, parentLeft+menuWidth, parentBottom);
		} else {
			menu.layout(parentRight-menuWidth, parentTop, parentRight, parentBottom);
		}
		
		if(mAlwaysOpened) {
			if(mDirection == DIRECTION_LEFT) {
				mOffset = menuWidth;
			} else {
				mOffset = 0;
			}
		} else if(mMode == MODE_FINISHED) {
			mOffset = mDirection*menuWidth;
		} else if(mMode == MODE_READY) {
			mOffset = 0;
		}
		
		View main = getChildAt(1);
		main.layout(
					parentLeft + mOffset,
					parentTop,
					parentLeft + mOffset + main.getMeasuredWidth(),
					parentBottom
				);
		
		invalidate();
		
		Runnable rn;
        while((rn = mWhenReady.poll()) != null) {
        	rn.run();
        }
	}
	
	private boolean isReadyForSlide() {
		return (getWidth() > 0 && getHeight() > 0);
	}
	
	@Override
    protected void onMeasure(int wSp, int hSp) {
		mMenuView = getChildAt(0);
		
        if(mAlwaysOpened) {
            View main = getChildAt(1);
            
            if(mMenuView != null && main != null) {
            	measureChild(mMenuView, wSp, hSp);
                LayoutParams lp = (LayoutParams) main.getLayoutParams();
                
                if(mDirection == DIRECTION_LEFT) {
                	lp.leftMargin = mMenuView.getMeasuredWidth();
                } else {
                	lp.rightMargin = mMenuView.getMeasuredWidth();
                }
            }
        }
        
        super.onMeasure(wSp, hSp);
    }

	private byte mFrame = 0;
	
	@Override
	protected void dispatchDraw(Canvas canvas) {
		try {
			if(mMode == MODE_SLIDE) {
				View main = getChildAt(1);
				if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
					/*
					 * On new versions we redrawing main layout only
					 * if it's marked as dirty 
					 */
					if(main.isDirty()) {
						mCachedCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
						main.draw(mCachedCanvas);
				}
				} else {
					/*
					 * On older versions we just redrawing our cache
					 * every 5th frame
					 */
					if(++mFrame % 5 == 0) {
						mCachedCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
						main.draw(mCachedCanvas);
					}
				}

				/*
				 * Draw only visible part of menu
				 */
				
				View menu = getChildAt(0);
				final int scrollX = menu.getScrollX();
				final int scrollY = menu.getScrollY();
				
				canvas.save();
				
				if(mDirection == DIRECTION_LEFT) {
					canvas.clipRect(0, 0, mOffset, menu.getHeight(), Op.REPLACE);
				} else {
					int menuWidth = menu.getWidth();
					int menuLeft = menu.getLeft();
					
					canvas.clipRect(menuLeft+menuWidth+mOffset, 0, menuLeft+menuWidth, menu.getHeight());
				}
				
				canvas.translate(menu.getLeft(), menu.getTop());
				canvas.translate(-scrollX, -scrollY);
				
				menu.draw(canvas);
				
				canvas.restore();
				
				canvas.drawBitmap(mCachedBitmap, mOffset, 0, mCachedPaint);
			} else {
				if(!mAlwaysOpened && mMode == MODE_READY) {
		        	mMenuView.setVisibility(View.GONE);
		        }
				
				super.dispatchDraw(canvas);
			}
		} catch(IndexOutOfBoundsException e) {
			/*
			 * Possibility of crashes on some devices (especially on Samsung).
			 * Usually, when ListView is empty.
			 */
		}
	}
	
	private int mHistoricalX = 0;
	private boolean mCloseOnRelease = false;
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		if(((!mEnabled || !mInterceptTouch) && mMode == MODE_READY) || mAlwaysOpened) {
			return super.dispatchTouchEvent(ev);
		}
		
		if(mMode != MODE_FINISHED) {
			onTouchEvent(ev);
			
			if(mMode != MODE_SLIDE) {
				super.dispatchTouchEvent(ev);
			} else {
				MotionEvent cancelEvent = MotionEvent.obtain(ev);
				cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
				super.dispatchTouchEvent(cancelEvent);
				cancelEvent.recycle();
			}
			
			return true;
		} else {
			final int action = ev.getAction();
			
			Rect rect = new Rect();
			View menu = getChildAt(0);
			menu.getHitRect(rect);
			
			if(!rect.contains((int) ev.getX(), (int) ev.getY())) {
				if (action == MotionEvent.ACTION_UP && mCloseOnRelease && !mDispatchWhenOpened) {
					close();
					mCloseOnRelease = false;
				} else {
					if(action == MotionEvent.ACTION_DOWN && !mDispatchWhenOpened) {
						mCloseOnRelease = true;
					}
					
					onTouchEvent(ev);
				}
				
				if(mDispatchWhenOpened) {
					super.dispatchTouchEvent(ev);
				}
				
				return true;
			} else {
				onTouchEvent(ev);
				
				ev.offsetLocation(-menu.getLeft(), -menu.getTop());
				menu.dispatchTouchEvent(ev);
				
				return true;
			}
		}
	}
	
	private boolean handleTouchEvent(MotionEvent ev) {
		if(!mEnabled) {
			return false;
		}
		
		float x = ev.getX();
		
		if(ev.getAction() == MotionEvent.ACTION_DOWN) {
			mHistoricalX = (int) x;
			
			return true;
		}
		
		if(ev.getAction() == MotionEvent.ACTION_MOVE) {

			float diff = x - mHistoricalX;

			if((mDirection*diff > 50 && mMode == MODE_READY) || (mDirection*diff < -50 && mMode == MODE_FINISHED)) {
				mHistoricalX = (int) x;
				
				initSlideMode();
			} else if(mMode == MODE_SLIDE) {
				mOffset += diff;
				
				mHistoricalX = (int) x;
				
				if(!isSlideAllowed()) {
					finishSlide();
				}
			} else {
				return false;
			}
		}
		
		if(ev.getAction() == MotionEvent.ACTION_UP) {
			if(mMode == MODE_SLIDE) {
				finishSlide();
			}
			
			mCloseOnRelease = false;
			
			return false;
		}
		
		return mMode == MODE_SLIDE;
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		boolean handled = handleTouchEvent(ev);
		
		invalidate();
		
		return handled;
	}
	
	private void initSlideMode() {
		mCloseOnRelease = false;
		
		View v = getChildAt(1);
		
		if(mMode == MODE_READY) {
			mStartOffset = 0;
			mEndOffset = mDirection*getChildAt(0).getWidth();
		} else {
			mStartOffset = mDirection*getChildAt(0).getWidth();
			mEndOffset = 0;
		}
		
		mOffset = mStartOffset;
		
		if(mCachedBitmap == null || mCachedBitmap.isRecycled() || mCachedBitmap.getWidth() != v.getWidth()) {
			mCachedBitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
			mCachedCanvas = new Canvas(mCachedBitmap);
		} else {
			mCachedCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
		}
		
		v.setVisibility(View.VISIBLE);
		
		mCachedCanvas.translate(-v.getScrollX(), -v.getScrollY());
		v.draw(mCachedCanvas);
		
		mMode = MODE_SLIDE;
		
		mMenuView.setVisibility(View.VISIBLE);
	}
	
	private boolean isSlideAllowed() {
		return (mDirection*mEndOffset > 0 && mDirection*mOffset < mDirection*mEndOffset && mDirection*mOffset >= mDirection*mStartOffset)
				|| (mEndOffset == 0 && mDirection*mOffset > mDirection*mEndOffset && mDirection*mOffset <= mDirection*mStartOffset);
	}
	
	private void completeOpening() {
		mOffset = mDirection*mMenuView.getWidth();
		requestLayout();
		
		post(new Runnable() {
			
			@Override
			public void run() {
				mMode = MODE_FINISHED;
				mMenuView.setVisibility(View.VISIBLE);
			}
		});
		
		if(mListener != null) {
			mListener.onSlideCompleted(true);
		}
	}
	
	private Animation.AnimationListener mOpenListener = new Animation.AnimationListener() {
		
		@Override
		public void onAnimationStart(Animation animation) {}
		
		@Override
		public void onAnimationRepeat(Animation animation) {}
		
		@Override
		public void onAnimationEnd(Animation animation) {
			completeOpening();
		}
	};
	
	private void completeClosing() {
		mOffset = 0;
		requestLayout();
		
		post(new Runnable() {
			
			@Override
			public void run() {
				mMode = MODE_READY;
				mMenuView.setVisibility(View.GONE);
			}
		});
		
		if(mListener != null) {
			mListener.onSlideCompleted(false);
		}
	}
	
	private Animation.AnimationListener mCloseListener = new Animation.AnimationListener() {
		
		@Override
		public void onAnimationStart(Animation animation) {}
		
		@Override
		public void onAnimationRepeat(Animation animation) {}
		
		@Override
		public void onAnimationEnd(Animation animation) {
			completeClosing();
		}
	};
	
	private void finishSlide() {
		if(mDirection*mEndOffset > 0) {
			if(mDirection*mOffset > mDirection*mEndOffset/2) {
				if(mDirection*mOffset > mDirection*mEndOffset) mOffset = mEndOffset;
				
				Animation anim = new SlideAnimation(mOffset, mEndOffset);
				anim.setAnimationListener(mOpenListener);
				startAnimation(anim);
			} else {
				if(mDirection*mOffset < mDirection*mStartOffset) mOffset = mStartOffset;
				
				Animation anim = new SlideAnimation(mOffset, mStartOffset);
				anim.setAnimationListener(mCloseListener);
				startAnimation(anim);
			}
		} else {
			if(mDirection*mOffset < mDirection*mStartOffset/2) {
				if(mDirection*mOffset < mDirection*mEndOffset) mOffset = mEndOffset;
				
				Animation anim = new SlideAnimation(mOffset, mEndOffset);
				anim.setAnimationListener(mCloseListener);
				startAnimation(anim);
			} else {
				if(mDirection*mOffset > mDirection*mStartOffset) mOffset = mStartOffset;
				
				Animation anim = new SlideAnimation(mOffset, mStartOffset);
				anim.setAnimationListener(mOpenListener);
				startAnimation(anim);
			}
		}
	}
	
	private class SlideAnimation extends Animation {
		
		private static final float SPEED = 0.6f;
		
		private float mStart;
		private float mEnd;
		
		public SlideAnimation(float fromX, float toX) {
			mStart = fromX;
			mEnd = toX;
			
			setInterpolator(new DecelerateInterpolator());

			float duration = Math.abs(mEnd - mStart) / SPEED;
			setDuration((long) duration);
		}
		
		@Override
		protected void applyTransformation(float interpolatedTime, Transformation t) {
			super.applyTransformation(interpolatedTime, t);
			
			float offset = (mEnd - mStart) * interpolatedTime + mStart;
			mOffset = (int) offset;
			
			postInvalidate();
		}
		
	}
	
	public static interface OnSlideListener {
		public void onSlideCompleted(boolean opened);
	}

}

使用:

package com.agimind.sidemenuexample;

import com.agimind.widget.SlideHolder;

import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.app.ActionBar;
import android.app.Activity;

public class MainActivity extends Activity {

	private SlideHolder mSlideHolder;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		mSlideHolder = (SlideHolder) findViewById(R.id.slideHolder);
		// mSlideHolder.setAllowInterceptTouch(false);
		// mSlideHolder.setAlwaysOpened(true);
		/*
		 * toggleView can actually be any view you want. Here, for simplicity,
		 * we're using TextView, but you can easily replace it with button.
		 * 
		 * Note, when menu opens our textView will become invisible, so it quite
		 * pointless to assign toggle-event to it. In real app consider using UP
		 * button instead. In our case toggle() can be replaced with open().
		 */

		ActionBar actionBar = getActionBar();
		actionBar.setDisplayShowHomeEnabled(true);
		actionBar.setHomeButtonEnabled(true);
		
		View toggleView = findViewById(R.id.textView);
		toggleView.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				mSlideHolder.toggle();
			}
		});
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
		case android.R.id.home:
			mSlideHolder.toggle();
			break;

		default:
			break;
		}
		return super.onOptionsItemSelected(item);
	}
}
布局如下:

<com.agimind.widget.SlideHolder xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/slideHolder"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    tools:context=".MainActivity" >

    <ScrollView
        android:layout_width="200dp"
        android:layout_height="fill_parent"
        android:background="@android:color/black" >

        <LinearLayout
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:orientation="vertical" >

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />

            <Button
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/menu_settings" />
        </LinearLayout>
    </ScrollView>

    <RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:text="@string/swipe"
            android:textSize="25sp" />

    </RelativeLayout>

</com.agimind.widget.SlideHolder>


下载:点击





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值