Add TypingIndicatorDecoration to CFV2.

This commit is contained in:
Alex Hart 2023-06-30 14:37:09 -03:00 committed by Greyson Parrelli
parent 27e7383db6
commit 47b97aafc6
5 changed files with 172 additions and 2 deletions

View file

@ -80,6 +80,10 @@ public class ConversationTypingView extends ConstraintLayout {
indicator.startAnimation();
}
public boolean isActive() {
return indicator.isActive();
}
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
avatar1.setVisibility(VISIBLE);

View file

@ -125,4 +125,8 @@ public class TypingIndicatorView extends LinearLayout {
public void stopAnimation() {
isActive = false;
}
public boolean isActive() {
return isActive;
}
}

View file

@ -427,6 +427,7 @@ class ConversationFragment :
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
private lateinit var dateHeaderDecoration: DateHeaderDecoration
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
private lateinit var typingIndicatorDecoration: TypingIndicatorDecoration
private var animationsAllowed = false
private var actionMode: ActionMode? = null
@ -859,6 +860,8 @@ class ConversationFragment :
.getScheduledMessagesCount()
.subscribeBy { count -> handleScheduledMessagesCountChange(count) }
.addTo(disposables)
presentTypingIndicator()
}
private fun initializeInlineSearch() {
@ -889,6 +892,23 @@ class ConversationFragment :
.addTo(disposables)
}
private fun presentTypingIndicator() {
typingIndicatorDecoration = TypingIndicatorDecoration(requireContext(), binding.conversationItemRecycler)
binding.conversationItemRecycler.addItemDecoration(typingIndicatorDecoration)
ApplicationDependencies.getTypingStatusRepository().getTypists(args.threadId).observe(viewLifecycleOwner) {
val recipient = viewModel.recipientSnapshot ?: return@observe
typingIndicatorDecoration.setTypists(
GlideApp.with(this),
it.typists,
recipient.isGroup,
recipient.hasWallpaper(),
it.isReplacedByIncomingMessage
)
}
}
private fun presentStoryRing() {
if (SignalStore.storyValues().isFeatureDisabled) {
return

View file

@ -0,0 +1,142 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.View.MeasureSpec
import androidx.core.graphics.withTranslation
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ConversationTypingView
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Displays a typing indicator as a part of the very last (first) item in the adapter.
*/
class TypingIndicatorDecoration(
private val context: Context,
private val rootView: RecyclerView
) : ItemDecoration() {
private val typingView: ConversationTypingView by lazy(LazyThreadSafetyMode.NONE) {
LayoutInflater.from(context).inflate(R.layout.conversation_typing_view, rootView, false) as ConversationTypingView
}
private var displayIndicator = false
private var animationFraction = 0f
private var offsetAnimator: ValueAnimator? = null
init {
rootView.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
remeasureTypingView()
rootView.invalidateItemDecorations()
}
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (!displayIndicator && animationFraction == 0f) {
return outRect.set(0, 0, 0, 0)
}
if (parent.getChildAdapterPosition(view) == 0) {
remeasureTypingView()
outRect.set(0, 0, 0, (typingView.measuredHeight * animationFraction).toInt())
} else {
outRect.set(0, 0, 0, 0)
}
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (!displayIndicator && offsetAnimator?.isRunning != true) {
return
}
val firstChild = parent.children.firstOrNull() ?: return
if (parent.getChildAdapterPosition(firstChild) == 0) {
c.withTranslation(
x = firstChild.left.toFloat(),
y = firstChild.bottom.toFloat()
) {
typingView.draw(this)
}
if (typingView.isActive) {
rootView.post { rootView.invalidateItemDecorations() }
}
}
}
fun setTypists(
glideRequests: GlideRequests,
typists: List<Recipient>,
isGroupThread: Boolean,
hasWallpaper: Boolean,
isReplacedByIncomingMessage: Boolean
) {
val isEdge = displayIndicator != typists.isNotEmpty()
displayIndicator = typists.isNotEmpty()
typingView.setTypists(
glideRequests,
typists,
isGroupThread,
hasWallpaper
)
remeasureTypingView()
rootView.invalidateItemDecorations()
if (isReplacedByIncomingMessage) {
offsetAnimator?.cancel()
animationFraction = 0f
} else if (isEdge) {
animateOffset()
}
}
private fun animateOffset() {
offsetAnimator?.cancel()
val (start, end) = if (displayIndicator) {
animationFraction to 1f
} else {
animationFraction to 0f
}
offsetAnimator = ValueAnimator.ofFloat(start, end).apply {
addUpdateListener {
animationFraction = it.animatedValue as Float
rootView.invalidateItemDecorations()
}
start()
}
}
private fun remeasureTypingView() {
with(typingView) {
measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
)
layout(
0,
0,
typingView.measuredWidth,
typingView.measuredHeight
)
}
}
}

View file

@ -117,7 +117,7 @@
app:barrierDirection="end"
app:constraint_referenced_ids="typing_avatar_1, typing_avatar_2, typing_avatar_3, typing_count" />
<com.google.android.material.card.MaterialCardView
<org.thoughtcrime.securesms.components.ClippedCardView
android:id="@+id/indicator_card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -143,6 +143,6 @@
app:typingIndicator_tint="@color/signal_inverse_primary" />
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
</org.thoughtcrime.securesms.components.ClippedCardView>
</org.thoughtcrime.securesms.components.ConversationTypingView>