长按回收器查看项目动画
Recycler view item animation on long press
我需要长按recyclerview中的圆角矩形裁剪所有项目,并在每个项目视图的两个极端显示两个视图到select并重新排列项目。如何做到这一点?
我正在使用 Paging 3 库和由 roomdb 支持的 RemoteMediator 来显示项目。
长按:
- 动画将所有项目翻译到左边 ->
- 动画形状蒙版(不缩放项目但应用圆角矩形蒙版(或剪辑)以显示更少)并应用渐变叠加以显示编辑模式
- 每个项目视图两侧的两个视图的动画外观(显示)
最后,我创建了独立的 Animator 对象并使用 AnimatorSet 一起播放它们。
RecyclerViewItemAnimatorSet class 获取所有视图持有者并应用在其构造函数中收到的动画
import android.animation.Animator
import android.animation.AnimatorSet
import androidx.core.animation.doOnEnd
import androidx.recyclerview.widget.RecyclerView
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
/**
* @author sundara.santhanam
* @since 09-09-2021
*/
class RecyclerViewItemAnimatorSet<in VH : RecyclerView.ViewHolder>(
private val recyclerView: RecyclerView,
private val animatorProviders: List<AnimatorProvider<VH>>,
private val duration: Long,
private val afterAnim: (SelectionModeAnimationState) -> Unit
) {
private val forwardPlayInProgress = AtomicBoolean(false)
private val reversePlayInProgress = AtomicBoolean(false)
fun playTogether() {
if (reversePlayInProgress.get().not()) {
forwardPlayInProgress.set(true)
playAnimation({ viewHolder ->
animatorProviders.map { it.getForwardAnimator(viewHolder as VH) }
}) {
forwardPlayInProgress.set(false)
theEnd(true)
}
}
}
private fun theEnd(selectionMode: Boolean) {
var selectionModeAnimationState = SelectionModeAnimationState(selectionMode = selectionMode)
animatorProviders.forEach {
selectionModeAnimationState = it.mutateWithFinalValue(selectionModeAnimationState)
}
Timber.d("SelectionModeAnimationState: %s", selectionModeAnimationState)
afterAnim(selectionModeAnimationState)
}
fun reversePlayTogether() {
if (forwardPlayInProgress.get().not()) {
reversePlayInProgress.set(true)
playAnimation({ viewHolder -> animatorProviders.map { it.getReverseAnimator(viewHolder as VH) } }) {
reversePlayInProgress.set(false)
theEnd(false)
}
}
}
private fun playAnimation(
animatorFetcher: (RecyclerView.ViewHolder) -> List<Animator>,
performAtEnd: () -> Unit
) {
for (index in recyclerView.visibleRange()) {
recyclerView.findViewHolderForAdapterPosition(index)?.let { viewHolder ->
val animatorSet = AnimatorSet()
animatorSet.playTogether(animatorFetcher(viewHolder))
animatorSet.duration = duration
animatorSet.start()
animatorSet.doOnEnd {
performAtEnd.invoke()
}
}
}
}
}
AnimatorProvider 合约:
/**
* @author sundara.santhanam
* @since 09-09-2021
*/
abstract class AnimatorProvider<in VH : RecyclerView.ViewHolder> {
private var prevMode = 0
fun getForwardAnimator(viewHolder: VH): Animator {
val mode = 1
if (prevMode != mode) {
prevMode = mode
onModeChange(mode, viewHolder)
}
return getAnimator(viewHolder, mode)
}
fun getReverseAnimator(viewHolder: VH): Animator {
val mode = -1
if (prevMode != mode) {
prevMode = mode
onModeChange(mode, viewHolder)
}
return getAnimator(viewHolder, mode)
}
abstract fun mutateWithFinalValue(selectionModeAnimationState: SelectionModeAnimationState): SelectionModeAnimationState
val defaultValueAnimator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f)
// @param mode =1 is forward animation mode and -1 is reverse animation mode
abstract fun onModeChange(
mode: Int,
viewHolder: VH
)
abstract fun getAnimator(viewHolder: VH, mode: Int): Animator
}
动画师提供者示例:OutlineProvider 动画师
/**
* @author sundara.santhanam
* @since 09-09-2021
*/
class OutLineAnimatorProvider : AnimatorProvider<FavoritesViewDelegate.FavViewHolder>() {
private var finalRight: Int = -1
private var initRight: Int = -1
private var initHeight: Int = -1
private var diff = 0f
override fun getAnimator(
viewHolder: FavoritesViewDelegate.FavViewHolder,
mode: Int
): Animator {
Timber.d(
"%s initRight:%d finalRight:%s", (if (mode == 1) {
"reverse"
} else {
"forward"
}), initRight, finalRight
)
defaultValueAnimator.addUpdateListener {
viewHolder.binding.itemContainer.updateRoundedCornersOutlineProvider(
(initRight - mode * diff * (it.animatedValue as Float)).toInt(), initHeight
)
viewHolder.binding.itemContainer.requestLayout()
}
return defaultValueAnimator
}
override fun onModeChange(
mode: Int,
viewHolder: FavoritesViewDelegate.FavViewHolder
) {
val prevInit = initRight
initRight = if (finalRight == -1) {
val bounds = Rect()
viewHolder.binding.itemContainer.getDrawingRect(bounds)
initHeight = bounds.height()
diff = bounds.width() * FINAL_RIGHT_FACTOR
Timber.d("initRight=%d width=%d diff=%f", bounds.right, bounds.width(), diff)
bounds.right
} else {
finalRight
}
finalRight =
if (prevInit != -1) {
prevInit
} else {
(initRight - mode * diff).toInt()
}
}
override fun mutateWithFinalValue(selectionModeAnimationState: SelectionModeAnimationState) =
selectionModeAnimationState.copy(
finalWidth = finalRight,
initHeight = initHeight
)
companion object {
const val FINAL_RIGHT_FACTOR = 0.2f
}
}
圆角轮廓提供者:
/**
* @author sundara.santhanam
* @since 09-09-2021
*/
data class RoundedCornersOutlineProvider(
val radius: Float? = null,
val width: Int? = null,
val height: Int? = null
) : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
val left = 0
val top = 0
val right = width ?: view.width
val bottom = height ?: view.height
if (radius != null) {
val cornerRadius = radius
outline.setRoundRect(left, top, right, bottom, cornerRadius)
}
}
}
fun View.updateRoundedCornersOutlineProvider(width: Int, height: Int) {
outlineProvider = try {
(outlineProvider as RoundedCornersOutlineProvider).copy(
width = width,
height = height
)
} catch (e: Exception) {
RoundedCornersOutlineProvider(
width = width,
height = height
)
}
}
fun View.setRoundedCornerOutlineProvider(radiusDp: Float) {
Utils.init(context)
val radius = convertDpToPixel(radiusDp)
val bounds = Rect()
getDrawingRect(bounds)
outlineProvider =
RoundedCornersOutlineProvider(radius)
clipToOutline = true
}
fun View.getOutlineRight(): Int {
val bounds = Rect()
getDrawingRect(bounds)
return bounds.right
}
recyclerview 中的用法:
private fun setupRecyclerView() {
favoriteViewAnimatorSet = RecyclerViewItemAnimatorSet(
binding.rvWishList, listOf(
OutLineAnimatorProvider(),
TranslationAnimatorProvider(),
AlphaAnimatorProvider()
), 500L
) { selectionMode ->
favoritesViewDelegate.selectionModeAnimationState.set(selectionMode)
binding.rvWishList.adapter?.run {
repeat(itemCount) { index ->
if (index !in binding.rvWishList.visibleRange()) {
notifyItemChanged(index)
}
}
}
binding.rvWishList.enableScroll(recyclerTouchDisabler, recyclerTouchListener)
}
binding.rvWishList.adapter =
FavoritesAdapter(getListOfDelegates())
recyclerTouchListener = RecyclerTouchListener(
context, binding.rvWishList,
clickListener = object : ClickListener {
override fun onClick(view: View?, position: Int) {
}
override fun onLongClick(view: View?, position: Int) {
favoritesViewDelegate.selectionModeAnimationState.set(
selectionMode().copy(
selectionMode = !selectionMode().selectionMode
)
)
if (selectionMode().selectionMode) {
recyclerTouchDisabler =
binding.rvWishList.disableScrollAndGetDisabler(recyclerTouchListener)
favoriteViewAnimatorSet.playTogether()
} else {
recyclerTouchDisabler =
binding.rvWishList.disableScrollAndGetDisabler(recyclerTouchListener)
favoriteViewAnimatorSet.reversePlayTogether()
}
}
})
binding.rvWishList.addOnItemTouchListener(recyclerTouchListener)
}
我需要长按recyclerview中的圆角矩形裁剪所有项目,并在每个项目视图的两个极端显示两个视图到select并重新排列项目。如何做到这一点? 我正在使用 Paging 3 库和由 roomdb 支持的 RemoteMediator 来显示项目。
长按:
- 动画将所有项目翻译到左边 ->
- 动画形状蒙版(不缩放项目但应用圆角矩形蒙版(或剪辑)以显示更少)并应用渐变叠加以显示编辑模式
- 每个项目视图两侧的两个视图的动画外观(显示)
最后,我创建了独立的 Animator 对象并使用 AnimatorSet 一起播放它们。
RecyclerViewItemAnimatorSet class 获取所有视图持有者并应用在其构造函数中收到的动画
import android.animation.Animator
import android.animation.AnimatorSet
import androidx.core.animation.doOnEnd
import androidx.recyclerview.widget.RecyclerView
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
/**
* @author sundara.santhanam
* @since 09-09-2021
*/
class RecyclerViewItemAnimatorSet<in VH : RecyclerView.ViewHolder>(
private val recyclerView: RecyclerView,
private val animatorProviders: List<AnimatorProvider<VH>>,
private val duration: Long,
private val afterAnim: (SelectionModeAnimationState) -> Unit
) {
private val forwardPlayInProgress = AtomicBoolean(false)
private val reversePlayInProgress = AtomicBoolean(false)
fun playTogether() {
if (reversePlayInProgress.get().not()) {
forwardPlayInProgress.set(true)
playAnimation({ viewHolder ->
animatorProviders.map { it.getForwardAnimator(viewHolder as VH) }
}) {
forwardPlayInProgress.set(false)
theEnd(true)
}
}
}
private fun theEnd(selectionMode: Boolean) {
var selectionModeAnimationState = SelectionModeAnimationState(selectionMode = selectionMode)
animatorProviders.forEach {
selectionModeAnimationState = it.mutateWithFinalValue(selectionModeAnimationState)
}
Timber.d("SelectionModeAnimationState: %s", selectionModeAnimationState)
afterAnim(selectionModeAnimationState)
}
fun reversePlayTogether() {
if (forwardPlayInProgress.get().not()) {
reversePlayInProgress.set(true)
playAnimation({ viewHolder -> animatorProviders.map { it.getReverseAnimator(viewHolder as VH) } }) {
reversePlayInProgress.set(false)
theEnd(false)
}
}
}
private fun playAnimation(
animatorFetcher: (RecyclerView.ViewHolder) -> List<Animator>,
performAtEnd: () -> Unit
) {
for (index in recyclerView.visibleRange()) {
recyclerView.findViewHolderForAdapterPosition(index)?.let { viewHolder ->
val animatorSet = AnimatorSet()
animatorSet.playTogether(animatorFetcher(viewHolder))
animatorSet.duration = duration
animatorSet.start()
animatorSet.doOnEnd {
performAtEnd.invoke()
}
}
}
}
}
AnimatorProvider 合约:
/**
* @author sundara.santhanam
* @since 09-09-2021
*/
abstract class AnimatorProvider<in VH : RecyclerView.ViewHolder> {
private var prevMode = 0
fun getForwardAnimator(viewHolder: VH): Animator {
val mode = 1
if (prevMode != mode) {
prevMode = mode
onModeChange(mode, viewHolder)
}
return getAnimator(viewHolder, mode)
}
fun getReverseAnimator(viewHolder: VH): Animator {
val mode = -1
if (prevMode != mode) {
prevMode = mode
onModeChange(mode, viewHolder)
}
return getAnimator(viewHolder, mode)
}
abstract fun mutateWithFinalValue(selectionModeAnimationState: SelectionModeAnimationState): SelectionModeAnimationState
val defaultValueAnimator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f)
// @param mode =1 is forward animation mode and -1 is reverse animation mode
abstract fun onModeChange(
mode: Int,
viewHolder: VH
)
abstract fun getAnimator(viewHolder: VH, mode: Int): Animator
}
动画师提供者示例:OutlineProvider 动画师
/**
* @author sundara.santhanam
* @since 09-09-2021
*/
class OutLineAnimatorProvider : AnimatorProvider<FavoritesViewDelegate.FavViewHolder>() {
private var finalRight: Int = -1
private var initRight: Int = -1
private var initHeight: Int = -1
private var diff = 0f
override fun getAnimator(
viewHolder: FavoritesViewDelegate.FavViewHolder,
mode: Int
): Animator {
Timber.d(
"%s initRight:%d finalRight:%s", (if (mode == 1) {
"reverse"
} else {
"forward"
}), initRight, finalRight
)
defaultValueAnimator.addUpdateListener {
viewHolder.binding.itemContainer.updateRoundedCornersOutlineProvider(
(initRight - mode * diff * (it.animatedValue as Float)).toInt(), initHeight
)
viewHolder.binding.itemContainer.requestLayout()
}
return defaultValueAnimator
}
override fun onModeChange(
mode: Int,
viewHolder: FavoritesViewDelegate.FavViewHolder
) {
val prevInit = initRight
initRight = if (finalRight == -1) {
val bounds = Rect()
viewHolder.binding.itemContainer.getDrawingRect(bounds)
initHeight = bounds.height()
diff = bounds.width() * FINAL_RIGHT_FACTOR
Timber.d("initRight=%d width=%d diff=%f", bounds.right, bounds.width(), diff)
bounds.right
} else {
finalRight
}
finalRight =
if (prevInit != -1) {
prevInit
} else {
(initRight - mode * diff).toInt()
}
}
override fun mutateWithFinalValue(selectionModeAnimationState: SelectionModeAnimationState) =
selectionModeAnimationState.copy(
finalWidth = finalRight,
initHeight = initHeight
)
companion object {
const val FINAL_RIGHT_FACTOR = 0.2f
}
}
圆角轮廓提供者:
/**
* @author sundara.santhanam
* @since 09-09-2021
*/
data class RoundedCornersOutlineProvider(
val radius: Float? = null,
val width: Int? = null,
val height: Int? = null
) : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
val left = 0
val top = 0
val right = width ?: view.width
val bottom = height ?: view.height
if (radius != null) {
val cornerRadius = radius
outline.setRoundRect(left, top, right, bottom, cornerRadius)
}
}
}
fun View.updateRoundedCornersOutlineProvider(width: Int, height: Int) {
outlineProvider = try {
(outlineProvider as RoundedCornersOutlineProvider).copy(
width = width,
height = height
)
} catch (e: Exception) {
RoundedCornersOutlineProvider(
width = width,
height = height
)
}
}
fun View.setRoundedCornerOutlineProvider(radiusDp: Float) {
Utils.init(context)
val radius = convertDpToPixel(radiusDp)
val bounds = Rect()
getDrawingRect(bounds)
outlineProvider =
RoundedCornersOutlineProvider(radius)
clipToOutline = true
}
fun View.getOutlineRight(): Int {
val bounds = Rect()
getDrawingRect(bounds)
return bounds.right
}
recyclerview 中的用法:
private fun setupRecyclerView() {
favoriteViewAnimatorSet = RecyclerViewItemAnimatorSet(
binding.rvWishList, listOf(
OutLineAnimatorProvider(),
TranslationAnimatorProvider(),
AlphaAnimatorProvider()
), 500L
) { selectionMode ->
favoritesViewDelegate.selectionModeAnimationState.set(selectionMode)
binding.rvWishList.adapter?.run {
repeat(itemCount) { index ->
if (index !in binding.rvWishList.visibleRange()) {
notifyItemChanged(index)
}
}
}
binding.rvWishList.enableScroll(recyclerTouchDisabler, recyclerTouchListener)
}
binding.rvWishList.adapter =
FavoritesAdapter(getListOfDelegates())
recyclerTouchListener = RecyclerTouchListener(
context, binding.rvWishList,
clickListener = object : ClickListener {
override fun onClick(view: View?, position: Int) {
}
override fun onLongClick(view: View?, position: Int) {
favoritesViewDelegate.selectionModeAnimationState.set(
selectionMode().copy(
selectionMode = !selectionMode().selectionMode
)
)
if (selectionMode().selectionMode) {
recyclerTouchDisabler =
binding.rvWishList.disableScrollAndGetDisabler(recyclerTouchListener)
favoriteViewAnimatorSet.playTogether()
} else {
recyclerTouchDisabler =
binding.rvWishList.disableScrollAndGetDisabler(recyclerTouchListener)
favoriteViewAnimatorSet.reversePlayTogether()
}
}
})
binding.rvWishList.addOnItemTouchListener(recyclerTouchListener)
}