Move multiselect animation code to decorator.

fork-5.53.8
Alex Hart 2021-10-21 16:57:58 -03:00 zatwierdzone przez Greyson Parrelli
rodzic c1820459b7
commit 16ab27084c
4 zmienionych plików z 108 dodań i 112 usunięć

Wyświetl plik

@ -23,7 +23,7 @@ class BodyBubbleLayoutTransition(bodyBubble: ConversationItemBodyBubble) : Layou
val parentRecycler: RecyclerView? = bodyBubble.parent.parent as? RecyclerView
try {
parentRecycler?.invalidateItemDecorations()
parentRecycler?.invalidate()
} catch (e: IllegalStateException) {
// In scroll or layout. Skip this frame.
}

Wyświetl plik

@ -266,18 +266,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
} else {
return Util.hasItems(adapter.getSelectedItems());
}
}, () -> listSubmissionCount < 2, multiselectPart -> {
ConversationAdapter adapter = getListAdapter();
if (adapter == null) {
return false;
} else {
return adapter.getSelectedItems().contains(multiselectPart);
}
}, () -> list.canScrollVertically(1) || list.canScrollVertically(-1));
}, () -> listSubmissionCount < 2, () -> list.canScrollVertically(1) || list.canScrollVertically(-1));
multiselectItemDecoration = new MultiselectItemDecoration(requireContext(),
() -> conversationViewModel.getWallpaper().getValue(),
multiselectItemAnimator::getSelectedProgressForPart,
multiselectItemAnimator::isInitialMultiSelectAnimation);
() -> conversationViewModel.getWallpaper().getValue());
list.setHasFixedSize(false);
list.setLayoutManager(layoutManager);

Wyświetl plik

@ -14,15 +14,9 @@ import androidx.recyclerview.widget.RecyclerView
class MultiselectItemAnimator(
private val isInMultiSelectMode: () -> Boolean,
private val isLoadingInitialContent: () -> Boolean,
private val isPartSelected: (MultiselectPart) -> Boolean,
private val isParentFilled: () -> Boolean
) : RecyclerView.ItemAnimator() {
private data class Selection(
val multiselectPart: MultiselectPart,
val viewHolder: RecyclerView.ViewHolder
)
private data class SlideInfo(
val viewHolder: RecyclerView.ViewHolder,
val operation: Operation
@ -33,25 +27,10 @@ class MultiselectItemAnimator(
CHANGE
}
var isInitialMultiSelectAnimation: Boolean = true
private set
private val selected: MutableSet<MultiselectPart> = mutableSetOf()
private val pendingSelectedAnimations: MutableSet<Selection> = mutableSetOf()
private val pendingSlideAnimations: MutableSet<SlideInfo> = mutableSetOf()
private val selectedAnimations: MutableMap<Selection, ValueAnimator> = mutableMapOf()
private val slideAnimations: MutableMap<SlideInfo, ValueAnimator> = mutableMapOf()
fun getSelectedProgressForPart(multiselectPart: MultiselectPart): Float {
return if (pendingSelectedAnimations.any { it.multiselectPart == multiselectPart }) {
0f
} else {
selectedAnimations.filter { it.key.multiselectPart == multiselectPart }.values.firstOrNull()?.animatedFraction ?: 1f
}
}
override fun animateDisappearance(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo?): Boolean {
dispatchAnimationFinished(viewHolder)
return false
@ -102,52 +81,20 @@ class MultiselectItemAnimator(
}
val isInMultiSelectMode = isInMultiSelectMode()
if (!isInMultiSelectMode) {
selected.clear()
isInitialMultiSelectAnimation = true
return if (preLayoutInfo.top == postLayoutInfo.top) {
return if (!isInMultiSelectMode) {
if (preLayoutInfo.top == postLayoutInfo.top) {
dispatchAnimationFinished(newHolder)
false
} else {
animateSlide(newHolder, preLayoutInfo, postLayoutInfo, Operation.CHANGE)
}
}
var isAnimationStarted = false
val parts: MultiselectCollection? = (newHolder.itemView as? Multiselectable)?.conversationMessage?.multiselectCollection
if (parts == null || parts.isExpired()) {
dispatchAnimationFinished(newHolder)
return false
}
parts.toSet().forEach { part ->
val partIsSelected = isPartSelected(part)
if (selected.contains(part) && !partIsSelected) {
pendingSelectedAnimations.add(Selection(part, newHolder))
selected.remove(part)
isAnimationStarted = true
} else if (!selected.contains(part) && partIsSelected) {
pendingSelectedAnimations.add(Selection(part, newHolder))
selected.add(part)
isAnimationStarted = true
} else if (isInitialMultiSelectAnimation) {
pendingSelectedAnimations.add(Selection(part, newHolder))
isAnimationStarted = true
}
}
if (isAnimationStarted) {
dispatchAnimationStarted(newHolder)
} else {
dispatchAnimationFinished(newHolder)
false
}
return isAnimationStarted
}
override fun runPendingAnimations() {
runPendingSelectedAnimations()
runPendingSlideAnimations()
}
@ -157,7 +104,7 @@ class MultiselectItemAnimator(
slideAnimations[slideInfo] = animator
animator.duration = 150L
animator.addUpdateListener {
(slideInfo.viewHolder.itemView.parent as RecyclerView?)?.invalidateItemDecorations()
(slideInfo.viewHolder.itemView.parent as RecyclerView?)?.invalidate()
}
animator.doOnEnd {
dispatchAnimationFinished(slideInfo.viewHolder)
@ -169,51 +116,22 @@ class MultiselectItemAnimator(
pendingSlideAnimations.clear()
}
private fun runPendingSelectedAnimations() {
for (selection in pendingSelectedAnimations) {
val animator = ValueAnimator.ofFloat(0f, 1f)
selectedAnimations[selection] = animator
animator.duration = 150L
animator.addUpdateListener {
(selection.viewHolder.itemView.parent as RecyclerView?)?.invalidateItemDecorations()
}
animator.doOnEnd {
dispatchAnimationFinished(selection.viewHolder)
selectedAnimations.remove(selection)
isInitialMultiSelectAnimation = false
}
animator.start()
}
pendingSelectedAnimations.clear()
}
override fun endAnimation(item: RecyclerView.ViewHolder) {
endSelectedAnimation(item)
endSlideAnimation(item)
}
override fun endAnimations() {
endSelectedAnimations()
endSlideAnimations()
dispatchAnimationsFinished()
}
override fun isRunning(): Boolean {
return (selectedAnimations.values + slideAnimations.values).any { it.isRunning }
return slideAnimations.values.any { it.isRunning }
}
override fun onAnimationFinished(viewHolder: RecyclerView.ViewHolder) {
val parent = (viewHolder.itemView.parent as? RecyclerView)
parent?.post { parent.invalidateItemDecorations() }
}
private fun endSelectedAnimation(item: RecyclerView.ViewHolder) {
val selections = selectedAnimations.filter { (k, _) -> k.viewHolder == item }
selections.forEach { (k, v) ->
v.end()
selectedAnimations.remove(k)
}
parent?.post { parent.invalidate() }
}
private fun endSlideAnimation(item: RecyclerView.ViewHolder) {
@ -224,11 +142,6 @@ class MultiselectItemAnimator(
}
}
fun endSelectedAnimations() {
selectedAnimations.values.forEach { it.end() }
selectedAnimations.clear()
}
fun endSlideAnimations() {
slideAnimations.values.forEach { it.end() }
slideAnimations.clear()

Wyświetl plik

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.mutiselect
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
@ -32,9 +33,7 @@ import java.lang.Integer.max
*/
class MultiselectItemDecoration(
context: Context,
private val chatWallpaperProvider: () -> ChatWallpaper?,
private val selectedAnimationProgressProvider: (MultiselectPart) -> Float,
private val isInitialAnimation: () -> Boolean
private val chatWallpaperProvider: () -> ChatWallpaper?
) : RecyclerView.ItemDecoration(), DefaultLifecycleObserver {
private val path = Path()
@ -54,6 +53,10 @@ class MultiselectItemDecoration(
private val ultramarine30 = ContextCompat.getColor(context, R.color.core_ultramarine_33)
private val ultramarine = ContextCompat.getColor(context, R.color.signal_accent_primary)
private val selectedParts: MutableSet<MultiselectPart> = mutableSetOf()
private var enterExitAnimation: ValueAnimator? = null
private val multiselectPartAnimatorMap: MutableMap<MultiselectPart, ValueAnimator> = mutableMapOf()
private var checkedBitmap: Bitmap? = null
private var focusedItem: MultiselectPart? = null
@ -99,7 +102,34 @@ class MultiselectItemDecoration(
style = Paint.Style.FILL
}
private fun getCurrentSelection(parent: RecyclerView): Set<MultiselectPart> {
return (parent.adapter as ConversationAdapter).selectedItems
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val currentSelection = getCurrentSelection(parent)
if (selectedParts.isEmpty() && currentSelection.isNotEmpty()) {
enterExitAnimation?.end()
enterExitAnimation = ValueAnimator.ofFloat(enterExitAnimation?.animatedFraction ?: 0f, 1f).apply {
duration = 150L
start()
}
} else if (selectedParts.isNotEmpty() && currentSelection.isEmpty()) {
enterExitAnimation?.end()
enterExitAnimation = ValueAnimator.ofFloat(enterExitAnimation?.animatedFraction ?: 1f, 0f).apply {
duration = 150L
start()
}
}
if (view is Multiselectable) {
val parts = view.conversationMessage.multiselectCollection.toSet()
parts.forEach { updateMultiselectPartAnimator(currentSelection, it) }
}
selectedParts.clear()
selectedParts.addAll(currentSelection)
outRect.setEmpty()
updateChildOffsets(parent, view)
}
@ -151,6 +181,8 @@ class MultiselectItemDecoration(
canvas.restore()
}
drawChecks(parent, canvas, adapter)
}
/**
@ -160,9 +192,12 @@ class MultiselectItemDecoration(
val adapter = parent.adapter as ConversationAdapter
if (adapter.selectedItems.isEmpty()) {
drawFocusShadeOverIfNecessary(canvas, parent)
return
}
invalidateIfAnimatorsAreRunning(parent)
}
private fun drawChecks(parent: RecyclerView, canvas: Canvas, adapter: ConversationAdapter) {
val drawCircleBehindSelector = chatWallpaperProvider()?.isPhoto == true
val multiselectChildren: Sequence<Multiselectable> = parent.children.filterIsInstance(Multiselectable::class.java)
@ -190,7 +225,7 @@ class MultiselectItemDecoration(
drawPhotoCircle(canvas, parent, topBoundary, bottomBoundary)
}
val alphaProgress = selectedAnimationProgressProvider(it)
val alphaProgress = selectedAnimationProgress(it)
if (adapter.selectedItems.contains(it)) {
drawUnselectedCircle(canvas, parent, topBoundary, bottomBoundary, 1f - alphaProgress)
drawSelectedCircle(canvas, parent, topBoundary, bottomBoundary, alphaProgress)
@ -271,7 +306,6 @@ class MultiselectItemDecoration(
val isLtr = ViewUtil.isLtr(child)
if (adapter.selectedItems.isNotEmpty() && child is Multiselectable) {
val firstPart = child.conversationMessage.multiselectCollection.toSet().first()
val target = child.getHorizontalTranslationTarget()
if (target != null) {
@ -282,7 +316,7 @@ class MultiselectItemDecoration(
}
val translation: Float = if (isInitialAnimation()) {
max(0, gutter - start) * selectedAnimationProgressProvider(firstPart)
max(0, gutter - start) * (enterExitAnimation?.animatedFraction ?: 1f)
} else {
max(0, gutter - start).toFloat()
}
@ -341,4 +375,62 @@ class MultiselectItemDecoration(
canvas.restore()
}
}
private fun isInitialAnimation(): Boolean {
return (enterExitAnimation?.animatedFraction ?: 0f) < 1f
}
// This is reentrant
private fun updateMultiselectPartAnimator(currentSelection: Set<MultiselectPart>, multiselectPart: MultiselectPart) {
val difference: Difference = getDifferenceForPart(currentSelection, multiselectPart)
val animator: ValueAnimator? = multiselectPartAnimatorMap[multiselectPart]
when (difference) {
Difference.SAME -> Unit
Difference.ADDED -> {
val newAnimator = ValueAnimator.ofFloat(animator?.animatedFraction ?: 0f, 1f).apply {
duration = 150L
start()
}
animator?.end()
multiselectPartAnimatorMap[multiselectPart] = newAnimator
}
Difference.REMOVED -> {
val newAnimator = ValueAnimator.ofFloat(animator?.animatedFraction ?: 1f, 0f).apply {
duration = 150L
start()
}
animator?.end()
multiselectPartAnimatorMap[multiselectPart] = newAnimator
}
}
}
private fun selectedAnimationProgress(multiselectPart: MultiselectPart): Float {
val animator = multiselectPartAnimatorMap[multiselectPart]
return animator?.animatedFraction ?: 1f
}
private fun getDifferenceForPart(currentSelection: Set<MultiselectPart>, multiselectPart: MultiselectPart): Difference {
val isSelected = currentSelection.contains(multiselectPart)
val wasSelected = selectedParts.contains(multiselectPart)
return when {
isSelected && !wasSelected -> Difference.ADDED
!isSelected && wasSelected -> Difference.REMOVED
else -> Difference.SAME
}
}
private fun invalidateIfAnimatorsAreRunning(parent: RecyclerView) {
if (enterExitAnimation?.isRunning == true || multiselectPartAnimatorMap.values.any { it.isRunning }) {
parent.invalidate()
}
}
private enum class Difference {
REMOVED,
ADDED,
SAME
}
}