开源项目:AndroidSideMenu(轻量级侧边栏)

项目地址:https://github.com/dmitry-zaitsev/AndroidSideMenu

这个侧边栏的代码只有一个文件:SlideHolder.java

源码分析如下所示:

/*
 * 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;

/**
 * 轻量级侧边栏:分为菜单栏和内容栏。
 * 仅支持从左边或右边开启侧边栏菜单,不支持同时,且滑开菜单栏的动画不能修改;
 * 实现方法:滑动的是内容栏,菜单栏不动。这个是使用Canvas.translate()配合动画来完成内容栏滑动效果的。
 * 不过我们自己写的话,大多数会采用scrollTo()函数来完成。
 */
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; // 标记侧边栏是否是持续打开的,用于大屏幕的Pad
	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;
	}

	/**
	 * 判断是否允许侧边栏截获触摸手势事件
	 * @return
	 */
	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;
	}

	/**
	 * 设置侧边栏总是打开,用于大屏幕的Pad设备
	 * @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();
	}

	/**
	 * 获取侧边栏菜单的偏移量
	 * @return
	 */
	public int getMenuOffset() {
		return mOffset;
	}

	public void setOnSlideListener(OnSlideListener lis) {
		mListener = lis;
	}

	/**
	 * 菜单栏是否已经打开
	 * @return
	 */
	public boolean isOpened() {
		return mAlwaysOpened || mMode == MODE_FINISHED;
	}

	/**
	 * 菜单栏的开关
	 * @param immediately
	 */
	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();
		}
	}

	/**
	 * 开启菜单栏
	 * @return
	 */
	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;
	}

	/**
	 * 立即打开侧边栏菜单
	 * @return
	 */
	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;
	}

	/**
	 * 关闭菜单栏
	 * @return
	 */
	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;
	}

	/**
	 * 快速关闭菜单栏
	 * @return
	 */
	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); // 直接把菜单栏设为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();
		}
	}

	/**
	 * 是否准备好打开菜单栏或关闭菜单栏,即是否可以滑动??
	 * @return 返回false的唯一情况是:当前正在滑动菜单栏中...
	 */
	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;
			}
		}
	}

	/**
	 * 处理触摸手势事件
	 * @param ev
	 * @return
	 */
	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;
	}

	/*
	 * 初始化侧边栏菜单的模式:打开和关闭菜单栏的时候都需要重新设置 侧边栏参数,
	 * 这个函数在配合onLayout()函数,就实现了侧边栏动画。
	 */
	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);
	}
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值