likes
comments
collection
share

RecyclerView的Item可见性检测助手

作者站长头像
站长
· 阅读数 19

序言

ItemVisibilityHelper可以计算itemViewRecyclerView中的可见范围,帮助我们实现在RecyclerView上播放视频等其他功能。

RecyclerView的Item可见性检测助手

基本逻辑

首先我们有一个前置条件,即RecyclerView中同一时刻只允许有一个itemView处于活跃状态。然后我们来梳理一下RecyclerView中某一个itemView在什么时候会被激活。

这里我们只在RecyclerView滑动停止后进行激活操作,滑动中我们会去做关闭的操作。所以,

  • 滑动中:有itemview被移出显示范围,就关闭它。
  • 滑动停止:如果激活中的itemview可见范围大于50%则保持不变,否则关闭。同时激活显示范围中可见范围最大并且不小于50%的itemview,如果可见范围最大的依然是激活中的itemview,那么可见范围大于50%则保持不变,否则不会关闭,会进入暂停状态,等可见范围不小于50%时恢复。 另外,如果激活中的itemview可见范围小于50%,但是显示范围中没有其他itemview需要激活, 此时这个itemview也进入会暂停状态,等可见范围不小于50%时恢复。

这便是ItemVisibilityHelper激活和关闭itemview的基本逻辑。

核心代码

ItemVisibilityHelper的工作原理是通过获取itemView在显示范围中的位置,从而计算出itemView的可见百分比,进而可以实现我们上述的基本逻辑。在实现基本逻辑的同时,也需要考虑一些其他细节。例如,当显示范围中有多个可见百分比一样的itemView时,我们应该激活哪一个呢?在ItemVisibilityHelper中的规则是激活离上一个被关闭的itemView最近的那一个。那么这时我们就需要判断被关闭的itemView是从顶部还是底部被移出显示范围的。当然,还有其他我们需要考虑的情况,RecyclerView是竖向还是横向、是否是反向布局等。

    /*计算itemView可见百分比*/
    fun calculateVisiblePercent(itemview: View?, orientation: Int): Int {
        if (view == null) return 0
        if (view.getLocalVisibleRect(outRect)) {
            val viewHeight: Int = if (orientation == RecyclerView.HORIZONTAL)
                view.width else view.height
            val displayHeight: Int = if (orientation == RecyclerView.HORIZONTAL) {
                _recyclerView?.computeHorizontalScrollExtent() ?: 0
            } else {
                _recyclerView?.computeVerticalScrollExtent() ?: 0
            }
            //outRect.top==outRect.left
            val top: Int = if (orientation == RecyclerView.HORIZONTAL)
                outRect.left else outRect.top
            //outRect.bottom==outRect.right
            val bottom: Int = if (orientation == RecyclerView.HORIZONTAL)
                outRect.right else outRect.bottom
            /*哪一边被遮挡的多则认为更靠近那一边。例如,顶部被遮挡的大于底部被遮挡的,则认为view的位置  更靠近顶部。
              有可能顶部和底部被遮挡的一样则默认view更靠近顶部。
              实际上也无关紧要,因为出现相等的情况发生在,
              1、view高度大于显示高度且view正好在显示范围的正中,顶部和底部的遮挡范围相同,
              2、view高度小于等于显示范围高度,此时顶部和底部都没有被遮挡,顶部和底部的遮挡范围都为0,
              此时不会发生激活Item动作,不影响我们去判断是更靠近顶部还是底部。
              而随着view的滑动总有一边会被遮挡或遮挡范围不一致,进而能准确判断出是更靠近顶部还是底部。*/
            //top等于顶部被遮挡的高度,(viewHeight - bottom)等于底部被遮挡的高度。
            _isTopCloser = if (_isReverseLayout)
                top < (viewHeight - bottom) else top >= (viewHeight - bottom)
            //view高度大于等于显示范围高度的情况。
            if (viewHeight >= displayHeight) {
                //(bottom - top)等于view在显示范围中的高度。
                return (bottom - top) * 100 / displayHeight
            }
            /*(top > 0)顶部被遮挡。
              (viewHeight - bottom > 0)底部被遮挡。
              以上条件有可能都不成立,此时view的高度小于或等于显示范围高度,并且完全在显示范围中,
              因此percent默认为100。*/
            var percent = 100
            if (top > 0) {
                percent = (viewHeight - top) * 100 / viewHeight
            } else if (viewHeight - bottom > 0) {
                percent = bottom * 100 / viewHeight
            }
            return percent
        }
        return 0
    }

依据被关闭的itemView是从哪个方向被移出显示范围的来决定从哪个方向遍历RecyclerView显示范围中可见百分比最大的itemView

    //简略代码
    fun findNextMostVisibleItem(firstPosition: Int, lastPosition: Int) {
        ...
        //从前往后
        for (index in firstPosition..lastPosition) {
            val item = getItem(index)
            val percent = getItemVisiblePercent(item)
            //得到itemView的可见百分比
        }
        ...
    }
    
    //简略代码
    fun findLastMostVisibleItem(lastPosition: Int, firstPosition: Int) {
        ...
        //从后往前
        for (index in lastPosition downTo firstPosition) {
            val item = getItem(index)
            val percent = getItemVisiblePercent(item)
            //得到itemView的可见百分比
        }
        ...
    }

最后

ItemVisibilityHelper实现了一个非常简单的功能,完全脱离相关业务代码,只负责激活和关闭itewView。同时使用也非常简单,只需要绑定到RecyclerView即可,相关业务功能在对应的时机处理就好。

helper.attachToRecyclerView(recyclerView) {
    activateItem { view, position ->
        //itemView被激活
    }
    deactivateItem { view, position ->
        //itemView被关闭
    }
    pauseItem { view, position ->
        //itemView被暂停
    }
    resumeItem { view, position ->
        //itemView被恢复
    }
}

更多细节和使用方法欢迎查看项目主页。项目地址 谢谢你读到这里。