Add animations to call screen.

master
Alex Hart 2020-09-17 14:50:38 -03:00 committed by Greyson Parrelli
parent 0c73ddc08b
commit e05f137bd8
11 changed files with 178 additions and 39 deletions

View File

@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -17,7 +18,9 @@ import java.util.List;
*/
public final class CallParticipantsState {
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
private static final int SMALL_GROUP_MAX = 6;
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
Collections.emptyList(),
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
null,
@ -55,21 +58,29 @@ public final class CallParticipantsState {
}
public @NonNull List<CallParticipant> getGridParticipants() {
if (getAllRemoteParticipants().size() > 6) {
return getAllRemoteParticipants().subList(0, 6);
if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
} else {
return getAllRemoteParticipants();
}
}
public @NonNull List<CallParticipant> getListParticipants() {
List<CallParticipant> listParticipants = new ArrayList<>();
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
return getAllRemoteParticipants().subList(1, getAllRemoteParticipants().size());
} else if (getAllRemoteParticipants().size() > 6) {
return getAllRemoteParticipants().subList(6, getAllRemoteParticipants().size());
listParticipants.addAll(getAllRemoteParticipants().subList(1, getAllRemoteParticipants().size()));
} else if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
listParticipants.addAll(getAllRemoteParticipants().subList(SMALL_GROUP_MAX, getAllRemoteParticipants().size()));
} else {
return Collections.emptyList();
}
listParticipants.add(CallParticipant.EMPTY);
Collections.reverse(listParticipants);
return listParticipants;
}
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
@ -88,6 +99,10 @@ public final class CallParticipantsState {
return localRenderState;
}
public boolean isLargeVideoGroup() {
return getAllRemoteParticipants().size() > SMALL_GROUP_MAX;
}
public boolean isInPipMode() {
return isInPipMode;
}

View File

@ -9,6 +9,7 @@ import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
@ -16,7 +17,6 @@ import androidx.core.view.GestureDetectorCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
import java.util.Arrays;
@ -26,11 +26,14 @@ import java.util.Queue;
public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener {
private static final float DECELERATION_RATE = 0.99f;
private static final float DECELERATION_RATE = 0.99f;
private static final Interpolator FLING_INTERPOLATOR = new ViscousFluidInterpolator();
private static final Interpolator ADJUST_INTERPOLATOR = new AccelerateDecelerateInterpolator();
private final ViewGroup parent;
private final View child;
private final int framePadding;
private final ViewGroup parent;
private final View child;
private final int framePadding;
private final Queue<Runnable> runAfterFling;
private int pipWidth;
private int pipHeight;
@ -46,7 +49,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
private VelocityTracker velocityTracker;
private int maximumFlingVelocity;
private boolean isLockedToBottomEnd;
private Queue<Runnable> runAfterFling;
private Interpolator interpolator;
@SuppressLint("ClickableViewAccessibility")
public static PictureInPictureGestureHelper applyTo(@NonNull View child) {
@ -101,6 +104,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
this.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height);
this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity();
this.runAfterFling = new LinkedList<>();
this.interpolator = ADJUST_INTERPOLATOR;
}
public void clearVerticalBoundaries() {
@ -130,8 +134,12 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
pipHeight = child.getMeasuredHeight();
if (isAnimating) {
interpolator = ADJUST_INTERPOLATOR;
fling();
} else if (!isDragging) {
interpolator = ADJUST_INTERPOLATOR;
onFling(null, null, 0, 0);
}
}
@ -160,6 +168,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
isDragging = true;
pipWidth = child.getMeasuredWidth();
pipHeight = child.getMeasuredHeight();
interpolator = FLING_INTERPOLATOR;
return true;
}
@ -216,7 +225,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
.translationX(getTranslationXForPoint(nearestCornerPosition))
.translationY(getTranslationYForPoint(nearestCornerPosition))
.setDuration(250)
.setInterpolator(new ViscousFluidInterpolator())
.setInterpolator(interpolator)
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {

View File

@ -14,13 +14,20 @@ import org.thoughtcrime.securesms.events.CallParticipant;
class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant, WebRtcCallParticipantsRecyclerAdapter.ViewHolder> {
private static final int PARTICIPANT = 0;
private static final int EMPTY = 1;
protected WebRtcCallParticipantsRecyclerAdapter() {
super(new DiffCallback());
}
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_item, parent, false));
if (viewType == PARTICIPANT) {
return new ParticipantViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_item, parent, false));
} else {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.webrtc_call_participant_recycler_empty_item, parent, false));
}
}
@Override
@ -28,15 +35,29 @@ class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant,
holder.bind(getItem(position));
}
@Override
public int getItemViewType(int position) {
return getItem(position) == CallParticipant.EMPTY ? EMPTY : PARTICIPANT;
}
static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(@NonNull View itemView) {
super(itemView);
}
void bind(@NonNull CallParticipant callParticipant) {}
}
private static class ParticipantViewHolder extends ViewHolder {
private final CallParticipantView callParticipantView;
ViewHolder(@NonNull View itemView) {
ParticipantViewHolder(@NonNull View itemView) {
super(itemView);
callParticipantView = itemView.findViewById(R.id.call_participant);
}
@Override
void bind(@NonNull CallParticipant callParticipant) {
callParticipantView.setCallParticipant(callParticipant);
}

View File

@ -20,8 +20,10 @@ import androidx.constraintlayout.widget.Guideline;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.AutoTransition;
import androidx.transition.ChangeBounds;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
import androidx.transition.TransitionSet;
import androidx.viewpager2.widget.MarginPageTransformer;
import androidx.viewpager2.widget.ViewPager2;
@ -51,6 +53,7 @@ public class WebRtcCallView extends FrameLayout {
public static final int FADE_OUT_DELAY = 5000;
public static final int PIP_RESIZE_DURATION = 300;
public static final int CONTROLS_HEIGHT = 98;
private WebRtcAudioOutputToggleButton audioToggle;
private AccessibleToggleButton videoToggle;
@ -62,6 +65,7 @@ public class WebRtcCallView extends FrameLayout {
private TextView recipientName;
private TextView status;
private ConstraintLayout parent;
private ConstraintLayout participantsParent;
private ControlsListener controlsListener;
private RecipientId recipientId;
private ImageView answer;
@ -75,6 +79,8 @@ public class WebRtcCallView extends FrameLayout {
private ViewPager2 callParticipantsPager;
private RecyclerView callParticipantsRecycler;
private Toolbar toolbar;
private int pagerBottomMarginDp;
private boolean controlsVisible = true;
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
@ -114,6 +120,7 @@ public class WebRtcCallView extends FrameLayout {
recipientName = findViewById(R.id.call_screen_recipient_name);
status = findViewById(R.id.call_screen_status);
parent = findViewById(R.id.call_screen);
participantsParent = findViewById(R.id.call_screen_participants_parent);
answer = findViewById(R.id.call_screen_answer_call);
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
hangup = findViewById(R.id.call_screen_end_call);
@ -229,6 +236,12 @@ public class WebRtcCallView extends FrameLayout {
pagerAdapter.submitList(pages);
recyclerAdapter.submitList(state.getListParticipants());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
if (state.isLargeVideoGroup()) {
layoutParticipantsForLargeCount();
} else {
layoutParticipantsForSmallCount();
}
}
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
@ -371,6 +384,12 @@ public class WebRtcCallView extends FrameLayout {
updateButtonStateForLargeButtons();
}
if (webRtcControls.displayRemoteVideoRecycler()) {
callParticipantsRecycler.setVisibility(View.VISIBLE);
} else {
callParticipantsRecycler.setVisibility(View.GONE);
}
if (webRtcControls.isFadeOutEnabled()) {
if (!controls.isFadeOutEnabled()) {
scheduleFadeOut();
@ -443,9 +462,44 @@ public class WebRtcCallView extends FrameLayout {
scheduleFadeOut();
}
private void fadeControls(int visibility) {
private void layoutParticipantsForSmallCount() {
pagerBottomMarginDp = 0;
layoutParticipants();
}
private void layoutParticipantsForLargeCount() {
pagerBottomMarginDp = 104;
layoutParticipants();
}
private int withControlsHeight(int margin) {
if (margin == 0) {
return 0;
}
return controlsVisible ? margin + CONTROLS_HEIGHT : margin;
}
private void layoutParticipants() {
Transition transition = new AutoTransition().setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.beginDelayedTransition(participantsParent, transition);
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(participantsParent);
constraintSet.setMargin(R.id.call_screen_participants_pager, ConstraintSet.BOTTOM, ViewUtil.dpToPx(withControlsHeight(pagerBottomMarginDp)));
constraintSet.applyTo(participantsParent);
}
private void fadeControls(int visibility) {
controlsVisible = visibility == VISIBLE;
Transition transition = new AutoTransition().setOrdering(TransitionSet.ORDERING_TOGETHER)
.setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet();
@ -456,6 +510,8 @@ public class WebRtcCallView extends FrameLayout {
}
constraintSet.applyTo(parent);
layoutParticipants();
}
private void fadeInNewUiState(@NonNull Set<View> previouslyVisibleViewSet, boolean useSmallMargins) {

View File

@ -147,6 +147,18 @@ public class WebRtcCallViewModel extends ViewModel {
callState = WebRtcControls.CallState.INCOMING;
answerWithVideoAvailable = isRemoteVideoOffer;
break;
case CALL_OUTGOING:
case CALL_RINGING:
callState = WebRtcControls.CallState.OUTGOING;
break;
case CALL_ACCEPTED_ELSEWHERE:
case CALL_DECLINED_ELSEWHERE:
case CALL_ONGOING_ELSEWHERE:
case CALL_NEEDS_PERMISSION:
case CALL_BUSY:
case CALL_DISCONNECTED:
callState = WebRtcControls.CallState.ENDING;
break;
default:
callState = WebRtcControls.CallState.ONGOING;
}

View File

@ -41,23 +41,27 @@ public final class WebRtcControls {
}
boolean displayEndCall() {
return isOngoing();
return isAtLeastOutgoing();
}
boolean displayMuteAudio() {
return isOngoing();
return isAtLeastOutgoing();
}
boolean displayVideoToggle() {
return isOngoing();
return isAtLeastOutgoing();
}
boolean displayAudioToggle() {
return isOngoing() && (!isLocalVideoEnabled || isBluetoothAvailable);
return isAtLeastOutgoing() && (!isLocalVideoEnabled || isBluetoothAvailable);
}
boolean displayCameraToggle() {
return isOngoing() && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
return isAtLeastOutgoing() && isLocalVideoEnabled && isMoreThanOneCameraAvailable;
}
boolean displayRemoteVideoRecycler() {
return isOngoing();
}
boolean displayAnswerWithAudio() {
@ -77,15 +81,15 @@ public final class WebRtcControls {
}
boolean isFadeOutEnabled() {
return isOngoing() && isRemoteVideoEnabled;
return isAtLeastOutgoing() && isRemoteVideoEnabled;
}
boolean displaySmallOngoingCallButtons() {
return isOngoing() && displayAudioToggle() && displayCameraToggle();
return isAtLeastOutgoing() && displayAudioToggle() && displayCameraToggle();
}
boolean displayLargeOngoingCallButtons() {
return isOngoing() && !(displayAudioToggle() && displayCameraToggle());
return isAtLeastOutgoing() && !(displayAudioToggle() && displayCameraToggle());
}
boolean displayTopViews() {
@ -104,9 +108,19 @@ public final class WebRtcControls {
return callState == CallState.INCOMING;
}
private boolean isAtLeastOutgoing() {
return callState.isAtLeast(CallState.OUTGOING);
}
public enum CallState {
NONE,
INCOMING,
ONGOING
OUTGOING,
ONGOING,
ENDING;
boolean isAtLeast(@NonNull CallState other) {
return compareTo(other) >= 0;
}
}
}

View File

@ -12,6 +12,8 @@ import java.util.Objects;
public class CallParticipant {
public static final CallParticipant EMPTY = createRemote(Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false);
private final @NonNull CameraState cameraState;
private final @NonNull Recipient recipient;
private final @Nullable IdentityKey identityKey;

View File

@ -78,6 +78,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

View File

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

View File

@ -11,6 +11,7 @@
android:background="@color/transparent_black_40" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/call_screen_participants_parent"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -24,19 +25,6 @@
app:layout_constraintBottom_toBottomOf="parent"
android:orientation="vertical" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/call_screen_participants_recycler"
android:layout_width="match_parent"
android:layout_height="72dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:reverseLayout="true"
tools:listitem="@layout/webrtc_call_participant_recycler_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
@ -93,6 +81,20 @@
app:layout_constraintBottom_toBottomOf="parent"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/call_screen_participants_recycler"
android:layout_width="match_parent"
android:layout_height="72dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:orientation="horizontal"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@id/call_screen_video_toggle"
app:reverseLayout="true"
tools:listitem="@layout/webrtc_call_participant_recycler_item" />
<org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout
android:id="@+id/call_screen_pip_area"
android:layout_width="0dp"

View File

@ -148,7 +148,7 @@
<dimen name="debug_log_text_size">12sp</dimen>
<dimen name="picture_in_picture_gesture_helper_frame_padding">12dp</dimen>
<dimen name="picture_in_picture_gesture_helper_frame_padding">16dp</dimen>
<dimen name="picture_in_picture_gesture_helper_pip_width">90dp</dimen>
<dimen name="picture_in_picture_gesture_helper_pip_height">160dp</dimen>