activity——main.xml
layout_drawer_menu.xml
layout_main_content.xml
SlideMenu.java
1.继承ViewGroup,重写构造方法
2.重写onMeasure()和onLayout()方法
注:
继承ViewGroup,要摆放子控件,一定需要重写onLayout()方法。(onMeasure --> onLayout --> onDraw)
继承View,一般不需要重写onLayout()方法。(onMeasure --> onDraw)
package com.example.drawerlayout.view;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
import androidx.annotation.RequiresApi;
import androidx.core.view.MenuCompat;
import com.example.drawerlayout.util.LogUtil;
import com.example.drawerlayout.util.StatusBarUtil;
/**
* @Author xxc
* @Date 2019/12/29 0:54
* @Version 1.0
* @Description 自定义控件--侧滑面板/抽屉面板
*/
public class SlideMenu extends ViewGroup {
public static final int MENU_CLOSE = 0;
public static final int MENU_OPEN = 1;
private static final String TAG = SlideMenu.class.getSimpleName();
private Context mContext;
private Activity mActivity;
private int drawerMenuMeasureWidth;
private int drawerMenuWidth;
private int menuState = 0; // 默认菜单为关闭状态
private Scroller scroller;
private float downX;
private float downY;
private float moveX;
public SlideMenu(Context context) {
super(context);
init(context);
}
public SlideMenu(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context context) {
mContext = context;
mActivity = (Activity) context;
// StatusBarUtil.setTranslucent(mActivity);
// 初始化滚动器,数值模拟器
scroller = new Scroller(mContext);
}
/**
* @description: 测量子控件的宽高
* @author: xxc
* @date: 2019/12/29 1:20
* @param widthMeasureSpec 当前控件宽度测量规则
* @param heightMeasureSpec 当前控件高度测量规则
* @return void
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 设置侧滑面板宽高
View drawerMenu = getChildAt(0);
drawerMenuWidth = drawerMenu.getLayoutParams().width;
drawerMenu.measure(drawerMenuWidth, heightMeasureSpec);
View mainContent = getChildAt(1);
// int mainContentWidth = mainContent.getLayoutParams().width;
mainContent.measure(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* @description: 摆放子控件
* @author: xxc
* @date: 2019/12/29 1:26
* @param changed 当前控件大小,位置是否发生改变
* @param left 当前控件左边距
* @param top 当前控件顶边距
* @param right 当前控件右边距
* @param bottom 当前控件底边距
* @return void
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
View drawerMenu = getChildAt(0);
drawerMenuMeasureWidth = drawerMenu.getMeasuredWidth();
drawerMenu.layout(-drawerMenuWidth, 0, 0, bottom);
View mainContent = getChildAt(1);
mainContent.layout(left, top, right, bottom);
}
/**
* 处理触摸事件
* @param event
* @return true -->完全自定义控件时,返回true,消费事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
moveX = event.getX();
// 计算滑动偏移量
int scrollX = (int) (downX - moveX);
// 计算将要滚动到的位置
int newScrollPosition = getScrollX() + scrollX;
if (newScrollPosition < -drawerMenuWidth) {
scrollTo(-drawerMenuWidth, 0);
} else if (newScrollPosition > 0) {
scrollTo(0, 0);
} else {
scrollBy(scrollX, 0);
}
downX = moveX;
break;
case MotionEvent.ACTION_UP:
int drawerMenuCenter = (int) (-drawerMenuWidth / 2.0f);
// 根据当前滚动到的位置,与菜单宽度的一半作比较,确定打开或关闭菜单
if (getScrollX() < drawerMenuCenter) {
// 打开菜单
// scrollTo(-drawerMenuWidth, 0);
menuState = MENU_OPEN;
openOrCloseMenu();
} else {
// 关闭菜单
// scrollTo(0, 0);
menuState = MENU_CLOSE;
openOrCloseMenu();
}
break;
default:
break;
}
return true;
}
/**
* @description: 根据当前状态,执行menu的开/关动画
* @author: xxc
* @date: 2019/12/30 22:57
* @param
* @return
*/
private void openOrCloseMenu() {
int startX = getScrollX();
int dx = 0;
if (menuState == MENU_OPEN) {
// 打开菜单
// dx = 结束位置 - 当前手指滑动到抬起的位置(也就是startX)
dx = -drawerMenuWidth - startX;
} else if (menuState == MENU_CLOSE) {
// 关闭菜单
dx = 0 - startX;
}
// 平滑滚动
/**
* startX:开始滚动的X坐标
* startY:开始滚动的Y坐标
* dx:水平方向要移动的距离
* dy:垂直方向要移动的距离
* duration:动画时长
*/
int duration = Math.abs(dx * 2);
// 1.此时只是模拟了开始平滑的数据
scroller.startScroll(startX, 0, dx, 0, duration);
// 强制重绘界面,从而持续执行动画。-->drawChild()-->computeScroll();
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()) { // 直到duration事件之后结束
// 获取要滚动到的位置
int currX = scroller.getCurrX();
LogUtil.d(TAG + ":" + currX);
scrollTo(currX, 0);
invalidate();
}
}
/**
* 触摸事件拦截判断
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = ev.getX();
downY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float scrollX = Math.abs(ev.getX() - downX);
float scrollY = Math.abs(ev.getY() - downY);
if (scrollX > scrollY && scrollX > 5) {
// 拦截事件,交由自己的onTouchEvent()处理事件
return true;
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
public void openMenu() {
menuState = MENU_OPEN;
openOrCloseMenu();
}
public void closeMenu() {
menuState = MENU_CLOSE;
openOrCloseMenu();
}
public int getMenuState() {
return menuState;
}
public void switchMenuState() {
if (menuState == MENU_CLOSE) {
openMenu();
} else if (menuState == MENU_OPEN) {
closeMenu();
}
}
}
在onTouchEvent()中,左右滑动打开或关闭侧滑面板。
简单说一下scrollTo与scrollBy:
scrollTo是每次从最开始位置滚动到新位置,如scrollTo(0,0) --> scrollTo(10, 0),scrollTo(0,0) --> scrollTo(20, 0);(想象成一个点最终滚动到(20, 0)这个位置)。
scrollBy是每次在新的位置继续滚动,如scrollBy(0,0) --> scrollBy(10, 0) --> scrollBy(20, 0);(想象成一个点最终滚动到(30, 0)这个位置)。
所以在onTouchEvent里需要注意ACTION_MOVE的处理。
注:这里还有一个问题(downX-moveX)在向右滑动时是为负的,你可能会有疑问,手机不是向右和向下为正吗,为什么向右滑动,scroll**的X却是取负值呢?我们要显示的菜单是在屏幕的左侧的,当我们向右滑动时菜单显示出来,此时真正被滑动的并不是菜单,而是手机四周的一个框,其相对于我们的手势做反方向运动,从而将菜单面板显示出来。(想象一下手机屏幕就是一个框,这个框左侧还没显示的就是菜单面板,当我们手势向右,框就向左运动,从而将菜单面板显示出来!)
ACTION_UP中根据根据打开的菜单面板大小判断是要完全打开或者关闭菜单。
为了不使打开或关闭菜单太过突兀,在openOrCloseMenu()方法中执行了一个平移动画。
其中invalidate()方法会使界面重绘,从而持续执行动画(computeScroll()),直到完全打开或关闭菜单。
onInterceptTouchEvent()方法中,根据情况拦截事件,执行自己的onTouchEvent()方法,使得在菜单范围内的左右滑动事件也生效,可以关闭or打开菜单!
BaseActivity(稍微搞了一下沉浸式状态栏,没搞好)
package com.example.drawerlayout.activity;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.example.drawerlayout.R;
import com.example.drawerlayout.util.StatusBarUtil;
import com.example.drawerlayout.util.StatusBarUtils;
public abstract class BaseActivity extends AppCompatActivity {
private Context mContext;
private Activity mActivity;
private Context appContext;
private Application mApplication;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStatusBar();
}
protected void setStatusBar() {
/**
* 这里做了两件事情,
* 1.使状态栏透明并使contentView填充到状态栏
* 2.预留出状态栏的位置,防止界面上的控件离顶部靠的太近。这样就可以实现开头说的第二种情况的沉浸式状态栏了
*/
StatusBarUtil.setTransparent(this);
}
}
MainActivity
package com.example.drawerlayout.activity;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import com.example.drawerlayout.R;
import com.example.drawerlayout.util.StatusBarUtil;
import com.example.drawerlayout.view.SlideMenu;
public class MainActivity extends BaseActivity {
private ImageButton mBackDrawerMenu;
private SlideMenu mainSlideMenu;
// private Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setStatusBar();
initViews();
onClickEvent();
}
/**
* @description: 实现控件的点击事件
* @author: xxc
* @date: 2019/12/30 23:51
* @param
* @return
*/
private void onClickEvent() {
mBackDrawerMenu.setOnClickListener(v -> {
mainSlideMenu.switchMenuState();
});
}
/**
* @description: 初始化控件
* @author: xxc
* @date: 2019/12/30 23:48
* @param
* @return
*/
private void initViews() {
mBackDrawerMenu = findViewById(R.id.image_for_drawer_menu);
mainSlideMenu = findViewById(R.id.main_slide_menu);
}
// @RequiresApi(api = Build.VERSION_CODES.M)
@Override
protected void setStatusBar() {
// StatusBarUtil.setColor(this, getColor(R.color.status_bar));
// 0:表示完全透明
StatusBarUtil.setColor(this, getResources().getColor(R.color.status_bar), 0);
}
}