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();
|
indicator.startAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return indicator.isActive();
|
||||||
|
}
|
||||||
|
|
||||||
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
|
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
|
||||||
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
|
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
|
||||||
avatar1.setVisibility(VISIBLE);
|
avatar1.setVisibility(VISIBLE);
|
||||||
|
|
|
@ -125,4 +125,8 @@ public class TypingIndicatorView extends LinearLayout {
|
||||||
public void stopAnimation() {
|
public void stopAnimation() {
|
||||||
isActive = false;
|
isActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -427,6 +427,7 @@ class ConversationFragment :
|
||||||
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
|
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
|
||||||
private lateinit var dateHeaderDecoration: DateHeaderDecoration
|
private lateinit var dateHeaderDecoration: DateHeaderDecoration
|
||||||
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
|
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
|
||||||
|
private lateinit var typingIndicatorDecoration: TypingIndicatorDecoration
|
||||||
|
|
||||||
private var animationsAllowed = false
|
private var animationsAllowed = false
|
||||||
private var actionMode: ActionMode? = null
|
private var actionMode: ActionMode? = null
|
||||||
|
@ -859,6 +860,8 @@ class ConversationFragment :
|
||||||
.getScheduledMessagesCount()
|
.getScheduledMessagesCount()
|
||||||
.subscribeBy { count -> handleScheduledMessagesCountChange(count) }
|
.subscribeBy { count -> handleScheduledMessagesCountChange(count) }
|
||||||
.addTo(disposables)
|
.addTo(disposables)
|
||||||
|
|
||||||
|
presentTypingIndicator()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeInlineSearch() {
|
private fun initializeInlineSearch() {
|
||||||
|
@ -889,6 +892,23 @@ class ConversationFragment :
|
||||||
.addTo(disposables)
|
.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() {
|
private fun presentStoryRing() {
|
||||||
if (SignalStore.storyValues().isFeatureDisabled) {
|
if (SignalStore.storyValues().isFeatureDisabled) {
|
||||||
return
|
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:barrierDirection="end"
|
||||||
app:constraint_referenced_ids="typing_avatar_1, typing_avatar_2, typing_avatar_3, typing_count" />
|
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:id="@+id/indicator_card"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -143,6 +143,6 @@
|
||||||
app:typingIndicator_tint="@color/signal_inverse_primary" />
|
app:typingIndicator_tint="@color/signal_inverse_primary" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</org.thoughtcrime.securesms.components.ClippedCardView>
|
||||||
|
|
||||||
</org.thoughtcrime.securesms.components.ConversationTypingView>
|
</org.thoughtcrime.securesms.components.ConversationTypingView>
|
Loading…
Add table
Reference in a new issue