自定义View-侧滑菜单
最近在使用酷我音乐软件时看到它的侧滑菜单,突然想起来当年qq5.0时的侧滑菜单,虽然网上有很多实现方式,但是为了纪念我Q还是自己来码一个纪念我的青春,效果如下图,啥也不说来直接开干吧!
一、搭建基本框架
-
创建类ViewGroup的子类SlideLayout,实现提示的内容如下:
public class SlideLayout extends ViewGroup { public SlideLayout(Context context) { this(context, null); } public SlideLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { } }
-
先不管
onLayout
方法,因为我们还不知道相应View
的宽高,无法进行onLayout
,所以我们直接进行onMeasure
测量,个人觉得在侧滑菜单
和主功能界面
基本上不会使用到margin
,所以弃用此功能,因为我们只有两个部分,所以我们需要保证SlideLayout
只能有两个子View
否则就抛出异常,然后调用方法measure(widthMeasureSpec, heightMeasureSpec)
统一一次性测量两个View
,代码如下:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (getChildCount() != 2) { throw new IllegalArgumentException("The SlideLayout only have two child view"); } measureChildren(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
-
测量完后我们就可以进行
layout
了,因为侧滑菜单在关闭状态下时是隐藏在左边或者右边的,我们就以左边为基准吧,但是获取子View
进行layout
时,我们无法得知哪个View
是侧滑菜单
,所以我们还需要确定两个View
中哪个是侧滑菜单模块
, 这里有两个思路,第一个思路是我们可以自定义ViewGroup-SlideMenuLayout
来管理侧滑菜单,在SlideLayout
中通过instanceof
关键字来判断哪个是侧滑菜单模块,第二个思路比较简单,直接自定义属性slideMenuId
来映射侧滑菜单,在通过对应ViewId
来判断当前的模块是否是我们的侧滑菜单,我选择第二种代码如下:
1)创建自定义属性slideMenuId
如下:<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="SlideLayout" > <!--侧滑菜单模块的ViewId--> <attr name="slideMenuId" format="reference" /> </declare-styleable> </resources>
2)在构造函数
public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr)
中获取侧滑菜单的ViewId
,若未能获取到侧滑菜单Id
则抛出异常/** * 侧滑菜单Id */ private int mSlideMenuId; public SlideLayout(Context context) { this(context, null); } public SlideLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //获取自定义属性 TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.SlideLayout); mSlideMenuId = styledAttributes.getResourceId(R.styleable.SlideLayout_slideMenuId, -1); styledAttributes.recycle(); //判断是否成功获取到侧滑菜单Id if (mSlideMenuId == -1) { throw new IllegalArgumentException("Can't find SlideMenuId"); } }
3)进行
onLayout
操作,如果是我们的主功能ViewGroup
模块,则填充整个屏幕,若是侧滑菜单则在主功能的左侧也就是贴着屏幕的左边,如下:@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < 2; i++) { View childView = getChildAt(i); if (childView.getId() == mSlideMenuId) { childView.layout(-childView.getMeasuredWidth(), 0, 0, childView.getMeasuredHeight()); } else { childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); } } }
-
到此简单的
onMeasure
和onLayout
操作就完了,我们来运行测试看一下效果吧。
1)布局文件:<?xml version="1.0" encoding="utf-8"?> <vip.zhuahilong.jdapplication.widget.SlideLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:slideMenuId="@id/slideMenuId" tools:context=".ui.activity.MainActivity"> <LinearLayout android:id="@+id/mainViewLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <android.support.v7.widget.Toolbar android:id="@+id/toolBar" android:layout_width="match_parent" android:background="@color/colorPrimary" android:layout_height="?actionBarSize" app:contentInsetStart="@dimen/dp0"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/title_tv" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center" android:layout_centerInParent="true" android:text="@string/app_name" android:textColor="@color/colorAccent" android:textSize="@dimen/sp20" /> </RelativeLayout> </android.support.v7.widget.Toolbar> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="主要内容" android:textColor="@color/colorPrimary" android:textSize="@dimen/sp30" /> </LinearLayout> <LinearLayout android:id="@+id/slideMenuId" android:background="@color/colorPrimary" android:layout_width="@dimen/dp200" android:layout_height="match_parent" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" android:layout_height="@dimen/dp200" android:background="@drawable/slide_menu_mead_bg"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:src="@mipmap/ic_launcher" /> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/gray" android:orientation="vertical"> <TextView android:id="@+id/slideMenuItem1" android:layout_width="match_parent" android:layout_height="?actionBarSize" android:layout_marginTop="@dimen/dp50" android:gravity="center" android:text="slideMenuItem1" android:textSize="@dimen/sp18" /> <TextView android:id="@+id/slideMenuItem2" android:layout_width="match_parent" android:layout_height="?actionBarSize" android:layout_marginTop="@dimen/dp10" android:gravity="center" android:text="slideMenuItem1" android:textSize="@dimen/sp18" /> <TextView android:id="@+id/slideMenuItem3" android:layout_width="match_parent" android:layout_height="?actionBarSize" android:layout_marginTop="@dimen/dp10" android:gravity="center" android:text="slideMenuItem1" android:textSize="@dimen/sp18" /> <TextView android:id="@+id/slideMenuItem4" android:layout_width="match_parent" android:layout_height="?actionBarSize" android:layout_marginTop="@dimen/dp10" android:gravity="center" android:text="slideMenuItem1" android:textSize="@dimen/sp18" /> </LinearLayout> </LinearLayout> </vip.zhuahilong.jdapplication.widget.SlideLayout>
运行效果为:
由于我们还未编码侧滑菜单的打开和关闭,所以下现在无法看到侧滑菜单,下面来编码打开关闭侧滑菜单部分。
二、实现侧滑菜单功能
经分析可知,我们的菜单主要是通过滑动来打开,再通过滑动或点击部分区域来关闭,因此我们要明确是否发生来滑动事件且要实时更新当前事件类型和菜单的状态。
-
定义侧滑菜单状态
MENU_STATE_OPEN-打开状态
、MENU_STATE_CLOSE-关闭状态、MENU_STATE_MASK-侧滑菜单状态标志位
及Touch事件EVENT_CLICK-单击事件
和EVENT_MOVE-滑动事件、EVENT_MASK-Touch事件标志位
,定义变量mSlideLayoutFlag
来存储这些状态,并在构造方法里初始化mSlideLayoutFlag = (~EVENT_MASK) | (~MENU_STATE_MASK);
/** * TouchEvent标志位 */ private int EVENT_MASK = 0x1; /** * TouchClick事件 */ private int EVENT_CLICK = 0x0; /** * TouchMove事件 */ private int EVENT_MOVE = 0x1; /** * 侧滑菜状态标志位 */ private int MENU_STATE_MASK = 0x02; /** * 侧滑菜单 打开状态 */ private int MENU_STATE_OPEN = 0x02; /** * 侧滑菜单 关闭状态 */ private int MENU_STATE_CLOSE = 0x00; /** * 侧滑菜单状态存储器 */ private int mSlideLayoutFlag; public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //略 mSlideLayoutFlag = ITEM_MENU_CLOSE | DIS_INTERCEPT_EVENT; }
-
创建滑动工具类
private Scroller mScroller
且在构造方法里初始化mScroller = new Scroller(context);
-
重写
onTouchEvent
方法,实现菜单滑动
1)滑动菜单顾名思义就是滑动打开菜单,且为了更方便的关闭菜单我们采用点击主功能页面区域来关闭当前打开的滑动菜单的,也就说打开菜单我们只是用滑动的方式,关闭菜单我们可以使用滑动和点击部分区域来关闭,因此我们要区分当前的事件到时是点击事件还是滑动事件,所以我们要在ACTION_DOWN
中更改状态存储器mSlideLayoutFlag
对应事件标志位值位EVENT_CLICK
,同理在ACTION_MOVE
中更新为EVENT_MOVE
,为此还需要创建更新mSlideLayoutFlag
的方法updateTargetMaskValue
,具体代码如下:@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case 0: updateTargetMaskValue(EVENT_MASK, EVENT_CLICK); break; case 2: if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) { updateTargetMaskValue(EVENT_MASK, EVENT_MOVE); } break; case 1: case 3: break; } return true; } /** * 更新flag的位值 * * @param mask 目标标记位 * @param value 最新值 */ private void updateTargetMaskValue(int mask, int value) { mSlideLayoutFlag = (mSlideLayoutFlag & ~mask) | (mask & value); }
2)尽管上面已经定义的事件类型,为了更简单直接,我们先处理滑动事件,后面再回过头来处理点击事件,想要能够滑动我们只需要知道滑动的距离及方向即可,因此我们要记录上次
TouchEvent
的X
坐标,定义变量lastX
来存储,且在TouchEvent
的Down
事件中进行初始化,然后在ACTION_MOVE
事件中计算我们需要滑动距离,正负号
表示滑动的方向,负号
表示内容向右滑动即打开菜单正号表示内容向左滑动即关闭菜单,所以我们计算floatdeltaX=lastX-event.getX()
即是我们需要移动的差值且包含来正确的方向,调用方法scrollBy(deltaX,0)
即完成来此处滑动,但是我们还需要注意滑动距离的范围问题 ,有可能我们的滑动超过来菜单的宽度,导致我们看到一片空白,所以我们还要对滑动的距离作出限制,因为滑动范围是和侧滑菜单的宽度有关系的,所以在onlayout
方法中获取到我们侧滑菜单的宽度用mSlideMenuWidth
存储,则滑动的距离mScrollX
的范围为[-mSlideMenuWidth,0]
,我们根据getScrollX+deltaX
的值判断是否在这个范围内,若getScrollX()+deltaX<=mSlideMenuWidth
则scrollTo(-mSlideMenuWidth,0)
,若getScrollX()+deltaX>=0
则scrollTo(0,0),
否则为scrollBy(deltaX,0)
,最后更新lastX
的值,代码如下:/** * last touch-event X */ private float lastX; /** * 菜单的宽度 */ private int mSlideMenuWidth; @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < 2; i++) { View childView = getChildAt(i); if (childView.getId() == mSlideMenuId) { mSlideMenuWidth = childView.getMeasuredWidth(); childView.layout(-mSlideMenuWidth, 0, 0, childView.getMeasuredHeight()); } else { childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); } } } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case 0: updateTargetMaskValue(EVENT_MASK, EVENT_CLICK); lastX = event.getX(); break; case 2: if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) { updateTargetMaskValue(EVENT_MASK, EVENT_MOVE); } float deltaX = lastX - event.getX(); if (getScrollX() + deltaX <= -mSlideMenuWidth) { scrollTo(-mSlideMenuWidth, 0); } else if (getScrollX() + deltaX >= 0) { scrollTo(0, 0); } else { scrollBy((int) deltaX, 0); } lastX = event.getX(); break; case 1: case 3: break; } return true; }
运行看一下效果如下图:
3)从上面来看使用滑动打开关闭菜单的功能基本已经完成,但是若在菜单打开或者关闭一半时,手机离开屏幕,则菜单保持当前状态不变了,体验极差,所以我们还要处理事件ACTION_UP和ACTION_CANCEL
来完成接下来未完成的动作,我们根据滑动的距离mScrollX
即getScrollX(
)和菜单宽度的一半来判断当前的目标状态,若getScrollX() < -mSlideMenuWidth / 2
则更新菜单状态为打开,使用工具类mScroller
完成当前菜单位置到打开状态下菜单位置的过度过程,若getScrollX() >= -mSlideMenuWidth / 2
则更新菜单状态为关闭,使用工具类mScroller
完成当前菜单位置到关闭状态下菜单位置的过度过程,调用方法invalidate()
开始过渡,其中还需要我们重写方法 computeScroll()
,来不停的刷新绘制某一状态下菜单的位置,直到完成整个过程,为止,代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case 0:
updateTargetMaskValue(EVENT_MASK, EVENT_CLICK);
lastX = event.getX();
break;
case 2:
if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
updateTargetMaskValue(EVENT_MASK, EVENT_MOVE);
}
float deltaX = lastX - event.getX();
if (getScrollX() + deltaX <= -mSlideMenuWidth) {
scrollTo(-mSlideMenuWidth, 0);
} else if (getScrollX() + deltaX >= 0) {
scrollTo(0, 0);
} else {
scrollBy((int) deltaX, 0);
}
lastX = event.getX();
break;
case 1:
case 3:
if (getScrollX() < -mSlideMenuWidth / 2) {
updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_OPEN);
mScroller.startScroll(getScrollX(), getScrollY(), -mSlideMenuWidth - getScrollX(), 0);
} else {
updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE);
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
}
invalidate();
break;
}
return true;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
运行看一下效果,见下图:
4)由上可见基本的滑动菜单效果就实现了,接下来就是缩放View了,为了更加灵活,在自定义属性中增加主页View
和菜单View
的目标缩放值, 默认值都为0.7f
,具体为:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SlideLayout">
<!--侧滑菜单模块的ViewId-->
<attr name="slideMenuId" format="reference" />
<!--侧滑菜单模块的缩放值-->
<attr name="slideMenuTargetScale" format="float" />
<!--主页View模块的缩放值-->
<attr name="contentViewTargetScale" format="float" />
</declare-styleable>
</resources>
/**
* 侧滑菜单的目标缩放值
*/
private float mSlideMenuTargetScale;
/**
* 主功能View的目标缩放值
*/
private float mContentViewTargetScale;
public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取自定义属性
TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.SlideLayout);
mSlideMenuId = styledAttributes.getResourceId(R.styleable.SlideLayout_slideMenuId, -1);
mSlideMenuTargetScale = styledAttributes.getFloat(R.styleable.SlideLayout_slideMenuTargetScale, 0.7f);
mContentViewTargetScale = styledAttributes.getFloat(R.styleable.SlideLayout_contentViewTargetScale, 0.7f);
styledAttributes.recycle();
//判断是否成功获取到侧滑菜单Id
if (mSlideMenuId == -1) {
throw new IllegalArgumentException("Can't find SlideMenuId");
}
//初始化标志位状态
mSlideLayoutFlag = ITEM_MENU_CLOSE | DIS_INTERCEPT_EVENT;
//初始化滑动工具类
mScroller = new Scroller(context);
}
同时我们定义侧滑菜单View:mSlideMenuView
和主页View:mContentView
在onLayout
中获取到两个模块的View引用,根据我们的滑动距离来动态的缩放View,在菜单打开状态下,mSlideMenuView
为原始大小,mContentView
缩放到原始的mContentViewTargetScale
倍;在菜单关闭状态下,mSlideMenuView
为原始大小的mSlideMenuTargetScale
倍,mContentView
为原始大小,所以我们可以根据滑动的距离和缩放的范围来动态的计算当前对应模块的缩放值,mSlideMenuView
缩放值为mSlideMenuTargetScale + Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mSlideMenuTargetScale)
,mContentView
缩放值为1 - Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mContentViewTargetScale)
代码如下:
/**
* 侧滑菜单View引用
*/
private View mSlideMenuView;
/**
* 主页View引用
*/
private View mContentView;
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < 2; i++) {
View childView = getChildAt(i);
if (childView.getId() == mSlideMenuId) {
mSlideMenuView = childView;
mSlideMenuWidth = childView.getMeasuredWidth();
childView.layout(-mSlideMenuWidth, 0, 0, childView.getMeasuredHeight());
} else {
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
mContentView = childView;
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case 0:
updateTargetMaskValue(EVENT_MASK, EVENT_CLICK);
lastX = event.getX();
break;
case 2:
if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
updateTargetMaskValue(EVENT_MASK, EVENT_MOVE);
}
float deltaX = lastX - event.getX();
if (getScrollX() + deltaX <= -mSlideMenuWidth) {
scrollTo(-mSlideMenuWidth, 0);
} else if (getScrollX() + deltaX >= 0) {
scrollTo(0, 0);
} else {
scrollBy((int) deltaX, 0);
}
float currentSlideMenuViewScale = mSlideMenuTargetScale + Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mSlideMenuTargetScale);
float currentContentViewScale = 1 - Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mContentViewTargetScale);
mSlideMenuView.setScaleX(currentSlideMenuViewScale);
mSlideMenuView.setScaleY(currentSlideMenuViewScale);
mContentView.setScaleX(currentContentViewScale);
mContentView.setScaleY(currentContentViewScale);
lastX = event.getX();
break;
case 1:
case 3:
if (getScrollX() < -mSlideMenuWidth / 2) {
updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_OPEN);
mScroller.startScroll(getScrollX(), getScrollY(), -mSlideMenuWidth - getScrollX(), 0);
} else {
updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE);
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
}
invalidate();
break;
}
return true;
}
运行效果如下:
这样就基本完成来我们仿QQ5.0的大致效果,但是看起来不是那么优雅,我们修改侧滑菜单的背景为透明,给SlideLayout
设置背景图片,布局文件为:
<?xml version="1.0" encoding="utf-8"?>
<vip.zhuahilong.jdapplication.widget.SlideLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/star_bg"
app:slideMenuId="@id/slideMenuId"
tools:context=".ui.activity.MainActivity">
<LinearLayout
android:id="@+id/mainViewLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolBar"
android:layout_width="match_parent"
android:background="@color/colorPrimary"
android:layout_height="?actionBarSize"
app:contentInsetStart="@dimen/dp0">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/title_tv"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:layout_centerInParent="true"
android:text="@string/app_name"
android:textColor="@color/colorAccent"
android:textSize="@dimen/sp20" />
</RelativeLayout>
</android.support.v7.widget.Toolbar>
<TextView
android:id="@+id/content_tv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray"
android:gravity="center"
android:text="主要内容"
android:textColor="@color/colorPrimary"
android:textSize="@dimen/sp30" />
</LinearLayout>
<LinearLayout
android:id="@+id/slideMenuId"
android:background="@android:color/transparent"
android:layout_width="@dimen/dp200"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/dp200">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/ic_launcher" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/slideMenuItem1"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_marginTop="@dimen/dp50"
android:gravity="center"
android:text="slideMenuItem1"
android:textSize="@dimen/sp18" />
<TextView
android:id="@+id/slideMenuItem2"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_marginTop="@dimen/dp10"
android:gravity="center"
android:text="slideMenuItem1"
android:textSize="@dimen/sp18" />
<TextView
android:id="@+id/slideMenuItem3"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_marginTop="@dimen/dp10"
android:gravity="center"
android:text="slideMenuItem1"
android:textSize="@dimen/sp18" />
<TextView
android:id="@+id/slideMenuItem4"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_marginTop="@dimen/dp10"
android:gravity="center"
android:text="slideMenuItem1"
android:textSize="@dimen/sp18" />
</LinearLayout>
</LinearLayout>
</vip.zhuahilong.jdapplication.widget.SlideLayout>
运行效果如下:
三、处理单机事件关闭侧滑菜单
上面已经完成了滑动打开关闭侧滑菜单,下面我们来实现单击事件来关闭侧滑菜单,原理比较简单。
首先上面已经在触发ACTION_DOWN和ACTION_MOVE
时更改来对应的标志位状态,所以在事件ACTION_CANCEL
和ACTION_UP
中我们就根据(EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK
是否位真来判断当前的事件是否是单击事件了,若为单击事件,判断当前的菜单是否为打开状态,若为打开状态呢则判断当前事件的落点是否在菜单范围外,在外则关闭,否则不处理,再者我们还要确定触发关闭菜单的单击区域,也就是我们缩放后可见的mContentView
区域,所以我们创建mCloseMenuClickRectF
,根据缩放比例来确定其范围,它的left
为菜单的宽度加上mContentView
宽度缩放差值的一半即mSlideMenuWidth + (1 - mContentViewTargetScale) * mContentView.getMeasuredWidth() / 2
,它的top
为mContentView
在高度上缩放差值的一半:(1 - mContentViewTargetScale) * mContentView.getMeasuredHeight() / 2
,它的right
为mContentView
原始的宽度也就是屏幕的right
即mContentView.getMeasuredWidth()
,它的bottom
为mContentView
的原始高度减去缩放差值的一半即:(0.5f + mContentViewTargetScale / 2) * mContentView.getMeasuredHeight()
,在onLayout
中初始化,上述具体代码如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < 2; i++) {
View childView = getChildAt(i);
if (childView.getId() == mSlideMenuId) {
mSlideMenuView = childView;
mSlideMenuWidth = childView.getMeasuredWidth();
childView.layout(-mSlideMenuWidth, 0, 0, childView.getMeasuredHeight());
} else {
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
mContentView = childView;
}
mCloseMenuClickRectF.set(
mSlideMenuWidth + (1 - mContentViewTargetScale) * mContentView.getMeasuredWidth() / 2,
(1 - mContentViewTargetScale) * mContentView.getMeasuredHeight() / 2,
mContentView.getMeasuredWidth(),
(0.5f + mContentViewTargetScale / 2) * mContentView.getMeasuredHeight()
);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case 0:
updateTargetMaskValue(EVENT_MASK, EVENT_CLICK);
lastX = event.getX();
break;
case 2:
if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
updateTargetMaskValue(EVENT_MASK, EVENT_MOVE);
}
float deltaX = lastX - event.getX();
if (getScrollX() + deltaX <= -mSlideMenuWidth) {
scrollTo(-mSlideMenuWidth, 0);
} else if (getScrollX() + deltaX >= 0) {
scrollTo(0, 0);
} else {
scrollBy((int) deltaX, 0);
}
float currentSlideMenuViewScale = mSlideMenuTargetScale + Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mSlideMenuTargetScale);
float currentContentViewScale = 1 - Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mContentViewTargetScale);
mSlideMenuView.setScaleX(currentSlideMenuViewScale);
mSlideMenuView.setScaleY(currentSlideMenuViewScale);
mContentView.setScaleX(currentContentViewScale);
mContentView.setScaleY(currentContentViewScale);
lastX = event.getX();
break;
case 1:
case 3:
if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
if (slideMenuIsOpen() && mCloseMenuClickRectF.contains(event.getX(), event.getY())) {
updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE);
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
invalidate();
}
} else {
if (getScrollX() < -mSlideMenuWidth / 2) {
updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_OPEN);
mScroller.startScroll(getScrollX(), getScrollY(), -mSlideMenuWidth - getScrollX(), 0);
} else {
updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE);
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
}
invalidate();
}
break;
}
return true;
}
/**
* 判断菜单是否打开
*
* @return
*/
private boolean slideMenuIsOpen() {
return (mSlideLayoutFlag & MENU_STATE_MASK) == MENU_STATE_OPEN;
}
运行效果如下:
至此我们的仿qq5.0的侧滑菜单效果就完成了,但是也仅仅是完成了部分的效果,其中部分还是要根据实际的功能需求来处理
TouchEvent
,谢谢大家,如果不对的地方,还请不吝赐教!
四、代码
-
SlideLayout
自定义属性<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="SlideLayout"> <!--侧滑菜单模块的ViewId--> <attr name="slideMenuId" format="reference" /> <!--侧滑菜单模块的缩放值--> <attr name="slideMenuTargetScale" format="float" /> <!--主页View模块的缩放值--> <attr name="contentViewTargetScale" format="float" /> </declare-styleable> </resources>
-
SlideLayout
代码public class SlideLayout extends ViewGroup { /** * TouchEvent标志位 */ private int EVENT_MASK = 0x1; /** * TouchClick事件 */ private int EVENT_CLICK = 0x0; /** * TouchMove事件 */ private int EVENT_MOVE = 0x1; /** * 侧滑菜状态标志位 */ private int MENU_STATE_MASK = 0x02; /** * 侧滑菜单 打开状态 */ private int MENU_STATE_OPEN = 0x02; /** * 侧滑菜单 关闭状态 */ private int MENU_STATE_CLOSE = 0x00; /** * 侧滑菜单状态存储器 */ private int mSlideLayoutFlag; /** * 侧滑菜单Id */ private int mSlideMenuId; /** * 侧滑菜单的目标缩放值 */ private float mSlideMenuTargetScale; /** * 主功能View的目标缩放值 */ private float mContentViewTargetScale; /** * 滑动工具类 */ private Scroller mScroller; /** * last touch-event X */ private float lastX; /** * 菜单的宽度 */ private int mSlideMenuWidth; /** * 侧滑菜单View引用 */ private View mSlideMenuView; /** * 主页View引用 */ private View mContentView; /** * 触犯关闭菜单的单击区域 */ private RectF mCloseMenuClickRectF; public SlideLayout(Context context) { this(context, null); } public SlideLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //获取自定义属性 TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.SlideLayout); mSlideMenuId = styledAttributes.getResourceId(R.styleable.SlideLayout_slideMenuId, -1); mSlideMenuTargetScale = styledAttributes.getFloat(R.styleable.SlideLayout_slideMenuTargetScale, 0.7f); mContentViewTargetScale = styledAttributes.getFloat(R.styleable.SlideLayout_contentViewTargetScale, 0.7f); styledAttributes.recycle(); //判断是否成功获取到侧滑菜单Id if (mSlideMenuId == -1) { throw new IllegalArgumentException("Can't find SlideMenuId"); } //初始化标志位状态 mSlideLayoutFlag = ITEM_MENU_CLOSE | DIS_INTERCEPT_EVENT; //初始化滑动工具类 mScroller = new Scroller(context); mCloseMenuClickRectF = new RectF(); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case 0: updateTargetMaskValue(EVENT_MASK, EVENT_CLICK); lastX = event.getX(); break; case 2: if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) { updateTargetMaskValue(EVENT_MASK, EVENT_MOVE); } float deltaX = lastX - event.getX(); if (getScrollX() + deltaX <= -mSlideMenuWidth) { scrollTo(-mSlideMenuWidth, 0); } else if (getScrollX() + deltaX >= 0) { scrollTo(0, 0); } else { scrollBy((int) deltaX, 0); } float currentSlideMenuViewScale = mSlideMenuTargetScale + Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mSlideMenuTargetScale); float currentContentViewScale = 1 - Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mContentViewTargetScale); mSlideMenuView.setScaleX(currentSlideMenuViewScale); mSlideMenuView.setScaleY(currentSlideMenuViewScale); mContentView.setScaleX(currentContentViewScale); mContentView.setScaleY(currentContentViewScale); lastX = event.getX(); break; case 1: case 3: if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) { if (slideMenuIsOpen() && mCloseMenuClickRectF.contains(event.getX(), event.getY())) { updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE); mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0); invalidate(); } } else { if (getScrollX() < -mSlideMenuWidth / 2) { updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_OPEN); mScroller.startScroll(getScrollX(), getScrollY(), -mSlideMenuWidth - getScrollX(), 0); } else { updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE); mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0); } invalidate(); } break; } return true; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); float currentSlideMenuViewScale = mSlideMenuTargetScale + Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mSlideMenuTargetScale); float currentContentViewScale = 1 - Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mContentViewTargetScale); mSlideMenuView.setScaleX(currentSlideMenuViewScale); mSlideMenuView.setScaleY(currentSlideMenuViewScale); mContentView.setScaleX(currentContentViewScale); mContentView.setScaleY(currentContentViewScale); } } /** * 判断菜单是否打开 * * @return */ private boolean slideMenuIsOpen() { return (mSlideLayoutFlag & MENU_STATE_MASK) == MENU_STATE_OPEN; } /** * 更新flag的位值 * * @param mask 目标标记位 * @param value 最新值 */ private void updateTargetMaskValue(int mask, int value) { mSlideLayoutFlag = (mSlideLayoutFlag & ~mask) | (mask & value); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < 2; i++) { View childView = getChildAt(i); if (childView.getId() == mSlideMenuId) { mSlideMenuView = childView; mSlideMenuWidth = childView.getMeasuredWidth(); childView.layout(-mSlideMenuWidth, 0, 0, childView.getMeasuredHeight()); } else { childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); mContentView = childView; } mCloseMenuClickRectF.set( mSlideMenuWidth + (1 - mContentViewTargetScale) * mContentView.getMeasuredWidth() / 2, (1 - mContentViewTargetScale) * mContentView.getMeasuredHeight() / 2, mContentView.getMeasuredWidth(), (0.5f + mContentViewTargetScale / 2) * mContentView.getMeasuredHeight() ); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (getChildCount() != 2) { throw new IllegalArgumentException("The SlideLayout only have two child view"); } measureChildren(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }