美文网首页Android开发Android技术知识Android知识
优雅的实现侧滑抽屉-HorizontalDrawerLayout

优雅的实现侧滑抽屉-HorizontalDrawerLayout

作者: SharryChoo | 来源:发表于2018-02-06 17:56 被阅读62次

    使用场景

    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();
        }
    }
    

    相关文章

      网友评论

        本文标题:优雅的实现侧滑抽屉-HorizontalDrawerLayout

        本文链接:https://www.haomeiwen.com/subject/upzgzxtx.html