1、滚动画廊控件
本节实现的效果如下图所示:
滚动画廊.gif
2、实现横向布局
2.1、开启横向滚动
在自定义LayoutManager之复用与回收二中已经介绍了如何通过自定义LayoutManager实现垂直滚动的效果,由于本文中效果是中横向滚动的,故在此基础上进行修改。
首先删除canScrollVertically()
和scrollVerticallyBy
函数,改为:
@Override
public boolean canScrollHorizontally() {
return true;
}
@Override
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
…………
}
在经过上面修改后,可以成功运行,但是布局依然是竖直布局的,很明显需要修改onLayoutChildren()
进行横向布局。
2.2、实现横向布局
最关键的问题就是,我们在初始化布局时,会通过mItemRects来保存所有item的位置,所以这里需要修改成横向布局的计算方式。
int offsetX = 0;
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
mItemRects.put(i, rect);
mHasAttachedItems.put(i, false);
offsetX += mItemWidth;
}
然后在获取visibleCount时,需要修改为:
int visibleCount = getHorizontalSpace() / mItemWidth;
同时,在onLayoutChildren最后,有个计算mTotalHeight的逻辑,我们需要改为计算totalWidth的逻辑:
@Override
public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
…………
mTotalWidth = Math.max(offsetX, getHorizontalSpace());
}
private int getHorizontalSpace() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
同时,在getVisibleArea函数也需要修改,因为我们现在已经是横向滚动了,已经不再是竖向滚动了,所以可见区域应该是横向滚动后的可见区域:
private Rect getVisibleArea() {
Rect result = new Rect(getPaddingLeft() + mSumDx, getPaddingTop(), getWidth() - getPaddingRight() + mSumDx, getHeight()-getPaddingBottom());
return result;
}
onLayoutChildren函数中的其它代码不需要更改,此时onLayoutChildren的代码如下:
public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {//没有Item,界面空着吧
detachAndScrapAttachedViews(recycler);
return;
}
mHasAttachedItems.clear();
mItemRects.clear();
detachAndScrapAttachedViews(recycler);
//将item的位置存储起来
View childView = recycler.getViewForPosition(0);
measureChildWithMargins(childView, 0, 0);
mItemWidth = getDecoratedMeasuredWidth(childView);
mItemHeight = getDecoratedMeasuredHeight(childView);
int visibleCount = getVerticalSpace() / mItemWidth;
//定义水平方向的偏移量
int offsetX = 0;
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
mItemRects.put(i, rect);
mHasAttachedItems.put(i, false);
offsetX += mItemWidth;
}
Rect visibleRect = getVisibleArea();
for (int i = 0; i < visibleCount; i++) {
insertView(i, visibleRect, recycler, false);
}
//如果所有子View的宽度和没有填满RecyclerView的宽度,
// 则将宽度设置为RecyclerView的宽度
mTotalWidth = Math.max(offsetX, getHorizontalSpace());
}
private int getHorizontalSpace() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
private Rect getVisibleArea() {
Rect result = new Rect(getPaddingLeft() + mSumDx, getPaddingTop(), getWidth() - getPaddingRight() + mSumDx, getHeight()-getPaddingBottom());
return result;
}
修改后的效果如下图所示:
image.png
到此已经实现了横向的布局,下面修改下横向滚动的逻辑。
2.3、实现横向滚动
横向滚动是放在scrollHorizontallyBy中处理,主要的修改如下:
- 1、边界处理修改
int travel = dx;
//如果滑动到最顶部
if (mSumDx + dx < 0) {
travel = -mSumDx;
} else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
//如果滑动到最底部
travel = mTotalWidth - getHorizontalSpace() - mSumDx;
}
边界的处理和垂直的处理逻辑大致相同,只需要进行简单的修改。
- 2、在回收越界时,已经在屏幕上的item重新Layout的修改:
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
int position = getPosition(child);
Rect rect = mItemRects.get(position);
if (!Rect.intersects(rect, visibleRect)) {
removeAndRecycleView(child, recycler);
mHasAttachedItems.put(position, false);
} else {
layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
mHasAttachedItems.put(position, true);
}
}
这里只需要修改layoutDecoratedWithMargins
函数即可,在布局时,根据mSumDx布局item的left和right坐标:layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
,因为是横向布局,所以top和bottom都不变。
- 3、在移动后需要处理空白区域的填充,同样涉及到layout操作,故需要处理。
private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
Rect rect = mItemRects.get(pos);
if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
View child = recycler.getViewForPosition(pos);
if (firstPos) {
addView(child, 0);
} else {
addView(child);
}
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
mHasAttachedItems.put(pos, true);
}
}
到此就实现了横向的滚动效果了,效果如下:
横向滚动.gif
完整的scrollHorizontallyBy代码如下
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
if (getChildCount() <= 0) {
return dx;
}
int travel = dx;
//如果滑动到最顶部
if (mSumDx + dx < 0) {
travel = -mSumDx;
} else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
//如果滑动到最底部
travel = mTotalWidth - getHorizontalSpace() - mSumDx;
}
mSumDx += travel;
Rect visibleRect = getVisibleArea();
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
int position = getPosition(child);
Rect rect = mItemRects.get(position);
if (!Rect.intersects(rect, visibleRect)) {
removeAndRecycleView(child, recycler);
mHasAttachedItems.put(position, false);
} else {
layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
mHasAttachedItems.put(position, true);
}
}
//填充空白区域
View lastView = getChildAt(getChildCount() - 1);
View firstView = getChildAt(0);
if (travel >= 0) {
int minPos = getPosition(firstView);
for (int i = minPos; i < getItemCount(); i++) {
insertView(i, visibleRect, recycler, false);
}
} else {
int maxPos = getPosition(lastView);
for (int i = maxPos; i >= 0; i--) {
insertView(i, visibleRect, recycler, true);
}
}
return travel;
}
private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
Rect rect = mItemRects.get(pos);
if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
View child = recycler.getViewForPosition(pos);
if (firstPos) {
addView(child, 0);
} else {
addView(child);
}
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
mHasAttachedItems.put(pos, true);
}
}
2.4、实现卡片叠加
从最终的效果图中可以看出,我们两个卡片之间并不是并排排列的,而是叠加在一起的。在这个例子中,两个卡片之间叠加的部分是半个卡片的大小。所以,我们需要修改排列卡片的代码,使卡片叠加起来。
首先,申请一个变量,保存两个卡片之间的距离:
private int mIntervalWidth;
private int getIntervalWidth() {
return mItemWidth / 2;
}
然后在onLayoutChildren
中,首先给mIntervalWidth初始化,然后在计算每个卡片的起始位置时,offsetX每次位移距离,改为offsetX += mIntervalWidth,具体代码如下:
mIntervalWidth = getIntervalWidth();
//定义水平方向的偏移量
int offsetX = 0;
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
mItemRects.put(i, rect);
mHasAttachedItems.put(i, false);
offsetX += mIntervalWidth;
}
这里需要注意的是,在计算每个卡片的位置时Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight)
,在这个Rect的right位置,不能改为offsetX + mIntervalWidth
,因为我们只是更改了卡片布局时的起始位置,并没有更改卡片的大小,所以每个卡片的长度和宽度是不能变的。
然后在初始化时插入item时,在计算visibleCount时,需要改为int visibleCount = getHorizontalSpace() / mIntervalWidth,代码如下:
int visibleCount = getHorizontalSpace() / mIntervalWidth;
Rect visibleRect = getVisibleArea();
for (int i = 0; i < visibleCount; i++) {
insertView(i, visibleRect, recycler, false);
}
因为在scrollHorizontallyBy中处理滚动时,每个卡片的位置都是直接从mItemRects中取的,所以,我们并不需要在修改滚动时的代码。
到这里,就实现了卡片叠加的功能,效果如下图所示:
卡片叠加.gif
2.5、修改卡片的起始位置
到现在,我们卡片都还是在最左侧开始展示的,但在开篇的效果图中可以看出,在初始化时,第一个item是在最屏幕中间显示的,这是怎么做到的呢?
首先,我们需要先申请一个变量mStartX,来保存卡片后移的距离。
很明显,这里也只是改变每个卡片的布局位置,所以我们也只需要在onLayoutChildren中,在mItemRects中初始化每个item位置时,将每个item后移mStartX就可以了。
所以核心代码如下:
private int mStartX;
public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
…………
mStartX = getWidth()/2 - mIntervalWidth;
//定义水平方向的偏移量
int offsetX = 0;
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(mStartX + offsetX, 0, mStartX + offsetX + mItemWidth, mItemHeight);
mItemRects.put(i, rect);
mHasAttachedItems.put(i, false);
offsetX += mIntervalWidth;
}
…………
}
首先,是mStartX的初始化,因为我们需要第一个卡片的中间位置在屏幕正中间的位置,从下图中明显可以看出,mStartX的值应该是:mStartX = getWidth()/2 - mIntervalWidth;
然后,在计算每个item的rect时,将每个item后移mStartX距离:new Rect(mStartX + offsetX, 0, mStartX + offsetX + mItemWidth, mItemHeight)
就这样,我们就完成了移动初始化位置的功能,效果如下图所示:
修改初始位置.gif
2.6、更改默认显示顺序
2.6.1、 更改默认显示顺序的原理
现在,我们每个item的显示顺序还是后一个卡片压在前一个卡片上显示的,这是因为,在RecyclerView绘制时,先绘制的第一个item,然后再绘制第二个item,然后再绘制第三个item,……,默认就是这样的绘制顺序。即越往前的item越优先绘制。绘制原理示图如下:
image.png
这里显示的三个item绘制次序,很明显,正是由于后面的item把前面的item叠加部分盖住了,才造成了现在的每个item只显示出一半的情况。
那如果我们更改下显示顺序,将两边的先绘制,将屏幕中间的Item(当前选中的item)最后绘制,就会成为这个情况:
image.png
形成的效果就是本节开篇的效果。
那关键的部分来:要怎么更改Item的绘制顺序呢?
其实,只需要重写RecyclerView的getChildDrawingOrder方法即可。
该方法的详细声明如下:
protected int getChildDrawingOrder(int childCount, int i)
- childCount:表示当前屏幕上可见的item的个数
- i:表示item的索引,一般而言,i的值就是在list中可见item的顺序,通过getChildAt(i)即可得到当前item的视图。
- return int:返回值表示当前item的绘制顺序,返回值越小,越先绘制,返回值越大,越最后绘制。很显然,要实现我们开篇的效果,中间item的返回值应该是最大的,才能让它最后绘制,以显示在最上面。
需要注意的是,默认情况下,即便重写getChildDrawingOrder
函数,代码也不会执行到getChildDrawingOrder
里面的,我们需要在RecyclerView初始化时,显式调用setChildrenDrawingOrderEnabled(true);
开启重新排序。
所以开启重新排序,总共需要有两步:
- 1.调用setChildrenDrawingOrderEnabled(true);开启重新排序
- 2.在getChildDrawingOrder中重新返回每个item的绘制顺序
2.6.2、重写RecyclerView
因为我们要重写getChildDrawingOrder,所以我们必须重写RecylcerView:
public class RecyclerCoverFlowView extends RecyclerView {
public RecyclerCoverFlowView(Context context) {
super(context);
init();
}
public RecyclerCoverFlowView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public RecyclerCoverFlowView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init(){
setChildrenDrawingOrderEnabled(true); //开启重新排序
}
/**
* 获取LayoutManger,并强制转换为CoverFlowLayoutManger
*/
public CoverFlowLayoutManager getCoverFlowLayout() {
return ((CoverFlowLayoutManager)getLayoutManager());
}
@Override
protected int getChildDrawingOrder(int childCount, int i) {
return super.getChildDrawingOrder(childCount, i);
}
}
在这里,我们主要做了两步:
在初始化时,使用setChildrenDrawingOrderEnabled(true);开启重新排序
因为后面,我们需要用到我们自定义的LayoutManager,所以我们额外提供了一个函数public CoverFlowLayoutManager getCoverFlowLayout(),以供后续使用
接下来,我们就来看看如何在getChildDrawingOrder中返回对应item的绘制顺序。
2.6.3、计算绘制顺序原理
下图展示了位置索引与绘图顺序的关系:
image.png
在这个图中,总共有7个Item,带有圆圈的0,1,2,3,4,5,6是当前在屏幕中显示的item位置索引,它的值也是默认的绘图顺序,默认的绘图顺序就是越靠前的item越先绘制。
要想达到图上所示的效果,它的绘图顺序可以是0,1,2,6,5,4,3;因为数值代表的是绘制顺序,所以值越大的越后绘制,所以左侧的三个的顺序是0,1,2;所以,第一个item先绘制,然后第二个item盖在第一个上面;再然后,第三个item再绘制,它会盖在第二个item的上面。所以这样就保证的中间卡片左侧部分的叠加效果。右侧三个的绘制顺序是5,4,3;所以最后一个item先绘制,然后是倒数第二个,最后是倒数第三个;同样,右侧三个也可以保证图中的叠加效果。最中间的Item绘制顺序为6,所以最后绘制,所以它会盖在所有item的上面显示出来。
注意:我这里讲到这个效果的绘图顺序时,说的是“可以是”,而不是“必须是”!,只要保证下面两点,所有的绘图顺序都是正确的:
绘图顺序的返回值范围在0到childCount-1之间,其中(childCount表示当前屏幕中的可见item个数)
此绘图顺序在叠加后,可以保证最终效果
所以,如果我们把绘图顺序改为3,4,5,6,2,1,0;同样是可以达到上面的效果的。
为了方便计算规则,我们使用0,1,2,6,5,4,3的绘图顺序。
很明显,我们需要先找到所有在显示item的中间位置,中间位置的绘图顺序是count -1;
然后中间位置之前的绘图顺序和它的排列排序相同,在getChildDrawingOrder函数中,排列顺序是i,那么绘图顺序也是i;
最难的部分是中间位置之后的部分,它们的绘图顺序怎么算。
很明显,最后一个item的绘图顺序始终是center(指屏幕显示的中间item的索引,这里是3)。倒数第二个的绘图顺序是center+1,倒数第三个的绘图顺序是center+2;从这个计算中可以看出,后面的item的绘图顺序总是center+m,而m的值就是当前的item和最后一个item所间隔的个数。那当前item和最后一个item间隔的个数怎么算呢?它等于count - 1 - i;不知道大家能不能理解,count-1正常显示顺序下最后一个item的索引,也就是当前可见的item中的最大的索引,而i是屏幕中显示的item的索引,也就是上图圆圈内的数值。所以,中间后面的item的绘图顺序的计算方法是center + count - 1- i;
需要非常注意的是,这里的i是指屏幕中显示item的索引,总是从0开始的,并不是指在Adapter中所有item中的索引值。它的意义与getChildAt(i)中的i是一样的。
所以总结来讲:
- 中间位置的绘图顺序为order = count -1;
- 中间位置之前的item的绘图顺序为 order = i;
- 中间位置之后的item的绘图顺序为 order = center + count - i - i;
2.6.4、重写getChildDrawingOrder
在理解了如何计算绘图顺序以后,现在就开始写代码了,在上面总结中,可以看到,这里count和 i 都是getChildDrawingOrder中现成的,唯一缺少的就是center值。center值是当前可见item中间位置从0开始的索引。我们可以通过中间位置的position减去第一个可见的item的position得到。
所以,我们需要在CoverFlowLayoutManager中添加一个函数(获取中间item的positon–指在adapter中的position):
public int getCenterPosition(){
int pos = (int) (mSumDx / getIntervalWidth());
int more = (int) (mSumDx % getIntervalWidth());
if (more > getIntervalWidth() * 0.5f) pos++;
return pos;
}
因为我们每个item的间隔都是getIntervalWidth(),所以通过mSumDx / getIntervalWidth()就可以知道当前移到了多少个item了。因为我们已经将第一个item移到了中间,所以这里的pos就是移动mSumDx以后,中间位置item的索引。
但是又因为我们通过mSumDx / getIntervalWidth()取整数时,它的结果是向下取整的。所以,但是我们想要在中间item移动时,超过一半就切换到下一个item显示。所以我们需要做一个兼容处理:
int more = (int) (mSumDx % getIntervalWidth());
if (more > getIntervalWidth() * 0.5f) pos++;
利用(int) (mSumDx % getIntervalWidth())得到当前正在移动的item移动过的距离,如果more大于半个item的话,那就让pos++,将下一个item标记为center,从而让它最后绘制,显示在最上层。
在得到中间位置的position之后,我们还需要得到第一个可见的item的position:
public int getFirstVisiblePosition() {
if (getChildCount() <= 0){
return 0;
}
View view = getChildAt(0);
int pos = getPosition(view);
return pos;
}
这里的原理也非常简单,就是利用getChildAt(0)得到当前在显示的,第一个可见的item的View,然后通过getPosition(View)得到这个view在Adapter中的position。
接下来,我们就重写getChildDrawingOrder,根据原理可得如下代码:
protected int getChildDrawingOrder(int childCount, int i) {
int center = getCoverFlowLayout().getCenterPosition()
- getCoverFlowLayout().getFirstVisiblePosition(); //计算正在显示的所有Item的中间位置
int order;
if (i == center) {
order = childCount - 1;
} else if (i > center) {
order = center + childCount - 1 - i;
} else {
order = i;
}
return order;
}
在获得绘图顺序的原理理解了之后,上面的代码就没有难度了,这里就不再细讲了。到这里,我们就实现了通过更改绘图顺序的方式,让当前选中的item在中间全部展示出来。
这样,我们修改绘制顺序的代码就完成了,效果如下图所示:
绘图顺序.gif
2.7、 添加滚动缩放功能
2.7.1、代码实现
在讲解《RecyclerView回收实现方式二》时,我们就已经实现了,在滚动时让Item旋转的功能,其实非常简单,只需要在layoutDecoratedWithMargins后,调用setRotate系列函数即可,同样的,我们先写一个针对刚添加的ChildView进行缩放的函数:
private void handleChildView(View child,int moveX){
float radio = computeScale(moveX);
child.setScaleX(radio);
child.setScaleY(radio);
}
private float computeScale(int x) {
float scale = 1 -Math.abs(x * 1.0f / (8f*getIntervalWidth()));
if (scale < 0) scale = 0;
if (scale > 1) scale = 1;
return scale;
}
在这两个函数中,handleChildView函数非常容易理解,就是先通过computeScale(moveX)计算出一个要缩放的值,然后调用setScale系列函数来缩放
这里先实现效果,至于computeScale(moveX)里的公式是如何得来的,我们最后再讲解,这里先用着。
接着,我们需要把handleChildView放在所有的layoutDecoratedWithMargins后,进行对刚布局的view进行缩放:
public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
…………
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
…………
if (!Rect.intersects(rect, visibleRect)) {
removeAndRecycleView(child, recycler);
mHasAttachedItems.put(position, false);
} else {
layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
handleChildView(child,rect.left - mStartX - mSumDx);
mHasAttachedItems.put(position, true);
}
}
…………
}
private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
…………
if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
…………
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
handleChildView(child,rect.left - mStartX - mSumDx);
mHasAttachedItems.put(pos, true);
}
}
到这里,我们就实现了开篇的效果:
缩放.gif
2.7.2、缩放系数计算原理
我们要实现在卡片滑动时平滑的缩放,所以,在滑动过程中得到的缩放因子肯定是要连续的,所以它的函数必定是可以用直线或者曲线表示的。
在这里,我直接用一条直线来计算滚动过程中缩放因子,此直线如下图所示:
- Y轴:表示图片的缩放比例
- X轴:表示item距离中心点的距离。很明显,当中间的item的左上角在mStartX上时,此时距离中心点的距离为0,应该是最大状态,缩放因子应该是1.我这里假设在相距一个间距(getIntervalWidth())时,大小变为7/8,当然这个值,大家都可以随意定。
所以(0,1)、(1,7/8)这两个点就形成了一条直线(两点连成一条线),现在是要利用三角形相似,求出来这条直线的公式。
image.png这里根据三角形相似求出来公式倒是难度不大,但需要注意的是,x轴上的单位是getIntervalWidth(),所以在x轴上1实际代表的是1*getIntervalWidth();
公式求出来以后,就是输入X值,得到对应的缩放因子。那值要怎么得到呢?
我们知道X的意思是当前item与startX的间距。当间距是0时,得到1。所以x值是:rect.left - mSumDx - mStartX;
其中rect.left - mSumDx表示的是当前item在屏幕上位置。所以rect.left - mSumDx - mStartX表示的是当前item在屏幕上与mStartX的距离。
这样,缩放系数的计算原理就讲完了,当然大家也可以使用其它的缩放公式,而且也并不一定是用直线,也可以用曲线,无论用什么公式,但一定要保证是线,不能断,一旦出现断裂的情况,就会导致缩放不顺畅,会出现突然变大或者突然变小的情况。现在,大家就可以根据自己的知识储备自由发挥了。
2.8、bug修复
这里看似效果效果实现的非常完美,但是,当你滑动到底的时候,问题来了:
image.png
从图中可以看到,在滑动到底的时候,停留在了倒数第二个Item被选中的状态,应该让最后一个item被选中,才是真正的到底。那怎么解决呢?
还记得吗?我们在讲解《自定义LayoutManager》中,在刚写好LinearLayoutManager时,到顶和到底后都是可以继续上滑和下滑的。我们为了到顶和到底时,不让它继续滑动,特地添加了边界判断:
public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
int travel = dx;
//如果滑动到最顶部
if (mSumDx + dx < 0) {
travel = -mSumDx;
} else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
//如果滑动到最底部
travel = mTotalWidth - getHorizontalSpace() - mSumDx;
}
…………
}
很明显,正是到底的时候,我们添加了判断,让它停留在了最后一个Item在边界的状态。所以,在这里,我们需要对到底判断加以调整,让它可滑动到最后一个item被选中的状态为止。
首先,我们需要求出来最长能滚动的距离,因为每个item之间的间距是getIntervalWidth(),当一个item滚动距离超过getIntervalWidth()时,就会切换到下一个item被选中,所以一个item最长的滚动距离其实是getIntervalWidth(),所以最大的滚动距离是:
private int getMaxOffset() {
return (getItemCount() - 1) * getIntervalWidth();
}
同样,我们使用在《自定义LayoutManager》中计算较正travel的方法:
travel + mSumDx = getMaxOffset();
=> travel = getMaxOffset() - mSumDx;
所以,我们把边界判断的代码改为:
public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
int travel = dx;
//如果滑动到最顶部
if (mSumDx + dx < 0) {
travel = -mSumDx;
} else if (mSumDx + dx > getMaxOffset()) {
//如果滑动到最底部
travel = getMaxOffset() - mSumDx;
}
…………
}
现在修复了以后,到底之后就正常了,效果如下图所示:
image.png
网友评论