From a763e1729c99368b425b8e28083394f8f1519fa9 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 7 Dec 2023 15:46:19 -0500 Subject: [PATCH] Update audio indicator for new designs. --- .../components/webrtc/AudioIndicatorView.kt | 19 ++-- .../webrtc/CallParticipantView.java | 92 ++++++++++++++++++- .../PictureInPictureExpansionHelper.java | 64 ++++++++----- .../components/webrtc/WebRtcCallView.java | 37 +++++--- .../symbol_mic_slash_fill_compact_16.xml | 21 +++++ .../main/res/layout/audio_indicator_view.xml | 6 +- .../main/res/layout/call_participant_item.xml | 4 +- ...c_call_participant_recycler_empty_item.xml | 4 +- app/src/main/res/values/dimens.xml | 2 +- 9 files changed, 194 insertions(+), 55 deletions(-) create mode 100644 app/src/main/res/drawable/symbol_mic_slash_fill_compact_16.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioIndicatorView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioIndicatorView.kt index 8edbcd1f03..73576787a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioIndicatorView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/AudioIndicatorView.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.webrtc import android.animation.ValueAnimator import android.content.Context +import android.content.res.ColorStateList import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint @@ -10,6 +11,7 @@ import android.util.AttributeSet import android.view.View import android.view.animation.DecelerateInterpolator import android.widget.FrameLayout +import androidx.core.content.ContextCompat import org.signal.core.util.DimensionUnit import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.events.CallParticipant @@ -31,9 +33,9 @@ class AudioIndicatorView(context: Context, attrs: AttributeSet) : FrameLayout(co } private val barRect = RectF() - private val barWidth = DimensionUnit.DP.toPixels(4f) + private val barWidth = DimensionUnit.DP.toPixels(3f) private val barRadius = DimensionUnit.DP.toPixels(32f) - private val barPadding = DimensionUnit.DP.toPixels(4f) + private val barPadding = DimensionUnit.DP.toPixels(3f) private var middleBarAnimation: ValueAnimator? = null private var sideBarAnimation: ValueAnimator? = null @@ -43,6 +45,9 @@ class AudioIndicatorView(context: Context, attrs: AttributeSet) : FrameLayout(co init { inflate(context, R.layout.audio_indicator_view, this) setWillNotDraw(false) + + setBackgroundResource(R.drawable.circle_tintable) + backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(context, R.color.transparent_black_70)) } private val micMuted: View = findViewById(R.id.mic_muted) @@ -55,11 +60,11 @@ class AudioIndicatorView(context: Context, attrs: AttributeSet) : FrameLayout(co if (showAudioLevel) { val scaleFactor = when (level!!) { - CallParticipant.AudioLevel.LOWEST -> 0.2f - CallParticipant.AudioLevel.LOW -> 0.4f - CallParticipant.AudioLevel.MEDIUM -> 0.6f - CallParticipant.AudioLevel.HIGH -> 0.8f - CallParticipant.AudioLevel.HIGHEST -> 1.0f + CallParticipant.AudioLevel.LOWEST -> 0.1f + CallParticipant.AudioLevel.LOW -> 0.3f + CallParticipant.AudioLevel.MEDIUM -> 0.5f + CallParticipant.AudioLevel.HIGH -> 0.65f + CallParticipant.AudioLevel.HIGHEST -> 0.8f } middleBarAnimation?.end() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java index dc26d5d2ae..7d4d7dbcec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java @@ -9,12 +9,13 @@ import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.AppCompatImageView; import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.content.ContextCompat; import androidx.core.view.ViewKt; import androidx.core.widget.ImageViewCompat; +import androidx.transition.Transition; import androidx.transition.TransitionManager; import com.bumptech.glide.load.engine.DiskCacheStrategy; @@ -37,6 +38,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.AvatarUtil; import org.thoughtcrime.securesms.util.ViewUtil; import org.webrtc.RendererCommon; +import org.whispersystems.signalservice.api.util.Preconditions; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -57,6 +59,8 @@ public class CallParticipantView extends ConstraintLayout { private boolean infoMode; private Runnable missingMediaKeysUpdater; + private SelfPipMode selfPipMode = SelfPipMode.NOT_SELF_PIP; + private AppCompatImageView backgroundAvatar; private AvatarImageView avatar; private BadgeImageView badge; @@ -211,6 +215,85 @@ public class CallParticipantView extends ConstraintLayout { pipBadge.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE); } + /** + * Adjust UI elements for the various self PIP positions. If called after a {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)}, + * the changes to the UI elements will animate. + */ + void setSelfPipMode(@NonNull SelfPipMode selfPipMode) { + Preconditions.checkArgument(selfPipMode != SelfPipMode.NOT_SELF_PIP); + + if (this.selfPipMode == selfPipMode) { + return; + } + + this.selfPipMode = selfPipMode; + + ConstraintSet constraints = new ConstraintSet(); + constraints.clone(this); + + switch (selfPipMode) { + case NORMAL_SELF_PIP -> { + constraints.connect( + R.id.call_participant_audio_indicator, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + ViewUtil.dpToPx(6) + ); + constraints.clear( + R.id.call_participant_audio_indicator, + ConstraintSet.END + ); + constraints.setMargin( + R.id.call_participant_audio_indicator, + ConstraintSet.BOTTOM, + ViewUtil.dpToPx(6) + ); + } + case EXPANDED_SELF_PIP -> { + constraints.connect( + R.id.call_participant_audio_indicator, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + ViewUtil.dpToPx(8) + ); + constraints.clear( + R.id.call_participant_audio_indicator, + ConstraintSet.END + ); + constraints.setMargin( + R.id.call_participant_audio_indicator, + ConstraintSet.BOTTOM, + ViewUtil.dpToPx(8) + ); + } + case MINI_SELF_PIP -> { + constraints.connect( + R.id.call_participant_audio_indicator, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + 0 + ); + constraints.connect( + R.id.call_participant_audio_indicator, + ConstraintSet.END, + ConstraintSet.PARENT_ID, + ConstraintSet.END, + 0 + ); + constraints.setMargin( + R.id.call_participant_audio_indicator, + ConstraintSet.BOTTOM, + ViewUtil.dpToPx(6) + ); + } + } + + constraints.applyTo(this); + } + void hideAvatar() { avatar.setAlpha(0f); badge.setAlpha(0f); @@ -302,4 +385,11 @@ public class CallParticipantView extends ConstraintLayout { return photo; } } + + public enum SelfPipMode { + NOT_SELF_PIP, + NORMAL_SELF_PIP, + EXPANDED_SELF_PIP, + MINI_SELF_PIP + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java index f211f4d586..593f5d1725 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/PictureInPictureExpansionHelper.java @@ -3,13 +3,14 @@ package org.thoughtcrime.securesms.components.webrtc; import android.graphics.Point; import android.view.View; import android.view.ViewGroup; -import android.view.animation.Animation; import androidx.annotation.MainThread; import androidx.annotation.NonNull; +import androidx.transition.AutoTransition; +import androidx.transition.Transition; +import androidx.transition.TransitionListenerAdapter; +import androidx.transition.TransitionManager; -import org.thoughtcrime.securesms.animation.ResizeAnimation; -import org.thoughtcrime.securesms.mediasend.SimpleAnimationListener; import org.thoughtcrime.securesms.util.ViewUtil; /** @@ -22,14 +23,22 @@ final class PictureInPictureExpansionHelper { private static final int EXPANDED_PIP_WIDTH_DP = 170; private static final int EXPANDED_PIP_HEIGHT_DP = 300; - private final View selfPip; - private final Point expandedDimensions; + public static final int NORMAL_PIP_WIDTH_DP = 90; + public static final int NORMAL_PIP_HEIGHT_DP = 160; + + public static final int MINI_PIP_WIDTH_DP = 40; + public static final int MINI_PIP_HEIGHT_DP = 72; + + private final View selfPip; + private final ViewGroup parent; + private final Point expandedDimensions; private State state = State.IS_SHRUNKEN; private Point defaultDimensions; public PictureInPictureExpansionHelper(@NonNull View selfPip) { this.selfPip = selfPip; + this.parent = (ViewGroup) selfPip.getParent(); this.defaultDimensions = new Point(selfPip.getLayoutParams().width, selfPip.getLayoutParams().height); this.expandedDimensions = new Point(ViewUtil.dpToPx(EXPANDED_PIP_WIDTH_DP), ViewUtil.dpToPx(EXPANDED_PIP_HEIGHT_DP)); } @@ -42,7 +51,11 @@ final class PictureInPictureExpansionHelper { return state == State.IS_SHRUNKEN || state == State.IS_SHRINKING; } - public void setDefaultSize(@NonNull Point dimensions, @NonNull Callback callback) { + public boolean isMiniSize() { + return defaultDimensions.x < ViewUtil.dpToPx(NORMAL_PIP_WIDTH_DP); + } + + public void startDefaultSizeTransition(@NonNull Point dimensions, @NonNull Callback callback) { if (defaultDimensions.equals(dimensions)) { return; } @@ -53,21 +66,15 @@ final class PictureInPictureExpansionHelper { return; } - ViewGroup.LayoutParams layoutParams = selfPip.getLayoutParams(); - if (layoutParams.width == defaultDimensions.x && layoutParams.height == defaultDimensions.y) { - callback.onAnimationHasFinished(); - return; - } - - resizeSelfPip(defaultDimensions, callback); + beginResizeSelfPipTransition(defaultDimensions, callback); } - public void expand() { + public void beginExpandTransition() { if (isExpandedOrExpanding()) { return; } - resizeSelfPip(expandedDimensions, new Callback() { + beginResizeSelfPipTransition(expandedDimensions, new Callback() { @Override public void onAnimationWillStart() { state = State.IS_EXPANDING; @@ -80,12 +87,12 @@ final class PictureInPictureExpansionHelper { }); } - public void shrink() { + public void beginShrinkTransition() { if (isShrunkenOrShrinking()) { return; } - resizeSelfPip(defaultDimensions, new Callback() { + beginResizeSelfPipTransition(defaultDimensions, new Callback() { @Override public void onAnimationWillStart() { state = State.IS_SHRINKING; @@ -98,23 +105,30 @@ final class PictureInPictureExpansionHelper { }); } - private void resizeSelfPip(@NonNull Point dimension, @NonNull Callback callback) { - ResizeAnimation resizeAnimation = new ResizeAnimation(selfPip, dimension); - resizeAnimation.setDuration(PIP_RESIZE_DURATION_MS); - resizeAnimation.setAnimationListener(new SimpleAnimationListener() { + private void beginResizeSelfPipTransition(@NonNull Point dimension, @NonNull Callback callback) { + TransitionManager.endTransitions(parent); + + Transition transition = new AutoTransition().setDuration(PIP_RESIZE_DURATION_MS); + transition.addListener(new TransitionListenerAdapter() { @Override - public void onAnimationStart(Animation animation) { + public void onTransitionStart(@NonNull Transition transition) { callback.onAnimationWillStart(); } @Override - public void onAnimationEnd(Animation animation) { + public void onTransitionEnd(@NonNull Transition transition) { callback.onAnimationHasFinished(); } }); - selfPip.clearAnimation(); - selfPip.startAnimation(resizeAnimation); + TransitionManager.beginDelayedTransition(parent, transition); + + ViewGroup.LayoutParams params = selfPip.getLayoutParams(); + + params.width = dimension.x; + params.height = dimension.y; + + selfPip.setLayoutParams(params); } enum State { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java index 51c230bf0e..509ee41c68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java @@ -497,21 +497,22 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { videoToggle.setChecked(localCallParticipant.isVideoEnabled(), false); smallLocalRender.setRenderInPip(true); + smallLocalRender.setCallParticipant(localCallParticipant); + smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT); if (state == WebRtcLocalRenderState.EXPANDED) { - pictureInPictureExpansionHelper.expand(); + pictureInPictureExpansionHelper.beginExpandTransition(); + smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.EXPANDED_SELF_PIP); return; } else if ((state.isAnySmall() || state == WebRtcLocalRenderState.GONE) && pictureInPictureExpansionHelper.isExpandedOrExpanding()) { - pictureInPictureExpansionHelper.shrink(); + pictureInPictureExpansionHelper.beginShrinkTransition(); + smallLocalRender.setSelfPipMode(pictureInPictureExpansionHelper.isMiniSize() ? CallParticipantView.SelfPipMode.MINI_SELF_PIP : CallParticipantView.SelfPipMode.NORMAL_SELF_PIP); if (state != WebRtcLocalRenderState.GONE) { return; } } - smallLocalRender.setCallParticipant(localCallParticipant); - smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT); - switch (state) { case GONE: largeLocalRender.attachBroadcastVideoSink(null); @@ -806,26 +807,34 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { private void animatePipToLargeRectangle(boolean isLandscape) { final Point dimens; if (isLandscape) { - dimens = new Point(ViewUtil.dpToPx(160), ViewUtil.dpToPx(90)); + dimens = new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_HEIGHT_DP), + ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_WIDTH_DP)); } else { - dimens = new Point(ViewUtil.dpToPx(90), ViewUtil.dpToPx(160)); + dimens = new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_WIDTH_DP), + ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_HEIGHT_DP)); } - pictureInPictureExpansionHelper.setDefaultSize(dimens, new PictureInPictureExpansionHelper.Callback() { + pictureInPictureExpansionHelper.startDefaultSizeTransition(dimens, new PictureInPictureExpansionHelper.Callback() { @Override public void onAnimationHasFinished() { pictureInPictureGestureHelper.enableCorners(); } }); + + smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.NORMAL_SELF_PIP); } private void animatePipToSmallRectangle() { - pictureInPictureExpansionHelper.setDefaultSize(new Point(ViewUtil.dpToPx(54), ViewUtil.dpToPx(72)), new PictureInPictureExpansionHelper.Callback() { - @Override - public void onAnimationHasFinished() { - pictureInPictureGestureHelper.lockToBottomEnd(); - } - }); + pictureInPictureExpansionHelper.startDefaultSizeTransition(new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.MINI_PIP_WIDTH_DP), + ViewUtil.dpToPx(PictureInPictureExpansionHelper.MINI_PIP_HEIGHT_DP)), + new PictureInPictureExpansionHelper.Callback() { + @Override + public void onAnimationHasFinished() { + pictureInPictureGestureHelper.lockToBottomEnd(); + } + }); + + smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.MINI_SELF_PIP); } private void toggleControls() { diff --git a/app/src/main/res/drawable/symbol_mic_slash_fill_compact_16.xml b/app/src/main/res/drawable/symbol_mic_slash_fill_compact_16.xml new file mode 100644 index 0000000000..9418c3e654 --- /dev/null +++ b/app/src/main/res/drawable/symbol_mic_slash_fill_compact_16.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/layout/audio_indicator_view.xml b/app/src/main/res/layout/audio_indicator_view.xml index b2fe24d236..d44ea85194 100644 --- a/app/src/main/res/layout/audio_indicator_view.xml +++ b/app/src/main/res/layout/audio_indicator_view.xml @@ -5,8 +5,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/mic_muted" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="16dp" + android:layout_height="16dp" android:layout_gravity="center" - app:srcCompat="@drawable/ic_mic_off_solid_18" + app:srcCompat="@drawable/symbol_mic_slash_fill_compact_16" app:tint="@color/core_white" /> diff --git a/app/src/main/res/layout/call_participant_item.xml b/app/src/main/res/layout/call_participant_item.xml index d752b70879..d3007a370b 100644 --- a/app/src/main/res/layout/call_participant_item.xml +++ b/app/src/main/res/layout/call_participant_item.xml @@ -86,8 +86,8 @@ - \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 09b2d859d7..520668725b 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -196,7 +196,7 @@ 18dp 48dp - 14dp + 8dp 8dp 0dp