Android开发Android技术知识Android知识

优雅的实现侧滑抽屉-HorizontalDrawerLayout

2018-02-06  本文已影响62人  SharryChoo

使用场景

RecyclerView的侧滑菜单

效果展示

侧滑抽屉.gif

使用方式

  1. 在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>
  1. 代码使用
    • 为了方便代码展示, 这里使用了 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)
        }
    }

}

实现思路

  1. 自定义ViewGroup去实现
  2. 创建一个HorizontalDrawerLayout布局, 布局中只能存放两个View, 第一个为MenuView, 第二个为ConcreateView
  3. MenuView是固定的不可拖动的
  4. 通过拖动ConcreateView只可以在水平方向上拖动
  5. 处理好与RecyclerView和子View点击事件的冲突

事件拦截

  1. 必须大于View响应点击事件的距离(这里选择了1/2, 防止滑动时出现不连贯的效果)
  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();
    }
}
上一篇 下一篇

猜你喜欢

热点阅读