Update audio indicator for new designs.

This commit is contained in:
Cody Henthorne
2023-12-07 15:46:19 -05:00
parent 6aac250990
commit a763e1729c
9 changed files with 194 additions and 55 deletions

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.webrtc
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
@@ -10,6 +11,7 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import org.signal.core.util.DimensionUnit import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.events.CallParticipant import org.thoughtcrime.securesms.events.CallParticipant
@@ -31,9 +33,9 @@ class AudioIndicatorView(context: Context, attrs: AttributeSet) : FrameLayout(co
} }
private val barRect = RectF() 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 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 middleBarAnimation: ValueAnimator? = null
private var sideBarAnimation: ValueAnimator? = null private var sideBarAnimation: ValueAnimator? = null
@@ -43,6 +45,9 @@ class AudioIndicatorView(context: Context, attrs: AttributeSet) : FrameLayout(co
init { init {
inflate(context, R.layout.audio_indicator_view, this) inflate(context, R.layout.audio_indicator_view, this)
setWillNotDraw(false) 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) private val micMuted: View = findViewById(R.id.mic_muted)
@@ -55,11 +60,11 @@ class AudioIndicatorView(context: Context, attrs: AttributeSet) : FrameLayout(co
if (showAudioLevel) { if (showAudioLevel) {
val scaleFactor = when (level!!) { val scaleFactor = when (level!!) {
CallParticipant.AudioLevel.LOWEST -> 0.2f CallParticipant.AudioLevel.LOWEST -> 0.1f
CallParticipant.AudioLevel.LOW -> 0.4f CallParticipant.AudioLevel.LOW -> 0.3f
CallParticipant.AudioLevel.MEDIUM -> 0.6f CallParticipant.AudioLevel.MEDIUM -> 0.5f
CallParticipant.AudioLevel.HIGH -> 0.8f CallParticipant.AudioLevel.HIGH -> 0.65f
CallParticipant.AudioLevel.HIGHEST -> 1.0f CallParticipant.AudioLevel.HIGHEST -> 0.8f
} }
middleBarAnimation?.end() middleBarAnimation?.end()

View File

@@ -9,12 +9,13 @@ import android.widget.ImageView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatImageView;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.view.ViewKt; import androidx.core.view.ViewKt;
import androidx.core.widget.ImageViewCompat; import androidx.core.widget.ImageViewCompat;
import androidx.transition.Transition;
import androidx.transition.TransitionManager; import androidx.transition.TransitionManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy; 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.AvatarUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.RendererCommon; import org.webrtc.RendererCommon;
import org.whispersystems.signalservice.api.util.Preconditions;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -57,6 +59,8 @@ public class CallParticipantView extends ConstraintLayout {
private boolean infoMode; private boolean infoMode;
private Runnable missingMediaKeysUpdater; private Runnable missingMediaKeysUpdater;
private SelfPipMode selfPipMode = SelfPipMode.NOT_SELF_PIP;
private AppCompatImageView backgroundAvatar; private AppCompatImageView backgroundAvatar;
private AvatarImageView avatar; private AvatarImageView avatar;
private BadgeImageView badge; private BadgeImageView badge;
@@ -211,6 +215,85 @@ public class CallParticipantView extends ConstraintLayout {
pipBadge.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE); 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() { void hideAvatar() {
avatar.setAlpha(0f); avatar.setAlpha(0f);
badge.setAlpha(0f); badge.setAlpha(0f);
@@ -302,4 +385,11 @@ public class CallParticipantView extends ConstraintLayout {
return photo; return photo;
} }
} }
public enum SelfPipMode {
NOT_SELF_PIP,
NORMAL_SELF_PIP,
EXPANDED_SELF_PIP,
MINI_SELF_PIP
}
} }

View File

@@ -3,13 +3,14 @@ package org.thoughtcrime.securesms.components.webrtc;
import android.graphics.Point; import android.graphics.Point;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.Animation;
import androidx.annotation.MainThread; import androidx.annotation.MainThread;
import androidx.annotation.NonNull; 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; 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_WIDTH_DP = 170;
private static final int EXPANDED_PIP_HEIGHT_DP = 300; private static final int EXPANDED_PIP_HEIGHT_DP = 300;
private final View selfPip; public static final int NORMAL_PIP_WIDTH_DP = 90;
private final Point expandedDimensions; 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 State state = State.IS_SHRUNKEN;
private Point defaultDimensions; private Point defaultDimensions;
public PictureInPictureExpansionHelper(@NonNull View selfPip) { public PictureInPictureExpansionHelper(@NonNull View selfPip) {
this.selfPip = selfPip; this.selfPip = selfPip;
this.parent = (ViewGroup) selfPip.getParent();
this.defaultDimensions = new Point(selfPip.getLayoutParams().width, selfPip.getLayoutParams().height); 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)); 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; 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)) { if (defaultDimensions.equals(dimensions)) {
return; return;
} }
@@ -53,21 +66,15 @@ final class PictureInPictureExpansionHelper {
return; return;
} }
ViewGroup.LayoutParams layoutParams = selfPip.getLayoutParams(); beginResizeSelfPipTransition(defaultDimensions, callback);
if (layoutParams.width == defaultDimensions.x && layoutParams.height == defaultDimensions.y) {
callback.onAnimationHasFinished();
return;
}
resizeSelfPip(defaultDimensions, callback);
} }
public void expand() { public void beginExpandTransition() {
if (isExpandedOrExpanding()) { if (isExpandedOrExpanding()) {
return; return;
} }
resizeSelfPip(expandedDimensions, new Callback() { beginResizeSelfPipTransition(expandedDimensions, new Callback() {
@Override @Override
public void onAnimationWillStart() { public void onAnimationWillStart() {
state = State.IS_EXPANDING; state = State.IS_EXPANDING;
@@ -80,12 +87,12 @@ final class PictureInPictureExpansionHelper {
}); });
} }
public void shrink() { public void beginShrinkTransition() {
if (isShrunkenOrShrinking()) { if (isShrunkenOrShrinking()) {
return; return;
} }
resizeSelfPip(defaultDimensions, new Callback() { beginResizeSelfPipTransition(defaultDimensions, new Callback() {
@Override @Override
public void onAnimationWillStart() { public void onAnimationWillStart() {
state = State.IS_SHRINKING; state = State.IS_SHRINKING;
@@ -98,23 +105,30 @@ final class PictureInPictureExpansionHelper {
}); });
} }
private void resizeSelfPip(@NonNull Point dimension, @NonNull Callback callback) { private void beginResizeSelfPipTransition(@NonNull Point dimension, @NonNull Callback callback) {
ResizeAnimation resizeAnimation = new ResizeAnimation(selfPip, dimension); TransitionManager.endTransitions(parent);
resizeAnimation.setDuration(PIP_RESIZE_DURATION_MS);
resizeAnimation.setAnimationListener(new SimpleAnimationListener() { Transition transition = new AutoTransition().setDuration(PIP_RESIZE_DURATION_MS);
transition.addListener(new TransitionListenerAdapter() {
@Override @Override
public void onAnimationStart(Animation animation) { public void onTransitionStart(@NonNull Transition transition) {
callback.onAnimationWillStart(); callback.onAnimationWillStart();
} }
@Override @Override
public void onAnimationEnd(Animation animation) { public void onTransitionEnd(@NonNull Transition transition) {
callback.onAnimationHasFinished(); callback.onAnimationHasFinished();
} }
}); });
selfPip.clearAnimation(); TransitionManager.beginDelayedTransition(parent, transition);
selfPip.startAnimation(resizeAnimation);
ViewGroup.LayoutParams params = selfPip.getLayoutParams();
params.width = dimension.x;
params.height = dimension.y;
selfPip.setLayoutParams(params);
} }
enum State { enum State {

View File

@@ -497,21 +497,22 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
videoToggle.setChecked(localCallParticipant.isVideoEnabled(), false); videoToggle.setChecked(localCallParticipant.isVideoEnabled(), false);
smallLocalRender.setRenderInPip(true); smallLocalRender.setRenderInPip(true);
smallLocalRender.setCallParticipant(localCallParticipant);
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
if (state == WebRtcLocalRenderState.EXPANDED) { if (state == WebRtcLocalRenderState.EXPANDED) {
pictureInPictureExpansionHelper.expand(); pictureInPictureExpansionHelper.beginExpandTransition();
smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.EXPANDED_SELF_PIP);
return; return;
} else if ((state.isAnySmall() || state == WebRtcLocalRenderState.GONE) && pictureInPictureExpansionHelper.isExpandedOrExpanding()) { } 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) { if (state != WebRtcLocalRenderState.GONE) {
return; return;
} }
} }
smallLocalRender.setCallParticipant(localCallParticipant);
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
switch (state) { switch (state) {
case GONE: case GONE:
largeLocalRender.attachBroadcastVideoSink(null); largeLocalRender.attachBroadcastVideoSink(null);
@@ -806,26 +807,34 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
private void animatePipToLargeRectangle(boolean isLandscape) { private void animatePipToLargeRectangle(boolean isLandscape) {
final Point dimens; final Point dimens;
if (isLandscape) { 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 { } 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 @Override
public void onAnimationHasFinished() { public void onAnimationHasFinished() {
pictureInPictureGestureHelper.enableCorners(); pictureInPictureGestureHelper.enableCorners();
} }
}); });
smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.NORMAL_SELF_PIP);
} }
private void animatePipToSmallRectangle() { private void animatePipToSmallRectangle() {
pictureInPictureExpansionHelper.setDefaultSize(new Point(ViewUtil.dpToPx(54), ViewUtil.dpToPx(72)), new PictureInPictureExpansionHelper.Callback() { pictureInPictureExpansionHelper.startDefaultSizeTransition(new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.MINI_PIP_WIDTH_DP),
@Override ViewUtil.dpToPx(PictureInPictureExpansionHelper.MINI_PIP_HEIGHT_DP)),
public void onAnimationHasFinished() { new PictureInPictureExpansionHelper.Callback() {
pictureInPictureGestureHelper.lockToBottomEnd(); @Override
} public void onAnimationHasFinished() {
}); pictureInPictureGestureHelper.lockToBottomEnd();
}
});
smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.MINI_SELF_PIP);
} }
private void toggleControls() { private void toggleControls() {

View File

@@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="#FF000000"
android:pathData="M4.85 7.5V7.32l3.32 3.33H8c-1.74 0-3.15-1.41-3.15-3.15Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M8 11.85c0.42 0 0.82-0.06 1.2-0.17l1.02 1.02c-0.49 0.2-1.02 0.35-1.57 0.41v0.99h2.1c0.36 0 0.65 0.3 0.65 0.65 0 0.36-0.3 0.65-0.65 0.65h-5.5c-0.36 0-0.65-0.3-0.65-0.65 0-0.36 0.3-0.65 0.65-0.65h2.1v-0.99c-2.81-0.32-5-2.7-5-5.61V6.75C2.35 6.39 2.65 6.1 3 6.1c0.36 0 0.65 0.3 0.65 0.65V7.5c0 2.4 1.95 4.35 4.35 4.35Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M11.9 9.43l0.96 0.95c0.5-0.84 0.79-1.83 0.79-2.88V6.75c0-0.36-0.3-0.65-0.65-0.65-0.36 0-0.65 0.3-0.65 0.65V7.5c0 0.7-0.16 1.35-0.45 1.93Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M5.07 2.6l5.91 5.91c0.11-0.32 0.17-0.66 0.17-1.01V3.75C11.15 2.01 9.74 0.6 8 0.6c-1.33 0-2.47 0.83-2.93 2Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M2.46 1.54c-0.25-0.25-0.67-0.25-0.92 0s-0.25 0.67 0 0.92l12 12c0.25 0.25 0.67 0.25 0.92 0s0.25-0.67 0-0.92l-12-12Z"/>
</vector>

View File

@@ -5,8 +5,8 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/mic_muted" android:id="@+id/mic_muted"
android:layout_width="wrap_content" android:layout_width="16dp"
android:layout_height="wrap_content" android:layout_height="16dp"
android:layout_gravity="center" 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" /> app:tint="@color/core_white" />

View File

@@ -86,8 +86,8 @@
<org.thoughtcrime.securesms.components.webrtc.AudioIndicatorView <org.thoughtcrime.securesms.components.webrtc.AudioIndicatorView
android:id="@+id/call_participant_audio_indicator" android:id="@+id/call_participant_audio_indicator"
android:layout_width="20dp" android:layout_width="28dp"
android:layout_height="20dp" android:layout_height="28dp"
android:layout_marginStart="@dimen/webrtc_audio_indicator_margin" android:layout_marginStart="@dimen/webrtc_audio_indicator_margin"
android:layout_marginBottom="@dimen/webrtc_audio_indicator_margin" android:layout_marginBottom="@dimen/webrtc_audio_indicator_margin"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Space xmlns:android="http://schemas.android.com/apk/res/android" <android.widget.Space xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true" tools:viewBindingIgnore="true"
android:layout_width="54dp" android:layout_width="40dp"
android:layout_height="72dp" android:layout_height="72dp"
tools:background="@color/red_500" tools:background="@color/red_500"
tools:visibility="visible" /> tools:visibility="visible" />

View File

@@ -196,7 +196,7 @@
<dimen name="signal_context_menu_corner_radius">18dp</dimen> <dimen name="signal_context_menu_corner_radius">18dp</dimen>
<dimen name="webrtc_button_size">48dp</dimen> <dimen name="webrtc_button_size">48dp</dimen>
<dimen name="webrtc_audio_indicator_margin">14dp</dimen> <dimen name="webrtc_audio_indicator_margin">8dp</dimen>
<dimen name="segmentedprogressbar_default_segment_margin">8dp</dimen> <dimen name="segmentedprogressbar_default_segment_margin">8dp</dimen>
<dimen name="segmentedprogressbar_default_corner_radius">0dp</dimen> <dimen name="segmentedprogressbar_default_corner_radius">0dp</dimen>