自定义View-侧滑菜单
-
-
- 一、搭建基本框架
- 二、实现侧滑菜单功能
- 三、处理单机事件关闭侧滑菜单
- 四、代码
-
最近在使用酷我音乐软件时看到它的侧滑菜单,突然想起来当年qq5.0时的侧滑菜单,虽然网上有很多实现方式,但是为了纪念我Q还是自己来码一个纪念我的青春,效果如下图,啥也不说来直接开干吧!![]()
自定义View-侧滑菜单
一、搭建基本框架
- 创建类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
2)在构造函数<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="SlideLayout" > <!--侧滑菜单模块的ViewId--> <attr name="slideMenuId" format="reference" /> </declare-styleable> </resources>
中获取侧滑菜单的public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr)
,若未能获取到ViewId
则抛出异常侧滑菜单Id
3)进行/** * 侧滑菜单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"); } }
操作,如果是我们的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>
由于我们还未编码侧滑菜单的打开和关闭,所以下现在无法看到侧滑菜单,下面来编码打开关闭侧滑菜单部分。自定义View-侧滑菜单
二、实现侧滑菜单功能
经分析可知,我们的菜单主要是通过滑动来打开,再通过滑动或点击部分区域来关闭,因此我们要明确是否发生来滑动事件且要实时更新当前事件类型和菜单的状态。
- 定义侧滑菜单状态
、MENU_STATE_OPEN-打开状态
及Touch事件MENU_STATE_CLOSE-关闭状态、MENU_STATE_MASK-侧滑菜单状态标志位
和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
2)尽管上面已经定义的事件类型,为了更简单直接,我们先处理滑动事件,后面再回过头来处理点击事件,想要能够滑动我们只需要知道滑动的距离及方向即可,因此我们要记录上次@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); }
的TouchEvent
坐标,定义变量X
来存储,且在lastX
的TouchEvent
事件中进行初始化,然后在Down
事件中计算我们需要滑动距离,ACTION_MOVE
表示滑动的方向,正负号
表示内容向右滑动即打开菜单正号表示内容向左滑动即关闭菜单,所以我们计算负号
即是我们需要移动的差值且包含来正确的方向,调用方法floatdeltaX=lastX-event.getX()
即完成来此处滑动,但是我们还需要注意滑动距离的范围问题 ,有可能我们的滑动超过来菜单的宽度,导致我们看到一片空白,所以我们还要对滑动的距离作出限制,因为滑动范围是和侧滑菜单的宽度有关系的,所以在scrollBy(deltaX,0)
方法中获取到我们侧滑菜单的宽度用onlayout
存储,则滑动的距离mSlideMenuWidth
的范围为mScrollX
,我们根据[-mSlideMenuWidth,0]
的值判断是否在这个范围内,若getScrollX+deltaX
则sgetScrollX()+deltaX<=mSlideMenuWidth
,若crollTo(-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;
}
运行效果如下:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiZpdmL2cjN4UzNzMjM2EDOwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.gif)
至此我们的仿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); } }