LayoutManager详解及使用(二)
上一篇文章介绍了LayoutManager的基本知识,这一篇文章我们来进行一个实际的功能开发,制作一个滚动的Banner,没什么比写代码更加爽的事情了————吧?
效果图
中间的卡片显示的更大一点,两边可以看到旁边的卡片的一角,离中心距离越近卡片越大。
准备
正式编写前,我们先新建一个MyLayoutManager
继承至RecyclerView.LayoutManager
并且实现一个必须实现的方法:
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
简单显示
首先我们先让屏幕有东西显示出来,先不考虑数据的大小,adapter的所有数据显示出来,重写onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
方法,如下:
private int offsetX = 0; //水平偏移
private int mLeftX = 0; //卡片左端点的位置
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//没有数据就不进行处理
if (getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
//将所有的子view临时移除并且回收
detachAndScrapAttachedViews(recycler);
//进行布局
offsetX = 0;
for (int i = 0; i < getItemCount(); i++) {
View child = recycler.getViewForPosition(i);
addView(child);
measureAndLayout(child, i);
}
}
/**
* 计算view的大小并且设置位置
*
* @param view 目标view
* @param position 在数据里面的位置
*/
private void measureAndLayout(View view, int position) {
//开始计算大小
measureChildWithMargins(view, 0, 0);
//计算宽度
int width = getDecoratedMeasuredWidth(view);
//计算高度
int height = getDecoratedMeasuredHeight(view);
//将view放置在RecyclerView里面
layoutDecoratedWithMargins(view, mLeftX , 0, mLeftX + width, height);
//更新水平位移
offsetX += width; }
效果如下:
简单交互
接下来我们来添加简单的手指滑动交互:
/**
* 允许水平滑动
*
* @return
*/
@Override
public boolean canScrollHorizontally() {
return true;
}
/**
* 禁止竖直滑动
*
* @return
*/
@Override
public boolean canScrollVertically() {
return false;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
Log.i(TAG, "scrollHorizontallyBy: " + dx);
//移动元素
offsetChildrenHorizontal(-dx);
offsetX -= dx;
return -dx;
}
添加回收机制
RecyclerView
的强大之处在于资源回收复用,上面的代码并没有体现,接下来我们来写一个简单的水平列表的LayoutManager
,先看效果图:
实现回收的一个重要的点就是判断哪一个view需要remove,哪些只需要detach,这里我定义了几个变量用来判断,
private int maxCache = 3; //最大的缓存数量
private int mLeftIndex = 0; //缓存最左边的卡片的下标
private int mRightIndex = 0; //缓存最右边的卡片的下标
private int mCenterIndex = 0; //中间的卡片的下标
如果view的位置小于mLeftIndex
或者大于mRightIndex
,则remove,否则只需要detach。所以重点就变成了怎么显示区域可见的view的位置下标。我们暂时假设每一个卡片的宽度是一致的,在实际的应用中,Banner卡片的宽度是一致的。我们需要记录view的偏移量,根据偏移量来计算可视范围的view的下标。下面定义几个变量:
private int mOffsetX = 0; //水平位移
private int mItemWidth = 1; //单个卡片宽度
private int mLeftX = 0; //卡片左端点的位置
我的思路
1.在view移动的时候更新
mOffsetX
和mLeftX
,这两个变量的意义在代码里面详细解释。
2.通过mOffsetX
和mItemWidth
计算可视范围的卡片下标。 3.对布局中的view下标进行判断,移除不需要的。
4.重新添加view,移动view。
代码如下:
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
//dx>0 左滑
//实际要滑动的距离
int travel = dx;
//计算偏移量
mOffsetX += dx;
mLeftX -= travel;
//更新下标
updateIndex();
//移除显示区外的卡片
removeUnuseCard(recycler, state);
//重新添加
layoutItems(recycler, state);
//移动
offsetChildrenHorizontal(travel);
Log.i(TAG, "scrollHorizontallyBy: count:" + getChildCount());
return travel;
}
/**
* 更新下标
*/
private void updateIndex() {
/**
* 计算中心卡片的下标
*/
mCenterIndex = (mOffsetX) / mItemWidth;
//如果位移量除以 单个卡片的宽度的余数大于半个卡片则说明这个余下的卡片已经超过半个屏幕了
if (mOffsetX % mItemWidth > mItemWidth / 2) {
mCenterIndex++;
}
mCenterIndex = Math.max(0, mCenterIndex);
mCenterIndex = Math.min(getItemCount() - 1, mCenterIndex);
/**
* 计算最左边卡片的下标
*/
mLeftIndex = mCenterIndex - maxCache / 2;
mLeftIndex = Math.max(0, mLeftIndex);
/**
* 计算最右边卡片的下标
*/
mRightIndex = mLeftIndex + maxCache - 1;
mRightIndex = Math.min(getItemCount() - 1, mRightIndex);
}
/**
* 移除不需要的卡片
*/
private void removeUnuseCard(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0)
return;
/**
* 将显示范围外的完全移除
*/
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
if (getPosition(view) < mLeftIndex || getPosition(view) > mRightIndex) {
removeAndRecycleView(view, recycler);
}
}
}
打印log,查看页面的view数量,发现稳定在3个
加入缩放效果
下面我们来实现卡片的缩放效果,越靠近屏幕中心点的卡片显示越大。我们需要判断出卡片与中心点的距离,以此来计算卡片应该缩放的比例。在measureAndLayout(View view, int position)
方法的最下面调用下面的方法:
/**
* 缩放卡片
*
* @param item 需要缩放的对象
*/
private void scaleView(View item) {
//偏移量 : 卡片默认的第一张卡片左边与可视范围左边的偏移量
float leftX = (getWidth() - mItemWidth) / 2;
//卡片与中心点的距离
float distance = item.getX() - leftX;
//计算绝对距离
float d = Math.abs(distance);
d = Math.min(d, getItemWidth());
/**
* 距离最大为一个卡片的宽度
*/
if (d < getItemWidth() / 4) {
item.setScaleX(1);
item.setScaleY(1);
} else {
//卡片缩放值范围1 - 0.9
float scale = (float) (1.1 - (0.4 * d / getItemWidth()));
item.setScaleX(scale);
item.setScaleY(scale);
}
}