「满足常乐」,不少人不知足现状,各类折腾,每每舍本逐末,常乐才能少一分浮躁,多一分宁静。近期在小编身上发生了许多事情,心态也发生了很大的改变,有感于现实的无奈,在离家乡遥远城市里的落寂,追逐名利的浮躁;可能生活就是这样的,每一个年龄段都有本身的烦恼。java
说道折腾,好久之前就看到了各类自定义LayoutManager作出各类炫酷的动画,就想本身也要实现。但每次都由于系统自带的LinearLayoutManager源码搞得一脸懵逼。正好这段时间不忙,折腾了一天,写了个简单的Demo,效果以下:git
mRecyclerView.setLayoutManager(stackLayoutManager = new StackLayoutManager(this));
复制代码
跟系统的LinearLayoutManager使用方式一致,文本只是简单的Demo,功能单一,主要讲解流程与步骤,请根据特定的需求修改。github
各属性意义见图: 面试
有关自定义LayoutManager基础知识,请查阅如下文章,写的很是棒:缓存
一、陈小缘的自定义LayoutManager第十一式之飞龙在天(小缘大佬自定义文章逻辑清晰明了,堪称教科书,很是经典)ide
二、 张旭童的掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,经常使用API动画
blog.csdn.net/zxt0601/art…this
三、张旭童的掌握自定义LayoutManager(二) 实现流式布局spa
四、勇朝陈的Android仿豆瓣书影音频道推荐表单堆叠列表RecyclerView-LayoutManager
这几篇文章针对自定义LayoutManager的误区、注意事项,分析的很是到位,来来回回我看了好几篇,但愿对你有所帮助。
咱们在自定义ViewGroup中,想要显示子View,无非就三件事:
其实在自定义LayoutManager中,在流程上也是差很少的,咱们须要重写onLayoutChildren方法,这个方法会在初始化或者Adapter数据集更新时回调,在这方法里面,须要作如下事情:
以上内容出自陈小缘的自定义LayoutManager第十一式之飞龙在天。
再看下相关参数:
索引值为0的view 一次彻底滑出屏幕所须要的移动距离,定位为 firstChildCompleteScrollLength
;非索引值为0的view滑出屏幕所须要移动的距离为: firstChildCompleteScrollLength
+ onceCompleteScrollLength
; item 之间的间距为 normalViewGap
咱们在 scrollHorizontallyBy
方法中记录偏移量 dx
,保存一个累计偏移量 mHorizontalOffset
,而后针对索引值为0与非0两种状况,在 mHorizontalOffset
小于 firstChildCompleteScrollLength
状况下,用该偏移量除以 firstChildCompleteScrollLength
获取到已经滚动了的百分比 fraction
;同理索引值非0的状况下,偏移量须要减去 firstChildCompleteScrollLength
来获取到滚动的百分比。根据百分比,怎么布局childview就很容易了。
接下来开始写代码,先取个比较接地气的名字,就叫 StackLayoutManager
,好普通的名字,哈哈。
StackLayoutManager
继承 RecyclerView.LayoutManager
,须要重写 generateDefaultLayoutParams
方法:
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}
复制代码
先看当作员变量:
/** * 一次完整的聚焦滑动所须要的移动距离 */
private float onceCompleteScrollLength = -1;
/** * 第一个子view的偏移量 */
private float firstChildCompleteScrollLength = -1;
/** * 屏幕可见第一个view的position */
private int mFirstVisiPos;
/** * 屏幕可见的最后一个view的position */
private int mLastVisiPos;
/** * 水平方向累计偏移量 */
private long mHorizontalOffset;
/** * view之间的margin */
private float normalViewGap = 30;
private int childWidth = 0;
/** * 是否自动选中 */
private boolean isAutoSelect = true;
// 选中动画
private ValueAnimator selectAnimator;
复制代码
接着看看 scrollHorizontallyBy
方法:
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
// 手指从右向左滑动,dx > 0; 手指从左向右滑动,dx < 0;
// 位移0、没有子View 固然不移动
if (dx == 0 || getChildCount() == 0) {
return 0;
}
// 偏差处理
float realDx = dx / 1.0f;
if (Math.abs(realDx) < 0.00000001f) {
return 0;
}
mHorizontalOffset += dx;
dx = fill(recycler, state, dx);
return dx;
}
private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
int resultDelta = dx;
resultDelta = fillHorizontalLeft(recycler, state, dx);
recycleChildren(recycler);
return resultDelta;
}
private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
//----------------一、边界检测-----------------
if (dx < 0) {
// 已到达左边界
if (mHorizontalOffset < 0) {
mHorizontalOffset = dx = 0;
}
}
if (dx > 0) {
if (mHorizontalOffset >= getMaxOffset()) {
// 根据最大偏移量来计算滑动到最右侧边缘
mHorizontalOffset = (long) getMaxOffset();
dx = 0;
}
}
// 分离所有的view,加入到临时缓存
detachAndScrapAttachedViews(recycler);
float startX = 0;
float fraction = 0f;
boolean isChildLayoutLeft = true;
View tempView = null;
int tempPosition = -1;
if (onceCompleteScrollLength == -1) {
// 由于mFirstVisiPos在下面可能被改变,因此用tempPosition暂存一下
tempPosition = mFirstVisiPos;
tempView = recycler.getViewForPosition(tempPosition);
measureChildWithMargins(tempView, 0, 0);
childWidth = getDecoratedMeasurementHorizontal(tempView);
}
// 修正第一个可见view mFirstVisiPos 已经滑动了多少个完整的onceCompleteScrollLength就表明滑动了多少个item
firstChildCompleteScrollLength = getWidth() / 2 + childWidth / 2;
if (mHorizontalOffset >= firstChildCompleteScrollLength) {
startX = normalViewGap;
onceCompleteScrollLength = childWidth + normalViewGap;
mFirstVisiPos = (int) Math.floor(Math.abs(mHorizontalOffset - firstChildCompleteScrollLength) / onceCompleteScrollLength) + 1;
fraction = (Math.abs(mHorizontalOffset - firstChildCompleteScrollLength) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f);
} else {
mFirstVisiPos = 0;
startX = getMinOffset();
onceCompleteScrollLength = firstChildCompleteScrollLength;
fraction = (Math.abs(mHorizontalOffset) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f);
}
// 临时将mLastVisiPos赋值为getItemCount() - 1,放心,下面遍历时会判断view是否已溢出屏幕,并及时修正该值并结束布局
mLastVisiPos = getItemCount() - 1;
float normalViewOffset = onceCompleteScrollLength * fraction;
boolean isNormalViewOffsetSetted = false;
//----------------三、开始布局-----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
View item;
if (i == tempPosition && tempView != null) {
// 若是初始化数据时已经取了一个临时view
item = tempView;
} else {
item = recycler.getViewForPosition(i);
}
addView(item);
measureChildWithMargins(item, 0, 0);
if (!isNormalViewOffsetSetted) {
startX -= normalViewOffset;
isNormalViewOffsetSetted = true;
}
int l, t, r, b;
l = (int) startX;
t = getPaddingTop();
r = l + getDecoratedMeasurementHorizontal(item);
b = t + getDecoratedMeasurementVertical(item);
layoutDecoratedWithMargins(item, l, t, r, b);
startX += (childWidth + normalViewGap);
if (startX > getWidth() - getPaddingRight()) {
mLastVisiPos = i;
break;
}
}
return dx;
}
复制代码
涉及的方法:
/** * 最大偏移量 * * @return */
private float getMaxOffset() {
if (childWidth == 0 || getItemCount() == 0) return 0;
return (childWidth + normalViewGap) * (getItemCount() - 1);
}
/** * 获取某个childView在水平方向所占的空间,将margin考虑进去 * * @param view * @return */
public int getDecoratedMeasurementHorizontal(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
/** * 获取某个childView在竖直方向所占的空间,将margin考虑进去 * * @param view * @return */
public int getDecoratedMeasurementVertical(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
复制代码
这里使用Android仿豆瓣书影音频道推荐表单堆叠列表RecyclerView-LayoutManager中使用的回收技巧:
/** * @param recycler * @param state * @param delta */
private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int delta) {
int resultDelta = delta;
//。。。省略
recycleChildren(recycler);
log("childCount= [" + getChildCount() + "]" + ",[recycler.getScrapList().size():" + recycler.getScrapList().size());
return resultDelta;
}
/** * 回收需回收的Item。 */
private void recycleChildren(RecyclerView.Recycler recycler) {
List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
for (int i = 0; i < scrapList.size(); i++) {
RecyclerView.ViewHolder holder = scrapList.get(i);
removeAndRecycleView(holder.itemView, recycler);
}
}
复制代码
回收复用这里就不验证了,感兴趣的小伙伴可自行验证。
private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
// 省略 ......
//----------------三、开始布局-----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
// 省略 ......
// 缩放子view
final float minScale = 0.6f;
float currentScale = 0f;
final int childCenterX = (r + l) / 2;
final int parentCenterX = getWidth() / 2;
isChildLayoutLeft = childCenterX <= parentCenterX;
if (isChildLayoutLeft) {
final float fractionScale = (parentCenterX - childCenterX) / (parentCenterX * 1.0f);
currentScale = 1.0f - (1.0f - minScale) * fractionScale;
} else {
final float fractionScale = (childCenterX - parentCenterX) / (parentCenterX * 1.0f);
currentScale = 1.0f - (1.0f - minScale) * fractionScale;
}
item.setScaleX(currentScale);
item.setScaleY(currentScale);
item.setAlpha(currentScale);
layoutDecoratedWithMargins(item, l, t, r, b);
// 省略 ......
}
return dx;
}
复制代码
childView
越向屏幕中间移动缩放比越大,越向两边移动缩放比越小。
监听 onScrollStateChanged
,在滚动中止时计算出应当停留的 position
,再计算出停留时的 mHorizontalOffset
值,播放属性动画将当前 mHorizontalOffset
不断更新至最终值便可。相关代码以下:
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
switch (state) {
case RecyclerView.SCROLL_STATE_DRAGGING:
//当手指按下时,中止当前正在播放的动画
cancelAnimator();
break;
case RecyclerView.SCROLL_STATE_IDLE:
//当列表滚动中止后,判断一下自动选中是否打开
if (isAutoSelect) {
//找到离目标落点最近的item索引
smoothScrollToPosition(findShouldSelectPosition());
}
break;
default:
break;
}
}
/** * 平滑滚动到某个位置 * * @param position 目标Item索引 */
public void smoothScrollToPosition(int position) {
if (position > -1 && position < getItemCount()) {
startValueAnimator(position);
}
}
private int findShouldSelectPosition() {
if (onceCompleteScrollLength == -1 || mFirstVisiPos == -1) {
return -1;
}
int position = (int) (Math.abs(mHorizontalOffset) / (childWidth + normalViewGap));
int remainder = (int) (Math.abs(mHorizontalOffset) % (childWidth + normalViewGap));
// 超过一半,应当选中下一项
if (remainder >= (childWidth + normalViewGap) / 2.0f) {
if (position + 1 <= getItemCount() - 1) {
return position + 1;
}
}
return position;
}
private void startValueAnimator(int position) {
cancelAnimator();
final float distance = getScrollToPositionOffset(position);
long minDuration = 100;
long maxDuration = 300;
long duration;
float distanceFraction = (Math.abs(distance) / (childWidth + normalViewGap));
if (distance <= (childWidth + normalViewGap)) {
duration = (long) (minDuration + (maxDuration - minDuration) * distanceFraction);
} else {
duration = (long) (maxDuration * distanceFraction);
}
selectAnimator = ValueAnimator.ofFloat(0.0f, distance);
selectAnimator.setDuration(duration);
selectAnimator.setInterpolator(new LinearInterpolator());
final float startedOffset = mHorizontalOffset;
selectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
mHorizontalOffset = (long) (startedOffset + value);
requestLayout();
}
});
selectAnimator.start();
}
复制代码
咱们能够直接拿到 view
的 position
,直接调用 smoothScrollToPosition
方法,就能够实现自动选中为焦点。
效果是这样的:
RecyclerView
继承于 ViewGroup
,那么在添加子view addView(View child, int index)
中 index
的索引值越大,越显示在上层。那么能够得出,为2的绿色卡片被添加是 index
最大,分析能够得出如下结论:
index
的大小:
0 < 1 < 2 > 3 > 4
中间最大,两边逐渐减少的原则。
获取到中间 view
的索引值,若是小于等于该索引值则调用 addView(item)
,反之调用 addView(item, 0)
;相关代码以下:
private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state, int dx) {
//省略 ......
//----------------三、开始布局-----------------
for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
//省略 ......
int focusPosition = (int) (Math.abs(mHorizontalOffset) / (childWidth + normalViewGap));
if (i <= focusPosition) {
addView(item);
} else {
addView(item, 0);
}
//省略 ......
}
return dx;
}
复制代码
文章到这里就差很少要结束了。
源码地址:
给 个 star 呗 ~
爱笑的人,运气通常都不会太差。同时也给本身一个鼓励,咱们下期见。