项目地址: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);
}
}