Fix animation

This commit is contained in:
andrew 2023-11-01 14:00:59 +10:30
parent 1b5b7cfccc
commit 555209bec1
6 changed files with 87 additions and 152 deletions

View File

@ -1,128 +0,0 @@
package org.thoughtcrime.securesms.conversation.v2.components;
import android.graphics.drawable.Drawable;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import org.session.libsession.utilities.Util;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;
import network.loki.messenger.R;
public class ExpirationTimerView {
private final ImageView imageView;
private final Integer iconColor;
private long startedAt;
private long expiresIn;
private boolean visible = false;
private boolean stopped = true;
private final int[] frames = new int[]{ R.drawable.timer00,
R.drawable.timer05,
R.drawable.timer10,
R.drawable.timer15,
R.drawable.timer20,
R.drawable.timer25,
R.drawable.timer30,
R.drawable.timer35,
R.drawable.timer40,
R.drawable.timer45,
R.drawable.timer50,
R.drawable.timer55,
R.drawable.timer60 };
public ExpirationTimerView(ImageView imageView, Integer iconColor) {
this.imageView = imageView;
this.iconColor = iconColor;
}
public void setExpirationTime(long startedAt, long expiresIn) {
this.startedAt = startedAt;
this.expiresIn = expiresIn;
setPercentComplete(calculateProgress(this.startedAt, this.expiresIn));
}
public void setPercentComplete(float percentage) {
float percentFull = 1 - percentage;
int frame = (int) Math.ceil(percentFull * (frames.length - 1));
frame = Math.max(0, Math.min(frame, frames.length - 1));
Drawable drawable = AppCompatResources.getDrawable(imageView.getContext(), frames[frame]).mutate();
if (iconColor != null) drawable.setTint(iconColor);
imageView.setImageDrawable(drawable);
}
public void startAnimation() {
synchronized (this) {
visible = true;
if (!stopped) return;
else stopped = false;
}
Util.runOnMainDelayed(new AnimationUpdateRunnable(this), calculateAnimationDelay(this.startedAt, this.expiresIn));
}
public void stopAnimation() {
synchronized (this) {
visible = false;
}
}
private float calculateProgress(long startedAt, long expiresIn) {
long progressed = System.currentTimeMillis() - startedAt;
float percentComplete = (float)progressed / (float)expiresIn;
return Math.max(0, Math.min(percentComplete, 1));
}
private long calculateAnimationDelay(long startedAt, long expiresIn) {
long progressed = System.currentTimeMillis() - startedAt;
long remaining = expiresIn - progressed;
if (remaining <= 0) {
return 0;
} else if (remaining < TimeUnit.SECONDS.toMillis(30)) {
return 1000;
} else {
return 5000;
}
}
private static class AnimationUpdateRunnable implements Runnable {
private final WeakReference<ExpirationTimerView> expirationTimerViewReference;
private AnimationUpdateRunnable(@NonNull ExpirationTimerView expirationTimerView) {
this.expirationTimerViewReference = new WeakReference<>(expirationTimerView);
}
@Override
public void run() {
ExpirationTimerView timerView = expirationTimerViewReference.get();
if (timerView == null) return;
long nextUpdate = timerView.calculateAnimationDelay(timerView.startedAt, timerView.expiresIn);
synchronized (timerView) {
if (timerView.visible) {
timerView.setExpirationTime(timerView.startedAt, timerView.expiresIn);
} else {
timerView.stopped = true;
return;
}
if (nextUpdate <= 0) {
timerView.stopped = true;
return;
}
}
Util.runOnMainDelayed(this, nextUpdate);
}
}
}

View File

@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.conversation.v2.components
import android.content.Context
import android.graphics.drawable.AnimationDrawable
import android.util.AttributeSet
import android.widget.ImageView
import androidx.core.content.ContextCompat
import network.loki.messenger.R
import org.session.libsession.snode.SnodeAPI.nowWithOffset
import kotlin.math.round
class ExpirationTimerView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ImageView(context, attrs, defStyleAttr) {
private val frames = intArrayOf(
R.drawable.timer00,
R.drawable.timer05,
R.drawable.timer10,
R.drawable.timer15,
R.drawable.timer20,
R.drawable.timer25,
R.drawable.timer30,
R.drawable.timer35,
R.drawable.timer40,
R.drawable.timer45,
R.drawable.timer50,
R.drawable.timer55,
R.drawable.timer60
)
fun setExpirationTime(startedAt: Long, expiresIn: Long) {
val elapsedTime = nowWithOffset - startedAt
val remainingTime = expiresIn - elapsedTime
val remainingPercent = (remainingTime / expiresIn.toFloat()).coerceIn(0f, 1f)
val frameCount = round(frames.size * remainingPercent).toInt().coerceIn(1, frames.size)
val frameTime = round(remainingTime / frameCount.toFloat()).toInt()
AnimationDrawable().apply {
frames.take(frameCount).reversed().forEach { addFrame(ContextCompat.getDrawable(context, it)!!, frameTime) }
isOneShot = true
}.also(::setImageDrawable).apply(AnimationDrawable::start)
}
}

View File

@ -6,6 +6,8 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewControlMessageBinding import network.loki.messenger.databinding.ViewControlMessageBinding
@ -17,7 +19,6 @@ class ControlMessageView : LinearLayout {
private lateinit var binding: ViewControlMessageBinding private lateinit var binding: ViewControlMessageBinding
// region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
@ -26,27 +27,27 @@ class ControlMessageView : LinearLayout {
binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true) binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true)
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
} }
// endregion
// region Updating
fun bind(message: MessageRecord, previous: MessageRecord?) { fun bind(message: MessageRecord, previous: MessageRecord?) {
binding.dateBreakTextView.showDateBreak(message, previous) binding.dateBreakTextView.showDateBreak(message, previous)
binding.iconImageView.visibility = View.GONE binding.iconImageView.isGone = true
binding.expirationTimerView.isGone = true
var messageBody: CharSequence = message.getDisplayBody(context) var messageBody: CharSequence = message.getDisplayBody(context)
binding.root.contentDescription= null binding.root.contentDescription= null
when { when {
message.isExpirationTimerUpdate -> { message.isExpirationTimerUpdate -> {
ExpirationTimerView(binding.iconImageView, context.getColorFromAttr(android.R.attr.textColorPrimary)).apply { binding.expirationTimerView.apply {
isVisible = true
setExpirationTime(message.expireStarted, message.expiresIn) setExpirationTime(message.expireStarted, message.expiresIn)
startAnimation()
} }
binding.iconImageView.visibility = View.VISIBLE
} }
message.isMediaSavedNotification -> { message.isMediaSavedNotification -> {
binding.iconImageView.setImageDrawable( binding.iconImageView.apply {
setImageDrawable(
ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme) ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme)
) )
binding.iconImageView.visibility = View.VISIBLE isVisible = true
}
} }
message.isMessageRequestResponse -> { message.isMessageRequestResponse -> {
messageBody = context.getString(R.string.message_requests_accepted) messageBody = context.getString(R.string.message_requests_accepted)
@ -59,8 +60,10 @@ class ControlMessageView : LinearLayout {
message.isFirstMissedCall -> R.drawable.ic_info_outline_light message.isFirstMissedCall -> R.drawable.ic_info_outline_light
else -> R.drawable.ic_missed_call else -> R.drawable.ic_missed_call
} }
binding.iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, drawable, context.theme)) binding.iconImageView.apply {
binding.iconImageView.visibility = View.VISIBLE setImageDrawable(ResourcesCompat.getDrawable(resources, drawable, context.theme))
isVisible = true
}
} }
} }
@ -70,5 +73,4 @@ class ControlMessageView : LinearLayout {
fun recycle() { fun recycle() {
} }
// endregion
} }

View File

@ -29,7 +29,6 @@ import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.contacts.Contact.ContactContext import org.session.libsession.messaging.contacts.Contact.ContactContext
import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.getColorFromAttr import org.session.libsession.utilities.getColorFromAttr
@ -37,7 +36,6 @@ import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.MmsDatabase
@ -264,11 +262,15 @@ class VisibleMessageView : LinearLayout {
val isLastMessage = message.id == lastMessageID val isLastMessage = message.id == lastMessageID
binding.messageStatusTextView.isVisible = binding.messageStatusTextView.isVisible =
textId != null && (!message.isSent || isLastMessage || disappearing) textId != null && (!message.isSent || isLastMessage || disappearing)
val showTimer = disappearing && !message.isPending
binding.messageStatusImageView.isVisible = binding.messageStatusImageView.isVisible =
iconID != null && (!message.isSent || isLastMessage || disappearing) iconID != null && !showTimer && (!message.isSent || isLastMessage)
binding.messageStatusImageView.bringToFront() binding.messageStatusImageView.bringToFront()
if (disappearing && !message.isPending) updateExpirationTimer(message, iconColor) binding.expirationTimerView.bringToFront()
binding.expirationTimerView.isVisible = showTimer
if (showTimer) updateExpirationTimer(message)
} else { } else {
binding.messageStatusTextView.isVisible = false binding.messageStatusTextView.isVisible = false
binding.messageStatusImageView.isVisible = false binding.messageStatusImageView.isVisible = false
@ -342,20 +344,16 @@ class VisibleMessageView : LinearLayout {
) )
} }
private fun updateExpirationTimer(message: MessageRecord, iconColor: Int?) { private fun updateExpirationTimer(message: MessageRecord) {
val expirationTimerView = ExpirationTimerView(binding.messageStatusImageView, iconColor) val expirationTimerView = binding.expirationTimerView
if (!message.isOutgoing) binding.messageStatusTextView.bringToFront() if (!message.isOutgoing) binding.messageStatusTextView.bringToFront()
if (message.expiresIn > 0) { if (message.expiresIn > 0) {
expirationTimerView.setPercentComplete(0.0f)
if (message.expireStarted > 0) { if (message.expireStarted > 0) {
expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn) expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn)
expirationTimerView.startAnimation()
ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule() ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule()
} else { } else {
expirationTimerView.setPercentComplete(0.0f)
expirationTimerView.stopAnimation()
ThreadUtils.queue { ThreadUtils.queue {
val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager
val id = message.getId() val id = message.getId()

View File

@ -29,6 +29,16 @@
tools:src="@drawable/ic_timer" tools:src="@drawable/ic_timer"
tools:visibility="visible"/> tools:visibility="visible"/>
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
android:id="@+id/expirationTimerView"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginBottom="@dimen/small_spacing"
android:visibility="gone"
app:tint="?android:textColorPrimary"
tools:src="@drawable/ic_timer"
tools:visibility="visible"/>
<TextView <TextView
android:id="@+id/textView" android:id="@+id/textView"
android:contentDescription="@string/AccessibilityId_control_message" android:contentDescription="@string/AccessibilityId_control_message"

View File

@ -164,6 +164,13 @@
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_delivery_status_sent" /> android:src="@drawable/ic_delivery_status_sent" />
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
android:id="@+id/expirationTimerView"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center"
android:tint="?message_status_color" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>