kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add new chat bubble pulse.
rodzic
af0fbdd2b2
commit
396742f3ad
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue