效果
代码实现
/**
* 上下拽托移动布局
*/
public class VerticalDragLayout extends FrameLayout {
/**
* 触发阈值
*/
private static final float TRIGGER_THRESHOLD_VALUE = 0.75f;
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 isClosed = true;
/**
* 拽托状态回调
*/
private OnDragStateChangeListener mOnDragStateChangeListener;
public VerticalDragLayout(Context context) {
super(context);
init();
}
public VerticalDragLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public VerticalDragLayout(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.min(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 minMoveDistance = releasedChild.getHeight() / 3;
//计算移动距离
int distanceX = currentLeft - mDownX;
int distanceY = currentTop - mDownY;
//滚动到顶部
Runnable scrollToTop = new Runnable() {
@Override
public void run() {
mViewDragHelper.settleCapturedViewAt(finalLeft, 0);
}
};
//滚动到底部
Runnable scrollToBottom = new Runnable() {
@Override
public void run() {
int finalTop = releasedChild.getHeight();
mViewDragHelper.settleCapturedViewAt(finalLeft, finalTop);
}
};
//根据移动距离占总大小的百分比,决定滚动到顶部还是底部
Runnable scrollByDistancePercentage = new Runnable() {
@Override
public void run() {
//当前已移动距离的百分比值
float movePercentage = (releasedChild.getTop() * 1f) / releasedChild.getHeight();
//剩余的内容占的百分比
float remainDistancePercentage = 1f - movePercentage;
//剩余的内容,小于阈值,则移动到底部
if (remainDistancePercentage <= TRIGGER_THRESHOLD_VALUE) {
scrollToBottom.run();
} else {
//小于阈值,回弹回顶部
scrollToTop.run();
}
}
};
//判断是否是上下滑,左右滑不需要动
if (Math.abs(distanceY) > Math.abs(distanceX)) {
//上下滑,滑动得少,并且速度很快,则为fling操作
if (Math.abs(distanceY) < minMoveDistance && 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();
}
isClosed = (mContentView.getTop() == mContentView.getHeight());
if (isClosed) {
if (mOnDragStateChangeListener != null) {
mOnDragStateChangeListener.onDragToBottom();
}
}
}
}
}
@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) {
//将onInterceptTouchEvent委托给ViewDragHelper
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//将onTouchEvent委托给ViewDragHelper
mViewDragHelper.processTouchEvent(event);
return true;
}
public boolean isClosed() {
return isClosed;
}
/**
* 结束回调
*/
public interface OnDragStateChangeListener {
void onStartDrag();
void onStopDrag();
void onDragToBottom();
}
public void setOnDragStateChangeListener(OnDragStateChangeListener listener) {
this.mOnDragStateChangeListener = listener;
}
}
使用
<com.psy1.cosleep.library.view.VerticalDragLayout 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:id="@+id/vertical_drag_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
//....
</com.psy1.cosleep.library.view.VerticalDragLayout>
VerticalDragLayout verticalDragLayout = findViewById(R.id.vertical_drag_layout);
verticalDragLayout.setOnDragStateChangeListener(new VerticalDragLayout.OnDragStateChangeListener() {
@Override
public void onStartDrag() {
//开始拽托
}
@Override
public void onStopDrag() {
//停止拽托
}
@Override
public void onDragToBottom() {
//拽托到底部,可关闭页面
}
});