Hanbit the Developer
[Kotlin] Recycler View Animation Using Custom Layout Manager 본문
[Kotlin] Recycler View Animation Using Custom Layout Manager
hanbikan 2022. 3. 24. 23:53Before get started, be aware of that this post contains only about the 'custom layout manager' which means overrided LinearLayoutManager by programmers, not about a recycler view moving like a carousel. If you want to implement the recycler view, I refer to my other post explaining it: https://rccode.tistory.com/entry/Kotlin-Profile-Card-RecyclerView-With-PagerSnapHelper
> Results
Let's see what we'll build. First there are 2 modes, 'Scale mode' and 'Y mode'. A scale mode means a scale of items changes when scrolling, and the last one means a y position of that does.
> Create CustomLayoutManager which implements LinearLayoutManager
class CustomLayoutManager constructor(
context: Context,
snapHelper: SnapHelper,
mode: Mode,
minScale: Float,
yOffset: Float
): LinearLayoutManager(context) {
private val snapHelper = snapHelper
/** Mode of an animation */
private val mode = mode
/** A limit of scale getting smaller(ex. 0.5f then items will be smaller by up to half) */
private val minScale = minScale
/** A degree to which y position changes */
private val yOffset = yOffset
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
}
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
super.onLayoutChildren(recycler, state)
if(state != null){
layoutItems(state)
if(mode == Mode.SCALE_MODE){
for(i in 0 until itemCount) findViewByPosition(i)?.pivotY = (height/2).toFloat()
}
}
}
override fun scrollHorizontallyBy(
dx: Int,
recycler: RecyclerView.Recycler?,
state: RecyclerView.State?
): Int {
if(state != null){
layoutItems(state)
}
return super.scrollHorizontallyBy(dx, recycler, state)
}
private fun layoutItems(state: RecyclerView.State) {
if(state.isPreLayout) return
val centerPos = getCenterPosition() ?: return
for(pos in centerPos-1..centerPos+1){
val item = findViewByPosition(pos) ?: continue
if(item.tag == "DRAGGED") continue // A item longclicked for moving
layoutItem(item)
}
}
private fun getCenterPosition(): Int? {
val centerView = snapHelper.findSnapView(this)
return centerView?.let { getPosition(it) }
}
private fun layoutItem(item: View) {
val itemCenterX = item.x + item.width/2
when(mode){
Mode.Y_MODE -> {
item.y = computeY(itemCenterX)
}
Mode.SCALE_MODE -> {
val scale = computeScale(itemCenterX)
item.scaleX = scale
item.scaleY = scale
item.pivotX = computePivot(itemCenterX, item.width)
}
}
}
private fun computeY(itemCenterX: Float): Float {
return if(itemCenterX < width/2) {
min(1f, itemCenterX / (width/2)) * yOffset - yOffset
}else{
min(1f, (width - itemCenterX) / (width/2)) * yOffset - yOffset
}
}
private fun computeScale(itemCenterX: Float): Float {
return if(itemCenterX < width/2) {
min(1f, itemCenterX / (width/2)) * (1 - minScale) + minScale
}else{
min(1f, (width - itemCenterX) / (width/2)) * (1 - minScale) + minScale
}
}
private fun computePivot(itemCenterX: Float, itemWidth: Int): Float {
return if(itemCenterX < width/2) itemWidth.toFloat() else 0f
}
enum class Mode {
SCALE_MODE, Y_MODE
}
class Builder constructor(context: Context, snapHelper: SnapHelper){
private val context = context
private val snapHelper = snapHelper
private var mode = Mode.Y_MODE
private var minScale = 0.85f
private var yOffset = -75f
fun setMode(mode: Mode): Builder {
this.mode = mode
return this
}
fun setMinScale(minScale: Float): Builder {
this.minScale = minScale
return this
}
fun setYOffset(yOffset: Float): Builder {
this.yOffset = yOffset
return this
}
fun build(): CustomLayoutManager {
return CustomLayoutManager(context, snapHelper, mode, minScale, yOffset)
}
}
}
Only one purpose of CustomLayoutManager is just to make items animated when scrolling, because I implemented a recycler view moving like a carousel by SnapHelper simply and 'longclick to move an item' before.
So, the point is simple. Just put a function 'layoutItems()' to onLayoutChildren() and scrollHorizontallyBy() which are override functions of LinearLayoutManager. First, onLayoutChildren() means literally a function called when items begin to be placed therefore we put layoutItems()──I'll descript it later──on it. And scrollHorizontallyBy() also does.
The last thing we have to do is to write the function layoutItems() which get the items moved. And, I told there are 2 modes but I'll talk about only 'Y mode' because implementations of those things are very similar. Let's define how items move on. Here is a graph describing it.
It's easy if you think of it as a trajectory left by an item moving from left to right. To compute y position by an item's central x position, I writed a function named computeY().
Now, I can get y position for x position of an item and make a function layoutItem() which layouts an item using computeY().
And finally I'll create layoutItems() function. There are only 3 items to consider because our recycler view shows 3 items in a screen. So, just call layoutItem() 3 times for items of n-1, n, n+1 index where n is a center item position that I can get using findSnapView() of SnapHelper. Implementation is now complete.
Use CustomLayoutManager like below:
// Initialize Adapter
binding.recyclerView.adapter = adapter
// Initialize PagerSnapHelper
val snapHelper = PagerSnapHelper()
snapHelper.attachToRecyclerView(binding.recyclerView)
// Initialize LayoutManager
layoutManager = CustomLayoutManager.Builder(requireContext(), snapHelper)
.setMode(CustomLayoutManager.Mode.Y_MODE)
// Or .setMode(CustomLayoutManager.Mode.SCALE_MODE)
.setYOffset(0.8f)
// Or .setMinScale(0.8f)
.build()
layoutManager.orientation = LinearLayoutManager.HORIZONTAL
binding.myPetListRecyclerView.layoutManager = layoutManager
'Mobile > Android' 카테고리의 다른 글
[Kotlin] Multiple View Types를 지원하는 RecyclerView 구현 (0) | 2022.07.02 |
---|---|
JVM, DVM, ART(URL) (0) | 2022.03.27 |
[Kotlin] Android DataBinding 예제(+ 양방향 데이터 바인딩, 클릭 이벤트, 삼항 연산자) (0) | 2022.02.10 |
[Retrofit2] Video View와 HTTP Range Header에 대해 (0) | 2022.01.11 |
[Kotlin] Carousel RecyclerView With PagerSnapHelper (0) | 2022.01.01 |