Android仿豆瓣书影音频道推荐表单堆叠列表RecyclerView-LayoutManager

Android仿豆瓣书影音频道推荐表单堆叠列表RecyclerView-LayoutManager


项目地址:https://github.com/CCY0122/FocusLayoutManager


效果预览


截图:


GIF:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dq1PKEIw-1589702037735)(https://github.com/CCY0122/FocusLayoutManager/blob/master/pic/gif_hor_2.gif?raw=true)]
gif2


可自己监听滚动编写效果,如修改成仿MacOS文件浏览:

使用

 focusLayoutManager =
                new FocusLayoutManager.Builder()
                        .layerPadding(dp2px(this, 14))
                        .normalViewGap(dp2px(this, 14))
                        .focusOrientation(FocusLayoutManager.FOCUS_LEFT)
                        .isAutoSelect(true)
                        .maxLayerCount(3)
                        .setOnFocusChangeListener(new FocusLayoutManager.OnFocusChangeListener() {
                            @Override
                            public void onFocusChanged(int focusdPosition, int lastFocusdPosition) {
                                
                            }
                        })
                        .build();
recyclerView.setLayoutManager(focusLayoutManager);

各属性意义见图:




注意:因为item在不同区域随着滑动会有不同的缩放,所以实际layerPadding、normalViewGap会被缩放计算。

调整动画效果:

                new FocusLayoutManager.Builder()
                        ......
                        .setSimpleTrasitionListener(new FocusLayoutManager.SimpleTrasitionListener() {
                             @Override
                            public float getLayerViewMaxAlpha(int maxLayerCount) {
                                return super.getLayerViewMaxAlpha(maxLayerCount);
                            }

                            @Override
                            public float getLayerViewMinAlpha(int maxLayerCount) {
                                return super.getLayerViewMinAlpha(maxLayerCount);
                            }

                            @Override
                            public float getLayerChangeRangePercent() {
                                return super.getLayerChangeRangePercent();
                            }
                            //and more
                            
                            //更多可重写方法和释义见接口声明
                        })
                        .build();

自定义动画/滚动监听:

如果你想在滑动时不仅仅改变item的大小、透明度,你有更多的想法,可以监听TrasitionListener,该监听暴露了很多关键布局数据,


            ......
            .setSimpleTrasitionListener(null) //如果默认动画不想要,移除之。or use removeTrasitionlistener(XXX) 
            .addTrasitionListener(new FocusLayoutManager.TrasitionListener() {
                            @Override
                            public void handleLayerView(FocusLayoutManager focusLayoutManager,
                                                        View view, int viewLayer,
                                                        int maxLayerCount, int position,
                                                        float fraction, float offset) {
                                
                            }

                            @Override
                            public void handleFocusingView(FocusLayoutManager focusLayoutManager,
                                                           View view, int position,
                                                           float fraction, float offset) {

                            }

                            @Override
                            public void handleNormalView(FocusLayoutManager focusLayoutManager, View view, int position, float fraction, float offset) {

                            }
                        })

各参数意义见接口注释。
实际上SimpleTrasitionListener内部就会被转为TrasitionListener

解析

自定义LayoutManager基础知识

自备。
这个项目就我学习LayoutManager的实战项目。(断断续续学习过很多次,还是得实际编码才能掌握)
推荐几篇我觉得好的自定义LayoutManager文章:
1、 张旭童的掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API
2、张旭童的掌握自定义LayoutManager(二) 实现流式布局
3、陈小缘的自定义LayoutManager第十一式之飞龙在天

自定义LayoutManager的注意事项

上面张旭童的文章里有指出很多自定义LayoutManager的误区、注意事项,我补充几点:

1、不要遍历ItemCount

这个真的,是我认为最关键的一个注意事项。getItemCount获取到的是什么?是列表的总item数量,它可能有几千条几万条,甚至某些情况使用者会特意重写getItemCount将其返回为Integer.MAX_VALUE(比如为了实现无限循环轮播)。你之所以自定义LayoutManager而不自定义ViewGroup,就是为了不管itemCount多少你都能hold住。所以你不应该在布局相关代码中遍历ItemCount!!诚然,遍历它往往可以获取很多有用的数据,对后续的布局的计算、子View是否在屏幕内等判断非常有用,但请尽量不要遍历它(除非你的LM够特殊)。
张旭童说的没错,很多文章都存在误导,我还看到过有篇”喜欢“数很多的文章里有类似这么一段代码:

 for (int i = 0; i < getItemCount(); i++) {
            View view = recycler.getViewForPosition(i);
            addView(view);
            ......

???
对于初次布局,这不就是有多少item就onCreateViewHolder多少次了么。缓存池总数 = item总数?之后的回收复用操作也没意义了。

2、注意调用getChildCount时机

在列表滚动时,一般都要判断子View是否还在屏幕内,若不在了则回收。那么获取子View的逻辑应该在detachAndScrapAttachedViews(or detachAndScrapView等)之前。见下面代码的打印:

		//分离全部的view,放入临时缓存
        log("before。child count = " + getChildCount() + ";scrap count = " + recycler.getScrapList().size());
        detachAndScrapAttachedViews(recycler);
        log("after。child count = " + getChildCount() + ";scrap count = " + recycler.getScrapList().size());
        //打印结果:
        //before。child count = 5;scrap count = 0
        //after。child count = 0;scrap count = 5

另外,不用多说,recycler.getViewForPosition应在detachAndScrapAttachedViews之后

3、回收子View小技巧

这是在陈小缘那篇文章里学到的:
可以直接把Recycler里面的mAttachedScrap全部放进mRecyclerPool中,因为我们在一开始就已经调用了detachAndScrapAttachedViews方法将当前屏幕中有效的ViewHolder全部放进了mAttachedScrap,而在重新布局的时候,有用的Holder已经被重用了,也就是拿出去了,这个mAttachedScrap中剩下的Holder,都是不需要layout的,所以可以把它们都回收进mRecyclerPool中
实用哦。
(不知道对预布局是否有影响,但我代码中并没有判断过isPreLayout,也测试过notifyItemRemoved,动画正常)

布局实现

先把上面的细节图重新贴一下

首先无视掉view的缩放、透明度变化。那么布局其实就这样:
在这里插入图片描述
我们称一个view从”普通view“滚动到”焦点view“为一次完整的聚焦滑动所需要移动的距离,定义其为onceCompleteScrollLength

在普通view移动了一个onceCompleteScrollLength,堆叠View只移动了一个layerPadding。核心逻辑就这一句。

我们在scrollHorizontallyBy中记录偏移量dx,保存一个累计偏移量mHorizontalOffset,然后用该偏移量除以onceCompleteScrollLength,就知道当前已经滚动了多少个item了,换句话说就是屏幕内第一个可见view的position知道了。同时能计算出一个onceCompleteScrollLength已经滚动了的百分比fraction,再用这个百分比换算出堆叠区域和普通区域布局起始位置的偏移量,然后可以开始布局了,对于堆叠区域的view,彼此之间距离一个layerPadding,对于普通区域view,彼此之间距离一个onceCompleteScrollLength
见代码:


	 @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;
        }

        mHorizontalOffset += dx;//累加实际滑动距离


        dx = fill(recycler, state, dx);

        return dx;
    }
	/**
     * @param recycler
     * @param state
     * @param delta
     */
    private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int delta) {
        int resultDelta = delta;
        //省略
        resultDelta = fillHorizontalLeft(recycler, state, delta);
        //省略
         return resultDelta;

    }
/**
     * 水平滚动、向左堆叠布局
     *
     * @param recycler
     * @param state
     * @param dx       偏移量。手指从右向左滑动,dx > 0; 手指从左向右滑动,dx < 0;
     */
    private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state,
                                   int dx) {

        //----------------1、边界检测-----------------
        if (dx < 0) {
            //已达左边界
            if (mHorizontalOffset < 0) {
                mHorizontalOffset = dx = 0;
            }
        }

        if (dx > 0) {
            //滑动到只剩堆叠view,没有普通view了,说明已经到达右边界了
            if (mLastVisiPos - mFirstVisiPos <= maxLayerCount - 1) {
                //因为scrollHorizontallyBy里加了一次dx,现在减回去
                mHorizontalOffset -= dx;
                dx = 0;
            }
        }

        //分离全部的view,放入临时缓存
        detachAndScrapAttachedViews(recycler);

        //----------------2、初始化布局数据-----------------

        float startX = getPaddingLeft() - layerPadding;

        View tempView = null;
        int tempPosition = -1;
        if (onceCompleteScrollLength == -1) {
            //因为mFirstVisiPos在下面可能会被改变,所以用tempPosition暂存一下。
            tempPosition = mFirstVisiPos;
            tempView = recycler.getViewForPosition(tempPosition);
            measureChildWithMargins(tempView, 0, 0);
            onceCompleteScrollLength = getDecoratedMeasurementHorizontal(tempView) + normalViewGap;
        }
        //当前"一次完整的聚焦滑动"所在的进度百分比.百分比增加方向为向着堆叠移动的方向(即如果为FOCUS_LEFT,从右向左移动fraction将从0%到100%)
        float fraction =
                (Math.abs(mHorizontalOffset) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f);

        //堆叠区域view偏移量。在一次完整的聚焦滑动期间,其总偏移量是一个layerPadding的距离
        float layerViewOffset = layerPadding * fraction;
        //普通区域view偏移量。在一次完整的聚焦滑动期间,其总位移量是一个onceCompleteScrollLength
        float normalViewOffset = onceCompleteScrollLength * fraction;
        boolean isLayerViewOffsetSetted = false;
        boolean isNormalViewOffsetSetted = false;

        //修正第一个可见的view:mFirstVisiPos。已经滑动了多少个完整的onceCompleteScrollLength就代表滑动了多少个item
        mFirstVisiPos = (int) Math.floor(Math.abs(mHorizontalOffset) / onceCompleteScrollLength); //向下取整
        //临时将mLastVisiPos赋值为getItemCount() - 1,放心,下面遍历时会判断view是否已溢出屏幕,并及时修正该值并结束布局
        mLastVisiPos = getItemCount() - 1;

		//...省略监听回调

        //----------------3、开始布局-----------------

        for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
            //属于堆叠区域
            if (i - mFirstVisiPos < maxLayerCount) {
                View item;

                if (i == tempPosition && tempView != null) {
                    //如果初始化数据时已经取了一个临时view,可别浪费了!
                    item = tempView;
                } else {
                    item = recycler.getViewForPosition(i);
                }
                addView(item);
                measureChildWithMargins(item, 0, 0);

                startX += layerPadding;
                if (!isLayerViewOffsetSetted) {
                    startX -= layerViewOffset;
                    isLayerViewOffsetSetted = true;
                }

               //...省略监听回调

                int l, t, r, b;
                l = (int) startX;
                t = getPaddingTop();
                r = (int) (startX + getDecoratedMeasurementHorizontal(item));
                b = getPaddingTop() + getDecoratedMeasurementVertical(item);
                layoutDecoratedWithMargins(item, l, t, r, b);


            } else {//属于普通区域

                View item = recycler.getViewForPosition(i);
                addView(item);
                measureChildWithMargins(item, 0, 0);

                startX += onceCompleteScrollLength;
                if (!isNormalViewOffsetSetted) {
                    startX += layerViewOffset;
                    startX -= normalViewOffset;
                    isNormalViewOffsetSetted = true;
                }

                //...省略监听回调

                int l, t, r, b;
                l = (int) startX;
                t = getPaddingTop();
                r = (int) (startX + getDecoratedMeasurementHorizontal(item));
                b = getPaddingTop() + getDecoratedMeasurementVertical(item);
                layoutDecoratedWithMargins(item, l, t, r, b);

                //判断下一个view的布局位置是不是已经超出屏幕了,若超出,修正mLastVisiPos并跳出遍历
                if (startX + onceCompleteScrollLength > getWidth() - getPaddingRight()) {
                    mLastVisiPos = i;
                    break;
                }
            }
        }

        return dx;
    }

因为measure、layout调用的都是考虑了margin的api,所以布局时也要考虑到margin:

 /**
     * 获取某个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;
    }

回收复用

用上面讲的回收技巧:

 /**
     * @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);
        }
    }

接下来验证下。

  • 验证1
    张旭童:通过getChildCount()和recycler.getScrapList().size() 查看当前屏幕上的Item数量 和 scrapCache缓存区域的Item数量,合格的LayoutManager,childCount数量不应大于屏幕上显示的Item数量,而scrapCache缓存区域的Item数量应该是0.
    编写log并打印:
childCount= [5],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0
childCount= [5],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0
childCount= [6],[recycler.getScrapList().size():0

合格。

  • 验证2
    用最直接的方法,打印onCreateViewHolder、onBindViewHolder看看到底复用了没:
  @NonNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
            View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_card,
                    viewGroup, false);
            view.setTag(++index);
            Log.d("ccy", "onCreateViewHolder = " + index);
            return new ViewHolder(view);
        }

        @Override
        public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
            Log.d("ccy", "onBindViewHolder,index = " + (int) (viewHolder.itemView.getTag()));
        }

在onCreateViewHolder创建view时,给他一个tag,然后onBindViewHolder中打印这个tag,以此查看是不用复用了view。打印如下

onCreateViewHolder = 1
onBindViewHolder,index = 1
onCreateViewHolder = 2
onBindViewHolder,index = 2
onCreateViewHolder = 3
onBindViewHolder,index = 3
onCreateViewHolder = 4
onBindViewHolder,index = 4
onCreateViewHolder = 5
onBindViewHolder,index = 5
onCreateViewHolder = 6
onBindViewHolder,index = 6
onCreateViewHolder = 7
onBindViewHolder,index = 7
onCreateViewHolder = 8
onBindViewHolder,index = 8
onBindViewHolder,index = 1
onBindViewHolder,index = 2
onBindViewHolder,index = 3
onBindViewHolder,index = 4
onBindViewHolder,index = 5
onBindViewHolder,index = 6
onBindViewHolder,index = 7
onBindViewHolder,index = 8
onCreateViewHolder = 9
onBindViewHolder,index = 9
onBindViewHolder,index = 2
onBindViewHolder,index = 3
onBindViewHolder,index = 1
onBindViewHolder,index = 4
onBindViewHolder,index = 5
onBindViewHolder,index = 6

我测试时手机一屏内最多可见约6个,从打印中可见它最多调用了9次onCreateViewHolder,这个次数完全可以接受。并且onBindViewHolder也在复用view。完全ojbk没得问题

动画效果

我做的动画,就是在滑动期间渐变view的缩放比例、透明度,使得view看上去像一层一层堆叠上去的样子。其实就是各种y = kx + b之类的计算,因为fill系列方法中已经计算出很多有用的数据了。
我的做法是,暴露出这么个接口:

/**
     * 滚动过程中view的变换监听接口。属于高级定制,暴露了很多关键布局数据。若定制要求不高,考虑使用{@link SimpleTrasitionListener}
     */
    public interface TrasitionListener {

        /**
         * 处理在堆叠里的view。
         *
         * @param focusLayoutManager
         * @param view               view对象。请仅在方法体范围内对view做操作,不要外部强引用它,view是要被回收复用的
         * @param viewLayer          当前层级,0表示底层,maxLayerCount-1表示顶层
         * @param maxLayerCount      最大层级
         * @param position           item所在的position
         * @param fraction           "一次完整的聚焦滑动"所在的进度百分比.百分比增加方向为向着堆叠移动的方向(即如果为FOCUS_LEFT
         *                           ,从右向左移动fraction将从0%到100%)
         * @param offset             当次滑动偏移量
         */
        void handleLayerView(FocusLayoutManager focusLayoutManager, View view, int viewLayer,
                             int maxLayerCount, int position, float fraction, float offset);

        /**
         * 处理正聚焦的那个View(即正处在从普通位置滚向聚焦位置时的那个view,即堆叠顶层view)
         *
         * @param focusLayoutManager
         * @param view               view对象。请仅在方法体范围内对view做操作,不要外部强引用它,view是要被回收复用的
         * @param position           item所在的position
         * @param fraction           "一次完整的聚焦滑动"所在的进度百分比.百分比增加方向为向着堆叠移动的方向(即如果为FOCUS_LEFT
         *                           ,从右向左移动fraction将从0%到100%)
         * @param offset             当次滑动偏移量
         */
        void handleFocusingView(FocusLayoutManager focusLayoutManager, View view, int position,
                                float fraction, float offset);

        /**
         * 处理不在堆叠里的普通view(正在聚焦的那个view除外)
         *
         * @param focusLayoutManager
         * @param view               view对象。请仅在方法体范围内对view做操作,不要外部强引用它,view是要被回收复用的
         * @param position           item所在的position
         * @param fraction           "一次完整的聚焦滑动"所在的进度百分比.百分比增加方向为向着堆叠移动的方向(即如果为FOCUS_LEFT
         *                           ,从右向左移动fraction将从0%到100%)
         * @param offset             当次滑动偏移量
         */
        void handleNormalView(FocusLayoutManager focusLayoutManager, View view, int position,
                              float fraction, float offset);

    }

然后在fill系列方法的对应位置回调该接口即可:

/**
     * 变换监听接口。
     */
    private List<TrasitionListener> trasitionListeners;

		 /**
     * 水平滚动、向左堆叠布局
     *
     * @param recycler
     * @param state
     * @param dx       偏移量。手指从右向左滑动,dx > 0; 手指从左向右滑动,dx < 0;
     */
    private int fillHorizontalLeft(RecyclerView.Recycler recycler, RecyclerView.State state,
                                   int dx) {
         //省略。。。。。                         

        //----------------3、开始布局-----------------

        for (int i = mFirstVisiPos; i <= mLastVisiPos; i++) {
            //属于堆叠区域
            if (i - mFirstVisiPos < maxLayerCount) {

			 //省略。。。。。   
                if (trasitionListeners != null && !trasitionListeners.isEmpty()) {
                    for (TrasitionListener trasitionListener : trasitionListeners) {
                        trasitionListener.handleLayerView(this, item, i - mFirstVisiPos,
                                maxLayerCount, i, fraction, dx);
                    }
                }

            } else {//属于普通区域
            //省略。。。。。  
                if (trasitionListeners != null && !trasitionListeners.isEmpty()) {
                    for (TrasitionListener trasitionListener : trasitionListeners) {
                        if (i - mFirstVisiPos == maxLayerCount) {
                            trasitionListener.handleFocusingView(this, item, i, fraction, dx);
                        } else {
                            trasitionListener.handleNormalView(this, item, i, fraction, dx);
                        }
                    }
                }
            }
        }

        return dx;
    }

然后使用者可以自己注册该接口,天马行空。

那么我这个项目默认的动画具体实现是怎么样的呢?
先这样,再那样,效果就出来啦:

		@Override
        public void handleLayerView(FocusLayoutManager focusLayoutManager, View view,
                                    int viewLayer, int maxLayerCount, int position,
                                    float fraction, float offset) {
            /**
             * 期望效果:从0%开始到{@link SimpleTrasitionListener#getLayerChangeRangePercent()} 期间
             * view均匀完成渐变,之后一直保持不变
             */
            //转换为真实的渐变变化百分比
            float realFraction;
            if (fraction <= stl.getLayerChangeRangePercent()) {
                realFraction = fraction / stl.getLayerChangeRangePercent();
            } else {
                realFraction = 1.0f;
            }

            float minScale = stl.getLayerViewMinScale(maxLayerCount);
            float maxScale = stl.getLayerViewMaxScale(maxLayerCount);
            float scaleDelta = maxScale - minScale; //总缩放差
            float currentLayerMaxScale =
                    minScale + scaleDelta * (viewLayer + 1) / (maxLayerCount * 1.0f);
            float currentLayerMinScale = minScale + scaleDelta * viewLayer / (maxLayerCount * 1.0f);
            float realScale =
                    currentLayerMaxScale - (currentLayerMaxScale - currentLayerMinScale) * realFraction;

            float minAlpha = stl.getLayerViewMinAlpha(maxLayerCount);
            float maxAlpha = stl.getLayerViewMaxAlpha(maxLayerCount);
            float alphaDelta = maxAlpha - minAlpha; //总透明度差
            float currentLayerMaxAlpha =
                    minAlpha + alphaDelta * (viewLayer + 1) / (maxLayerCount * 1.0f);
            float currentLayerMinAlpha = minAlpha + alphaDelta * viewLayer / (maxLayerCount * 1.0f);
            float realAlpha =
                    currentLayerMaxAlpha - (currentLayerMaxAlpha - currentLayerMinAlpha) * realFraction;

//            log("layer =" + viewLayer + ";alpha = " + realAlpha + ";fraction = " + fraction);
            view.setScaleX(realScale);
            view.setScaleY(realScale);
            view.setAlpha(realAlpha);

        }

哈哈哈。代码中stl 存储着堆叠区域view、焦点view、普通view的最大和最小缩放比、透明度,然后利用fraction计算出当前位置真实的缩放比、透明度设置之。
上面只贴了堆叠区域view的实现,完整实现见源码中的TrasitionListenerConvert

自动选中

1、滚动停止后自动选中

我的实现方式是这样的:监听onScrollStateChanged,在滚动停止时计算出应当停留的position,再计算出停留时的mHorizontalOffset值,播放属性动画将当前mHorizontalOffset不断更新至最终值即可。具体代码参考源码中的onScrollStateChangedsmoothScrollToPosition
(思考:能通过自定义SnapHelper实现么?)

2、点击非焦点view自动将其选中为焦点view

已经实现了setFocusdPosition方法。内部逻辑就是计算出实际position并调用smoothScrollToPositionscrollToPosition 。示例代码:

 public ViewHolder(@NonNull final View itemView) {
                super(itemView);
                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        int pos = getAdapterPosition();
                        if (pos == focusLayoutManager.getFocusdPosition()) {
                        //是焦点view
                        } else {
                            focusLayoutManager.setFocusdPosition(pos, true);
                        }
                    }
                });
            }

无限循环滚动

因为FocusLayoutManager内部没有遍历itemCount这种bad操作,你可以自己通过重写getItemCount返回Integer.MAX_VALUE实现伪无限循环。示例代码:


	public void initView(){
		recyclerView.post(new Runnable() {
                        @Override
                        public void run() {
                            focusLayoutManager.scrollToPosition(1000); //差不多大行了,毕竟mHorizontalOffset是会一直累加的
                        }
                    });
	}

 public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
        @Override
        public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
            int realPosition = position % datas.size();
            Bean bean = datas.get(realPosition);
            //...
        }

        @Override
        public int getItemCount() {
            return Integer.MAX_VALUE;
        }

    }

让开头(堆叠数-1)个View可见

按目前布局逻辑,开头的position = 0position = maxLayerCount - 1个view永远只能在堆叠区域,没法拉出来到焦点view。解决方式也简单,给你的源数据开头插入maxLayerCount - 1个假数据,然后当adapter中识别到假数据时让其布局不可见即可

结束

剩下的三个堆叠方向的实现就是加加减减的变化,不用贴出来了。

思考:按目前代码逻辑,onceCompleteScrollLength赋值后是固定的,即“普通区域”的view之间的距离是一样的,这在所有item宽度(若是垂直滚动则指的是item高度)一样的情况下没什么问题。但如果item的宽度是不固定的,那么实际效果就不尽人意了。
onceCompleteScrollLength如果动态计算呢?我思考过。有很多难点。比如屏幕第一个可见view的position计算难度大大增加。。以后再说吧(逃)

给个赞呗~
给个star呗~
项目地址:https://github.com/CCY0122/FocusLayoutManager

阿里内推(长期)

可帮阿里内推。将想投的阿里招聘官网职位链接+简历简介+备注“csdn#1”(用于让我知道你是在哪里看到我的)发我邮箱671518768@qq.com。

tips:

1.简历绝对真实,背调阶段查出诚信问题会被阿里拉黑。

  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值