优雅的实现侧滑抽屉-HorizontalDrawerLayout的制作

2018-02-09 12:44:43来源:https://www.jianshu.com/p/4d9f228d790d作者:FrankChoo人点击

分享


使用场景

RecyclerView的侧滑菜单


效果展示


侧滑抽屉.gif
使用方式
在xml布局中
<com.frank.library.HorizontalDrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/hdl_container"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#eca726">
<!--抽屉布局-->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right">
<TextView
android:id="@+id/tv_delete"
android:layout_width="70dp"
android:layout_height="match_parent"
android:background="#eca726"
android:gravity="center"
android:text="删除" />
<TextView
android:id="@+id/tv_top"
android:layout_width="70dp"
android:layout_height="match_parent"
android:background="#dcdcdc"
android:gravity="center"
android:text="置顶" />
</LinearLayout>
<!--主体布局-->
<TextView
android:id="@+id/tv_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:gravity="center"
android:text="12345" />
</com.frank.library.HorizontalDrawerLayout>

代码使用
为了方便代码展示, 这里使用了 kotlin
CommonRecyclerAdapter 是一个通用的 RecyclerView.Adapter 封装类, 这里不用纠结其实现过程

class DemoAdapter : CommonRecyclerAdapter<String> {
constructor(context: Context?, data: List<String>?) : super(context, data)
override fun getLayoutRes(data: String?, position: Int): Int = if (position % 2 == 0)
R.layout.item_demo_rcv_right else R.layout.item_demo_rcv_left
override fun convert(holder: CommonViewHolder, data: String, position: Int) {
// 绑定主体文本
holder.setText(R.id.tv_item, data)
// 设置侧滑点击
val drawerLayout = holder.getView<HorizontalDrawerLayout>(R.id.hdl_container)
// 根据 position 的奇偶性, 来判断抽屉拖拽的方向
drawerLayout.setDirection(if (position % 2 == 0)
HorizontalDrawerLayout.Direction.RIGHT else HorizontalDrawerLayout.Direction.LEFT)
holder.getView<TextView>(R.id.tv_delete).setOnClickListener {
Toast.makeText(it.context, "delete", Toast.LENGTH_SHORT).show()
// 点击之后抽屉关闭的回调
drawerLayout.closeDrawer{
Toast.makeText(it.context, "delete", Toast.LENGTH_SHORT).show()
}
}
holder.getView<TextView>(R.id.tv_top).setOnClickListener {
Toast.makeText(it.context, "top", Toast.LENGTH_SHORT).show()
// 不想要回调可以传 null
drawerLayout.closeDrawer(null)
}
}
}

实现思路
自定义ViewGroup去实现
创建一个HorizontalDrawerLayout布局, 布局中只能存放两个View, 第一个为MenuView, 第二个为ConcreateView
MenuView是固定的不可拖动的
通过拖动ConcreateView只可以在水平方向上拖动
处理好与RecyclerView和子View点击事件的冲突
事件拦截
必须大于View响应点击事件的距离(这里选择了1/2, 防止滑动时出现不连贯的效果)
水平方向上的滑动距离必须大于竖直方向
具体实现
/**
* Created by FrankChoo on 2017/10/20.
* Email: frankchoochina@gmail.com
* Version: 1.2
* Description: 水平侧滑抽屉的布局, 只能包含两个子View, 第一个为固定的菜单, 第二个为可以拖拽的部分
*/
public class HorizontalDrawerLayout extends FrameLayout {
// 抽屉的状态
private final static int STATUS_OPENED = 0x00000001;
private final static int STATUS_CLOSED = 0x00000002;
private final static int STATUS_DRAG = 0x00000003;
private Direction mDirection = Direction.RIGHT;
private int mCurrentStatus = STATUS_CLOSED;
// 能否拖拽
private boolean mIsDraggable = true;
private ViewDragHelper mDragHelper;
private ViewDragHelper.Callback mCallback;
private View mDragView;
private int mDrawerWidth = 0;
private float mDownX = 0;
private float mDownY = 0;
public HorizontalDrawerLayout(Context context) {
this(context, null);
}
public HorizontalDrawerLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HorizontalDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
// 初始化Callback
mCallback = new ViewDragHelper.Callback() {
/**
* 尝试去捕获View
* @return true 表示可以拖动这个child
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 只允许拖动mDragView
if (child == mDragView) return true;
return false;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
switch (mDirection) {
case RIGHT: {
if (left > 0) return 0;
break;
}
case LEFT: {
if (left < 0) return 0;
break;
}
}
return left;
}
/**
* 相当于Up事件, 手指松开时View的走向
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 开启抽屉的两个条件: 水平速度达到1500/ 拖动距离大于需求宽度的一半
switch (mDirection) {
case RIGHT: {
if (Math.abs(xvel) > 1500) {
// 小于0代表左滑
if (xvel < 0) openDrawer();
else closeDrawer();
break;
}
if (Math.abs(mDragView.getLeft()) > mDrawerWidth / 2) {
openDrawer();
} else {
closeDrawer();
}
break;
}
case LEFT: {
if (Math.abs(xvel) > 1500) {
// 小于0代表左滑
if (xvel < 0) closeDrawer();
else openDrawer();
break;
}
if (Math.abs(mDragView.getLeft()) > mDrawerWidth / 2) {
openDrawer();
} else {
closeDrawer();
}
break;
}
}
invalidate();
}
};
mDragHelper = ViewDragHelper.create(this, mCallback);
}
/**
* 设置抽屉的方位
* 默认为右边的抽屉
*/
public void setDirection(Direction direction) {
mDirection = direction;
}
/**
* 设置是否可以拖拽的选项
*/
public void setDraggable(boolean isDraggable) {
if (getChildCount() < 2) {
mIsDraggable = false;
}
mIsDraggable = isDraggable;
}
/**
* 暴露给外界关闭抽屉的方法
*/
public void closeDrawer(final OnDrawerClosedListener listener) {
ValueAnimator animator = ValueAnimator.ofFloat(mDirection == Direction.RIGHT
? -mDrawerWidth : mDrawerWidth, 0f).setDuration(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float left = (float) animation.getAnimatedValue();
mDragView.layout((int) left, mDragView.getTop(),
(int) (left + mDragView.getMeasuredWidth()), mDragView.getBottom());
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (listener != null) {
listener.onClosed();
}
}
});
animator.start();
}
private void openDrawer() {
// 将DragView移动到抽屉开启的位置
mDragHelper.settleCapturedViewAt(
mDirection == Direction.RIGHT ? -mDrawerWidth : mDrawerWidth, 0);
mCurrentStatus = STATUS_OPENED;
}
private void closeDrawer() {
// 将DragView恢复到初始位置
mDragHelper.settleCapturedViewAt(0, 0);
mCurrentStatus = STATUS_CLOSED;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (getChildCount() == 1) { // 只有一个子View则说明不需要拖拽
mDragView = getChildAt(0);
mDrawerWidth = 0;
} else if (getChildCount() == 2) { // 有两个子View, 则允许拖拽
mDragView = getChildAt(1);
// 将拖拽View设置为可拖拽, 不让底层的view去响应点击事件
mDragView.setClickable(true);
if (mDrawerWidth == 0) {
mDrawerWidth = getChildAt(0).getMeasuredWidth();
}
} else if (getChildCount() > 2) {
throw new RuntimeException("HorizontalDrawerLayout只能存在两个子View(第一个为Drawer, 第二个为主体)");
}
super.onLayout(changed, left, top, right, bottom);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 处理不允许拖拽的情况
if (!mIsDraggable) {
return super.onInterceptTouchEvent(ev);
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
// 更新手指落下的坐标
mDownX = ev.getRawX();
mDownY = ev.getRawY();
mDragHelper.processTouchEvent(ev);
break;
}
case MotionEvent.ACTION_MOVE: {
float deltaX = mDownX - ev.getRawX();
float deltaY = mDownY - ev.getRawY();
// 1. 必须大于View响应点击事件的距离(这里选择了1/2, 防止滑动时出现不连贯的效果)
// 2. 水平方向上的滑动距离必须大于竖直方向
if (Math.abs(deltaX) > ViewConfiguration.get(getContext()).getScaledTouchSlop() / 2
&& Math.abs(deltaX) > Math.abs(deltaY)) {
// 更新标记位
mCurrentStatus = STATUS_DRAG;
getParent().requestDisallowInterceptTouchEvent(true);
return true;
}
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mIsDraggable) {
return super.onTouchEvent(event);
}
mDragHelper.processTouchEvent(event);
// 手指抬起时, 允许父容器拦截事件
if (event.getAction() == MotionEvent.ACTION_UP) {
getParent().requestDisallowInterceptTouchEvent(false);
}
return true;
}
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
invalidate();
}
}
// 抽屉方向
public enum Direction {
LEFT, // 左边抽屉
RIGHT // 右边抽屉
}
public interface OnDrawerClosedListener {
void onClosed();
}
}







最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台