效果
代码实现
/**
* 纵向抽屉
*/
public class VerticalDrawerLayout extends FrameLayout {
/**
* 触发阈值
*/
private static final float TRIGGER_THRESHOLD_VALUE = 0.3f;
private ViewDragHelper mViewDragHelper;
/**
* 唯一内容子View
*/
private View mContentView;
/**
* 当前内容子View的Top值,由于子View引起重绘或addView引起重绘会导致requestLayout(),会重新onLayout()子View
* 而layout子View,要在onLayout中处理。如果在onViewPositionChanged()中单独onLayout,并且又没有保存top值为成员变量,就会因为其他原因引起的requestLayout()
* 导致子View的top值默认值为0,会突然抖回顶部
*/
private int mCurrentTop = 0;
/**
* 是否已打开
*/
private boolean isOpened = false;
/**
* 拽托状态回调
*/
private OnDragStateChangeListener mOnDragStateChangeListener;
public VerticalDrawerLayout(Context context) {
super(context);
init();
}
public VerticalDrawerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public VerticalDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mViewDragHelper = ViewDragHelper.create(this, 0.5f, new ViewDragHelperCallBack());
}
private class ViewDragHelperCallBack extends ViewDragHelper.Callback {
/**
* 开始拽托时的X坐标
*/
private int mDownX;
/**
* 开始拽托时的Y坐标
*/
private int mDownY;
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
//返回ture则表示可以捕获该view,手指摸上一瞬间调用
return child == mContentView;
}
@Override
public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
mDownX = capturedChild.getLeft();
mDownY = capturedChild.getTop();
}
@Override
public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
//手指触摸移动时实时回调, top表示要到的y位置
//限制只想向上滑
if (top > 0) {
return 0;
}
//最多只能滑动到内容View的高度
int maxTop = -mContentView.getHeight();
return Math.max(top, maxTop);
}
@Override
public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
//手指释放时回调
int finalLeft = releasedChild.getLeft();
final int currentLeft = releasedChild.getLeft();
final int currentTop = releasedChild.getTop();
//计算移动距离
int distanceX = currentLeft - mDownX;
int distanceY = currentTop - mDownY;
//滚动到顶部
Runnable scrollToTop = new Runnable() {
@Override
public void run() {
int finalTop = -mContentView.getHeight();
mViewDragHelper.settleCapturedViewAt(finalLeft, finalTop);
}
};
//滚动到底部
Runnable scrollToBottom = new Runnable() {
@Override
public void run() {
mViewDragHelper.settleCapturedViewAt(finalLeft, 0);
}
};
//根据移动距离占总大小的百分比,决定滚动到顶部还是底部
Runnable scrollByDistancePercentage = new Runnable() {
@Override
public void run() {
//当前已移动距离的百分比值
float movePercentage = Math.abs((releasedChild.getTop() * 1f) / releasedChild.getHeight());
//剩余的内容,小于阈值,则移动到顶部
if (movePercentage >= TRIGGER_THRESHOLD_VALUE) {
scrollToTop.run();
} else {
//小于阈值,回弹回底部
scrollToBottom.run();
}
}
};
//判断是否是上下滑,左右滑不需要动
if (Math.abs(distanceY) > Math.abs(distanceX)) {
//上下滑,速度很快,则为fling操作
if (Math.abs(yvel) > Math.abs(mViewDragHelper.getMinVelocity())) {
//距离相减为负数,并且惯性速度为正数(如果先上滑,再回去,速度值会为负),则为往下滑
if (distanceY < 0 && yvel > 0) {
scrollToBottom.run();
} else {
//否则为向上滑
scrollToTop.run();
}
} else {
//不是fling操作,根据移动距离占总大小的百分比,决定滚动到顶部还是底部
scrollByDistancePercentage.run();
}
}
invalidate();
}
@Override
public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
mCurrentTop = top;
//统一在onLayout中,重新布局子View位置,来移动子View
requestLayout();
}
@Override
public int getViewVerticalDragRange(@NonNull View child) {
if (mContentView == null) {
return 0;
}
if (mContentView == child) {
return mContentView.getHeight();
} else {
return 0;
}
}
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
if (state == ViewDragHelper.STATE_DRAGGING) {
if (mOnDragStateChangeListener != null) {
mOnDragStateChangeListener.onStartDrag();
}
} else if (state == ViewDragHelper.STATE_IDLE) {
if (mOnDragStateChangeListener != null) {
mOnDragStateChangeListener.onStopDrag();
}
isOpened = (mContentView.getTop() == -mContentView.getHeight());
if (isOpened) {
if (mOnDragStateChangeListener != null) {
mOnDragStateChangeListener.onDragToTop();
}
}
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//移动内容子View
MarginLayoutParams params = (MarginLayoutParams) mContentView.getLayoutParams();
mContentView.layout(
params.leftMargin,
mCurrentTop + params.topMargin,
mContentView.getMeasuredWidth() + params.leftMargin,
mCurrentTop + mContentView.getMeasuredHeight() + params.topMargin
);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
int childCount = getChildCount();
if (childCount != 1) {
throw new RuntimeException("子View必须只有1个,就是抽屉View");
}
mContentView = getChildAt(0);
}
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (isOpened) {
return super.onInterceptTouchEvent(ev);
}
//将onInterceptTouchEvent委托给ViewDragHelper
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isOpened) {
return super.onTouchEvent(event);
}
//将onTouchEvent委托给ViewDragHelper
mViewDragHelper.processTouchEvent(event);
return true;
}
/**
* 打开抽屉
*/
public void openDrawer() {
mViewDragHelper.smoothSlideViewTo(mContentView, mContentView.getLeft(), 0);
invalidate();
}
/**
* 关闭抽屉
*/
public void closeDrawer() {
int finalTop = -mContentView.getHeight();
mViewDragHelper.smoothSlideViewTo(mContentView, mContentView.getLeft(), finalTop);
invalidate();
}
/**
* 是否已打开
*/
public boolean isOpened() {
return isOpened;
}
/**
* 结束回调
*/
public interface OnDragStateChangeListener {
void onStartDrag();
void onStopDrag();
void onDragToTop();
}
public void setOnDragStateChangeListener(OnDragStateChangeListener listener) {
this.mOnDragStateChangeListener = listener;
}
}
使用
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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">
//首页
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
//...
</FrameLayout>
//抽屉
<com.zh.verticaldrawerlayout.VerticalDrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/ic_screen_lock" />
</com.zh.verticaldrawerlayout.VerticalDrawerLayout>
</FrameLayout>
VerticalDrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
//设置拽托监听
drawerLayout.setOnDragStateChangeListener(new VerticalDrawerLayout.OnDragStateChangeListener() {
@Override
public void onStartDrag() {
//开始拽托
}
@Override
public void onStopDrag() {
//结束拽托
}
@Override
public void onDragToTop() {
//抽屉关闭
}
});
//打开抽屉
drawerLayout.openDrawer();
//关闭抽屉
drawerLayout.closeDrawer();