Android侧滑菜单实现
引言
所有代码已经上传到 GitHub
,有兴趣的可以下载查看。 在平时的生活中,我们经常会看到一些使用了侧滑菜单的应用,例如QQ、滴滴等,侧滑动画可以增加应用的活性,显得不那么死板,如图:
今天我们的任务是实现了一通用的侧滑布局,不仅仅支持左右滑,还支持上下滑。
自定义FrameLayout
我选择通过自定义的FrameLayout来实现侧滑菜单的效果,因为我希望能够实现四个边都可以侧滑,并且可以通过配置来自由的设置侧滑的view,每个侧滑菜单可以自由的设定显示比例。于是我设计了一个ViewItem
的类,用来存放view的缩放比例信息。
//布局和缩放比例的类,已经省略 set/get方法
class ViewItem {
View layout; //View视图
float scale; //显示比例
}
显示主界面
通过重写onLayout(boolean changed, int left, int top, int right, int bottom)
,我们可以自由的设定view的显示位置和大小。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//移除所有的布局
removeAllViewsInLayout();
//加入主界面
if (mHomeView != null) {
addView(mHomeView);
mHomeView.layout(0, 0, mWidth, mHeight);
}
}
其中mWidth
和mHeight
表示空间的宽高,可以在onSizeChanged(int w, int h, int oldw, int oldh)
方法里面设置,每次当空间大小发生变化的时候就更新宽高。 最后效果如下:
显示侧滑的菜单页面
在上面的代码后面加上如下的方法,添加上下左右的侧滑菜单。
//加入左边的界面
addItemView(mLeftViewItem, Gravity.LEFT);
//加入上面边的界面
addItemView(mTopViewItem, Gravity.TOP);
//加入右边的界面
addItemView(mRightViewItem, Gravity.RIGHT);
//加入下边的界面
addItemView(mBottomViewItem, Gravity.BOTTOM);
其中addItemView(SideslipViewItem viewItem, int position)
是根据传递的viewItem和position设置显示的内容和位置
/**
* 添加侧滑的view
*
* @param viewItem
* @param position 位置
*/
private void addItemView(SideslipViewItem viewItem, int position) {
if (viewItem == null || viewItem.getLayout() == null)
return;
switch (position) {
case Gravity.LEFT: {
int x = (int) (-mWidth * viewItem.getScale());
viewItem.getLayout().layout(x, 0, 0, mHeight);
}
case Gravity.TOP: {
int t = (int) (-mHeight * viewItem.getScale());
viewItem.getLayout().layout(0, t, 0, 0);
}
case Gravity.RIGHT: {
int r = (int) (mWidth * (1 + viewItem.getScale()));
viewItem.getLayout().layout(mWidth, 0, r, mHeight);
}
case Gravity.BOTTOM: {
int b = (int) (mHeight * (1 + viewItem.getScale()));
viewItem.getLayout().layout(0, mHeight, mWidth, b);
}
}
}
添加手指交互
为了不影响内部的view的点击响应,我们需要在onInterceptTouchEvent(MotionEvent ev)
里面处理手指触摸逻辑,返回值为true
则说明拦截掉触摸事件,不向下层传递。先定义几个变量:
private float mTouchStartX = 0; //手指按下的x
private float mTouchStartY = 0; //手指按下的y
private float mTouchMoveX = 0; //移动的x
private float mTouchMoveY = 0; //移动的y
private float mInterceptMoveX = 0; //手指移动的x
private float mInterceptMoveY = 0; //手指移动的y
重写onInterceptTouchEvent(MotionEvent ev)
:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
//保存手指按下的坐标
mTouchStartX = ev.getX();
mTouchStartY = ev.getY();
mInterceptMoveX = mTouchStartX;
mInterceptMoveY = mTouchStartY;
mTouchMoveX = mTouchStartX;
mTouchMoveY = mTouchStartY;
touchDownTime = System.currentTimeMillis(); //保存当前时间
break;
}
case MotionEvent.ACTION_MOVE: {
float x = ev.getX();
float y = ev.getY();
mInterceptMoveX = x - mInterceptMoveX;
mInterceptMoveY = y - mInterceptMoveY;
/**
* 先判断手指按下时是否在边界
* 如果在边界,并且手指滑动的距离大于10; 则拦截触摸事件
*/
if (computeIsTouchInSide(mTouchStartX, mTouchStartY) || isShowingSide) {
if (Math.abs(mInterceptMoveX) > 10 || Math.abs(mInterceptMoveY) > 10) {
return true;
}
}
break;
}
}
return super.onInterceptTouchEvent(ev);
}
/**
* 计算手指按下的位置是否是在侧滑响应区域
*
* @param x 按下x
* @param y 按下y
* @return
*/
private boolean computeIsTouchInSide(float x, float y) {
if (x < mWidth / 4 || x > mWidth / 4f * 3 || y < mHeight / 4 || y > mHeight / 4f * 3)
return true;
return false;
}
拦截了触摸事件之后,我们在onTouchEvent(MotionEvent event)
里面进行事件的处理。因为要实现手指跟随的效果,所以我们要对MotionEvent.ACTION_MOVE
进行处理。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
float x = event.getX();
float y = event.getY();
//变化的值
float disX = x - mTouchMoveX;
float disY = y - mTouchMoveY;
//更新上一次手指滑动的坐标
mTouchMoveX = x;
mTouchMoveY = y;
Log.d(TAG, "onTouchEvent: x:" + disX);
//移动view
touchMoveViews(disX, disY);
break;
}
}
最后效果如下:
添加动画效果
给布局加上动画,当手指抬起来的时候就执行,在上面的代码里面加上MotionEvent.ACTION_CANCEL
和MotionEvent.ACTION_UP
的处理
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
float x = event.getX();
float y = event.getY();
//移动距离不够则不进行动画
if (Math.abs(x - mTouchStartX) < 50 && Math.abs(y - mTouchStartY) < 50)
break;
/**
* 计算手指抬起的位置,判断是否应该显示或者隐藏侧滑菜单
*/
if (computeIsShowSide(event.getX(), event.getY()))
animateShowSideView(event.getX(), event.getY(), mMoveSide);
else
animateHideSideView(event.getX(), event.getY(), mMoveSide);
break;
}
判断是否需要显示或者隐藏的方法如下:
/**
* 计算是需要显示侧滑菜单还是隐藏
*
* @return true:显示侧滑菜单 false :隐藏侧滑菜单
*/
private boolean computeIsShowSide(float x, float y) {
//如果手指放下的时候没有在规定区域,则不进行动画
if (isShowingSide && !computeIsTouchInSide(mTouchStartX, mTouchStartY))
return true;
else if (!isShowingSide && !computeIsTouchInSide(mTouchStartX, mTouchStartY))
return false;
long time = System.currentTimeMillis() - touchDownTime;
float speed = 0;
switch (mMoveSide) {
case Gravity.LEFT: {
speed = (x - mTouchStartX) / time;
if (speed > 1)
return true;
break;
}
case Gravity.RIGHT: {
speed = (x - mTouchStartX) / time;
if (speed < -1)
return true;
break;
}
case Gravity.TOP: {
speed = (y - mTouchStartY) / time;
if (speed > 1)
return true;
break;
}
case Gravity.BOTTOM: {
speed = (y - mTouchStartY) / time;
if (speed < -1)
return true;
break;
}
}
//手指滑动超过半个屏幕也可以启动动画
if ((x - mTouchStartX) > mWidth / 2 || (y - mTouchStartY) > mHeight / 2)
return true;
return false;
}
设置动画,通过ObjectAnimator
可以设置动画,我们只需要用它来获取中间的插值,然后后更新view的位置就可以了
/**
* 执行view动画
* 在手指抬起的时候执行
*
* @param x
* @param y
* @param gravity //执行动画的侧边
*/
private void animateShowSideView(float x, float y, int gravity) {
float startVal = 0;
float endVal = 0;
switch (gravity) {
case Gravity.LEFT: {
if (mLeftViewItem == null)
break;
startVal = mLeftViewItem.getLayout().getX();
endVal = 0;
break;
}
case Gravity.TOP: {
if (mTopViewItem == null)
break;
startVal = mTopViewItem.getLayout().getY();
endVal = 0;
break;
}
case Gravity.RIGHT: {
if (mTopViewItem == null)
break;
startVal = mRightViewItem.getLayout().getX();
endVal = (1 - mRightViewItem.getScale()) * mWidth;
break;
}
case Gravity.BOTTOM: {
if (mBottomViewItem == null)
break;
startVal = mBottomViewItem.getLayout().getY();
endVal = (1 - mBottomViewItem.getScale()) * mHeight;
break;
}
}
//清除之前的动画
clearAnimation();
/**
* 主界面滑动
*/
BounceInterpolator bounceInterpolator = new BounceInterpolator(); //弹簧效果
final ObjectAnimator animate = ObjectAnimator.ofFloat(this, "sideslip", startVal, endVal);
animate.setInterpolator(bounceInterpolator);
animate.setDuration(mAnimateTime);
animate.start();
animate.addUpdateListener(updateListener);
//正在显示侧滑菜单
isShowingSide = true;
}
动画更新的监听器,对每一帧view的位置进行设置。
//动画更新监听
private ValueAnimator.AnimatorUpdateListener updateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float cVal = (float) animation.getAnimatedValue();
Log.i(TAG, "onAnimationUpdate: " + cVal);
switch (mMoveSide) {
case Gravity.LEFT: {
if (mLeftViewItem != null)
mLeftViewItem.getLayout().setX(cVal);
break;
}
case Gravity.TOP: {
if (mTopViewItem != null)
mTopViewItem.getLayout().setY(cVal);
break;
}
case Gravity.RIGHT: {
if (mRightViewItem != null)
mRightViewItem.getLayout().setX(cVal);
break;
}
case Gravity.BOTTOM: {
if (mBottomViewItem != null)
mBottomViewItem.getLayout().setY(cVal);
break;
}
}
}
};
最后的效果如下:
添加阴影
侧滑菜单弹出之后,我们可能希望大家把注意力放在侧滑菜单上面,所以我们可以给其他地方加上阴影,就像是Dialog
弹出的感觉,对此,我的方法是在主界面上面覆盖一个View
,当弹窗出来就调整View
的背景颜色。
先加入一个阴影View,在添加了主界面之后添加阴影View,这样就可以覆盖在主界面上面了。
//加入主界面
if (mHomeView != null) {
addView(mHomeView);
mHomeView.layout(0, 0, mWidth, mHeight);
}
//添加阴影view
mShadeView = new View(getContext());
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mShadeView.setLayoutParams(params);
addView(mShadeView);
当侧滑菜单滑动的时候去改变阴影的透明度就可以实现动态的阴影效果了,这里我已左边的侧滑菜单为例:在moveLeftView(float mx)
,在后面加如下的函数:
//计算移动的位置所占的比例
//菜单完全不可见的x坐标 减去 当前x坐标就是变化值,再求百分比就可以了
float change = Math.abs((-mWidth * mLeftViewItem.getScale()) - mLeftViewItem.getLayout().getX());
float p = change / (mWidth * mLeftViewItem.getScale());
mShadeView.setAlpha(p);
最终效果: