Add new chat bubble pulse.

main
Alex Hart 2023-01-06 16:31:29 -04:00
rodzic af0fbdd2b2
commit 396742f3ad
5 zmienionych plików z 173 dodań i 34 usunięć

Wyświetl plik

@ -120,6 +120,7 @@ public class ConversationAdapter
private Colorizer colorizer;
private boolean isTypingViewEnabled;
private boolean condensedMode;
private PulseRequest pulseRequest;
public ConversationAdapter(@NonNull Context context,
@NonNull LifecycleOwner lifecycleOwner,
@ -487,10 +488,18 @@ public class ConversationAdapter
int correctedPosition = isHeaderPosition(position) ? position + 1 : position;
recordToPulse = getItem(correctedPosition);
pulseRequest = new PulseRequest(position, recordToPulse.getMessageRecord().isOutgoing());
notifyItemChanged(correctedPosition);
}
}
@Nullable
public PulseRequest consumePulseRequest() {
PulseRequest request = pulseRequest;
pulseRequest = null;
return request;
}
/**
* Conversation search query updated. Allows rendering of text highlighting.
*/
@ -770,6 +779,37 @@ public class ConversationAdapter
}
}
public static class PulseRequest {
private final int position;
private final boolean isOutgoing;
PulseRequest(int position, boolean isOutgoing) {
this.position = position;
this.isOutgoing = isOutgoing;
}
public int getPosition() {
return position;
}
public boolean isOutgoing() {
return isOutgoing;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final PulseRequest that = (PulseRequest) o;
return position == that.position;
}
@Override
public int hashCode() {
return Objects.hash(position);
}
}
public interface ItemClickListener extends BindableConversationItem.EventListener {
void onItemClick(MultiselectPart item);
void onItemLongClick(View itemView, MultiselectPart item);

Wyświetl plik

@ -16,7 +16,6 @@
*/
package org.thoughtcrime.securesms.conversation;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
@ -189,7 +188,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private boolean groupThread;
private LiveRecipient recipient;
private GlideRequests glideRequests;
private ValueAnimator pulseOutlinerAlphaAnimator;
private Optional<MessageRecord> previousMessage;
private ConversationItemDisplayMode displayMode;
@ -667,8 +665,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
bodyBubble.setVideoPlayerProjection(null);
bodyBubble.setQuoteViewProjection(null);
cancelPulseOutlinerAnimation();
}
@Override
@ -764,6 +760,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
return conversationMessage;
}
public boolean isOutgoing() {
return conversationMessage.getMessageRecord().isOutgoing();
}
/// MessageRecord Attribute Parsers
private void setBubbleState(MessageRecord messageRecord, @NonNull Recipient recipient, boolean hasWallpaper, @NonNull Colorizer colorizer) {
@ -848,7 +848,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setSelected(true);
} else if (pulseMention) {
setSelected(false);
startPulseOutlinerAnimation();
} else {
setSelected(false);
}
@ -871,29 +870,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private void startPulseOutlinerAnimation() {
pulseOutlinerAlphaAnimator = ValueAnimator.ofInt(0, 0x66, 0).setDuration(600);
pulseOutlinerAlphaAnimator.setRepeatCount(1);
pulseOutlinerAlphaAnimator.addUpdateListener(animator -> {
pulseOutliner.setAlpha((Integer) animator.getAnimatedValue());
bodyBubble.invalidate();
if (mediaThumbnailStub.resolved()) {
mediaThumbnailStub.require().invalidate();
}
});
pulseOutlinerAlphaAnimator.start();
}
private void cancelPulseOutlinerAnimation() {
if (pulseOutlinerAlphaAnimator != null) {
pulseOutlinerAlphaAnimator.cancel();
pulseOutlinerAlphaAnimator = null;
}
pulseOutliner.setAlpha(0);
}
private boolean shouldDrawBodyBubbleOutline(MessageRecord messageRecord, boolean hasWallpaper) {
if (hasWallpaper) {
return false;
@ -2065,13 +2041,17 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@Override
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
return getSnapshotProjections(coordinateRoot, true);
return getSnapshotProjections(coordinateRoot, true, true);
}
public @NonNull ProjectionList getSnapshotProjections(@NonNull ViewGroup coordinateRoot, boolean clipOutMedia) {
return getSnapshotProjections(coordinateRoot, clipOutMedia, true);
}
public @NonNull ProjectionList getSnapshotProjections(@NonNull ViewGroup coordinateRoot, boolean clipOutMedia, boolean outgoingOnly) {
colorizerProjections.clear();
if (messageRecord.isOutgoing() &&
if ((messageRecord.isOutgoing() || !outgoingOnly) &&
!hasNoBubble(messageRecord) &&
!messageRecord.isRemoteDelete() &&
bodyBubbleCorners != null &&
@ -2133,7 +2113,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
if (messageRecord.isOutgoing() &&
if ((messageRecord.isOutgoing() || !outgoingOnly) &&
hasNoBubble(messageRecord) &&
hasWallpaper &&
bodyBubble.getVisibility() == VISIBLE)

Wyświetl plik

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.conversation.mutiselect
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context
@ -15,15 +17,19 @@ import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import androidx.core.view.animation.PathInterpolatorCompat
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.SimpleColorFilter
import com.google.android.material.animation.ArgbEvaluatorCompat
import org.signal.core.util.SetUtil
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.ConversationAdapter
import org.thoughtcrime.securesms.conversation.ConversationAdapter.PulseRequest
import org.thoughtcrime.securesms.conversation.ConversationItem
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
@ -59,6 +65,10 @@ class MultiselectItemDecoration(
private var hideShadeAnimation: ValueAnimator? = null
private val multiselectPartAnimatorMap: MutableMap<MultiselectPart, ValueAnimator> = mutableMapOf()
private val pulseIncomingColor = ContextCompat.getColor(context, R.color.pulse_incoming_message)
private val pulseOutgoingColor = ContextCompat.getColor(context, R.color.pulse_outgoing_message)
private val pulseRequestAnimators: MutableMap<PulseRequest, PulseAnimator> = mutableMapOf()
private var checkedBitmap: Bitmap? = null
private var focusedItem: MultiselectPart? = null
@ -139,6 +149,8 @@ class MultiselectItemDecoration(
outRect.setEmpty()
updateChildOffsets(parent, view)
consumePulseRequest(parent.adapter as ConversationAdapter)
}
/**
@ -214,7 +226,10 @@ class MultiselectItemDecoration(
drawFocusShadeOverIfNecessary(canvas, parent)
}
invalidateIfAnimatorsAreRunning(parent)
drawPulseShadeOverIfNecessary(canvas, parent)
invalidateIfPulseRequestAnimatorsAreRunning(parent)
invalidateIfEnterExitAnimatorsAreRunning(parent)
}
private fun drawChecks(parent: RecyclerView, canvas: Canvas, adapter: ConversationAdapter) {
@ -400,6 +415,34 @@ class MultiselectItemDecoration(
}
}
private fun drawPulseShadeOverIfNecessary(canvas: Canvas, parent: RecyclerView) {
if (!hasRunningPulseRequestAnimators()) {
return
}
for (child in parent.children) {
if (child is ConversationItem) {
path.reset()
canvas.save()
val adapterPosition = parent.getChildAdapterPosition(child)
val request = pulseRequestAnimators.keys.firstOrNull { it.position == adapterPosition && it.isOutgoing == child.isOutgoing } ?: continue
val animator = pulseRequestAnimators[request] ?: continue
if (!animator.isRunning) {
continue
}
child.getSnapshotProjections(parent, false, false).use { projectionList ->
projectionList.forEach { it.applyToPath(path) }
}
canvas.clipPath(path)
canvas.drawColor(animator.animatedValue)
canvas.restore()
}
}
}
private fun Canvas.drawShade() {
val progress = hideShadeAnimation?.animatedValue as? Float
if (progress == null) {
@ -417,7 +460,7 @@ class MultiselectItemDecoration(
duration = 150L
addUpdateListener {
invalidateIfAnimatorsAreRunning(list)
invalidateIfEnterExitAnimatorsAreRunning(list)
}
doOnEnd {
@ -474,7 +517,23 @@ class MultiselectItemDecoration(
}
}
private fun invalidateIfAnimatorsAreRunning(parent: RecyclerView) {
private fun cleanPulseAnimators() {
val toRemove = pulseRequestAnimators.filter { !it.value.isRunning }.keys
toRemove.forEach { pulseRequestAnimators.remove(it) }
}
private fun hasRunningPulseRequestAnimators(): Boolean {
cleanPulseAnimators()
return pulseRequestAnimators.any { (_, v) -> v.isRunning }
}
private fun invalidateIfPulseRequestAnimatorsAreRunning(parent: RecyclerView) {
if (hasRunningPulseRequestAnimators()) {
parent.invalidateItemDecorations()
}
}
private fun invalidateIfEnterExitAnimatorsAreRunning(parent: RecyclerView) {
if (enterExitAnimation?.isRunning == true ||
multiselectPartAnimatorMap.values.any { it.isRunning } ||
hideShadeAnimation?.isRunning == true
@ -483,6 +542,60 @@ class MultiselectItemDecoration(
}
}
private fun consumePulseRequest(adapter: ConversationAdapter) {
val pulseRequest = adapter.consumePulseRequest()
if (pulseRequest != null) {
val pulseColor = if (pulseRequest.isOutgoing) pulseOutgoingColor else pulseIncomingColor
pulseRequestAnimators[pulseRequest]?.cancel()
pulseRequestAnimators[pulseRequest] = PulseAnimator(pulseColor).apply { start() }
}
}
private class PulseAnimator(pulseColor: Int) {
companion object {
private val PULSE_BEZIER = PathInterpolatorCompat.create(0.17f, 0.17f, 0f, 1f)
}
private val animator = AnimatorSet().apply {
playSequentially(
pulseInAnimator(pulseColor),
pulseOutAnimator(pulseColor),
pulseInAnimator(pulseColor),
pulseOutAnimator(pulseColor)
)
interpolator = PULSE_BEZIER
}
val isRunning: Boolean get() = animator.isRunning
var animatedValue: Int = Color.TRANSPARENT
private set
fun start() = animator.start()
fun cancel() = animator.cancel()
private fun pulseInAnimator(pulseColor: Int): Animator {
return ValueAnimator.ofInt(Color.TRANSPARENT, pulseColor).apply {
duration = 200
setEvaluator(ArgbEvaluatorCompat.getInstance())
addUpdateListener {
this@PulseAnimator.animatedValue = animatedValue as Int
}
}
}
private fun pulseOutAnimator(pulseColor: Int): Animator {
return ValueAnimator.ofInt(pulseColor, Color.TRANSPARENT).apply {
startDelay = 200
duration = 200
setEvaluator(ArgbEvaluatorCompat.getInstance())
addUpdateListener {
this@PulseAnimator.animatedValue = animatedValue as Int
}
}
}
}
private enum class Difference {
REMOVED,
ADDED,

Wyświetl plik

@ -9,6 +9,9 @@
<color name="signal_accent_primary">@color/core_ultramarine_light</color>
<color name="signal_inverse_primary">@color/core_white</color>
<color name="pulse_incoming_message">@color/transparent_white_15</color>
<color name="pulse_outgoing_message">@color/transparent_white_25</color>
<color name="signal_background_primary">@color/signal_colorBackground</color>
<color name="signal_background_secondary">@color/signal_colorSurface1</color>
<color name="signal_background_tertiary">@color/core_grey_90</color>

Wyświetl plik

@ -8,6 +8,9 @@
<color name="signal_inverse_primary">@color/core_black</color>
<color name="pulse_incoming_message">@color/transparent_black_10</color>
<color name="pulse_outgoing_message">@color/transparent_black_25</color>
<color name="signal_background_primary">@color/signal_colorBackground</color>
<color name="signal_background_secondary">@color/signal_colorSurface1</color>
<color name="signal_background_tertiary">@color/core_grey_02</color>