Add TypingIndicatorDecoration to CFV2.
This commit is contained in:
parent
27e7383db6
commit
47b97aafc6
5 changed files with 172 additions and 2 deletions
|
@ -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);
|
||||
|
|
|
@ -125,4 +125,8 @@ public class TypingIndicatorView extends LinearLayout {
|
|||
public void stopAnimation() {
|
||||
isActive = false;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return isActive;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
Loading…
Add table
Reference in a new issue