kopia lustrzana https://github.com/ryukoposting/Signal-Android
Move multiselect animation code to decorator.
rodzic
c1820459b7
commit
16ab27084c
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue