重要的事情在前面说
- 本文介绍的方法是基于坐标动态计算该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;
}
}