Add expandable video pip to 1:1 conversations.
This commit is contained in:
parent
6c94be70dc
commit
cee2702fdf
8 changed files with 334 additions and 16 deletions
|
@ -718,5 +718,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
|
||||
viewModel.setIsViewingFocusedParticipant(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocalPictureInPictureClicked() {
|
||||
viewModel.onLocalPictureInPictureClicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -181,7 +181,8 @@ public final class CallParticipantsState {
|
|||
webRtcViewModel.getGroupState().isNotIdle(),
|
||||
webRtcViewModel.getState(),
|
||||
webRtcViewModel.getRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant);
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
List<CallParticipant> participantsByLastSpoke = new ArrayList<>(webRtcViewModel.getRemoteParticipants());
|
||||
Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke())));
|
||||
|
@ -207,7 +208,8 @@ public final class CallParticipantsState {
|
|||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant);
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||
|
||||
|
@ -223,6 +225,28 @@ public final class CallParticipantsState {
|
|||
oldState.remoteDevicesCount);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState setExpanded(@NonNull CallParticipantsState oldState, boolean expanded) {
|
||||
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
oldState.isViewingFocusedParticipant,
|
||||
expanded);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
oldState.remoteParticipants,
|
||||
oldState.localParticipant,
|
||||
oldState.focusedParticipant,
|
||||
localRenderState,
|
||||
oldState.isInPipMode,
|
||||
oldState.showVideoForOutgoing,
|
||||
oldState.isViewingFocusedParticipant,
|
||||
oldState.remoteDevicesCount);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
|
||||
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
|
||||
|
||||
|
@ -232,7 +256,8 @@ public final class CallParticipantsState {
|
|||
oldState.getGroupCallState().isNotIdle(),
|
||||
oldState.callState,
|
||||
oldState.getAllRemoteParticipants().size(),
|
||||
selectedPage == SelectedPage.FOCUSED);
|
||||
selectedPage == SelectedPage.FOCUSED,
|
||||
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
|
||||
|
||||
return new CallParticipantsState(oldState.callState,
|
||||
oldState.groupCallState,
|
||||
|
@ -252,12 +277,15 @@ public final class CallParticipantsState {
|
|||
boolean isNonIdleGroupCall,
|
||||
@NonNull WebRtcViewModel.State callState,
|
||||
int numberOfRemoteParticipants,
|
||||
boolean isViewingFocusedParticipant)
|
||||
boolean isViewingFocusedParticipant,
|
||||
boolean isExpanded)
|
||||
{
|
||||
boolean displayLocal = (numberOfRemoteParticipants == 0 || !isInPip) && (isNonIdleGroupCall || localParticipant.isVideoEnabled());
|
||||
WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
|
||||
|
||||
if (displayLocal || showVideoForOutgoing) {
|
||||
if (isExpanded && (localParticipant.isVideoEnabled() || isNonIdleGroupCall)) {
|
||||
return WebRtcLocalRenderState.EXPANDED;
|
||||
} else if (displayLocal || showVideoForOutgoing) {
|
||||
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
|
||||
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
|
||||
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Helps manage the expansion and shrinking of the in-app pip.
|
||||
*/
|
||||
@MainThread
|
||||
final class PictureInPictureExpansionHelper {
|
||||
|
||||
private State state = State.IS_SHRUNKEN;
|
||||
|
||||
public boolean isExpandedOrExpanding() {
|
||||
return state == State.IS_EXPANDED || state == State.IS_EXPANDING;
|
||||
}
|
||||
|
||||
public boolean isShrunkenOrShrinking() {
|
||||
return state == State.IS_SHRUNKEN || state == State.IS_SHRINKING;
|
||||
}
|
||||
|
||||
public void expand(@NonNull View toExpand, @NonNull Callback callback) {
|
||||
if (isExpandedOrExpanding()) {
|
||||
return;
|
||||
}
|
||||
|
||||
performExpandAnimation(toExpand, new Callback() {
|
||||
@Override
|
||||
public void onAnimationWillStart() {
|
||||
state = State.IS_EXPANDING;
|
||||
callback.onAnimationWillStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureExpanded() {
|
||||
callback.onPictureInPictureExpanded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureNotVisible() {
|
||||
callback.onPictureInPictureNotVisible();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationHasFinished() {
|
||||
state = State.IS_EXPANDED;
|
||||
callback.onAnimationHasFinished();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void shrink(@NonNull View toExpand, @NonNull Callback callback) {
|
||||
if (isShrunkenOrShrinking()) {
|
||||
return;
|
||||
}
|
||||
|
||||
performShrinkAnimation(toExpand, new Callback() {
|
||||
@Override
|
||||
public void onAnimationWillStart() {
|
||||
state = State.IS_SHRINKING;
|
||||
callback.onAnimationWillStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureExpanded() {
|
||||
callback.onPictureInPictureExpanded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureNotVisible() {
|
||||
callback.onPictureInPictureNotVisible();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationHasFinished() {
|
||||
state = State.IS_SHRUNKEN;
|
||||
callback.onAnimationHasFinished();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void performExpandAnimation(@NonNull View target, @NonNull Callback callback) {
|
||||
ViewGroup parent = (ViewGroup) target.getParent();
|
||||
|
||||
float x = target.getX();
|
||||
float y = target.getY();
|
||||
float scaleX = parent.getMeasuredWidth() / (float) target.getMeasuredWidth();
|
||||
float scaleY = parent.getMeasuredHeight() / (float) target.getMeasuredHeight();
|
||||
float scale = Math.max(scaleX, scaleY);
|
||||
|
||||
callback.onAnimationWillStart();
|
||||
|
||||
target.animate()
|
||||
.setDuration(200)
|
||||
.x((parent.getMeasuredWidth() - target.getMeasuredWidth()) / 2f)
|
||||
.y((parent.getMeasuredHeight() - target.getMeasuredHeight()) / 2f)
|
||||
.scaleX(scale)
|
||||
.scaleY(scale)
|
||||
.withEndAction(() -> {
|
||||
callback.onPictureInPictureExpanded();
|
||||
target.animate()
|
||||
.setDuration(100)
|
||||
.alpha(0f)
|
||||
.withEndAction(() -> {
|
||||
callback.onPictureInPictureNotVisible();
|
||||
|
||||
target.setX(x);
|
||||
target.setY(y);
|
||||
target.setScaleX(0f);
|
||||
target.setScaleY(0f);
|
||||
target.setAlpha(1f);
|
||||
|
||||
target.animate()
|
||||
.setDuration(200)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.withEndAction(callback::onAnimationHasFinished);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void performShrinkAnimation(@NonNull View target, @NonNull Callback callback) {
|
||||
ViewGroup parent = (ViewGroup) target.getParent();
|
||||
|
||||
float x = target.getX();
|
||||
float y = target.getY();
|
||||
float scaleX = parent.getMeasuredWidth() / (float) target.getMeasuredWidth();
|
||||
float scaleY = parent.getMeasuredHeight() / (float) target.getMeasuredHeight();
|
||||
float scale = Math.max(scaleX, scaleY);
|
||||
|
||||
callback.onAnimationWillStart();
|
||||
|
||||
target.animate()
|
||||
.setDuration(200)
|
||||
.scaleX(0f)
|
||||
.scaleY(0f)
|
||||
.withEndAction(() -> {
|
||||
target.setX((parent.getMeasuredWidth() - target.getMeasuredWidth()) / 2f);
|
||||
target.setY((parent.getMeasuredHeight() - target.getMeasuredHeight()) / 2f);
|
||||
target.setAlpha(0f);
|
||||
target.setScaleX(scale);
|
||||
target.setScaleY(scale);
|
||||
|
||||
callback.onPictureInPictureNotVisible();
|
||||
|
||||
target.animate()
|
||||
.setDuration(100)
|
||||
.alpha(1f)
|
||||
.withEndAction(() -> {
|
||||
callback.onPictureInPictureExpanded();
|
||||
|
||||
target.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.x(x)
|
||||
.y(y)
|
||||
.withEndAction(callback::onAnimationHasFinished);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
enum State {
|
||||
IS_EXPANDING,
|
||||
IS_EXPANDED,
|
||||
IS_SHRINKING,
|
||||
IS_SHRUNKEN
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
/**
|
||||
* Called when an animation (shrink or expand) will begin. This happens before any animation
|
||||
* is executed.
|
||||
*/
|
||||
void onAnimationWillStart();
|
||||
|
||||
/**
|
||||
* Called when the PiP is covering the whole screen. This is when any staging / teardown of the
|
||||
* large local renderer should occur.
|
||||
*/
|
||||
void onPictureInPictureExpanded();
|
||||
|
||||
/**
|
||||
* Called when the PiP is not visible on the screen anymore. This is when any staging / teardown
|
||||
* of the pip should occur.
|
||||
*/
|
||||
void onPictureInPictureNotVisible();
|
||||
|
||||
/**
|
||||
* Called when the animation is complete. Useful for e.g. adjusting the pip's final location to
|
||||
* make sure it is respecting the screen space available.
|
||||
*/
|
||||
void onAnimationHasFinished();
|
||||
}
|
||||
}
|
|
@ -222,8 +222,9 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
|||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
child.performClick();
|
||||
isDragging = false;
|
||||
|
||||
child.performClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package org.thoughtcrime.securesms.components.webrtc;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorInflater;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
|
@ -8,7 +12,13 @@ import android.util.AttributeSet;
|
|||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationSet;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
@ -102,6 +112,8 @@ public class WebRtcCallView extends FrameLayout {
|
|||
|
||||
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
|
||||
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
|
||||
private PictureInPictureExpansionHelper pictureInPictureExpansionHelper;
|
||||
|
||||
|
||||
private final Set<View> incomingCallViews = new HashSet<>();
|
||||
private final Set<View> topViews = new HashSet<>();
|
||||
|
@ -211,7 +223,14 @@ public class WebRtcCallView extends FrameLayout {
|
|||
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
|
||||
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
|
||||
|
||||
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
|
||||
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
|
||||
pictureInPictureExpansionHelper = new PictureInPictureExpansionHelper();
|
||||
|
||||
smallLocalRenderFrame.setOnClickListener(v -> {
|
||||
if (controlsListener != null) {
|
||||
controlsListener.onLocalPictureInPictureClicked();
|
||||
}
|
||||
});
|
||||
|
||||
startCall.setOnClickListener(v -> {
|
||||
if (controlsListener != null) {
|
||||
|
@ -301,7 +320,7 @@ public class WebRtcCallView extends FrameLayout {
|
|||
|
||||
pagerAdapter.submitList(pages);
|
||||
recyclerAdapter.submitList(state.getListParticipants());
|
||||
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
|
||||
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant(), state.getFocusedParticipant());
|
||||
|
||||
if (state.isLargeVideoGroup() && !state.isInPipMode()) {
|
||||
layoutParticipantsForLargeCount();
|
||||
|
@ -310,7 +329,7 @@ public class WebRtcCallView extends FrameLayout {
|
|||
}
|
||||
}
|
||||
|
||||
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
|
||||
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant, @NonNull CallParticipant focusedParticipant) {
|
||||
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
|
||||
largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
|
||||
|
||||
|
@ -321,9 +340,18 @@ public class WebRtcCallView extends FrameLayout {
|
|||
largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
|
||||
}
|
||||
|
||||
smallLocalRender.setCallParticipant(localCallParticipant);
|
||||
smallLocalRender.setRenderInPip(true);
|
||||
videoToggle.setChecked(localCallParticipant.isVideoEnabled(), false);
|
||||
smallLocalRender.setRenderInPip(true);
|
||||
|
||||
if (state == WebRtcLocalRenderState.EXPANDED) {
|
||||
expandPip(localCallParticipant, focusedParticipant);
|
||||
return;
|
||||
} else if (state == WebRtcLocalRenderState.SMALL_RECTANGLE && pictureInPictureExpansionHelper.isExpandedOrExpanding()) {
|
||||
shrinkPip(localCallParticipant);
|
||||
return;
|
||||
} else {
|
||||
smallLocalRender.setCallParticipant(localCallParticipant);
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case GONE:
|
||||
|
@ -559,6 +587,54 @@ public class WebRtcCallView extends FrameLayout {
|
|||
}
|
||||
}
|
||||
|
||||
private void expandPip(@NonNull CallParticipant localCallParticipant, @NonNull CallParticipant focusedParticipant) {
|
||||
pictureInPictureExpansionHelper.expand(smallLocalRenderFrame, new PictureInPictureExpansionHelper.Callback() {
|
||||
@Override
|
||||
public void onAnimationWillStart() {
|
||||
largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureExpanded() {
|
||||
largeLocalRenderFrame.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureNotVisible() {
|
||||
smallLocalRender.setCallParticipant(focusedParticipant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationHasFinished() {
|
||||
pictureInPictureGestureHelper.adjustPip();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void shrinkPip(@NonNull CallParticipant localCallParticipant) {
|
||||
pictureInPictureExpansionHelper.shrink(smallLocalRenderFrame, new PictureInPictureExpansionHelper.Callback() {
|
||||
@Override
|
||||
public void onAnimationWillStart() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureExpanded() {
|
||||
largeLocalRenderFrame.setVisibility(View.GONE);
|
||||
largeLocalRender.attachBroadcastVideoSink(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureNotVisible() {
|
||||
smallLocalRender.setCallParticipant(localCallParticipant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationHasFinished() {
|
||||
pictureInPictureGestureHelper.adjustPip();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void animatePipToLargeRectangle() {
|
||||
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
|
||||
animation.setDuration(PIP_RESIZE_DURATION);
|
||||
|
@ -753,5 +829,6 @@ public class WebRtcCallView extends FrameLayout {
|
|||
void onAcceptCallPressed();
|
||||
void onShowParticipantsList();
|
||||
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
|
||||
void onLocalPictureInPictureClicked();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,6 +135,16 @@ public class WebRtcCallViewModel extends ViewModel {
|
|||
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), page));
|
||||
}
|
||||
|
||||
public void onLocalPictureInPictureClicked() {
|
||||
CallParticipantsState state = participantsState.getValue();
|
||||
if (state.getGroupCallState() != WebRtcViewModel.GroupCallState.IDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
participantsState.setValue(CallParticipantsState.setExpanded(participantsState.getValue(),
|
||||
state.getLocalRenderState() != WebRtcLocalRenderState.EXPANDED));
|
||||
}
|
||||
|
||||
public void onDismissedVideoTooltip() {
|
||||
canDisplayTooltipIfNeeded = false;
|
||||
}
|
||||
|
|
|
@ -5,5 +5,6 @@ public enum WebRtcLocalRenderState {
|
|||
SMALL_RECTANGLE,
|
||||
SMALLER_RECTANGLE,
|
||||
LARGE,
|
||||
LARGE_NO_VIDEO
|
||||
LARGE_NO_VIDEO,
|
||||
EXPANDED
|
||||
}
|
||||
|
|
|
@ -95,10 +95,8 @@
|
|||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0">
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/call_screen_pip"
|
||||
|
@ -110,7 +108,8 @@
|
|||
android:translationY="100000dp"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="8dp"
|
||||
tools:background="@color/red"
|
||||
tools:translationX="0dp"
|
||||
tools:translationY="0dp"
|
||||
tools:visibility="visible">
|
||||
|
||||
<include
|
||||
|
|
Loading…
Add table
Reference in a new issue