前言:
在去年跟同事做一个项目的时候遇到了一个问题,那就是根据需求要求实现一个侧滑菜单的效果,那时候感觉这个好牛逼,跟QQ的效果差不多,感觉有点难做,所以今天我决定写一篇博客来分析下该如何做这个侧滑菜单,同时,在自定义ViewGroup中如何解决侧滑菜单与ListView冲突的问题。
View的事件分发,很多博客上都有,跟大家推荐下的是郭霖的博客,他写的那个事件分发的博客简单易懂,同时也可以看看任玉刚的《android艺术探索》,里面讲解的很深入,当然最好的是一边看博客或者书籍,一边自己对照着源码来分析。源码上虽然有些东西难懂,但是很多代码都是可以忽略的,选取其中的重点来看。
这是效果图:
这两个界面的整体是一个ViewGroup,在android中,View是可以看成无限大的,只是手机屏幕只有那么大,所以显示的区域只有灰色的那一部分,通过控制View的ScrollX来控制View在手机屏幕中显示的位置。
上图就是整个ViewGroup的布局,这个应该都不难理解,手机屏幕就是灰色的ListView的区域,而手机屏幕左侧的就是白色的ListView的区域,通过控制ViewGroup的偏移量ScrollX 来达到这个在手机上滑动的效果。上图自己在写代码的时候最好是自己手动画一下,这样方便自己理解。
好了,基本的东西基本都介绍了,那下面就来自定义我们自己的侧滑菜单了,先看布局代码,这样方便理解,虽然最开始做的部分是自定义ViewGroup。
activity_main.xml的布局代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#aaffaa"
tools:context="com.example.slidegroup.MainActivity" >
<com.example.slidegroup.SlideView
android:layout_width="wrap_content"
android:layout_height="match_parent" >
<include layout="@layout/menu" />
<include layout="@layout/content" />
</com.example.slidegroup.SlideView>
</RelativeLayout>
两个<include>包含的就是两个listView而已,很简单我就不贴代码了.可以看出我们自定义的ViewGroup有两个子View,下面就是我们的ViewGroup的代码:
public class SlideView extends ViewGroup {
/**
* 整个ViewGroup 由一个menu, 一个content组成,都是一个简单的listView。
*/
//屏幕宽度
private int mScreenWidth;
//屏幕高度
private int mScreenHeight;
//menu的宽度,左侧的菜单
private int mMenuWidth;
//左侧的menu
private ViewGroup mMenu;
//屏幕中默认显示的content
private ViewGroup mContent;
//上一次手指接触屏幕的X坐标
private float mLastX;
//上一次手指接触屏幕的Y坐标
private float mLastY;
//当手指点击屏幕的时候的X坐标,
private float mDownX;
//当menu完全显示的时候右侧content的显示的宽度
private int mMenuMargin = 80;
//帮助实现View滑动的类
private Scroller mScroller;
//当事件分发到ViewGroup的dispatchTouchEvent(MotionEvent ev)的时候,
//因为onInterceptTouchEvent是在dispatchTouchEvent中被调用的
//上次手指所处屏幕的X的坐标
private int mLastInterceptX = 0;
//上次手指所处屏幕的Y的坐标
private int mLastInterceptY = 0;
public SlideView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);
mScreenWidth = metrics.widthPixels;
mScreenHeight = metrics.heightPixels;
mMenuMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mMenuMargin, context.getResources().getDisplayMetrics());
mMenuWidth = mScreenWidth - mMenuMargin;
mScroller = new Scroller(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mMenu = (ViewGroup) getChildAt(0);
mMenu.getLayoutParams().width = (int) mMenuWidth;
mContent = (ViewGroup) getChildAt(1);
mContent.getLayoutParams().width = (int) mScreenWidth;
measureChild(mMenu, widthMeasureSpec, heightMeasureSpec);
measureChild(mContent, widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(mMenuWidth + mScreenWidth, mScreenHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
//android的机制,View在显示到屏幕的时候,会进行最少2次的OnMeasure,OnLayout...
//所以这里只在第一次的时候进行ViewGroup在手机屏幕中显示的位置的设定
mMenu.layout(-mMenuWidth, 0, 0, mScreenHeight);
mContent.layout(0, 0, mScreenWidth, mScreenHeight);
}
}
//通过重写ViewGroup的事件拦截方法来解决ViewGroup与ListView的滑动冲突
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// return super.onInterceptTouchEvent(ev);
boolean intercept = false;
int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if(!mScroller.isFinished()){
mScroller.abortAnimation();
intercept = true;
}
break;
case MotionEvent.ACTION_MOVE:
//判断滑动的时候,横向的距离与纵向的距离只差
//如果横向的距离大于纵向的距离,那就拦截,ViewGroup滑动(消费点击事件);
//如果横向的距离小于纵向的距离,那就不拦截,ListView滑动(消费点击事件);
int deltaX = (int) ev.getRawX() - mLastInterceptX;
int deltaY = (int) ev.getRawY() - mLastInterceptY;
Log.d("deltaX", deltaX+"");
Log.d("deltaY", deltaY+"");
if(Math.abs(deltaX) > Math.abs(deltaY)){
intercept = true;
}else{
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
mLastX = x;
mLastY = y;
mLastInterceptX = x;
mLastInterceptY = y;
return intercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getRawX();
mLastX = mDownX;
break;
case MotionEvent.ACTION_MOVE:
float mCurrentX = event.getRawX();
float mCurrentY = event.getRawY();
//获取滑动时上一个点到现在手指所处的点的距离
float scrolledX = mLastX - mCurrentX;
//边界值判断(这里最好是自己手动画图,便于理解)
if (-(getScrollX() + scrolledX) > mMenuWidth) {
scrollTo(-mMenuWidth, 0);
return true;
} else if (getScrollX() + scrolledX > 0) {
scrollTo(0, 0);
return true;
}
scrollBy((int) scrolledX, 0);
mLastX = mCurrentX;
mLastY = mCurrentY;
break;
case MotionEvent.ACTION_UP:
//当手指离开屏幕的时候,判断滑动的距离是否大于Menu宽度的1/2
//如果大于则显示menu,如果小于则不显示
if (-getScrollX() >= mMenuWidth / 2) {
mScroller.startScroll(getScrollX(), 0,
-(mMenuWidth + getScrollX()), 0);
invalidate();
} else {
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0);
invalidate();
}
break;
}
return super.onTouchEvent(event);
}
//这里是实现View缓慢滑动所要重写的方法
@Override
public void computeScroll() {
// TODO Auto-generated method stub
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
代码中都有注释,这里就不详解了,基本上原理就是这样,通过不断的获取ViewGroup的ScrollX来达到滑动的效果,同时判定ViewGroup在屏幕中显示的区域,当ViewGroup与子View都有滑动的需求的时候,这里要要考虑他们俩是否存在滑动冲突问题,(View的事件分发网上有很多资料,解决方法也多种多样,最主要的还是要了解自己的业务需求,这样才能定制好合适自己的方法)。