Hanbit the Developer
[Kotlin] Recycler View Animation Using Custom Layout Manager 본문
Before 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
'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 |