如何利用 Android 自定义控件实现炫酷的动画?|CSDN 博文精选
作者 | u012551350
本文精选自 CSDN 博客,已获作者授权
「知足常乐」,很多人不满足现状,各种折腾,往往舍本逐末,常乐才能少一分浮躁,多一分宁静。近期在笔者身上发生了许多事情,心态也发生了很大的改变,有感于现实的无奈,在离家乡遥远城市里的落寂,追逐名利的浮躁;可能生活就是这样的,每个年龄段都有自己的烦恼。
说到折腾,很久以前就看到了各种自定义LayoutManager做出各种炫酷的动画,就想自己也要实现。但每次都因为系统自带的LinearLayoutManager源码搞得一脸懵逼。正好这段时间不忙,折腾了一天,写了个简单的Demo,效果如下:
RecyclerView的重要性不必多说,据过往开发经验而谈,超过一屏可滑动的界面,基本都可以采用 「RecyclerView的多类型」 来做,不管维护还是扩展都是非常有效率的。RecyclerView相关的面试题也是各大厂常问的问题之一(权重非常高)。
使用
mRecyclerView.setLayoutManager(stackLayoutManager = new StackLayoutManager(this));this));
跟系统的LinearLayoutManager使用方式一致,文本只是简单的Demo,功能单一,主要讲解流程与步骤,请根据特定的需求修改。
各属性意义见图:
注意:因为item随着滑动会有不同的缩放,所以实际normalViewGap会被缩放计算。
自定义LayoutManager基础知识
有关自定义LayoutManager基础知识,请查阅以下文章,写的非常棒:
1、陈小缘的自定义LayoutManager第十一式之飞龙在天(小缘大佬自定义文章逻辑清晰明了,堪称教科书,非常经典)
https://blog.csdn.net/u011387817/article/details/81875021
2、 张旭童的掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API
https://blog.csdn.net/zxt0601/article/details/52948009
3、张旭童的掌握自定义LayoutManager(二) 实现流式布局
https://blog.csdn.net/zxt0601/article/details/52956504
4、勇朝陈的Android仿豆瓣书影音频道推荐表单堆叠列表RecyclerView-LayoutManager
https://blog.csdn.net/ccy0122/article/details/90515386
这几篇文章针对自定义LayoutManager的误区、注意事项,分析的非常到位,来来回回我看了好几篇,希望对你有所帮助。
自定义LayoutManager基本流程
让Items显示出来
我们在自定义ViewGroup中,想要显示子View,无非就三件事:
添加 通过addView方法把子View添加进ViewGroup或直接在xml中直接添加;
测量 重写onMeasure方法并在这里决定自身尺寸以及每一个子View大小;
布局 重写onLayout方法,在里面调用子View的layout方法来确定它的位置和尺寸。
其实在自定义LayoutManager中,在流程上也是差不多的,我们需要重写onLayoutChildren方法,这个方法会在初始化或者Adapter数据集更新时回调,在这方法里面,需要做以下事情:
进行布局之前,我们需要调用detachAndScrapAttachedViews方法把屏幕中的Items都分离出来,内部调整好位置和数据后,再把它添加回去(如果需要的话);
分离了之后,我们就要想办法把它们再添加回去了,所以需要通过addView方法来添加,那这些View在哪里得到呢? 我们需要调用 Recycler的getViewForPosition(int position) 方法来获取;
获取到Item并重新添加了之后,我们还需要对它进行测量,这时候可以调用measureChild或measureChildWithMargins方法,两者的区别我们已经了解过了,相信同学们都能根据需求选择更合适的方法;
在测量完还需要做什么呢? 没错,就是布局了,我们也是根据需求来决定使用layoutDecorated还是layoutDecoratedWithMargins方法;
在自定义ViewGroup中,layout完就可以运行看效果了,但在LayoutManager还有一件非常重要的事情,就是回收了,我们在layout之后,还要把一些不再需要的Items回收,以保证滑动的流畅度。
以上内容出自陈小缘的自定义LayoutManager第十一式之飞龙在天(https://blog.csdn.net/u011387817/article/details/81875021)。
布局实现
再看下相关参数:
如果去掉itemView的缩放,透明度动画,那么效果是这样的:
看到的效果与LinearLayoutManager一样,但本篇并不使用LinearLayoutManager,而是通过自定义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); }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;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) { //----------------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 = 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; //----------------3、开始布局----------------- 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; }// 手指从右向左滑动,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) {//----------------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 = getDecoratedMeasurementHorizontal(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; }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中(https://blog.csdn.net/ccy0122/article/details/90515386)使用的回收技巧:
/** * @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 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++) { // 省略 ...... // 缩放子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; }// 省略 ......//----------------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 不断更新至最终值即可。相关代码如下:
@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(); }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() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float value = (float) animation.getAnimatedValue();mHorizontalOffset = (long) (startedOffset + value);requestLayout();}});selectAnimator.start();}
2、点击非焦点view自动将其选中为焦点view
我们可以直接拿到 view 的 position,直接调用 smoothScrollToPosition 方法,就可以实现自动选中为焦点。
中间view覆盖在两边view之上,效果是这样的:
从效果中可以看出,索引为2的view覆盖在1,3的上面,同时1又覆盖在0的上面,以此内推。
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) { //省略 ...... //----------------3、开始布局----------------- 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; }//省略 ......//----------------3、开始布局-----------------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;}
文章到这里就差不多要结束了。
源码地址:https://github.com/HpWens/MeiWidgetView。
CSDN博客原文:https://blog.csdn.net/u012551350/article/details/93971801,欢迎大家入驻 CSDN 博客。
国家认证的Python工程师有哪些能力要求?
https://edu.csdn.net/topic/python115?utm_source=csdn_bw
【END】
热 文 推 荐
☞物联网终端五年后将超 270 亿!破竹之势下程序员如何修炼内功?
☞华为将发布鲲鹏 920 芯片数据;三星 S10 自燃;Mageia 7 正式发布 | 极客头条
☞不要让开源成为贸易战的牺牲品!
程序员们如何破局 5G?
软件为什么会沦为遗留系统?
因为有了 TA,搞定行业应用开发,不怕不怕啦!
除了V神,17个以太坊大会讲师的演讲精华都在这儿了!
☞2019年技术盘点容器篇(二):听腾讯云讲讲踏入成熟期的容器技术 | 程序员硬核评测
50行Python代码,获取公众号全部文章
不写一行代码,也能玩转Kaggle竞赛?
☞马云曾经偶像,终于把阿里留下的1400亿败光了!
点击阅读原文,输入关键词,即可搜索您想要的 CSDN 文章。
你点的每个“在看”,我都认真当成了喜欢
如何利用 Android 自定义控件实现炫酷的动画?|CSDN 博文精选相关推荐
- android svg动画框架,Android实现炫酷SVG动画效果
svg是目前十分流行的图像文件格式了,svg严格来说应该是一种开放标准的矢量图形语言,使用svg格式我们可以直接用代码来描绘图像,可以用任何文字处理工具打开svg图像,通过改变部分代码来使图像具有交互 ...
- android 天气动画,为app制作炫酷天气动画 – WeatherView
WeatherView 从1.1.0版本开始这个库使用了一个不同的setter结构. WeatherView是一个为app制作一个炫酷天气动画的Android库. Setup Android Stud ...
- Android开发——自定义炫酷PickerView惯性滚动魔改
Android开发--自定义炫酷PickerView快速滚动魔改 最近由于课内压力的增加和安卓课设项目,故没有怎么刷acm题,基本上学校要训练也就去水一波,程序设计相关内容也鸽了. 由于从来没有做过开 ...
- 一款炫酷Loading动画--加载失败
简介 上一篇文章一款炫酷Loading动画–加载成功,给大家介绍了成功动画的绘制过程,这篇文章将接着介绍加载失败特效的制作. 相比成功动画,有了前面的经验,失败动画的过程就显得比较简单了. 动画结构分 ...
- 如何做一个炫酷的动画网站-css实现图片上下浮动效果
目前网站制作技术已经非常成熟.所以要实现一个炫酷的动画网站还是非常容易,现在通过js和css就都能实现.直接css就能实现各种效果,下面我们来通过一个小动画看看如何用css来实现让你的网站图片上下浮动 ...
- 纯html+css炫酷地球仪动画效果
纯html+css炫酷地球仪动画效果 <!DOCTYPE html> <html lang="en"> <head><meta chars ...
- 炫酷的动画特效—css3旋转立方球体
炫酷的动画特效-css3旋转立方球体 想要实现旋转立方球体特效,以下的内容你不容错过. 要理解的知识点 形成一个3D空间: transform-style:preserve-3d (让父元素形成3D, ...
- unity 3D炫酷开场动画
2015/07/07// ///by xbw /环境 unity 4.6.1/// 游戏之前播放一段炫酷的动画是不是很能增加吸引力: unity支持的视频格式有mov. mpg. mpeg. mp4. ...
- html中flash的简单动画效果,css实现快速炫酷抖动动画效果
1.Animate.css简介 Animate.css是一个可在您的Web项目中使用的即用型跨浏览器动画库.非常适合强调,首页,滑块和引导注意的提示.它是一个来自国外的 CSS3 动画库,它预设了抖动 ...
最新文章
- JSP页面元素的解析顺序
- 全球及中国冠状动脉内支架行业运行现状与十四五发展状况分析报告2022版
- android中 onResume()方法什么时候执行 ??(转)
- mysql测试工作中的应用_Mysql精华总结,解决测试人员面试中的碰到的一切Mysql问题(一)...
- 两种列式存储格式:Parquet和ORC
- 原生仿微信社交社区即时通讯聊天双端APP源码开源带PC客户端文档说明
- (1)触发器systemverilog与VHDL编码
- 阿里云重磅推出物联网设备身份认证Link ID²
- php 判断 pc 移动设备,PHP判断是移动设备还是PC设备
- 代码整洁之道内容概括
- MFC Windows程序设计源代码免费下载
- linux第八周实验
- 硬核南大!一天两篇Nature正刊!
- 基于R语言的模型组合
- 动态SQL之、条件判断(转)
- 数据分析/挖掘的六个步骤
- Android 音乐资源管理与播放
- WIFI 认证 测试
- 基于Thinkphp5+EasyWeChat+fastadmin微信小程序授权登录获取手机号微信公众号网页---联合授权登录
- Qt多语言实现和动态切换(国际化)
热门文章
- python 进入E盘_anaconda python环境与原有python环境的坑
- 力扣--189旋转数组(中等)
- 项目解析jsx文件_仅含一个源文件:这5个开源项目值得一看
- 记录——《C Primer Plus (第五版)》第七章编程练习第三题
- [SQL实战]之获取所有部门中当前员工薪水最高的相关信息
- PAT 甲级 1004
- 中国啮齿动物口器行业市场供需与战略研究报告
- php Immutable,Immutable.js详解
- 信息泄露案件_圆通回应运单信息泄露案件:系主动报案,涉4万余条敏感信息...
- 索引添加后,ACCESS数据库表查询运行速度的区别