写点什么

Android 自定义控件进阶篇,自定义 LayoutManager,深入剖析

用户头像
Android架构
关注
发布于: 2021 年 11 月 05 日

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) {//----------------1、边界检测-----------------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 = getDe


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


coratedMeasurementHorizontal(tempView);}


// 修正第一个可见 view mFirstVisiPos 已经滑动了多少个完整的 onceCompleteScrollLength 就代表滑动了多少个 itemfirstChildCompleteScrollLength = 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;


//----------------3、开始布局-----------------for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {View item;if (i == tempPosition && tempView != null) {// 如果初始化数据时已经取了一个临时 viewitem = 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) {// 省略 ......//----------------3、开始布局-----------------for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {// 省略 ......


// 缩放子 viewfinal 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 越向屏幕中间移动缩放比越大,越向两边移动缩放比越小。

自动选中

1、滚动停止后自动选中

监听 onScrollStateChanged,在滚动停止时计算出应当停留的 position,再计算出停留时的 mHorizontalOffset 值,播放属性动画将当前 mHorizontalOffset 不断更新至最终值即可。相关代码如下:


@Overridepublic 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() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float value = (float) animation.getAnimatedValue();mHorizontalOffset = (long) (startedOffset + value);requestLayout();}});selectAnimator.start();}

2、点击非焦点 view 自动将其选中为焦点 view
用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android自定义控件进阶篇,自定义LayoutManager,深入剖析