自定义View:侧滑RecycleView

自定义View:侧滑RecycleView

控件功能详解:
1.侧滑RecycleView,侧滑弹出隐藏菜单(删除)
2.可在ViewPage中使用,解决了横向滑动冲突的问题
3.SideRecycleView 并没有暴露出侧滑菜单的点击事件,可在自己的ViewHolder中实现点击事件,优点就是可以添加多个隐藏菜单,只要保证这些隐藏菜单有一个共同的父布局即可。

使用注意事项:
1、使用SideRecycleView item样式必须为 水平父布局【1+1】模式。即显示的布局是一个共同的大布局,隐藏的菜单布局是一个共同的大布局。都可以包含若干个子布局。
2.使用SideRecycleView必须使用水平的LinearLayoutManager。
3.为解决RecycleView侧滑菜单和ViewPage嵌套带来的冲突, 添加速度判断,缓慢向左滑,显示隐藏菜单,快速滑动,切换ViewPage
4.SideRecycleView 并没有暴露出侧滑菜单的点击事件方法,可在自己的viewHolder中实现点击事件,所以在点击侧滑菜单部分时并不会恢复正常的显示,需要调用public方法closeMenu()。

效果图如下
正常效果
侧滑效果
好了,话不多说,看代码,注释也写的比较详细了

import android.content.Context;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.widget.Scroller;

/**
 * 侧滑RecycleView,侧滑弹出隐藏菜单(删除)
 * 注意事项:
 * 1、使用SideRecycleView item样式必须为 水平父布局【1+1】模式
 * 即显示的布局是一个共同的大布局,隐藏的菜单布局是一个共同的大布局。都可以包含若干个子布局
 * 2、使用SideRecycleView必须使用水平的LinearLayoutManager
 * 3、为解决RecycleView侧滑菜单和ViewPage嵌套带来的冲突,
 * 添加速度判断,缓慢向左滑,显示隐藏菜单,快速滑动,切换ViewPage
 */
public class SideRecycleView extends RecyclerView {
    /**
     * 上一次移动的X点
     */
    private int      lastMoveX;
    /**
     * 上一次移动的Y点
     */
    private int      lastMoveY;
    /**
     * 滚动条
     */
    private Scroller mScroller;
    /**
     * 当前选中的item的位置
     */
    private int      selectItemPosition;
    /**
     * 是否是第一次onTouch
     */
    private boolean  isFirst = true;

    /**
     * 本次选中的item
     */
    private View            item;
    /**
     * 上一次选择的item布局
     */
    private View            mLastItem;
    /**
     * 记录连续移动的长度
     */
    private int             mMoveWidth   = 0;
    /**
     * 隐藏布局的宽度
     */
    private int             mHiddenWidth = 0;
    /**
     * 速度追踪器
     */
    private VelocityTracker mVelocityTracker;
    /**
     * 设置最大速度(超过这个速度,不显示隐藏菜单)
     */
    private int             MaxVelocity  = 2000;

    public SideRecycleView(@NonNull Context context) {
        this(context, null);
    }

    public SideRecycleView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SideRecycleView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mScroller = new Scroller(context, new LinearInterpolator(context, null));
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int eventX = (int) event.getX();
        int eventY = (int) event.getY();
        obtainVelocity(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //记录当前按下点的坐标(以自身view左上角为原点)
                lastMoveX = eventX;
                lastMoveY = eventY;
                //计算当前选中的item的位置
                selectItemPosition = pointToPosition(eventX, eventY);
                if (isFirst) {
                    //第一次时,不用重置上一次的Item
                    isFirst = false;
                } else {
                    //屏幕再次接收到点击时,恢复上一次item状态
                    if (mLastItem != null && mMoveWidth > 0) {
                        //将Item右移,恢复原位
                        if (mLastItem == item) {
                            //点击同一个item
                            if (((ViewGroup) item).getChildCount() == 2) {
                                View view = ((ViewGroup) item).getChildAt(1);
                                Rect rect = new Rect();
                                view.getGlobalVisibleRect(rect);//以view在屏幕坐标系中的左上为原点,计算当前view在屏幕坐标中的可视区域,当前view的实际大小减去超出屏幕或被挡部分
                                if (!rect.contains((int) event.getRawX(), (int) event.getRawY())) {
                                    //event.getRawX()当前触摸事件距离整个屏幕左边的距离
                                    // event.getRawY()当前触摸事件距离整个屏幕顶边的距离
                                    //说明不在显示出来的菜单布局上
                                    scrollRight(mLastItem, (0 - mMoveWidth));
                                    //清空变量
                                    mMoveWidth = 0;
                                    mHiddenWidth = 0;

                                }
                            }
                        } else {
                            scrollRight(mLastItem, (0 - mMoveWidth));
                            //清空变量
                            mMoveWidth = 0;
                            mHiddenWidth = 0;
                        }
                    }
                }
                //取到当前选中的Item,赋给mCurItemLayout,以便对其进行左移
                int firstVisibleItemPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
                item = getChildAt(selectItemPosition - firstVisibleItemPosition);//获取当前选中的item
                if (item != null) {
                    if (((ViewGroup) item).getChildCount() == 2) {
                        //有两个子布局(可见布局+隐藏的菜单布局,菜单布局内可多个菜单item)
                        // getChildAt( 0 )是可见布局,getChildAt( 1 )是隐藏布局 菜单布局
                        //这里将删除按钮的宽度设为可以移动的距离
                        mHiddenWidth = ((ViewGroup) item).getChildAt(1).getWidth();
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mVelocityTracker.computeCurrentVelocity(1000);设置units的值为1000,意思为一秒时间内运动了多少个像素
                int xVelocity = (int) mVelocityTracker.getXVelocity();//获取X方向上的滑动速度
                int yVelocity = (int) mVelocityTracker.getYVelocity();//获取Y方向上的滑动速度
                if (Math.abs(xVelocity) > Math.abs(yVelocity) && Math.abs(xVelocity) < MaxVelocity) {
                    // X方向的速度大于Y方向的速度,并且X方向上的速度小于最大速度,让RecycleView执行侧滑
                    getParent().requestDisallowInterceptTouchEvent(true);
                    return true;
                } else {
                    closeMenu();
                    releaseVelocity();//重置速度追踪器
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                releaseVelocity();
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_CANCEL:
                //以外移动中断
                releaseVelocity();
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        return super.onInterceptTouchEvent(event);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int eventX = (int) event.getX();
        int eventY = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //没有拦截,调用不到
                break;
            case MotionEvent.ACTION_MOVE:
                int xMove = eventX;//移动后的X点坐标
                int yMove = eventY;//移动后的Y点坐标
                //值为负值时,手指向左滑动;值为正时,手指向右滑动。这与Android的屏幕坐标定义有关
                int x = xMove - lastMoveX;
                int y = yMove - lastMoveY;

                if (x < 0 && Math.abs(y) < Math.abs(x)) {
                    //x < 0 并且x轴滑动距离>y轴滑动距离时认为是左滑
                    int newScrollX = Math.abs(x);
                    if (mMoveWidth > mHiddenWidth) {
                        //本次移动的宽度>隐藏布局的宽度就不能继续移动了
                        newScrollX = 0;
                    } else if ((mMoveWidth + newScrollX) > mHiddenWidth) {
                        //本次移动的宽度+已经移动的距离 > 隐藏布局的宽度 就不能移动折磨多距离了,否则会显示出白边
                        //应该移动剩余隐藏布局的宽度
                        newScrollX = mHiddenWidth - mMoveWidth;
                    } else if ((mMoveWidth + newScrollX) < mHiddenWidth) {
                        newScrollX = Math.abs(x);
                    }
                    //左滑,每次滑动手指移动的距离
                    scrollLeft(item, newScrollX);
                    //计算已经移动的距离
                    mMoveWidth = mMoveWidth + newScrollX;
                    lastMoveX = eventX;
                } else if (x > 0 && Math.abs(y) < Math.abs(x)) {
                    //x > 0 并且 x轴滑动距离>y轴滑动距离时认为是右滑
                    scrollRight(item, 0 - mMoveWidth);//执行右滑,这里没有做跟随,瞬间恢复
                    mMoveWidth = 0;//清空数据
                }
                break;
            case MotionEvent.ACTION_UP:
                if (item != null) {
                    int scrollX = item.getScrollX();//已经滑动的距离
                    if (mHiddenWidth > mMoveWidth) {
                        int toX = mHiddenWidth - mMoveWidth;//剩余隐藏的长度
                        if (scrollX > mHiddenWidth / 2) {
                            //超过一半长度时松开,则自动滑到左侧
                            scrollLeft(item, toX);
                            mMoveWidth = mHiddenWidth;
                        } else {
                            //不到一半时松开,则恢复原状
                            scrollRight(item, 0 - mMoveWidth);
                            mMoveWidth = 0;
                        }
                    }
                    invalidate();//刷新视图
                    mLastItem = item;
                    releaseVelocity();
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                closeMenu();
                releaseVelocity();
                invalidate();//刷新视图
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            item.scrollBy(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
        super.computeScroll();
    }

    /**
     * 计算当前选中的item的位置
     *
     * @param x
     * @param y
     * @return
     */
    private int pointToPosition(int x, int y) {
        if (getLayoutManager() == null) {
            return -1;
        }
        int firstVisibleItemPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
        Rect itemRect = new Rect();
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == VISIBLE) {
                child.getHitRect(itemRect);//以父类左上角为原点
                if (itemRect.contains(x, y)) {
                    return firstVisibleItemPosition + i;
                }
            }
        }
        return -1;
    }

    /**
     * 向左滑动
     */
    private void scrollLeft(View item, int scrollX) {
        if (item != null) {
            item.scrollBy(scrollX, 0);
        }
    }

    /**
     * 向右滑动
     */
    private void scrollRight(View item, int scrollX) {
        if (item != null) {
            item.scrollBy(scrollX, 0);
        }
    }

    /**
     * 恢复原来的状态
     */
    public void closeMenu() {
        if (item != null && item.getScaleX() != 0) {
            item.scrollTo(0, 0);
            mHiddenWidth = 0;
            mMoveWidth = 0;
        }
    }

    /**
     * 清空速度追踪器
     */
    private void releaseVelocity() {
        if (mVelocityTracker != null) {
            mVelocityTracker.clear();
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    /**
     * 给速度追踪器绑定移动事件
     *
     * @param event
     */
    private void obtainVelocity(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
    }
}

上述代码就是整个SideRecycleView的全部逻辑,可以看出,主要逻辑就是在处理事件分发和重写onTouchEvent。

在代码中有这么一段,这句话的意思就是点击的这个item有两个子布局,即证明这个item有隐藏的菜单布局,可以侧滑显示菜单

((ViewGroup) item).getChildCount() == 2
  • 下面这段代码的意思就是判断上次选中的item有没有恢复原状
  • 如果上次选中的item侧滑菜单显示 && 上次选中的item和这次选中的item是同一个item
  • 获取到隐藏布局的矩形区域,判断触摸的点的坐标是否在这个区域内,
  • 如果是在这个区域内,证明点击了侧滑菜单,就不处理,等待点击事件的响应,在点击事件中关闭菜单。
  • 如过直接在代码里关闭侧滑菜单,就会出现还没有执行到点击事件,侧滑菜单就关闭了,点击事件无响应。
  • 原因就是onInterceptTouchEvent和onTouchEvent在事件分发处理机制中执行的优先级高于点击事件。
  • 如果不是,就关闭侧滑菜单
if (mLastItem != null && mMoveWidth > 0) {
                        //将Item右移,恢复原位
                        if (mLastItem == item) {
                            //点击同一个item
                            if (((ViewGroup) item).getChildCount() == 2) {
                                View view = ((ViewGroup) item).getChildAt(1);
                                Rect rect = new Rect();
                                view.getGlobalVisibleRect(rect);//以view在屏幕坐标系中的左上为原点,计算当前view在屏幕坐标中的可视区域,当前view的实际大小减去超出屏幕或被挡部分
                                if (!rect.contains((int) event.getRawX(), (int) event.getRawY())) {
                                    //event.getRawX()当前触摸事件距离整个屏幕左边的距离
                                    // event.getRawY()当前触摸事件距离整个屏幕顶边的距离
                                    //说明不在显示出来的菜单布局上
                                    scrollRight(mLastItem, (0 - mMoveWidth));
                                    //清空变量
                                    mMoveWidth = 0;
                                    mHiddenWidth = 0;

                                }
                            }
                        }

在onInterceptTouchEvent方法的MotionEvent.ACTION_DOWN:中有这么一段代码,在MotionEvent.ACTION_DOWN:中添加如下代码,是请求父布局不要拦截这个触摸事件。这样才能保证会走onInterceptTouchEvent:中的MotionEvent.ACTION_MOVE:方法,否则,父布局以上如果存在可滑动的控件,就会被消费掉。这里的move方法中的逻辑就不会走 (删除线部分我是这么理解的,可能不一定对,毕竟我还是个小白。滑稽保命!!!)

 getParent().requestDisallowInterceptTouchEvent(true);

onInterceptTouchEvent方法中的MotionEvent.ACTION_MOVE:中判断,如果水平方向上的滑动速度大于垂直方向的滑动速度,并且水平方向速度小于自己设置的最大速度,就认为是水平滑动,可以显示出隐藏菜单,就该执行onTouchEvent中的逻辑,所以要return true;,并且告诉父类请求父类不要拦截。

注意一定要在up事件中添加如下代码允许父类可以拦截,否则父类中的滚动逻辑等就不会被执行

getParent().requestDisallowInterceptTouchEvent(false);

在布局文件中直接使用即可

<com.xxx.view.SideRecycleView
            android:id="@+id/recycle"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

item的布局只要满足水平父布局【1+1】的效果,并且隐藏菜单布局宽度需要具体值,不能使用自适应或充满。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_item_fragment_content_sign_replenish_orders"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">必须是水平布局

    <FrameLayout
        android:id="@+id/frame_layout_item_fragment_content_sign_replenish_orders"
        android:layout_width="match_parent"这块必须是充满
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary">

        <RelativeLayout
            android:id="@+id/rl_item_fragment_content_sign_replenish_orders"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp">

            <RelativeLayout
                android:id="@+id/rl_item_fragment_content_sign_replenish_orders_item"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:clickable="false"
                android:paddingRight="6dp"
                android:paddingBottom="6dp">

                <RelativeLayout
                    android:layout_width="wrap_content"
                    android:layout_height="20dp"
                    android:clickable="false"
                    android:gravity="center"
                    android:paddingHorizontal="6dp">

                    <TextView
                        android:id="@+id/tv_item_fragment_content_sign_replenish_orders_recycle_item_title"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:textSize="16sp"
                        android:text="内容" />

                    <TextView
                        android:id="@+id/tv_item_fragment_content_sign_replenish_orders_recycle_item_title_content"

                        android:textSize="16sp"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="8dp"
                        android:layout_toRightOf="@id/tv_item_fragment_content_sign_replenish_orders_recycle_item_title"

                        android:text="给药方式:" />

                    <TextView
                        android:id="@+id/tv_item_fragment_content_sign_replenish_orders_recycle_item_content"

                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_toRightOf="@id/tv_item_fragment_content_sign_replenish_orders_recycle_item_title_content"

                        android:textSize="16sp"
                        android:text="40.6"/>
                </RelativeLayout>
            </RelativeLayout>



            <TextView
                android:id="@+id/tv_item_fragment_content_sign_replenish_orders_recycle_item_time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/rl_item_fragment_content_sign_replenish_orders_item"
                android:layout_alignParentRight="true"
                android:layout_marginTop="18dp"
                android:text="今天 2020.4.2 12:12"/>

        </RelativeLayout>
    </FrameLayout>

    <LinearLayout
        android:id="@+id/ll_delete"
        android:layout_width="72dp"  这块必须是具体值
        android:layout_height="match_parent"
        android:gravity="center">

        <ImageView
            android:id="@+id/img_add_content_sign_replenish_orders_right_delete"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="40dp"
            android:src="@color/colorPrimary"/>
    </LinearLayout>
</LinearLayout>

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值