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.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()

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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() {

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: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" />

View File

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

View File

@@ -1,8 +1,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"
tools:viewBindingIgnore="true"
android:layout_width="54dp"
android:layout_width="40dp"
android:layout_height="72dp"
tools:background="@color/red_500"
tools:visibility="visible" />

View File

@@ -196,7 +196,7 @@
<dimen name="signal_context_menu_corner_radius">18dp</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_corner_radius">0dp</dimen>