
,
ViewPager
( ViewPager2
) RecyclerView
.
,
. support library 24.2.0
, ..
SnapHelper
hildView
, ViewPager
. ViewPager2
, , ViewPager
(ViewPager2
โ
RecyclerView
,
ViewPager
api).
? . -,
RecyclerView
PageTransformer
hildView
(
ยซ ยป). -,
ViewPager2
final
,
onInterceptTouchEvent
(
),
recyclerView
.
,
RecyclerView
ViewPager2
MotionLayout
, ,
ViewPager.PageTransformer
RecyclerView
-
ViewPager2
.
, , :
ViewPager
sticky-- โโ (
BottomSheetBehavior
)
โ
.
? -
- " " :

1.
ConstraintLayout
recyclerView
. BottomSheetCallback
BottomSheetBehavior
:
class DinosaursActivity : AppCompatActivity(), SelectorTransformListener, ItemChangeListener {
private lateinit var bottomSheetBehavior: BottomSheetBehavior<FrameLayout>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupBottomSheetBehavior()
}
private fun setupBottomSheetBehavior() {
bottomSheetBehavior = BottomSheetBehavior.from(layoutBottomSheet).apply {
state = BottomSheetBehavior.STATE_EXPANDED
setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {}
override fun onSlide(bottomSheet: View, offset: Float) {
selector.transformation(offset)
}
})
}
}
}
transformation
, โโ
.
class AnimatedSelector(context: Context, attrs: AttributeSet? = null) :
ConstraintLayout(context, attrs), SnapListener {
constructor(context: Context) : this(context, null)
init {
LayoutInflater.from(context).inflate(R.layout.layout_animated_selector, this, true)
horizontalAdapter = InfinityAdapter()
verticalAdapter = DefaultAdapter()
horizontalRecycler.layoutManager =
LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
verticalRecycler.layoutManager =
LinearLayoutManager(context, RecyclerView.VERTICAL, false)
horizontalRecycler.adapter = horizontalAdapter
verticalRecycler.adapter = verticalAdapter
}
fun transformation(progress: Float) {
val verticalProgress = if (progress < 0.6f) progress / 0.6f else 1f
val horizontalProgress = 1f - (if (progress > 0.6f) (progress - 0.6f) / 0.4f else 0f)
}
}
2.
โ
ViewPager.PageTransformer.
โ ItemViewTransformer
transformItemView
, transformPage
ViewPager.PageTransformer
:
abstract class ItemViewTransformer {
fun attachToRecycler(recycler: RecyclerView) {}
private fun updatePositions() {}
abstract fun transformItemView(view: View, position: Float)
}
attachToRecycler
recyclerView
RecyclerView.AdapterDataObserver
RecyclerView.OnScrollListener
:
fun attachToRecycler(recycler: RecyclerView) {
check(recycler.layoutManager is LinearLayoutManager) { "Incorrect LayoutManager Type" }
this.layoutManager = recycler.layoutManager as LinearLayoutManager
this.recyclerView = recycler
this.attachedAdapter = recycler.adapter as RecyclerView.Adapter<*>
dataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
updatePositions()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
onChanged()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
onChanged()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
onChanged()
}
}
attachedAdapter.registerAdapterDataObserver(dataObserver)
scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
updatePositions()
}
}
recyclerView.addOnScrollListener(scrollListener)
}
updatePosition
transformItemView
childView
( childViews
/ ):
private fun updatePositions() {
val childCount = layoutManager.childCount
for (i in 0 until childCount) {
val view = layoutManager.getChildAt(i)
view?.let {
val position: Float = if (layoutManager.orientation == RecyclerView.HORIZONTAL) {
(view.left - currentFrameLeft) / (view.measuredWidth + view.marginLeft + view.marginRight)
} else {
(view.top - currentFrameLeft) / (view.measuredHeight + view.marginTop + view.marginBottom)
}
transformItemView(view, position)
}
}
}
, , ,
pageTransformer
, - ,
recyclerView
.
3. Sticky-
sticky-
.
:
class BouncingTransformer(currentItemViewOffset: Int) : ItemViewTransformer(currentItemViewOffset) {
override fun transformItemView(view: View, position: Float) {
when {
position > -1f && position < 0 -> {
view.scaleX = 1f
view.scaleY = 1f
view.translationX = 0f
}
position >= 0f && position < 0.5f -> {
view.scaleY = 1f - 0.2f * position / 0.5f
view.scaleX = 1f - 0.2f * position / 0.5f
view.translationX = view.width * 0.4f - view.width * 0.4f * (0.5f - position) / 0.5f
}
position >= 0.5f && position < 1f -> {
view.scaleY = 0.8f
view.scaleX = 0.8f
view.translationX = view.width * (0.9f - position)
}
position >= 1f && position < 2f -> {
view.scaleY = 0.8f
view.scaleX = 0.8f
view.translationX = -view.width * 0.1f
}
}
}
}
BouncingTransformer
recyclerView:
LayoutInflater.from(context).inflate(R.layout.layout_animated_selector, this, true)
horizontalAdapter = InfinityAdapter()
horizontalRecycler.layoutManager = ScrollDisablingLayoutManager(context, RecyclerView.HORIZONTAL, false)
horizontalRecycler.adapter = horizontalAdapter
bouncingTransformer = BouncingTransformer(-cardShift)
.apply { attachToRecycler(horizontalRecycler) }
}
sticky-:

-, , , ,
ItemDecoration
.
4. -
value- onUpdate
:
abstract class ItemViewAnimatorUpdater {
fun attachToRecycler(recycler: RecyclerView): ItemViewAnimatorUpdater {}
fun update(progress: Float = 0f) {}
abstract fun onUpdate(view: View, visiblePosition: Int, adapterPosition: Int, progress: Float)
}
attachToRecycler
recyclerView
:
fun attachToRecycler(recycler: RecyclerView): ItemViewAnimatorUpdater {
check(recycler.layoutManager is LinearLayoutManager) { "Incorrect LayoutManager Type" }
this.layoutManager = recycler.layoutManager as LinearLayoutManager
this.recyclerView = recycler
return this
}
update
:
fun update(progress: Float = 0f) {
val childCount = layoutManager.childCount
for (i in 0 until childCount) {
val view = layoutManager.getChildAt(i)
view?.let {
val visiblePosition = if (layoutManager.orientation == RecyclerView.HORIZONTAL) {
view.right.toFloat() / (view.measuredWidth + view.marginLeft + view.marginRight)
} else {
view.bottom.toFloat() / (view.measuredHeight + view.marginTop + view.marginBottom)
}
val adapterPosition = recyclerView.getChildAdapterPosition(view)
onUpdate(view, ceil(visiblePosition).toInt(), adapterPosition, progress)
}
}
}
5.
โโ recyclerView
onUpdate
:
class EmergingUpdater(private val horizontalOffset: Int, private val verticalOffset: Int) : ItemViewAnimatorUpdater() {
override fun onUpdate(view: View, visiblePosition: Int, adapterPosition: Int, progress: Float) {
view.translationX = progress * (visiblePosition) * horizontalOffset
if (visiblePosition > 1) {
view.translationY = -verticalOffset * visiblePosition + (1 - progress) * visiblePosition * verticalOffset
}
}
}
, ,
BottomSheetCallback
. :

6.
โโ
:
class ScatterUpdater(private val leftOffset: Int, private val rightOffset: Int) : ItemViewAnimatorUpdater() {
override fun onUpdate(view: View, visiblePosition: Int, adapterPosition: Int, progress: Float) {
if (visiblePosition == 1) {
view.translationX = -progress * leftOffset
} else {
view.translationX = progress * rightOffset - view.width * 0.1f
}
}
}
:

7.
childView
.
. ,
ItemViewAnimatorUpdater
:
class SlideLeftWithExcludeUpdater(private val excludedPosition: Int, private val leftOffset: Int) : ItemViewAnimatorUpdater() {
override fun onUpdate(view: View, visiblePosition: Int, adapterPosition: Int, progress: Float) {
if (adapterPosition != excludedPosition) {
view.translationX = -progress * leftOffset
}
}
}
ItemAnimator
:
class RemoveItemAnimator(private val leftOffset: Int, private val duration: Long) : DefaultItemAnimator() {
override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
val animator = ValueAnimator.ofFloat(0f, 1f)
animator.addUpdateListener { valueAnimator ->
holder.itemView.translationX = -(valueAnimator.animatedValue as Float) * leftOffset
}
animator.addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(p0: Animator?) {
}
override fun onAnimationEnd(p0: Animator?) {
animator.removeAllListeners()
}
override fun onAnimationCancel(p0: Animator?) {
}
override fun onAnimationStart(p0: Animator?) {
}
})
animator.duration = duration
animator.start()
return false
}
}
:

:
class SlideRightUpdater(private val rightOffset: Int) : ItemViewAnimatorUpdater() {
override fun onUpdate(view: View, visiblePosition: Int, adapterPosition: Int, progress: Float) {
if (visiblePosition == 1) {
view.translationX = 0f
} else {
view.translationX = progress * rightOffset - view.width * 0.1f
}
}
}
:

8.
.
:
- ViewPager, , ,
, data building - ViewPager.PageTransformer โ
- RecyclerView โ ,
, ItemAtimator, ItemDecoration
.
, . !
