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:
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 background
to achieve it by drawing . Everyone knows that RecyclerView
there is such an internal class ItemDecoration
that can provide the ability to draw foreground, background, and Item
dividing lines, so we can ItemDecoration
draw our background by constructing one .
RecyclerView
Observe 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:
- Customize one
ItemDecoration
and pass in a collection of background pictures - In
ItemDecoration
theonDraw
method, calculate the currentRecyclerView
sliding distance - According to
RecyclerView
the sliding distance andparallax
parallax coefficient, calculate the sliding distance of the current background - In terms of coordinates, according to the drawing background sliding distance
RecyclerView
ofCanvas
the - 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:
- Above this, the number of screen background image is fully visible
3
whenbg3
the right margin andscreen
the right margin of difference1px
, it indicates thatbg4
there is1px
content displayed on the screen, so the maximum number of visible picture of the current screen4
. - Let’s take a look at the picture below. Assuming that
bg3
the right margin ofscreen
the picture above is different from the right margin2px
, and the scene of the picture below appears during the sliding process, that isbg2
, the left margin andscrren
the left margin, andbg4
the right margin andscreen
When the right margins are different1px
, it means that the number of pictures that are completely visible on the current screen is3
, but the maximum number of visible pictures is5
. - 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
- In this way we know
onDraw
how 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
RecyclerView
is 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:
- We do not consider the parallax coefficient for the time being, and get the current
RecyclerView
sliding distance:
<ParallaxDecoration.kt>
...
// 当前recyclerView的滑动距离
val scrollOffset = RecyclerView.layoutManager.computeHorizontalScrollOffset(state)
// 滑动距离 / 单张图片宽度 = 当前是第几张图片
// 这里我们对图片集合的长度进行求余运算,即可获得当前第一个可见的图片索引
val firstVisible = (scrollOffset / bitmapWidth).toInt() % bitmapPool.size
// 获取当前第一张图片左边缘距离屏幕左边缘的偏移量
val firstVisibleOffset = scrollOffset % bitmapWidth
- 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()
}
- In the above drawing cycle process, we optimized values
bestDrawCount
, specific calculation logic, when thefirstVisibleOffset = 0
description of the current first left edge of the screen seen in FIG thereof, corresponds to the initial state, the maximum number is visiblemaxVisibleCount - 1
. AlthoughbitmapPool.szie
this condition needs to be triggered once every cycle ,RecyclerView
theonDraw
callback here is frequently triggered during the continuous sliding process. The performance improvement of reducing one cycle is still considerable. At the same time, wefirstVisible
do notbitmapCount
perform the remainder operation when calculating , becausedraw
When 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()
- At this point, our background image can be
RecyclerView
displayed in a loop following the sliding. For the parallax effect, onlyscrollOffset
a parallax coefficient needsparallax
to 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
ItemDecoration
is complete. Finally, there is another question, thatRecyclerView
is, 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 needcanvas.scale
to zoom when drawing to draw an automatically filled background image. It should be noted that we calculate the sliding distanceoffset
andfirstVisible
time needs to bebitmapWidth*scale
is the actualbitmapWidth
logic is relatively simple, there will not start, but also the need forRecyclerView
theLayoutManager
direction of sorting processing, self-interested to read the source code. -
Finally, the following is
ParallaxDecoration.onDraw
the 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:
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]