加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

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

(2014-08-27 10:43:16)
标签:

android

androidsidemenu

分类: Android
/
*
* 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<</span>Runnable> mWhenReady = new LinkedList<</span>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;
}

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 <</span> -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 <</span> 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 <</span> mDirection * mStartOffset) { // 防止滑过界
mOffset = mStartOffset;
}

// 关闭菜单栏动画
Animation anim = new SlideAnimation(mOffset, mStartOffset);
anim.setAnimationListener(mCloseListener);
startAnimation(anim);
}
} else { // 菜单栏在右侧
if (mDirection * mOffset <</span> mDirection * mStartOffset / 2) {
if (mDirection * mOffset <</span> 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

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有