最近没事就模仿scdn客户端的左右侧滑效果,自定义了一个SlindingMenu,虽然github上已经有了相当成熟的SlindingMenu开源框架,但本博客旨在帮助更多同学理解SlidingMenu的原理,使使用起来更得心应手。
1、分析
首先对其实现进行分析,SlindingMenu的布局需要有两部分,一个是菜单(menu)的布局,一个是内容(content)的布局。两个布局横向排列,菜单布局在左,内容布局在右。初始化的时候将菜单布局向左偏移,以至于能够完全隐藏,这样内容布局就会完全显示在Activity中。然后通过监听手指滑动事件,来改变菜单布局的左偏移距离,从而控制菜单布局的显示和隐藏。原理图如下:
当左菜单完全显示时,效果图如下:
2、实现
2.1、完成主内容main_content和left_menu的布局
两个布局很简单,主内容的布局分为添加标题栏和显示内容两部分即可,左菜单分为三部分,上下两部分使用高度使用layout_weight = 1,中间用LinearLayout包裹的5个TextView 控件,高度使用wrap_content即可。
2.2、自定义ViewGroup的子类,将前面两个布局组合在一起。
1、在activity—main中使用自定义的控件
<com.znouy.slidingmenuview.view.SlidingMenuView
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- 左侧菜单 -->
<include layout="@layout/left_menu" />
<!-- 主内容 -->
<include layout="@layout/main_content" />
</com.znouy.slidingmenuview.view.SlidingMenuView>
2、实现自定义ViewGroup的onMeasure()和onLayout()方法
首先是onMeasure()对孩子进行测量,前提是先要获取两个子孩子,通过重写onFinishInflate()方法,该方法在布局解析完成时会自动回调。代码如下:
@Override
protected void onFinishInflate() {
mLeft_menu = getChildAt(0);
mMain_content = getChildAt(1);
// 对左菜单进行精确测量
LayoutParams layoutParams = mLeft_menu.getLayoutParams();
mLeftmenu_width = layoutParams.width;
super.onFinishInflate();
}
其次是重写onMeasure()方法,对子组件分别进行测量,由于主内容是填充屏幕的,故只需模糊测量即可,即调用view的measure()进行测量即可。而对于左侧菜单,由于其宽是不确定的,所以要对其宽进行精确测量MeasureSpec.makeMeasureSpec(int size, int mode)获取宽,最后在调用setMeasuredDimension()方法设置测量后的值,注意这里的值是不带模式的。
/** 2.子组件测量 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 主内容测量
mMain_content.measure(widthMeasureSpec, heightMeasureSpec);
// 左菜单测量-对宽度的模式测量-精确
int leftWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mLeftmenu_width,
MeasureSpec.EXACTLY);
mLeft_menu.measure(leftWidthMeasureSpec, heightMeasureSpec);
// 设置测量后的值(不带模式)
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.getSize(heightMeasureSpec));
}
测量完成之后onLayout后,就会回调onLayout()进行布局,确定子孩子的位置。核心是其子孩子分别调用View的layout(l,t,r,b)方法进行确定各自的位置。
/** 2.子组件测量完成后进行布局 */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int dx = 0;// x位置的偏移量
// 确定主内容位置
int mainContentLeft = 0 + dx;
int mainContentTop = 0;
int mainContentRight = mMain_content.getMeasuredWidth() + dx;
int mainContentBottom = mMain_content.getMeasuredHeight();
mMain_content.layout(mainContentLeft, mainContentTop, mainContentRight,
mainContentBottom);
// 确定左菜单位置
int leftMenuleft = -mLeft_menu.getMeasuredWidth() + dx;
int leftMenuTop = 0;
int leftMenuRight = 0 + dx;
int leftMenuBottom = mLeft_menu.getMeasuredHeight();
mLeft_menu.layout(leftMenuleft, leftMenuTop, leftMenuRight,
leftMenuBottom);
}
2.3、自定义的ViewGroup触摸事件实现
当触摸滑动自定义的VIewGroup,就要对VIewGroup的 事件分发进行处理,我们知道,VIewGroup的事件分发机制机制主要涉及三个函数。
首先,会调用dispatchTouchEvent()判断是否分发事件,在这里就按默认方式处理。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 按默认方式处理-会到其onInterceptTouchEvent中判断是否将事件拦截
return super.dispatchTouchEvent(ev);
}
如果dispatchTouchEvent()按默认方式处理了,接着就会调用onInterceptTouchEvent()来判断是否进行事件拦截,如果返回true则表示拦截事件,那么将会执行自己的onTouchEvent(),如果返回false或者调用super.onInterceptTouchEvent(ev)则不拦截事件,则事件会交由子View的dispatchTouchEvent()。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 判断是横向滑动还是纵向滑动,横向滑动事件拦截,执行自己的onTouchEvent,使横向滑动时scrollView也可滑动,
// 纵向滑动放行,执行子容器的disPatchTouchEvent,就不执行onTouchEvent,不会横向滑动屏幕
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = ev.getX();
downY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float moveX = ev.getX();
float moveY = ev.getY();
float dx = moveX - downX;
float dy = moveY - downY;
if (Math.abs(dx) > Math.abs(dy)) {// 横向滑动拦截
return true;
}
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
最后,我们只需在onTouchEvent()中处理触摸事件的逻辑即可。我们在滑动的时候让View容器不用越界,再松开时调用getScrollX()(关于getScrollX()用法,请参考本篇博客
Android getScrollX()详解)和左菜单的二分之一的宽度比较小于如果其值小于负的左侧菜单宽度的二分之一,则完全显示左菜单,否则完全显示主内容,为了使该过程规则的滑动,这里使用了Scroller的startScroll()方法实现滑动动画。代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
// 处理触摸事件
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
// 获取View在x轴的位移
// int dx = Math.round(mDownX - moveX);// 为负数表示向左平移了View(画布)
int dx = -Math.round(moveX - mDownX);// 移动的量,-表示向左移动
// 获取平移前View在屏幕左边界的x坐标
int scrollX = getScrollX();
if (scrollX + dx < -mLeft_menu.getMeasuredWidth()) {// 左边界越界
// 完全显示左侧菜单
scrollTo(-mLeft_menu.getMeasuredWidth(), 0);
} else if (scrollX + dx > 0) {// 右边界越界
// 完全显示主内容
scrollTo(0, 0);
} else {// 没有越界
scrollBy(dx, 0);
}
mDownX = moveX;// 移动过程中不断改变按下的位置
break;
case MotionEvent.ACTION_UP:
// 获取屏幕松开时viewgroup左边界的位置(在屏幕左边界的位置)
int scrollX2 = getScrollX();
if (scrollX2 < -mLeft_menu.getMeasuredWidth() / 2) {
scrollAnimation();
// 完全显示左侧菜单,带有动画
// scrollTo(-mLeft_menu.getMeasuredWidth(), 0);
isOpen = true;
} else {
// scrollTo(0, 0);
isOpen = false;
}
scrollAnimation();
break;
default:
break;
}
return true;// 自己消费事件
}
/**
* 移动动画
*/
private void scrollAnimation() {
Log.d(tag, "isOpenLeftMenu==" + isOpen);
// 获取view在屏幕左边的开始坐标和结束坐标
int startX = getScrollX();
int endX = 0;
if (isOpen) {
endX = -mLeft_menu.getMeasuredWidth();
} else {
}
int dx = endX - startX;
int dy = 0;
int duration = Math.abs(dx) * 2;
mScroller.startScroll(startX, 0, dx, dy, duration);
invalidate();// 刷新界面
}
而要使startScroll()方法生效必须重写View的computeScroll()方法(关于startScroll()方法使用请参考
Android Scroller分析),代码如下:
@Override
public void computeScroll() {
// 判断动画是否结束
if (mScroller.computeScrollOffset()) {// 返回true表示没有完成
int currX = mScroller.getCurrX();
scrollTo(currX, 0);
invalidate();
}
super.computeScroll();
}
最后,附上下载地址:https://github.com/znouy/SlidingMenu