版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、DrawerLayout 的 demo
先来看一下 DrawerLayout 的简单 demo。
效果:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
tools:context="com.xiaoyue.drawerlayout.MainActivity">
<android.support.v4.widget.DrawerLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_editor_absolutyX="5dp"
tools:layout_editor_absolutyY="5dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/xiaoyue"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent"
android:layout_gravity="start">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="侧滑菜单" />
</LinearLayout>
</android.support.v4.widget.DrawerLayout>
</LinearLayout>
主要在布局文件里面引用 DrawerLayout 即可,DrawerLayout 下的子控件,当添加属性android:layout_gravity 的值 为 “start”,即可设置为侧边菜单。
上面虽然在侧边菜单设置宽度为 match_parent,但实际显示效果并没有占满屏幕。这是抽屉里的宽度不能超过320 dp, 所以用户总是可以看到内容视图的一部分。
**注:这边不要在最外层使用 ConstraintLayout。**DrawerLayout 要求宽高必须是 MeasureSpec.EXACTLY。正常情况下 match_parent 和具体值都是 MeasureSpec.EXACTLY,但 ConstraintLayout 的 match_parent,有点不同,不是。
二、DrawerLayout 方法介绍
1. setDrawerLockMode
锁住 DrawerLayout 的指定模式(侧滑界面出现或关闭),不再允许滑动改变。
setDrawerLockMode(@LockMode int lockMode):
传入指定的锁住模式,左右两边都不在允许侧滑。
setDrawerLockMode(@LockMode int lockMode, @EdgeGravity int edgeGravity):
传入指定的锁住模式和哪一边的侧滑进行锁住,对选择的侧滑进行锁住。
setDrawerLockMode(@LockMode int lockMode, View drawerView):
传入指定的锁住模式和要进行锁住的侧滑 view。
2.addDrawerListener
添加滑动的监听:
drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
/**
* 监听滑动距离
* @param drawerView 侧滑菜单
* @param slideOffset 滑动的百分比(完全打开的时候为 1)
*/
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
}
/**
* 当侧滑打开的时候
* @param drawerView
*/
@Override
public void onDrawerOpened(View drawerView) {
}
/**
* 当侧滑关闭的时候
* @param drawerView
*/
@Override
public void onDrawerClosed(View drawerView) {
}
/**
* 当状态改变的时候(有三种状态)
* DrawerLayout.STATE_IDLE 静止
* DrawerLayout.STATE_DRAGGING 准备滑动(手指一点上去就会触发)
* DrawerLayout.STATE_SETTLING 滑动完成之后
* @param newState
*/
@Override
public void onDrawerStateChanged(int newState) {
}
});
比较重要的是 onDrawerSlide,onDrawerStateChanged 的三种状态最好自己日志添加去试一下,不好讲清楚。
三、自定义侧滑菜单
1.效果
这边如果说侧滑菜单只使用一个自定义 ViewGroup 进行实现的话,会比较复杂。
1.要重写 onLayout 方法,当然也可以直接继承 LinearLayout 等。
2.ViewGroup 的 onDraw 方法不一定都会被调用。如果重写 dispatchOnDraw 方法,又会导致每个子控件也进行重新绘制。
3.监听事件的处理。
这边采用职责分明的策略,用多个自定义控件组合实现这个效果。
1.实现 MyDrawerLayout 继承 DrawerLayout,用自带的监听处理滑动事件
2.实现 MyDrawerLayoutSlideBar 继承LinearLayout,处理子控件的摆放问题
3.实现 MyDrawerLayoutBgView 继承 View,进行侧滑菜单的背景绘制(蓝色背景部分,绘制区域随手指变化)
4.实现 MyDrawerBgRelativeLayout 继承 RelativeLayout,把 MyDrawerLayoutSlideBar 和 MyDrawerLayoutBgView 组合在一起,形成真正的侧滑菜单
2.布局文件
MainActivity:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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"
tools:context="com.xiaoyue.drawerlayout.MainActivity">
<com.xiaoyue.widget.mydrawerlayout.MyDrawerLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--内容 区域-->
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@drawable/fake"
/>
<!--侧滑区域 LinearLayout ColorDrawable -->
<com.xiaoyue.widget.mydrawerlayout.MyDrawerLayoutSlideBar
android:layout_width="200dp"
android:layout_height="match_parent"
android:layout_gravity="start"
app:maxTranslationX="66dp"
android:background="@color/colorPrimaryDark"
>
<TextView
style="@style/MenuText"
android:drawableLeft="@drawable/circle"
android:text="朋友圈" />
<TextView
style="@style/MenuText"
android:drawableLeft="@drawable/wallet"
android:text="钱包" />
<TextView
style="@style/MenuText"
android:drawableLeft="@drawable/coupon"
android:text="优惠券" />
</com.xiaoyue.widget.mydrawerlayout.MyDrawerLayoutSlideBar>
</com.xiaoyue.widget.mydrawerlayout.MyDrawerLayout>
</RelativeLayout>
修改完成的自定义控件的使用尽量保证与原先的使用风格一样,这边在布局文件中就像是使用 DrawerLayout 一样。
3.自定义的 DrawerLayout
MyDrawerLayout:
public class MyDrawerLayout extends DrawerLayout implements DrawerLayout.DrawerListener{
//侧滑菜单
private MyDrawerLayoutSlideBar mSlideBar;
//用来装载 MyDrawerLayoutSlideBar
private MyDrawerBgRelativeLayout mBgRelativeLayout;
//内容
private View mContenView;
//侧滑菜单的滑动百分比
private float mSlideOffset;
//当前手指触摸的 Y 坐标
private float mTouchY;
public MyDrawerLayout(Context context) {
super(context);
}
public MyDrawerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
init();
}
private void init() {
//循环子控件,获取内容的 View 和侧滑菜单
for(int i = 0; i < getChildCount(); i ++) {
View childView = getChildAt(i);
if (childView instanceof MyDrawerLayoutSlideBar) {
mSlideBar = (MyDrawerLayoutSlideBar) childView;
} else {
mContenView = childView;
}
}
//偷梁换柱
//为 MyDrawerLayoutSlideBar 添加一层 MyDrawerBgRelativeLayout包装
//先移除 MyDrawerLayoutSlideBar
removeView( mSlideBar);
//把 MyDrawerLayoutSlideBar 添加到 MyDrawerBgRelativeLayout 下
mBgRelativeLayout = new MyDrawerBgRelativeLayout(mSlideBar);
addView(mBgRelativeLayout);
addDrawerListener(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//不能在 OnTouch 中获取 Y 的坐标,避免事件被子控件消费的时候,获取不到
mTouchY = ev.getY();
if (ev.getAction() == MotionEvent.ACTION_UP) {
//手指松开的时候,侧滑菜单关闭
closeDrawers();
mSlideBar.onMotionUp(mTouchY);
return super.dispatchTouchEvent(ev);
}
if (mSlideOffset == 1 ) {
mBgRelativeLayout.setTouchY(mTouchY, mSlideOffset);
}
return super.dispatchTouchEvent(ev);
}
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
mSlideOffset = slideOffset;
//传递触摸 Y 坐标,触发背景动画
mBgRelativeLayout.setTouchY(mTouchY, slideOffset);
//设置内容区域向右偏移为侧滑菜单的一半,形成视觉差
float contentViewOffset = drawerView.getWidth() * slideOffset / 2;
mContenView.setTranslationX(contentViewOffset);
}
@Override
public void onDrawerOpened(View drawerView) {
}
@Override
public void onDrawerClosed(View drawerView) {
}
@Override
public void onDrawerStateChanged(int newState) {
}
}
MyDrawerLayout 主要做三件事:一是对布局文件中的侧滑菜单 MyDrawerLayoutSlideBar 进行了一次包装,偷偷把 MyDrawerLayoutSlideBar 换成 MyDrawerBgRelativeLayout,并添加 MyDrawerLayoutBgView,这也是核心的一个实现思路。二是对侧滑菜单的滑动进行监听,实现对内容区域的同步滑动。三是监听手指的动作,每一次滑动进行背景的变化,以及每次手指离开屏幕的时候,关闭侧滑菜单。
4.侧滑菜单
MyDrawerLayoutSlideBar:
public class MyDrawerLayoutSlideBar extends LinearLayout {
//子控件的最大偏移量
private float maxTranslationX;
public MyDrawerLayoutSlideBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
//设置摆放方向为竖直方向
setOrientation(VERTICAL);
if (attrs != null) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SideBar);
maxTranslationX = typedArray.getDimension(R.styleable.SideBar_maxTranslationX, 0);
typedArray.recycle();
}
}
/**
* 对子控件进行相应的偏移
* @param y 当前触摸点 Y 坐标
* @param slideOffset 侧滑栏菜单滑出的百分比
*/
public void setTouchY(float y, float slideOffset) {
//遍历全部子控件 给每一个子控件进行偏移
for (int i=0;i<getChildCount();i++) {
View chlid= getChildAt(i);
//偏移方法
apply(getParent(), chlid, y, slideOffset);
}
}
/**
* 对子控件进行偏移
* @param parent 父控件
* @param childView 要偏移的子控件
* @param y 偏移最大的 Y 坐标
* @param slideOffset 偏移比例
*/
private void apply(ViewParent parent, View childView, float y, float slideOffset) {
//计算子控件的中点 Y 坐标
int centerY = (childView.getTop() + childView.getBottom()) / 2;
//计算子控件中点与手指触摸的 Y 方向距离
float distanceY = Math.abs(y - centerY);
//计算子控件偏移距离
float scale = distanceY / getHeight() * 3; //3 放大系数
float translationX = maxTranslationX * (1f - scale);
childView.setTranslationX(translationX);
}
/**
* 手指松开的时候处理
* @param y 触摸点 Y 坐标
*/
public void onMotionUp(float y) {
View childView;
for (int i=0; i<getChildCount(); i++) {
childView = getChildAt(i);
childView.setPressed(false);
//要判断 y坐落在哪一个子控件 松手的那一刻 进行回调 跳转其他页面
boolean isHover = y > childView.getTop() && y < childView.getBottom();
if (isHover) {
childView.performClick();
//回调操作,可以采用监听,这边使用吐司只是为了验证回调到了
Toast toast = Toast.makeText(getContext(), ((TextView) childView).getText(), Toast.LENGTH_SHORT);
toast.show();
break;
}
}
}
}
MyDrawerLayoutSlideBar 是被引用的布局文件里,从表面看这个是侧滑菜单,实际是上只是管理侧滑菜单的每一个子控件。当手指触摸屏幕进行滑动时候,背景变化,每个子控件做出相应的位移(位移大小没有具体参考值,需要对实际情况进行调整)。另外是在手指松开的时候,会去判断调用哪一个子控件的触摸方法(只根据 Y 进行判断,没有进行 X 的处理)。
5.背景绘制
MyDrawerLayoutBgView:
public class MyDrawerLayoutBgView extends View {
//画笔
private Paint mPaint;
//路径
private Path mPath;
private Drawable mDrawable;
public MyDrawerLayoutBgView(Context context) {
this(context, null);
}
public MyDrawerLayoutBgView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化参数
*/
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPath = new Path();
}
/**
* 设置当前触摸点的 Y 坐标,从而改变背景波浪
* @param y 当前触摸点 Y 坐标
* @param slideOffset 侧滑栏菜单滑出的百分比
*/
public void setTouchY(float y, float slideOffset) {
//重置路径
mPath.reset();
//获取侧滑菜单滑出来的宽度
float width = getWidth() * slideOffset;
float height = getHeight();
//计算贝塞尔曲线 Y 方向超出去的距离(8 效果可能会好一些)
float offsetY = height / 8;
//计算被赛尔曲线 X 方向偏移的距离(也是为了效果好一些)
float offsetX = width / 2;
mPath.lineTo(offsetX, - offsetY);
mPath.quadTo( width * 3 / 2, y, offsetX , height + offsetY);
mPath.lineTo(0,height);
mPath.close();
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDrawable == null) {
//绘制背景为颜色的
canvas.drawPath(mPath, mPaint);
} else {
//绘制背景为图片的,根据 Path 对 图片进行截取
mDrawable.setBounds(0, 0, getWidth(), getHeight());
canvas.clipPath(mPath);
mDrawable.draw(canvas);
}
}
/**
* 设置背景
* (供 MyDrawerBgRelativeLayout 传递 MyDrawerLayoutSlideBar 的 background )
* @param drawable
*/
public void setDrawable(Drawable drawable) {
if (drawable instanceof ColorDrawable) {
//支持背景为颜色
mPaint.setColor(((ColorDrawable) drawable).getColor());
}else {
//支持背景为图片
mDrawable = drawable;
}
}
}
MyDrawerLayoutBgView 对贝塞尔曲线(蓝色背景)进行确认,背景是在 MyDrawerBgRelativeLayout包装 MyDrawerLayoutSlideBar 的时候,把 MyDrawerLayoutSlideBar 的背景传递进来(目前支取支持颜色和背景图片,其他未尝试)。