Android—RecyclerView实现横向滑动翻页
*近项目需求要实现RecyclerView的分页滑动 先上效果图如下(视频压缩成的gif所以滑动切换效果有点卡顿了 效果为每页三条数据的滑动)
效果图
一:创建横向布局管理器
/**
* Created by Sunny on 2019/4/1.
*/
public class HorizontalPageLayoutManager extends RecyclerView.LayoutManager implements PageDecorationLastJudge {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return null;
}
int totalHeight = 0;
int totalWidth = 0;
int offsetY = 0;
int offsetX = 0;
public HorizontalPageLayoutManager(int rows, int columns) {
this.rows = rows;
this.columns = columns;
this.onePageSize = rows * columns;
}
@Override
public boolean canScrollHorizontally() {
return true;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
int newX = offsetX + dx;
int result = dx;
if (newX > totalWidth) {
result = totalWidth – offsetX;
} else if (newX < 0) {
result = 0 – offsetX;
}
offsetX += result;
offsetChildrenHorizontal(-result);
recycleAndFillItems(recycler, state);
return result;
}
private SparseArray<Rect> allItemFrames = new SparseArray<>();
private int getUsableWidth() {
return getWidth() – getPaddingLeft() – getPaddingRight();
}
private int getUsableHeight() {
return getHeight() – getPaddingTop() – getPaddingBottom();
}
int rows = 0;
int columns = 0;
int pageSize = 0;
int itemWidth = 0;
int itemHeight = 0;
int onePageSize = 0;
int itemWidthUsed;
int itemHeightUsed;
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
if (state.isPreLayout()) {
return;
}
//获取每个Item的平均宽高
itemWidth = getUsableWidth() / columns;
itemHeight = getUsableHeight() / rows;
//计算宽高已经使用的量,主要用于后期测量
itemWidthUsed = (columns – 1) * itemWidth;
itemHeightUsed = (rows – 1) * itemHeight;
//计算总的页数
// pageSize = state.getItemCount() / onePageSize + (state.getItemCount() % onePageSize == 0 ? 0 : 1);
computePageSize(state);
Log.i(“zzz”, “itemCount=” + getItemCount() + ” state itemCount=” + state.getItemCount() + ” pageSize=” + pageSize);
//计算可以横向滚动的*大值
totalWidth = (pageSize – 1) * getWidth();
//分离view
detachAndScrapAttachedViews(recycler);
int count = getItemCount();
for (int p = 0; p < pageSize; p++) {
for (int r = 0; r < rows; r++) {
for (int c = 0; c < columns; c++) {
int index = p * onePageSize + r * columns + c;
if (index == count) {
//跳出多重循环
c = columns;
r = rows;
p = pageSize;
break;
}
View view = recycler.getViewForPosition(index);
addView(view);
//测量item
measureChildWithMargins(view, itemWidthUsed, itemHeightUsed);
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
//记录显示范围
Rect rect = allItemFrames.get(index);
if (rect == null) {
rect = new Rect();
}
int x = p * getUsableWidth() + c * itemWidth;
int y = r * itemHeight;
rect.set(x, y, width + x, height + y);
allItemFrames.put(index, rect);
}
}
//每一页循环以后就回收一页的View用于下一页的使用
removeAndRecycleAllViews(recycler);
}
recycleAndFillItems(recycler, state);
}
private void computePageSize(RecyclerView.State state) {
pageSize = state.getItemCount() / onePageSize + (state.getItemCount() % onePageSize == 0 ? 0 : 1);
}
@Override
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
super.onDetachedFromWindow(view, recycler);
offsetX = 0;
offsetY = 0;
}
private void recycleAndFillItems(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.isPreLayout()) {
return;
}
Rect displayRect = new Rect(getPaddingLeft() + offsetX, getPaddingTop(), getWidth() – getPaddingLeft() – getPaddingRight() + offsetX, getHeight() – getPaddingTop() – getPaddingBottom());
Rect childRect = new Rect();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
childRect.left = getDecoratedLeft(child);
childRect.top = getDecoratedTop(child);
childRect.right = getDecoratedRight(child);
childRect.bottom = getDecoratedBottom(child);
if (!Rect.intersects(displayRect, childRect)) {
removeAndRecycleView(child, recycler);
}
}
for (int i = 0; i < getItemCount(); i++) {
if (Rect.intersects(displayRect, allItemFrames.get(i))) {
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, itemWidthUsed, itemHeightUsed);
Rect rect = allItemFrames.get(i);
layoutDecorated(view, rect.left – offsetX, rect.top, rect.right – offsetX, rect.bottom);
}
}
}
@Override
public boolean isLastRow(int index) {
if (index >= 0 && index < getItemCount()) {
int indexOfPage = index % onePageSize;
indexOfPage++;
if (indexOfPage > (rows – 1) * columns && indexOfPage <= onePageSize) {
return true;
}
}
return false;
}
@Override
public boolean isLastColumn(int position) {
if (position >= 0 && position < getItemCount()) {
position++;
if (position % columns == 0) {
return true;
}
}
return false;
}
@Override
public boolean isPageLast(int position) {
position++;
return position % onePageSize == 0;
}
@Override
public int computeHorizontalScrollRange(RecyclerView.State state) {
computePageSize(state);
return pageSize * getWidth();
}
@Override
public int computeHorizontalScrollOffset(RecyclerView.State state) {
return offsetX;
}
@Override
public int computeHorizontalScrollExtent(RecyclerView.State state) {
return getWidth();
}
}
二:创建接口PageDecorationLastJudge实现接口回调
public interface PageDecorationLastJudge {
/**
* Is the last row in one page
*
* @param position
* @return
*/
boolean isLastRow(int position);
/**
* Is the last Colum in one row;
*
* @param position
* @return
*/
boolean isLastColumn(int position);
boolean isPageLast(int position);
}
三:实现RecyclerView横向滚动工具类
/**
* 实现RecycleView分页滚动的工具类
* Created by Sunny on 2019/4/1.
*/
public class PagingScrollHelper {
RecyclerView mRecyclerView = null;
private MyOnScrollListener mOnScrollListener = new MyOnScrollListener();
private MyOnFlingListener mOnFlingListener = new MyOnFlingListener();
private int offsetY = 0;
private int offsetX = 0;
int startY = 0;
int startX = 0;
enum ORIENTATION {
HORIZONTAL, VERTICAL, NULL
}
private ORIENTATION mOrientation = ORIENTATION.HORIZONTAL;
public void setUpRecycleView(RecyclerView recycleView) {
if (recycleView == null) {
throw new IllegalArgumentException(“recycleView must be not null”);
}
mRecyclerView = recycleView;
//处理滑动
recycleView.setOnFlingListener(mOnFlingListener);
//设置滚动监听,记录滚动的状态,和总的偏移量
recycleView.setOnScrollListener(mOnScrollListener);
//记录滚动开始的位置
recycleView.setOnTouchListener(mOnTouchListener);
//获取滚动的方向
updateLayoutManger();
}
public void updateLayoutManger() {
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager != null) {
if (layoutManager.canScrollVertically()) {
mOrientation = ORIENTATION.VERTICAL;
} else if (layoutManager.canScrollHorizontally()) {
mOrientation = ORIENTATION.HORIZONTAL;
} else {
mOrientation = ORIENTATION.NULL;
}
if (mAnimator != null) {
mAnimator.cancel();
}
startX = 0;
startY = 0;
offsetX = 0;
offsetY = 0;
}
}
/**
* 获取总共的页数
*/
public int getPageCount() {
if (mRecyclerView != null) {
if (mOrientation == ORIENTATION.NULL) {
return 0;
}
if (mOrientation == ORIENTATION.VERTICAL && mRecyclerView.computeVerticalScrollExtent() != 0) {
return mRecyclerView.computeVerticalScrollRange() / mRecyclerView.computeVerticalScrollExtent();
} else if (mRecyclerView.computeHorizontalScrollExtent() != 0) {
Log.i(“zzz”,”rang=”+mRecyclerView.computeHorizontalScrollRange()+” extent=”+mRecyclerView.computeHorizontalScrollExtent());
return mRecyclerView.computeHorizontalScrollRange() / mRecyclerView.computeHorizontalScrollExtent();
}
}
return 0;
}
ValueAnimator mAnimator = null;
public void scrollToPosition(int position) {
if (mAnimator == null) {
mOnFlingListener.onFling(0, 0);
}
if (mAnimator != null) {
int startPoint = mOrientation == ORIENTATION.VERTICAL ? offsetY : offsetX, endPoint = 0;
if (mOrientation == ORIENTATION.VERTICAL) {
endPoint = mRecyclerView.getHeight() * position;
} else {
endPoint = mRecyclerView.getWidth() * position;
}
if (startPoint != endPoint) {
mAnimator.setIntValues(startPoint, endPoint);
mAnimator.start();
}
}
}
public class MyOnFlingListener extends RecyclerView.OnFlingListener {
@Override
public boolean onFling(int velocityX, int velocityY) {
if (mOrientation == ORIENTATION.NULL) {
return false;
}
//获取开始滚动时所在页面的index
int p = getStartPageIndex();
//记录滚动开始和结束的位置
int endPoint = 0;
int startPoint = 0;
//如果是垂直方向
if (mOrientation == ORIENTATION.VERTICAL) {
startPoint = offsetY;
if (velocityY < 0) {
p–;
} else if (velocityY > 0) {
p++;
}
//更具不同的速度判断需要滚动的方向
//注意,此处有一个技巧,就是当速度为0的时候就滚动会开始的页面,即实现页面复位
endPoint = p * mRecyclerView.getHeight();
} else {
startPoint = offsetX;
if (velocityX < 0) {
p–;
} else if (velocityX > 0) {
p++;
}
endPoint = p * mRecyclerView.getWidth();
}
if (endPoint < 0) {
endPoint = 0;
}
//使用动画处理滚动
if (mAnimator == null) {
mAnimator = new ValueAnimator().ofInt(startPoint, endPoint);
mAnimator.setDuration(300);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int nowPoint = (int) animation.getAnimatedValue();
if (mOrientation == ORIENTATION.VERTICAL) {
int dy = nowPoint – offsetY;
//这里通过RecyclerView的scrollBy方法实现滚动。
mRecyclerView.scrollBy(0, dy);
} else {
int dx = nowPoint – offsetX;
mRecyclerView.scrollBy(dx, 0);
}
}
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//回调监听
if (null != mOnPageChangeListener) {
mOnPageChangeListener.onPageChange(getPageIndex());
}
//修复双击item bug
mRecyclerView.stopScroll();
startY = offsetY;
startX = offsetX;
}
});
} else {
mAnimator.cancel();
mAnimator.setIntValues(startPoint, endPoint);
}
mAnimator.start();
return true;
}
}
public class MyOnScrollListener extends RecyclerView.OnScrollListener {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
//newState==0表示滚动停止,此时需要处理回滚
if (newState == 0 && mOrientation != ORIENTATION.NULL) {
boolean move;
int vX = 0, vY = 0;
if (mOrientation == ORIENTATION.VERTICAL) {
int absY = Math.abs(offsetY – startY);
//如果滑动的距离超过屏幕的一半表示需要滑动到下一页
move = absY > recyclerView.getHeight() / 2;
vY = 0;
if (move) {
vY = offsetY – startY < 0 ? -1000 : 1000;
}
} else {
int absX = Math.abs(offsetX – startX);
move = absX > recyclerView.getWidth() / 2;
if (move) {
vX = offsetX – startX < 0 ? -1000 : 1000;
}
}
mOnFlingListener.onFling(vX, vY);
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
//滚动结束记录滚动的偏移量
offsetY += dy;
offsetX += dx;
}
}
private MyOnTouchListener mOnTouchListener = new MyOnTouchListener();
private boolean firstTouch = true;
public class MyOnTouchListener implements View.OnTouchListener {
@Override
public boolean onTouch(View v, MotionEvent event) {
//手指按下的时候记录开始滚动的坐标
if (firstTouch) {
//*次touch可能是ACTION_MOVE或ACTION_DOWN,所以使用这种方式判断
firstTouch = false;
startY = offsetY;
startX = offsetX;
}
if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
firstTouch = true;
}
return false;
}
}
private int getPageIndex() {
int p = 0;
if (mRecyclerView.getHeight() == 0 || mRecyclerView.getWidth() == 0) {
return p;
}
if (mOrientation == ORIENTATION.VERTICAL) {
p = offsetY / mRecyclerView.getHeight();
} else {
p = offsetX / mRecyclerView.getWidth();
}
return p;
}
private int getStartPageIndex() {
int p = 0;
if (mRecyclerView.getHeight() == 0 || mRecyclerView.getWidth() == 0) {
//没有宽高无法处理
return p;
}
if (mOrientation == ORIENTATION.VERTICAL) {
p = startY / mRecyclerView.getHeight();
} else {
p = startX / mRecyclerView.getWidth();
}
return p;
}
onPageChangeListener mOnPageChangeListener;
public void setOnPageChangeListener(onPageChangeListener listener) {
mOnPageChangeListener = listener;
}
public interface onPageChangeListener {
void onPageChange(int index);
}
}
四:初始化RecyclerView同时设置为横向滑动
//使用通用RecyclerView组件
PagingScrollHelper scrollHelper = new PagingScrollHelper();//初始化横向管理器
HorizontalPageLayoutManager horizontalPageLayoutManager = new HorizontalPageLayoutManager(1, 3);//这里两个参数是行列,这里实现的是一行三列
titleAdapter = new RsdTitleAdapter(this);//设置适配器
mrecRsd.setAdapter(titleAdapter);
scrollHelper.setUpRecycleView(mrecRsd);//将横向布局管理器和recycler view绑定到一起
scrollHelper.setOnPageChangeListener(this);//设置滑动监听
mrecRsd.setLayoutManager(horizontalPageLayoutManager);//设置为横向
scrollHelper.updateLayoutManger();
scrollHelper.scrollToPosition(0);//默认滑动到*页
mrecRsd.setHorizontalScrollBarEnabled(true);
五:页面滑动监听
//页面切换
@Override
public void onPageChange(int index) {
//这里是配合圆点指示器实现的,可以忽略
for (int i = 0; i < dotViews.length; i++) {
if (index == i) {
dotViews[i].setSelected(true);
dotViews[i].setImageResource(R.drawable.red_circle);
} else {
dotViews[i].setSelected(false);
dotViews[i].setImageResource(R.drawable.gry_circle);
}
}
}
2019/8/13更新 添加圆点指示器代码
private ImageView[] dotViews;//创建存放图片集合
//生成相应数量的导航小圆点
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
//设置小圆点左右之间的间隔
params.setMargins(10, 0, 10, 0);
//得到页面个数
dotViews = new ImageView[6];//我这里是固定的六页 也可以根据自己需要设置圆点个数
for (int i = 0; i < 6; i++) {
ImageView imageView = new ImageView(getContext());
imageView.setLayoutParams(params);
imageView.setImageResource(R.drawable.gry_circle);//初始化六个灰色Img
if (i == 0) {
//默认启动时,选中*个小圆点
imageView.setSelected(true);
} else {
imageView.setSelected(false);//其他的设置不选择
}
//得到每个小圆点的引用,用于滑动页面时,(onPageSelected方法中)更改它们的状态。
dotViews[i] = imageView;
dotViews[0].setImageResource(R.drawable.red_circle);//设置*个页面选择
//添加到布局里面显示
img_layout.addView(imageView);//这里的img_layout就是我在布局中写的一个Linear Layout用来存放这些圆点img