仿QQ相册RecyclerView滑动选中

重要的事情在前面说

  • 本文介绍的方法是基于坐标动态计算该Item在RecyclerView中的行列值实现的,是我最初的设计思路,相对有局限性,后面借鉴RecyclerView的相关API进行了重新设计新的设计思路。

推荐阅读

演示视频

前言

  • 显示相册在app中是一个比较常见的操作,大致的操作就是通过ContentProvider获取多媒体资源进行展示,我综合了一下QQ 的和微信的显示效果,实现了一下,仿微信QQ显示手机相册,在QQ的相册选择时是支持滑动选中的,即手指碰到哪个就选中哪张照片,正好公司的项目中用到了这个功能,在网上找了找没有很好的解决方案,所以通过自定义控件处理事件,这篇文章主要介绍这个功能的实现。

大体思路

  • 打算继承FramLayout实现,当然继承别的也可以,习惯继承FramLayout

  • 当手指竖向滑动时,RecyclerView处理事件,进行滑动。

  • 当手指水平滑动时,SlidingCheckLayout截断事件,计算滑动距离,通过计算映射出手指滑动的位置在RecyclerView中的position

对外封闭

  • 为了尽量对外封闭,提供相对简单的API,一些操作需要内部来处理来获取相关数据,比如从内部获取RecyclerView,item的宽度,高度,GridLayoutManager的列数等参数,使用者就不需要再进行设置,相对使用起来更加简单。

  • 获取RecyclerView

    private void ensureTarget() {
        if (mTargetRv != null)
            return;
        for (int i = 0; i < getChildCount(); i++) {
            View childAt = getChildAt(i);
            if (childAt instanceof RecyclerView) {
                mTargetRv = (RecyclerView) childAt;
                // 设置监听事件
                initRecyclerView();
                return;
            }
        }
    }

    // 设置监听事件,因为使用RecyclerView的addOnScrollListener方法,所以对外提供一个兼容innerOnScrollListener来给RecyclerView设置监听
    private void initRecyclerView() {
        mTargetRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                mTotalScrollY += dy;
                if (innerOnScrollListener != null) {
                    innerOnScrollListener.onScrolled(recyclerView, dx, dy);
                }
            }

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (innerOnScrollListener != null) {
                    innerOnScrollListener.onScrollStateChanged(recyclerView, newState);
                }
            }
        });
    }
  • 处理LayoutManager
    /**
     * 换LayoutManager需要调用
     * 获取itemCount,初始化item的高度和宽度
     */
    public void ensureLayoutManager() {
        if (mTargetRv == null || itemSpanCount != -1)
            return;
        RecyclerView.LayoutManager lm = mTargetRv.getLayoutManager();
        if (lm == null)
            return;
        if (lm instanceof GridLayoutManager) {
            GridLayoutManager glm = (GridLayoutManager) lm;
            itemSpanCount = glm.getSpanCount();
            int size = (int) (getResources().getDisplayMetrics().widthPixels / (itemSpanCount * 1.0f));
            itemWidth = itemHeight = size;
            xTouchSlop = yTouchSlop = size * TOUCH_SLOP_RATE;
        } else {
            throw new IllegalStateException("only support grid layout manager now !");
        }
    }
  • 处理adapter
public void ensureAdapter() {
        if (mTargetRv == null || mDataCount != INVALID_PARAM)
            return;
        mAdapter = mTargetRv.getAdapter();
        if (mAdapter == null)
            return;
        mDataCount = mAdapter.getItemCount();
        mDataChangedObserver = new RecyclerView.AdapterDataObserver() {
            @Override
            public void onChanged() {
                super.onChanged();
                ensureAdapter();
            }
        };
        mAdapter.registerAdapterDataObserver(mDataChangedObserver);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mAdapter.unregisterAdapterDataObserver(mDataChangedObserver);
    }

拦截事件

private boolean isReadyToIntercept() {
    return mTargetRv != null 
    && mTargetRv.getAdapter() != null 
    && itemSpanCount != INVALID_PARAM;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

    if (!isEnabled())
        return super.onInterceptTouchEvent(ev);

    ensureTarget();
    ensureLayoutManager();
    ensureAdapter();

    if (!isReadyToIntercept())
        return super.onInterceptTouchEvent(ev);

    int action = MotionEventCompat.getActionMasked(ev);
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            // init
            mInitialDownX = ev.getX();
            mInitialDownY = ev.getY();
            break;
        case MotionEvent.ACTION_UP:
            // stop
            isBeingSlide = false;
            break;
        case MotionEvent.ACTION_MOVE:
            // handle
            // 水平滑动超过阈值,垂直滑动没有超过阈值时拦截事件
            float xDiff = Math.abs(ev.getX() - mInitialDownX);
            float yDiff = Math.abs(ev.getY() - mInitialDownY);
            if (yDiff < xTouchSlop && xDiff > yTouchSlop) {
                isBeingSlide = true;
                isBeingSlide = true;
                initRow = generateRow(ev.getX());
                initColumn = generateColumn(ev.getY());            }
            break;
    }
    return isBeingSlide;
}

计算行列值和位置

  • 列值等于x轴移动的距离/每个item 的宽度

  • 行值等于(RecyclerView滑动的距离+手指滑动的距离-RecylerView距离顶部偏移)(一般不设置magin就是0,兼容多种情况)。

    private int generateRow(float x) {
        return (int) (x / itemWidth);
    }


    private int generateColumn(float y) {
        return (int) ((y + mTotalScrollY - offsetTop) / itemHeight);
    }

    /**
     * 计算位置,根据行列值计算,比如
     *     0 1 2(row)
     *         
     * 0   0 1 2
     * 1   3 4 5
     * 2   6 7 8
     * (col)
     * 7 = 1(row) + 3 * 2(col)
     * @param col 行
     * @param row 列
     * @return
     */
    private int calculatePosition(int col, int row) {
        return row + itemSpanCount * col;
    }

触摸事件

  • 重点是up事件时重新初始化一些值

  • move事件时处理位置的移动

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = MotionEventCompat.getActionMasked(ev);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_UP:
                // stop
                isBeingSlide = false;
                xSlidingDist = 0;
                ySlidingDist = 0;
                preTouchPos = INVALID_PARAM;
                preRow = INVALID_PARAM;
                preColumn = INVALID_PARAM;
                break;
            case MotionEvent.ACTION_MOVE:
                handleMoveEvent(ev);
                break;
        }
        return isBeingSlide;
    }
    /**
     * 处理滑动手势
     *
     * @param ev move事件
     */
    private void handleMoveEvent(MotionEvent ev) {
        xSlidingDist = ev.getX();
        ySlidingDist = ev.getY();
        mRow = generateRow(xSlidingDist);
        if (mRow >= itemSpanCount)
            return;
        mColumn = generateColumn(ySlidingDist);
        int pos = (mRow + 3 * mColumn);
        if (onSlidingCheckListener == null || pos == preTouchPos)
            return;
        preTouchPos = pos;
        {
            publishSlidingCheck(mColumn, mRow);
        }
        preColumn = mColumn;
        preRow = mRow;
    }

    /**
     * 公开滑动选中事件
     *
     * @param col 行
     * @param row 列
     */
    private void publishSlidingCheck(int col, int row) {
        int pos = calculatePosition(col, row);
        if (onSlidingCheckListener != null && pos < mDataCount) {
            onSlidingCheckListener.onSlidingCheck(pos);
        }
    }

进阶

  • 对角线滑动选中多个

  • 先横向后垂直滑动选中多个

  • 使用这些手势达到很快选中多个的目的,但是相对也失去了滑动到哪里选中哪里的灵活性,二者选一吧
  • 完整的move事件处理,支持选中多个,感觉还是滑倒哪里选到哪里比较好
    private void handleMoveEvent(MotionEvent ev) {
        xSlidingDist = ev.getX();
        ySlidingDist = ev.getY();
        mRow = generateRow(xSlidingDist);
        if (mRow >= itemSpanCount)
            return;
        mColumn = generateColumn(ySlidingDist);
        int pos = (mRow + 3 * mColumn);
        if (onSlidingCheckListener == null || pos == preTouchPos)
            return;
        preTouchPos = pos;
        // 处理垂直滑动事件,如果支持7型手势,垂直滑动变为选中多张照片
        if (preRow != INVALID_PARAM && preRow == mRow && isSupport7Gesture) {
            // 先右划固定后上下滑选择
            if (mRow > initRow) {
                // 7型手势向下滑动
                if (preColumn < mColumn)
                    for (int row = initRow; row <= mRow; row++)
                        for (int col = preColumn + 1; col <= mColumn; col++)
                            publishSlidingCheck(col, row);
                // 7型手势向上划
                if (preColumn > mColumn)
                    for (int row = initRow; row <= mRow; row++)
                        for (int col = preColumn - 1; col >= mColumn; col--)
                            publishSlidingCheck(col, row);
            }
            //先左滑后上下滑
            else {
                // 7型手势向下滑动
                if (preColumn < mColumn)
                    for (int row = mRow; row <= initRow; row++)
                        for (int col = preColumn + 1; col <= mColumn; col++)
                            publishSlidingCheck(col, row);
                // 7型手势向上划
                if (preColumn > mColumn)
                    for (int row = mRow; row <= initRow; row++)
                        for (int col = preColumn - 1; col >= mColumn; col--)
                            publishSlidingCheck(col, row);
            }
        }
        // 简单滑动事件
        else {
            publishSlidingCheck(mColumn, mRow);
        }
        preColumn = mColumn;
        preRow = mRow;
    }

使用

  • 在xml中使用
<?xml version="1.0" encoding="utf-8"?>
<com.march.baselib.widget.SlidingCheckLayout
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:id="@+id/sliding">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_select_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never"
        app:spanCount="3"
        android:background="@color/white"
        android:clipChildren="false"
        android:clipToPadding="false"
        android:paddingBottom="50dp"
        tools:listitem="@layout/select_image_item_rv" />
</com.march.baselib.widget.SlidingCheckLayout>
  • 设置监听得到返回的pos进行处理
private SlidingCheckLayout mSlidingCheckLy;
mSlidingCheckLy.setOnSlidingCheckListener(new SlidingCheckLayout.OnSlidingCheckListener() {
            @Override
            public void onSlidingCheck(int pos) {
                ....
                adapter.notifyDataSetChanged();
            }
        });

源代码

package com.march.slidingcheck;

import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

import com.march.baselib.helper.Logger;

/**
 * Project  : SlidingCheck
 * Package  : com.march.slidingcheck
 * CreateAt : 16/9/6
 * Describe : 仿扣扣滑动选中照片
 *
 * @author chendong
 */
public class SlidingCheckLayout extends FrameLayout {

    private RecyclerView.Adapter mAdapter;
    private RecyclerView.AdapterDataObserver mDataChangedObserver;

    public SlidingCheckLayout(Context context) {
        this(context, null);
    }

    public SlidingCheckLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private static final float TOUCH_SLOP_RATE = 0.25f;
    // 初始化值
    private static final int INVALID_PARAM = -1;
    // 滑动选中监听
    private OnSlidingCheckListener onSlidingCheckListener;
    // 兼容滑动监听
    private RecyclerView.OnScrollListener innerOnScrollListener;

    private int offsetTop = 0;
    // rv在y轴滑动的距离
    private int mTotalScrollY = 0;
    // 横向的item数量
    private int itemSpanCount = INVALID_PARAM;
    // 数据量
    private int mDataCount = INVALID_PARAM;
    // 内部的rv
    private RecyclerView mTargetRv;
    // 是否支持7型手势操作
    private boolean isSupport7Gesture = false;
    // item宽度,默认屏幕宽度/span count
    private int itemWidth;
    // item高度,默认等于宽度,可以配置
    private int itemHeight;
    // 横轴滑动阈值,超过阈值表示触发横轴滑动
    private float xTouchSlop;
    // 纵轴滑动阈值,超过阈值表示触发纵轴滑动
    private float yTouchSlop;
    // down 事件初始值
    private float mInitialDownX;
    // down 事件初始值
    private float mInitialDownY;
    // 是否正在滑动
    private boolean isBeingSlide;
    // 行 值
    private int mColumn;
    // 列 值
    private int mRow;
    // 横向滑动距离
    private float xSlidingDist = 0;
    // 纵向滑动距离
    private float ySlidingDist = 0;
    // 上次触摸的位置
    private int preTouchPos = INVALID_PARAM;
    // 上次触摸的行数
    private int preColumn;
    // 上次触摸的列数
    private int preRow;

    // 初次down事件时触摸的行列值
    private int initRow;
    private int initColumn;
    private boolean isRowColumnEnable;

    public void setRowColumnEnable(boolean rowColumnEnable) {
        isRowColumnEnable = rowColumnEnable;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!isEnabled())
            return super.onInterceptTouchEvent(ev);
        ensureTarget();
        ensureLayoutManager();
        ensureAdapter();
        if (!isReadyToIntercept())
            return super.onInterceptTouchEvent(ev);
        int action = MotionEventCompat.getActionMasked(ev);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // init
                mInitialDownX = ev.getX();
                mInitialDownY = ev.getY();
                break;
            case MotionEvent.ACTION_UP:
                // stop
                isBeingSlide = false;
                break;
            case MotionEvent.ACTION_MOVE:
                // handle
                float xDiff = Math.abs(ev.getX() - mInitialDownX);
                float yDiff = Math.abs(ev.getY() - mInitialDownY);
                if (yDiff < xTouchSlop && xDiff > yTouchSlop) {
                    isBeingSlide = true;
                    initRow = generateRow(ev.getX());
                    initColumn = generateColumn(ev.getY());
                }
                break;
        }
        return isBeingSlide;
    }

    //  计算获取列值
    private int generateRow(float x) {
        return (int) (x / itemWidth);
    }

    // 计算获取列值
    private int generateColumn(float y) {
        return (int) ((y + mTotalScrollY - offsetTop) / itemHeight);
    }


    private float generateX(float x) {
        return x;
    }

    private float generateY(float y) {
        return y + mTotalScrollY - offsetTop;
    }


    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = MotionEventCompat.getActionMasked(ev);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_UP:
                // re init
                isBeingSlide = false;
                xSlidingDist = 0;
                ySlidingDist = 0;
                preTouchPos = INVALID_PARAM;
                preRow = INVALID_PARAM;
                preColumn = INVALID_PARAM;
                break;
            case MotionEvent.ACTION_MOVE:
                handleMoveEvent(ev);
                break;
        }
        return isBeingSlide;
    }


    /**
     * 处理滑动手势
     *
     * @param ev move事件
     */
    private void handleMoveEvent(MotionEvent ev) {
        publishSlidingCheck(ev);
        xSlidingDist = ev.getX();
        ySlidingDist = ev.getY();
        if (!isRowColumnEnable)
            return;
        mRow = generateRow(xSlidingDist);
        if (mRow >= itemSpanCount)
            return;
        mColumn = generateColumn(ySlidingDist);
        int pos = (mRow + 3 * mColumn);
        if (onSlidingCheckListener == null || pos == preTouchPos)
            return;
        preTouchPos = pos;
        // 处理垂直滑动事件,如果支持7型手势,垂直滑动变为选中多张照片
        if (preRow != INVALID_PARAM && preRow == mRow && isSupport7Gesture) {
            // 先右划固定后上下滑选择
            if (mRow > initRow) {
                // 7型手势向下滑动
                if (preColumn < mColumn)
                    for (int row = initRow; row <= mRow; row++)
                        for (int col = preColumn + 1; col <= mColumn; col++)
                            publishSlidingCheck(col, row);
                // 7型手势向上划
                if (preColumn > mColumn)
                    for (int row = initRow; row <= mRow; row++)
                        for (int col = preColumn - 1; col >= mColumn; col--)
                            publishSlidingCheck(col, row);
            }
            //先左滑后上下滑
            else {
                // 7型手势向下滑动
                if (preColumn < mColumn)
                    for (int row = mRow; row <= initRow; row++)
                        for (int col = preColumn + 1; col <= mColumn; col++)
                            publishSlidingCheck(col, row);
                // 7型手势向上划
                if (preColumn > mColumn)
                    for (int row = mRow; row <= initRow; row++)
                        for (int col = preColumn - 1; col >= mColumn; col--)
                            publishSlidingCheck(col, row);
            }
        }
        // 简单滑动事件
        else {
            publishSlidingCheck(mColumn, mRow);
        }
        preColumn = mColumn;
        preRow = mRow;
    }


    /**
     * 计算位置,根据行列值计算,比如
     * 0 1 2(row)
     * <p/>
     * 0   0 1 2
     * 1   3 4 5
     * 2   6 7 8
     * (col)
     * 7 = 1(row) + 3 * 2(col)
     *
     * @param col 行
     * @param row 列
     * @return
     */
    private int calculatePosition(int col, int row) {
        return row + itemSpanCount * col;
    }

    /**
     * 公开滑动选中事件
     *
     * @param col 行
     * @param row 列
     */
    private void publishSlidingCheck(int col, int row) {
        int pos = calculatePosition(col, row);
        if (onSlidingCheckListener != null && pos < mDataCount) {
            onSlidingCheckListener.onSlidingCheck(pos);
        }
    }

    private int preViewPos;

    private void publishSlidingCheck(MotionEvent event) {
        float x = generateX(event.getX());
        float y = generateY(event.getY()) - mTotalScrollY;
        View childViewUnder = mTargetRv.findChildViewUnder(x, y);
        if (onSlidingCheckListener != null && childViewUnder != null) {
            int pos = getPos(childViewUnder);
            if (pos != INVALID_PARAM && preViewPos != pos) {
                onSlidingCheckListener.onSlidingCheck(pos, childViewUnder);
                preViewPos = pos;
            }
        }
    }


    private int tagKey;

    public void setTagKey(int tagKey) {
        this.tagKey = tagKey;
    }

    public void mark(View parentView, int pos) {
        parentView.setTag(tagKey, pos);
    }

    public int getPos(View parentView) {
        int pos = INVALID_PARAM;
        Object tag = parentView.getTag(tagKey);
        if (tag != null)
            pos = (int) tag;
        return pos;
    }

    /**
     * 是否可以开始拦截处理事件,当recyclerView数据完全ok之后开始
     *
     * @return 是否可以开始拦截处理事件
     */

    private boolean isReadyToIntercept() {
        return mTargetRv != null
                && mTargetRv.getAdapter() != null
                && itemSpanCount != INVALID_PARAM;
    }

    /**
     * 获取RecyclerView
     */
    private void ensureTarget() {
        if (mTargetRv != null)
            return;
        for (int i = 0; i < getChildCount(); i++) {
            View childAt = getChildAt(i);
            if (childAt instanceof RecyclerView) {
                mTargetRv = (RecyclerView) childAt;
                initRecyclerView();
                return;
            }
        }
    }

    /**
     * 为RecyclerView设置监听事件
     */
    private void initRecyclerView() {
        mTargetRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                mTotalScrollY += dy;
                if (innerOnScrollListener != null) {
                    innerOnScrollListener.onScrolled(recyclerView, dx, dy);
                }
            }

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (innerOnScrollListener != null) {
                    innerOnScrollListener.onScrollStateChanged(recyclerView, newState);
                }
            }
        });
    }

    /**
     * 监测是否配置adapter
     */
    public void ensureAdapter() {
        if (mTargetRv == null || mDataCount != INVALID_PARAM)
            return;
        mAdapter = mTargetRv.getAdapter();
        if (mAdapter == null)
            return;
        mDataCount = mAdapter.getItemCount();
        mDataChangedObserver = new RecyclerView.AdapterDataObserver() {
            @Override
            public void onChanged() {
                super.onChanged();
                ensureAdapter();
            }
        };
        mAdapter.registerAdapterDataObserver(mDataChangedObserver);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mAdapter.unregisterAdapterDataObserver(mDataChangedObserver);
    }

    /**
     * 换LayoutManager需要调用
     * 获取itemCount,初始化item的高度和宽度
     */
    public void ensureLayoutManager() {
        if (mTargetRv == null || itemSpanCount != INVALID_PARAM)
            return;
        RecyclerView.LayoutManager lm = mTargetRv.getLayoutManager();
        if (lm == null)
            return;
        if (lm instanceof GridLayoutManager) {
            GridLayoutManager glm = (GridLayoutManager) lm;
            itemSpanCount = glm.getSpanCount();
            int size = (int) (getResources().getDisplayMetrics().widthPixels / (itemSpanCount * 1.0f));
            itemWidth = itemHeight = size;
            xTouchSlop = yTouchSlop = size * TOUCH_SLOP_RATE;
        } else {
            throw new IllegalStateException("only support grid layout manager now !");
        }
    }


    /**
     * 由于内部封闭了OnScrollListener,对外开放一个兼容方法
     *
     * @param innerOnScrollListener OnScrollListener
     */
    public void addOnScrollListener(RecyclerView.OnScrollListener innerOnScrollListener) {
        this.innerOnScrollListener = innerOnScrollListener;
    }

    /**
     * 设置item的高度,默认与宽度相同,可以自行设置。
     *
     * @param itemHeight 高度
     */
    public void setItemHeight(int itemHeight) {
        this.itemHeight = itemHeight;
        this.yTouchSlop = itemHeight * TOUCH_SLOP_RATE;
    }

    public void setOffsetTop(int offsetTop) {
        this.offsetTop = offsetTop;
    }

    public void setSupport7Gesture(boolean support7Gesture) {
        isSupport7Gesture = support7Gesture;
    }

    public void setOnSlidingCheckListener(OnSlidingCheckListener onSlidingCheckListener) {
        this.onSlidingCheckListener = onSlidingCheckListener;
    }
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值