RecyclerView parallax decorator-ParallaxDecoration detailed

Recently, the project has begun a drastic revision and iteration, and PM has also started its crazy CX Dafa again. But this has long been accustomed to this, after all, we have also read such a book "RR is PM". Haha, it’s a bit far-reaching, back to the topic, let’s take a look at the interactive effects (CX goals) to be achieved this time:

target.gif

In a brief description, the interface is a horizontal list. When sliding, the background image slides together with the parallax effect. As the sliding distance increases, the background image is displayed in a loop.

Seeing this effect, the list scheme is definitely the first choice RecyclerView, and then looking at the background parallax effect, the first thing that comes to mind is backgroundto achieve it by drawing . Everyone knows that RecyclerViewthere is such an internal class ItemDecorationthat can provide the ability to draw foreground, background, and Itemdividing lines, so we can ItemDecorationdraw our background by constructing one .

RecyclerViewObserve the content of the background carefully by sliding , and find that it is displayed in a loop, so it is guessed that the background should be a series of pictures side by side and put together into a long picture. In order to verify our conjecture, unzip the other party's apk and find the corresponding resource file. Sure enough, the previous conjecture was confirmed. The long background image is a series of pictures of the same size.

At this point, we can basically determine the target plan:

  1. Customize one ItemDecorationand pass in a collection of background pictures
  2. In ItemDecorationthe onDrawmethod, calculate the current RecyclerViewsliding distance
  3. According to RecyclerViewthe sliding distance and parallaxparallax coefficient, calculate the sliding distance of the current background
  4. In terms of coordinates, according to the drawing background sliding distance RecyclerViewof Canvasthe
  5. Need to deal with the loop drawing logic and draw only the number of pictures visible on the current screen
  • First look at the following two pictures:

visible.png

  1. Above this, the number of screen background image is fully visible 3when bg3the right margin and screenthe right margin of difference 1px, it indicates that bg4there is 1pxcontent displayed on the screen, so the maximum number of visible picture of the current screen 4.
  2. Let’s take a look at the picture below. Assuming that bg3the right margin of screenthe picture above is different from the right margin 2px, and the scene of the picture below appears during the sliding process, that is bg2, the left margin and scrrenthe left margin, and bg4the right margin and screenWhen the right margins are different 1px, it means that the number of pictures that are completely visible on the current screen is 3, but the maximum number of visible pictures is 5.
  3. Therefore, we can draw the following conclusions:
<ParallaxDecoration.kt>
...
// 完全可见的图片数量 = 屏幕宽度 / 单张图片宽度
val allInScreen = screenWidth / bitmapWidth
// 当前展示完完全可见图片数量后,距离屏幕边缘的剩余像素空间
val outOfScreenOffset = screenWidth % bitmapWidth
// 如果剩余像素 > 1px,说明会出现上面图2的场景
val outOfScreen = outOfScreenOffset > 1
// 因此得出最大可见数 = 屏幕剩余像素>1px ? 完全可见数+2 : 完全可见数+1
val maxVisibleCount = if (outOfScreen) allInScreen + 2 else allInScreen + 1
  1. In this way we know onDrawhow many pictures we need to draw in the method during the sliding process .
  • In the next step, we need to find the starting point of drawing. Because it RecyclerViewis slidable, the first visible picture on the screen is definitely not fixed. We only need to find the index of the first picture currently visible in our initial background image collection. We can draw them in order based on the number of pictures that need to be drawn calculated above. Similarly, let's look at a picture first:

offset.png

  1. We do not consider the parallax coefficient for the time being, and get the current RecyclerViewsliding distance:
<ParallaxDecoration.kt>
...
// 当前recyclerView的滑动距离
val scrollOffset = RecyclerView.layoutManager.computeHorizontalScrollOffset(state)
// 滑动距离 / 单张图片宽度 = 当前是第几张图片
// 这里我们对图片集合的长度进行求余运算,即可获得当前第一个可见的图片索引
val firstVisible = (scrollOffset / bitmapWidth).toInt() % bitmapPool.size
// 获取当前第一张图片左边缘距离屏幕左边缘的偏移量
val firstVisibleOffset = scrollOffset % bitmapWidth
  1. We have determined the index of the first visible picture on the current screen and the offset between the first picture and the left edge of the screen, and then we can start the real drawing:
<ParallaxDecoration.kt>
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
        ...
        c.save()
        // 把画布平移到第一张图片的左边缘
        c.translate(-firstVisibleOffset, 0f)
        // 循环绘制当前屏幕可见的图片数量
        for ((i, currentIndex) in (firstVisible until firstVisible + bestDrawCount).withIndex()) {
            c.drawBitmap(
                bitmapPool[currentIndex % bitmapCount],
                i * bitmapWidth.toFloat(),
                0f,
                null
            )
        }
        // 恢复画布
        c.restore()
    }
  1. In the above drawing cycle process, we optimized values bestDrawCount, specific calculation logic, when the firstVisibleOffset = 0description of the current first left edge of the screen seen in FIG thereof, corresponds to the initial state, the maximum number is visible maxVisibleCount - 1. Although bitmapPool.sziethis condition needs to be triggered once every cycle , RecyclerViewthe onDrawcallback here is frequently triggered during the continuous sliding process. The performance improvement of reducing one cycle is still considerable. At the same time, we firstVisibledo not bitmapCountperform the remainder operation when calculating , because drawWhen we still need to take the remainder to ensure the accuracy of the index:
<ParallaxDecoration.kt>
// 上面我们得出的maxVisibleCount
val maxVisibleCount = if (outOfScreen) allInScreen + 2 else allInScreen + 1
val bestDrawCount = if (firstVisibleOffset.toInt() == 0) maxVisibleCount - 1 else maxVisibleCount
// 计算firstVisible时暂不作取余,此时firstVisible = n * bitmapCount + firstIndex
val firstVisible = (scrollOffset / bitmapWidth).toInt()
  1. At this point, our background image can be RecyclerViewdisplayed in a loop following the sliding. For the parallax effect, only scrollOffseta parallax coefficient needs parallaxto be added in the calculation :
<ParallaxDecoration.kt>
// 当前recyclerView的滑动距离
val scrollOffset = RecyclerView.layoutManager.computeHorizontalScrollOffset(state)
// 添加视差系数,换算成背景的滑动距离,与RecyclerView产生视差效果
val parallaxOffset = scrollOffset * parallax
  • Well, this one that supports the background parallax effect ItemDecorationis complete. Finally, there is another question, that RecyclerViewis, what do we need to do when our background image can't cover the full height? This should be very simple for students who are familiar with drawing. You only need canvas.scaleto zoom when drawing to draw an automatically filled background image. It should be noted that we calculate the sliding distance offsetand firstVisibletime needs to be bitmapWidth*scaleis the actual bitmapWidthlogic is relatively simple, there will not start, but also the need for RecyclerViewthe LayoutManagerdirection of sorting processing, self-interested to read the source code.

  • Finally, the following is ParallaxDecoration.onDrawthe core logic. See the link at the bottom for the complete project and usage:

<ParallaxDecoration.kt>
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
        if (bitmapPool.isNotEmpty()) {
            // if layoutManager is null, just throw exception
            val lm = parent.layoutManager!!
            // step1. check orientation
            isHorizontal = lm.canScrollHorizontally()
            // step2. check maxVisible count
            // step3. if autoFill, calculate the scale bitmap size
            if (isHorizontal && screenWidth == 0) {
                screenWidth = c.width
                screenHeight = c.height
                if (autoFill) {
                    scale = screenHeight * 1f / bitmapHeight
                    scaleBitmapWidth = (bitmapWidth * scale).toInt()
                }
                val allInScreen = screenWidth / scaleBitmapWidth
                val outOfScreen = screenWidth % scaleBitmapWidth > 1
                maxVisibleCount = if (outOfScreen) allInScreen + 2 else allInScreen + 1
            } else if (!isHorizontal && screenHeight == 0) {
                screenWidth = c.width
                screenHeight = c.height
                if (autoFill) {
                    scale = screenWidth * 1f / bitmapWidth
                    scaleBitmapHeight = (bitmapHeight * scale).toInt()
                }
                val allInScreen = screenHeight / scaleBitmapHeight
                val outOfScreen = screenHeight % scaleBitmapHeight > 1
                maxVisibleCount = if (outOfScreen) allInScreen + 2 else allInScreen + 1
            }
            // step4. find the firstVisible index
            // step5. calculate the firstVisible offset
            val parallaxOffset: Float
            val firstVisible: Int
            val firstVisibleOffset: Float
            if (isHorizontal) {
                parallaxOffset = lm.computeHorizontalScrollOffset(state) * parallax
                firstVisible = (parallaxOffset / scaleBitmapWidth).toInt()
                firstVisibleOffset = parallaxOffset % scaleBitmapWidth
            } else {
                parallaxOffset = lm.computeVerticalScrollOffset(state) * parallax
                firstVisible = (parallaxOffset / scaleBitmapHeight).toInt()
                firstVisibleOffset = parallaxOffset % scaleBitmapHeight
            }
            // step6. calculate the best draw count
            val bestDrawCount =
                if (firstVisibleOffset.toInt() == 0) maxVisibleCount - 1 else maxVisibleCount
            // step7. translate to firstVisible offset
            c.save()
            if (isHorizontal) {
                c.translate(-firstVisibleOffset, 0f)
            } else {
                c.translate(0f, -firstVisibleOffset)
            }
            // step8. if autoFill, scale the canvas to draw
            if (autoFill) {
                c.scale(scale, scale)
            }
            // step9. draw from current first visible bitmap, the max looper count is the best draw count by step6
            for ((i, currentIndex) in (firstVisible until firstVisible + bestDrawCount).withIndex()) {
                if (isHorizontal) {
                    c.drawBitmap(
                        bitmapPool[currentIndex % bitmapCount],
                        i * bitmapWidth.toFloat(),
                        0f,
                        null
                    )
                } else {
                    c.drawBitmap(
                        bitmapPool[currentIndex % bitmapCount],
                        0f,
                        i * bitmapHeight.toFloat(),
                        null
                    )
                }
            }
            c.restore()
        }
    }
  • Show the effect:

parallax.gif

PS: About me


The author is a handsome Android siege lion with 6 years of development experience . Remember to like it after reading it and develop a habit. WeChat search "Programming Ape Development Center" pays attention to this programmer who likes to write dry goods.

In addition , the PDF of the complete test site for Android first-line interviews that took two years to organize and collect is released. The [full version] has been updated in my [Github] . Friends who need interviews can refer to it. If it helps you, you can Click on Star!

Github address: [https://github.com/733gh/xiongfan]

Guess you like

Origin blog.csdn.net/qq_39477770/article/details/110880368