mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 02:39:55 +01:00
Add support for inline video playback of gifs in Conversation.
This commit is contained in:
committed by
Greyson Parrelli
parent
32d79ead15
commit
281630e751
@@ -1,116 +0,0 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Logic for updating content and positioning of videos as the user scrolls the list of gifs.
|
||||
*/
|
||||
final class GiphyMp4AdapterPlaybackControllerCallback implements GiphyMp4AdapterPlaybackController.Callback {
|
||||
|
||||
private final List<GiphyMp4PlayerHolder> holders;
|
||||
private final SparseArray<GiphyMp4PlayerHolder> playing;
|
||||
private final SparseArray<GiphyMp4PlayerHolder> notPlaying;
|
||||
|
||||
GiphyMp4AdapterPlaybackControllerCallback(@NonNull List<GiphyMp4PlayerHolder> holders) {
|
||||
this.holders = holders;
|
||||
this.playing = new SparseArray<>(holders.size());
|
||||
this.notPlaying = new SparseArray<>(holders.size());
|
||||
}
|
||||
|
||||
@Override public void update(@NonNull List<GiphyMp4ViewHolder> holders,
|
||||
@NonNull GiphyMp4PlaybackRange range)
|
||||
{
|
||||
stopAndReleaseAssignedVideos(range);
|
||||
|
||||
for (final GiphyMp4ViewHolder holder : holders) {
|
||||
if (range.shouldPlayVideo(holder.getAdapterPosition())) {
|
||||
startPlayback(acquireHolderForPosition(holder.getAdapterPosition()), holder);
|
||||
} else {
|
||||
holder.show();
|
||||
}
|
||||
}
|
||||
|
||||
for (final GiphyMp4ViewHolder holder : holders) {
|
||||
GiphyMp4PlayerHolder playerHolder = getCurrentHolder(holder.getAdapterPosition());
|
||||
if (playerHolder != null) {
|
||||
updateDisplay(playerHolder, holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopAndReleaseAssignedVideos(@NonNull GiphyMp4PlaybackRange playbackRange) {
|
||||
List<Integer> markedForDeletion = new ArrayList<>(playing.size());
|
||||
for (int i = 0; i < playing.size(); i++) {
|
||||
if (!playbackRange.shouldPlayVideo(playing.keyAt(i))) {
|
||||
notPlaying.put(playing.keyAt(i), playing.valueAt(i));
|
||||
playing.valueAt(i).setMediaSource(null);
|
||||
playing.valueAt(i).setOnPlaybackReady(null);
|
||||
markedForDeletion.add(playing.keyAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
for (final Integer key : markedForDeletion) {
|
||||
playing.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisplay(@NonNull GiphyMp4PlayerHolder holder, @NonNull GiphyMp4ViewHolder giphyMp4ViewHolder) {
|
||||
holder.getContainer().setX(giphyMp4ViewHolder.itemView.getX());
|
||||
holder.getContainer().setY(giphyMp4ViewHolder.itemView.getY());
|
||||
|
||||
ViewGroup.LayoutParams params = holder.getContainer().getLayoutParams();
|
||||
if (params.width != giphyMp4ViewHolder.itemView.getWidth() || params.height != giphyMp4ViewHolder.itemView.getHeight()) {
|
||||
params.width = giphyMp4ViewHolder.itemView.getWidth();
|
||||
params.height = giphyMp4ViewHolder.itemView.getHeight();
|
||||
holder.getContainer().setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
private void startPlayback(@NonNull GiphyMp4PlayerHolder holder, @NonNull GiphyMp4ViewHolder giphyMp4ViewHolder) {
|
||||
if (!Objects.equals(holder.getMediaSource(), giphyMp4ViewHolder.getMediaSource())) {
|
||||
holder.setOnPlaybackReady(null);
|
||||
giphyMp4ViewHolder.show();
|
||||
|
||||
holder.setOnPlaybackReady(giphyMp4ViewHolder::hide);
|
||||
holder.setMediaSource(giphyMp4ViewHolder.getMediaSource());
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable GiphyMp4PlayerHolder getCurrentHolder(int adapterPosition) {
|
||||
if (playing.get(adapterPosition) != null) {
|
||||
return playing.get(adapterPosition);
|
||||
} else if (notPlaying.get(adapterPosition) != null) {
|
||||
return notPlaying.get(adapterPosition);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull GiphyMp4PlayerHolder acquireHolderForPosition(int adapterPosition) {
|
||||
GiphyMp4PlayerHolder holder = playing.get(adapterPosition);
|
||||
if (holder == null) {
|
||||
if (notPlaying.size() != 0) {
|
||||
holder = notPlaying.get(adapterPosition);
|
||||
if (holder == null) {
|
||||
int key = notPlaying.keyAt(0);
|
||||
holder = Objects.requireNonNull(notPlaying.get(key));
|
||||
notPlaying.remove(key);
|
||||
} else {
|
||||
notPlaying.remove(adapterPosition);
|
||||
}
|
||||
} else {
|
||||
holder = holders.remove(0);
|
||||
}
|
||||
playing.put(adapterPosition, holder);
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Updates the position and size of a GiphyMp4VideoPlayer. For use with gestures which
|
||||
* move around the projectable areas videos should play back in.
|
||||
*/
|
||||
public interface GiphyMp4DisplayUpdater {
|
||||
void updateDisplay(@NonNull RecyclerView recyclerView, @NonNull GiphyMp4Playable holder);
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -15,13 +13,9 @@ import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -55,44 +49,22 @@ public class GiphyMp4Fragment extends Fragment {
|
||||
GiphyMp4ViewModel viewModel = ViewModelProviders.of(requireActivity(), new GiphyMp4ViewModel.Factory(isForMms)).get(GiphyMp4ViewModel.class);
|
||||
GiphyMp4MediaSourceFactory mediaSourceFactory = new GiphyMp4MediaSourceFactory(ApplicationDependencies.getOkHttpClient());
|
||||
GiphyMp4Adapter adapter = new GiphyMp4Adapter(mediaSourceFactory, viewModel::saveToBlob);
|
||||
List<GiphyMp4PlayerHolder> holders = injectVideoViews(frameLayout);
|
||||
GiphyMp4AdapterPlaybackControllerCallback callback = new GiphyMp4AdapterPlaybackControllerCallback(holders);
|
||||
List<GiphyMp4ProjectionPlayerHolder> holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(requireContext(),
|
||||
getViewLifecycleOwner().getLifecycle(),
|
||||
frameLayout,
|
||||
GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults());
|
||||
GiphyMp4ProjectionRecycler callback = new GiphyMp4ProjectionRecycler(holders);
|
||||
|
||||
recycler.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
|
||||
recycler.setAdapter(adapter);
|
||||
recycler.setItemAnimator(null);
|
||||
progressBar.show();
|
||||
|
||||
GiphyMp4AdapterPlaybackController.attach(recycler, callback, GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults());
|
||||
GiphyMp4PlaybackController.attach(recycler, callback, GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults());
|
||||
viewModel.getImages().observe(getViewLifecycleOwner(), images -> {
|
||||
nothingFound.setVisibility(images.isEmpty() ? View.VISIBLE : View.INVISIBLE);
|
||||
adapter.submitList(images, progressBar::hide);
|
||||
});
|
||||
viewModel.getPagingController().observe(getViewLifecycleOwner(), adapter::setPagingController);
|
||||
}
|
||||
|
||||
private List<GiphyMp4PlayerHolder> injectVideoViews(@NonNull ViewGroup viewGroup) {
|
||||
int nPlayers = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInSearchResults();
|
||||
List<GiphyMp4PlayerHolder> holders = new ArrayList<>(nPlayers);
|
||||
GiphyMp4ExoPlayerProvider playerProvider = new GiphyMp4ExoPlayerProvider(requireContext());
|
||||
|
||||
for (int i = 0; i < nPlayers; i++) {
|
||||
FrameLayout container = (FrameLayout) LayoutInflater.from(requireContext())
|
||||
.inflate(R.layout.giphy_mp4_player, viewGroup, false);
|
||||
GiphyMp4VideoPlayer player = container.findViewById(R.id.video_player);
|
||||
ExoPlayer exoPlayer = playerProvider.create();
|
||||
GiphyMp4PlayerHolder holder = new GiphyMp4PlayerHolder(container, player);
|
||||
|
||||
getViewLifecycleOwner().getLifecycle().addObserver(player);
|
||||
player.setExoPlayer(exoPlayer);
|
||||
player.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL);
|
||||
exoPlayer.addListener(holder);
|
||||
|
||||
holders.add(holder);
|
||||
viewGroup.addView(container);
|
||||
}
|
||||
|
||||
return holders;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
||||
public interface GiphyMp4Playable {
|
||||
/**
|
||||
* Shows the area in which a video would be projected. Called when a video will not
|
||||
* play back.
|
||||
*/
|
||||
void showProjectionArea();
|
||||
|
||||
/**
|
||||
* Hides the area in which a video would be projected. Called when a video is ready
|
||||
* to play back.
|
||||
*/
|
||||
void hideProjectionArea();
|
||||
|
||||
/**
|
||||
* @return The MediaSource to play back in the given VideoPlayer
|
||||
*/
|
||||
default @Nullable MediaSource getMediaSource() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Playback policy enforcer, or null to loop forever.
|
||||
*/
|
||||
default @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The position this item is in it's corresponding adapter
|
||||
*/
|
||||
int getAdapterPosition();
|
||||
|
||||
/**
|
||||
* Width, height, and (x,y) of view which video player will "project" into
|
||||
*/
|
||||
@NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerview);
|
||||
|
||||
/**
|
||||
* Specifies whether the content can start playing.
|
||||
*/
|
||||
boolean canPlayContent();
|
||||
}
|
||||
@@ -3,33 +3,39 @@ package org.thoughtcrime.securesms.giph.mp4;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Controls playback of gifs in a {@link GiphyMp4Adapter}. The maximum number of gifs that will play back at any one
|
||||
* Controls playback of gifs in a {@link RecyclerView}. The maximum number of gifs that will play back at any one
|
||||
* time is determined by the passed parameter, and the exact gifs that play back is algorithmically determined, starting
|
||||
* with the center-most gifs.
|
||||
* <p>
|
||||
* This algorithm is devised to play back only those gifs which the user is most likely looking at.
|
||||
*/
|
||||
final class GiphyMp4AdapterPlaybackController extends RecyclerView.OnScrollListener implements View.OnLayoutChangeListener {
|
||||
public final class GiphyMp4PlaybackController extends RecyclerView.OnScrollListener implements View.OnLayoutChangeListener {
|
||||
|
||||
private final int maxSimultaneousPlayback;
|
||||
private final Callback callback;
|
||||
|
||||
private GiphyMp4AdapterPlaybackController(@NonNull Callback callback, int maxSimultaneousPlayback) {
|
||||
private GiphyMp4PlaybackController(@NonNull Callback callback, int maxSimultaneousPlayback) {
|
||||
this.maxSimultaneousPlayback = maxSimultaneousPlayback;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public static void attach(@NonNull RecyclerView recyclerView, @NonNull Callback callback, int maxSimultaneousPlayback) {
|
||||
GiphyMp4AdapterPlaybackController controller = new GiphyMp4AdapterPlaybackController(callback, maxSimultaneousPlayback);
|
||||
GiphyMp4PlaybackController controller = new GiphyMp4PlaybackController(callback, maxSimultaneousPlayback);
|
||||
|
||||
recyclerView.addOnScrollListener(controller);
|
||||
recyclerView.addOnLayoutChangeListener(controller);
|
||||
@@ -57,24 +63,30 @@ final class GiphyMp4AdapterPlaybackController extends RecyclerView.OnScrollListe
|
||||
return;
|
||||
}
|
||||
|
||||
int[] firstVisiblePositions = findFirstVisibleItemPositions(layoutManager);
|
||||
int[] lastVisiblePositions = findLastVisibleItemPositions(layoutManager);
|
||||
List<GiphyMp4Playable> playables = new LinkedList<>();
|
||||
Set<Integer> playablePositions = new HashSet<>();
|
||||
|
||||
GiphyMp4PlaybackRange playbackRange = getPlaybackRangeForMaximumDistance(firstVisiblePositions, lastVisiblePositions);
|
||||
for (int i = 0; i < recyclerView.getChildCount(); i++) {
|
||||
RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(recyclerView.getChildAt(i));
|
||||
|
||||
if (playbackRange != null) {
|
||||
List<GiphyMp4ViewHolder> holders = new LinkedList<>();
|
||||
if (holder instanceof GiphyMp4Playable) {
|
||||
GiphyMp4Playable playable = (GiphyMp4Playable) holder;
|
||||
playables.add(playable);
|
||||
|
||||
for (int i = 0; i < recyclerView.getChildCount(); i++) {
|
||||
GiphyMp4ViewHolder viewHolder = (GiphyMp4ViewHolder) recyclerView.getChildViewHolder(recyclerView.getChildAt(i));
|
||||
holders.add(viewHolder);
|
||||
if (playable.canPlayContent()) {
|
||||
playablePositions.add(playable.getAdapterPosition());
|
||||
}
|
||||
}
|
||||
|
||||
callback.update(holders, playbackRange);
|
||||
}
|
||||
|
||||
int[] firstVisiblePositions = findFirstVisibleItemPositions(layoutManager);
|
||||
int[] lastVisiblePositions = findLastVisibleItemPositions(layoutManager);
|
||||
Set<Integer> playbackSet = getPlaybackSetForMaximumDistance(playablePositions, firstVisiblePositions, lastVisiblePositions);
|
||||
|
||||
callback.update(recyclerView, playables, playbackSet);
|
||||
}
|
||||
|
||||
private @Nullable GiphyMp4PlaybackRange getPlaybackRangeForMaximumDistance(int[] firstVisiblePositions, int[] lastVisiblePositions) {
|
||||
private @NonNull Set<Integer> getPlaybackSetForMaximumDistance(@NonNull Set<Integer> playablePositions, int[] firstVisiblePositions, int[] lastVisiblePositions) {
|
||||
int firstVisiblePosition = Integer.MAX_VALUE;
|
||||
int lastVisiblePosition = Integer.MIN_VALUE;
|
||||
|
||||
@@ -83,28 +95,15 @@ final class GiphyMp4AdapterPlaybackController extends RecyclerView.OnScrollListe
|
||||
lastVisiblePosition = Math.max(lastVisiblePosition, lastVisiblePositions[i]);
|
||||
}
|
||||
|
||||
return getPlaybackRange(firstVisiblePosition, lastVisiblePosition);
|
||||
return getPlaybackSet(playablePositions, firstVisiblePosition, lastVisiblePosition);
|
||||
}
|
||||
|
||||
private @Nullable GiphyMp4PlaybackRange getPlaybackRange(int firstVisiblePosition, int lastVisiblePosition) {
|
||||
int distance = lastVisiblePosition - firstVisiblePosition;
|
||||
|
||||
if (maxSimultaneousPlayback == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (distance <= maxSimultaneousPlayback) {
|
||||
return new GiphyMp4PlaybackRange(firstVisiblePosition, lastVisiblePosition);
|
||||
} else {
|
||||
int center = (distance / 2) + firstVisiblePosition;
|
||||
if (maxSimultaneousPlayback == 1) {
|
||||
return new GiphyMp4PlaybackRange(center, center);
|
||||
} else {
|
||||
int first = Math.max(center - maxSimultaneousPlayback / 2, firstVisiblePosition);
|
||||
int last = Math.min(first + maxSimultaneousPlayback, lastVisiblePosition);
|
||||
return new GiphyMp4PlaybackRange(first, last);
|
||||
}
|
||||
}
|
||||
private @NonNull Set<Integer> getPlaybackSet(@NonNull Set<Integer> playablePositions, int firstVisiblePosition, int lastVisiblePosition) {
|
||||
return Stream.rangeClosed(firstVisiblePosition, lastVisiblePosition)
|
||||
.sorted(new RangeComparator(firstVisiblePosition, lastVisiblePosition))
|
||||
.filter(playablePositions::contains)
|
||||
.limit(maxSimultaneousPlayback)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static int[] findFirstVisibleItemPositions(@NonNull RecyclerView.LayoutManager layoutManager) {
|
||||
@@ -128,6 +127,31 @@ final class GiphyMp4AdapterPlaybackController extends RecyclerView.OnScrollListe
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
void update(@NonNull List<GiphyMp4ViewHolder> holders, @NonNull GiphyMp4PlaybackRange range);
|
||||
void update(@NonNull RecyclerView recyclerView, @NonNull List<GiphyMp4Playable> holders, @NonNull Set<Integer> playbackSet);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static final class RangeComparator implements Comparator<Integer> {
|
||||
|
||||
private final int center;
|
||||
|
||||
RangeComparator(int firstVisiblePosition, int lastVisiblePosition) {
|
||||
int delta = lastVisiblePosition - firstVisiblePosition;
|
||||
|
||||
center = firstVisiblePosition + (delta / 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(Integer o1, Integer o2) {
|
||||
int distance1 = Math.abs(o1 - center);
|
||||
int distance2 = Math.abs(o2 - center);
|
||||
int comp = Integer.compare(distance1, distance2);
|
||||
|
||||
if (comp == 0) {
|
||||
return Integer.compare(o1, o2);
|
||||
}
|
||||
|
||||
return comp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -17,28 +15,44 @@ import java.util.concurrent.TimeUnit;
|
||||
*/
|
||||
public final class GiphyMp4PlaybackPolicy {
|
||||
|
||||
private static final int MAXIMUM_SUPPORTED_PLAYBACK_PRE_23 = 6;
|
||||
private static final int MAXIMUM_SUPPORTED_PLAYBACK_PRE_23_LOW_MEM = 3;
|
||||
private static final float SEARCH_RESULT_RATIO = 0.75f;
|
||||
|
||||
private GiphyMp4PlaybackPolicy() { }
|
||||
|
||||
public static boolean sendAsMp4() {
|
||||
return FeatureFlags.mp4GifSendSupport();
|
||||
}
|
||||
|
||||
public static boolean autoplay() {
|
||||
return !DeviceProperties.isLowMemoryDevice(ApplicationDependencies.getApplication());
|
||||
}
|
||||
|
||||
public static int maxRepeatsOfSinglePlayback() {
|
||||
return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
public static long maxDurationOfSinglePlayback() {
|
||||
return TimeUnit.SECONDS.toMillis(6);
|
||||
return TimeUnit.SECONDS.toMillis(8);
|
||||
}
|
||||
|
||||
public static int maxSimultaneousPlaybackInConversation() {
|
||||
return maxSimultaneousPlaybackWithRatio(1f - SEARCH_RESULT_RATIO);
|
||||
}
|
||||
|
||||
public static int maxSimultaneousPlaybackInSearchResults() {
|
||||
return maxSimultaneousPlaybackWithRatio(SEARCH_RESULT_RATIO);
|
||||
}
|
||||
|
||||
private static int maxSimultaneousPlaybackWithRatio(float ratio) {
|
||||
int maxInstances = 0;
|
||||
|
||||
try {
|
||||
MediaCodecInfo info = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false);
|
||||
|
||||
if (info != null) {
|
||||
maxInstances = (int) (info.getMaxSupportedInstances() * 0.75f);
|
||||
if (info != null && info.getMaxSupportedInstances() > 0) {
|
||||
maxInstances = (int) (info.getMaxSupportedInstances() * ratio);
|
||||
}
|
||||
|
||||
} catch (MediaCodecUtil.DecoderQueryException ignored) {
|
||||
@@ -49,9 +63,9 @@ public final class GiphyMp4PlaybackPolicy {
|
||||
}
|
||||
|
||||
if (DeviceProperties.isLowMemoryDevice(ApplicationDependencies.getApplication())) {
|
||||
return 2;
|
||||
return (int) (MAXIMUM_SUPPORTED_PLAYBACK_PRE_23_LOW_MEM * ratio);
|
||||
} else {
|
||||
return 6;
|
||||
return (int) (MAXIMUM_SUPPORTED_PLAYBACK_PRE_23 * ratio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
/**
|
||||
* Enforces a video player to play back a specified number of loops given
|
||||
* video length and device policy.
|
||||
*/
|
||||
public final class GiphyMp4PlaybackPolicyEnforcer {
|
||||
|
||||
private final Callback callback;
|
||||
private final long maxDurationOfSinglePlayback;
|
||||
private final long maxRepeatsOfSinglePlayback;
|
||||
|
||||
private long loopsRemaining = -1;
|
||||
|
||||
public GiphyMp4PlaybackPolicyEnforcer(@NonNull Callback callback) {
|
||||
this(callback,
|
||||
GiphyMp4PlaybackPolicy.maxDurationOfSinglePlayback(),
|
||||
GiphyMp4PlaybackPolicy.maxRepeatsOfSinglePlayback());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
GiphyMp4PlaybackPolicyEnforcer(@NonNull Callback callback,
|
||||
long maxDurationOfSinglePlayback,
|
||||
long maxRepeatsOfSinglePlayback)
|
||||
{
|
||||
this.callback = callback;
|
||||
this.maxDurationOfSinglePlayback = maxDurationOfSinglePlayback;
|
||||
this.maxRepeatsOfSinglePlayback = maxRepeatsOfSinglePlayback;
|
||||
}
|
||||
|
||||
void setMediaDuration(long duration) {
|
||||
long maxLoopsByDuration = Math.max(1, maxDurationOfSinglePlayback / duration);
|
||||
|
||||
loopsRemaining = Math.min(maxLoopsByDuration, maxRepeatsOfSinglePlayback);
|
||||
}
|
||||
|
||||
public boolean endPlayback() {
|
||||
if (loopsRemaining < 0) throw new IllegalStateException("Must call setMediaDuration before calling this method.");
|
||||
else if (loopsRemaining == 0) return true;
|
||||
else {
|
||||
loopsRemaining--;
|
||||
if (loopsRemaining == 0) {
|
||||
callback.onPlaybackWillEnd();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface Callback {
|
||||
void onPlaybackWillEnd();
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Object describing the range of adapter positions for which playback should begin.
|
||||
*/
|
||||
final class GiphyMp4PlaybackRange {
|
||||
private final int startPosition;
|
||||
private final int endPosition;
|
||||
|
||||
GiphyMp4PlaybackRange(int startPosition, int endPosition) {
|
||||
this.startPosition = startPosition;
|
||||
this.endPosition = endPosition;
|
||||
}
|
||||
|
||||
boolean shouldPlayVideo(int adapterPosition) {
|
||||
if (adapterPosition == RecyclerView.NO_POSITION) return false;
|
||||
|
||||
return this.startPosition <= adapterPosition && this.endPosition > adapterPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return "PlaybackRange{" +
|
||||
"startPosition=" + startPosition +
|
||||
", endPosition=" + endPosition +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final GiphyMp4PlaybackRange that = (GiphyMp4PlaybackRange) o;
|
||||
return startPosition == that.startPosition &&
|
||||
endPosition == that.endPosition;
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return Objects.hash(startPosition, endPosition);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
||||
/**
|
||||
* Object which holds on to an injected video player.
|
||||
*/
|
||||
final class GiphyMp4PlayerHolder implements Player.EventListener {
|
||||
private final FrameLayout container;
|
||||
private final GiphyMp4VideoPlayer player;
|
||||
|
||||
private Runnable onPlaybackReady;
|
||||
private MediaSource mediaSource;
|
||||
|
||||
GiphyMp4PlayerHolder(@NonNull FrameLayout container, @NonNull GiphyMp4VideoPlayer player) {
|
||||
this.container = container;
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@NonNull FrameLayout getContainer() {
|
||||
return container;
|
||||
}
|
||||
|
||||
public void setMediaSource(@Nullable MediaSource mediaSource) {
|
||||
this.mediaSource = mediaSource;
|
||||
|
||||
if (mediaSource != null) {
|
||||
player.setVideoSource(mediaSource);
|
||||
player.play();
|
||||
} else {
|
||||
player.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable MediaSource getMediaSource() {
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
void setOnPlaybackReady(@Nullable Runnable onPlaybackReady) {
|
||||
this.onPlaybackReady = onPlaybackReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
if (onPlaybackReady != null) {
|
||||
onPlaybackReady.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.components.CornerMask;
|
||||
|
||||
/**
|
||||
* Describes the position and size of the area where a video should play.
|
||||
*/
|
||||
public final class GiphyMp4Projection {
|
||||
|
||||
private final float x;
|
||||
private final float y;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final CornerMask cornerMask;
|
||||
|
||||
public GiphyMp4Projection(float x, float y, int width, int height, @Nullable CornerMask cornerMask) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.cornerMask = cornerMask;
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public @Nullable CornerMask getCornerMask() {
|
||||
return cornerMask;
|
||||
}
|
||||
|
||||
public @NonNull GiphyMp4Projection translateX(float xTranslation) {
|
||||
return new GiphyMp4Projection(x + xTranslation, y, width, height, cornerMask);
|
||||
}
|
||||
|
||||
public static @NonNull GiphyMp4Projection forView(@NonNull RecyclerView recyclerView, @NonNull View view, @Nullable CornerMask cornerMask) {
|
||||
Rect viewBounds = new Rect();
|
||||
|
||||
view.getDrawingRect(viewBounds);
|
||||
recyclerView.offsetDescendantRectToMyCoords(view, viewBounds);
|
||||
return new GiphyMp4Projection(viewBounds.left, viewBounds.top, view.getWidth(), view.getHeight(), cornerMask);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
|
||||
import org.signal.glide.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.CornerMask;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Object which holds on to an injected video player.
|
||||
*/
|
||||
public final class GiphyMp4ProjectionPlayerHolder implements Player.EventListener {
|
||||
private final FrameLayout container;
|
||||
private final GiphyMp4VideoPlayer player;
|
||||
|
||||
private Runnable onPlaybackReady;
|
||||
private MediaSource mediaSource;
|
||||
private GiphyMp4PlaybackPolicyEnforcer policyEnforcer;
|
||||
|
||||
private GiphyMp4ProjectionPlayerHolder(@NonNull FrameLayout container, @NonNull GiphyMp4VideoPlayer player) {
|
||||
this.container = container;
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@NonNull FrameLayout getContainer() {
|
||||
return container;
|
||||
}
|
||||
|
||||
public void playContent(@NonNull MediaSource mediaSource, @Nullable GiphyMp4PlaybackPolicyEnforcer policyEnforcer) {
|
||||
this.mediaSource = mediaSource;
|
||||
this.policyEnforcer = policyEnforcer;
|
||||
|
||||
player.setVideoSource(mediaSource);
|
||||
player.play();
|
||||
}
|
||||
|
||||
public void clearMedia() {
|
||||
this.mediaSource = null;
|
||||
this.policyEnforcer = null;
|
||||
player.stop();
|
||||
}
|
||||
|
||||
public @Nullable MediaSource getMediaSource() {
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
public void setOnPlaybackReady(@Nullable Runnable onPlaybackReady) {
|
||||
this.onPlaybackReady = onPlaybackReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
if (onPlaybackReady != null) {
|
||||
if (policyEnforcer != null) {
|
||||
policyEnforcer.setMediaDuration(player.getDuration());
|
||||
}
|
||||
onPlaybackReady.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(int reason) {
|
||||
if (policyEnforcer != null && reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) {
|
||||
if (policyEnforcer.endPlayback()) {
|
||||
player.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull List<GiphyMp4ProjectionPlayerHolder> injectVideoViews(@NonNull Context context,
|
||||
@NonNull Lifecycle lifecycle,
|
||||
@NonNull ViewGroup viewGroup,
|
||||
int nPlayers)
|
||||
{
|
||||
List<GiphyMp4ProjectionPlayerHolder> holders = new ArrayList<>(nPlayers);
|
||||
GiphyMp4ExoPlayerProvider playerProvider = new GiphyMp4ExoPlayerProvider(context);
|
||||
|
||||
for (int i = 0; i < nPlayers; i++) {
|
||||
FrameLayout container = (FrameLayout) LayoutInflater.from(context)
|
||||
.inflate(R.layout.giphy_mp4_player, viewGroup, false);
|
||||
GiphyMp4VideoPlayer player = container.findViewById(R.id.video_player);
|
||||
ExoPlayer exoPlayer = playerProvider.create();
|
||||
GiphyMp4ProjectionPlayerHolder holder = new GiphyMp4ProjectionPlayerHolder(container, player);
|
||||
|
||||
lifecycle.addObserver(player);
|
||||
player.setExoPlayer(exoPlayer);
|
||||
player.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL);
|
||||
exoPlayer.addListener(holder);
|
||||
|
||||
holders.add(holder);
|
||||
viewGroup.addView(container);
|
||||
}
|
||||
|
||||
return holders;
|
||||
}
|
||||
|
||||
public void setCornerMask(@Nullable CornerMask cornerMask) {
|
||||
player.setCornerMask(cornerMask);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Logic for updating content and positioning of videos as the user scrolls the list of gifs.
|
||||
*/
|
||||
public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackController.Callback, GiphyMp4DisplayUpdater {
|
||||
|
||||
private final List<GiphyMp4ProjectionPlayerHolder> holders;
|
||||
private final SparseArray<GiphyMp4ProjectionPlayerHolder> playing;
|
||||
private final SparseArray<GiphyMp4ProjectionPlayerHolder> notPlaying;
|
||||
|
||||
public GiphyMp4ProjectionRecycler(@NonNull List<GiphyMp4ProjectionPlayerHolder> holders) {
|
||||
this.holders = holders;
|
||||
this.playing = new SparseArray<>(holders.size());
|
||||
this.notPlaying = new SparseArray<>(holders.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(@NonNull RecyclerView recyclerView,
|
||||
@NonNull List<GiphyMp4Playable> holders,
|
||||
@NonNull Set<Integer> playbackSet)
|
||||
{
|
||||
stopAndReleaseAssignedVideos(playbackSet);
|
||||
|
||||
for (final GiphyMp4Playable holder : holders) {
|
||||
if (playbackSet.contains(holder.getAdapterPosition())) {
|
||||
startPlayback(acquireHolderForPosition(holder.getAdapterPosition()), holder);
|
||||
} else {
|
||||
holder.showProjectionArea();
|
||||
}
|
||||
}
|
||||
|
||||
for (final GiphyMp4Playable holder : holders) {
|
||||
updateDisplay(recyclerView, holder);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDisplay(@NonNull RecyclerView recyclerView, @NonNull GiphyMp4Playable holder) {
|
||||
GiphyMp4ProjectionPlayerHolder playerHolder = getCurrentHolder(holder.getAdapterPosition());
|
||||
if (playerHolder != null) {
|
||||
updateDisplay(recyclerView, playerHolder, holder);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable View getVideoPlayerAtAdapterPosition(int adapterPosition) {
|
||||
GiphyMp4ProjectionPlayerHolder holder = getCurrentHolder(adapterPosition);
|
||||
|
||||
if (holder != null) return holder.getContainer();
|
||||
else return null;
|
||||
}
|
||||
|
||||
private void stopAndReleaseAssignedVideos(@NonNull Set<Integer> playbackSet) {
|
||||
List<Integer> markedForDeletion = new ArrayList<>(playing.size());
|
||||
for (int i = 0; i < playing.size(); i++) {
|
||||
if (!playbackSet.contains(playing.keyAt(i))) {
|
||||
notPlaying.put(playing.keyAt(i), playing.valueAt(i));
|
||||
playing.valueAt(i).clearMedia();
|
||||
playing.valueAt(i).setOnPlaybackReady(null);
|
||||
markedForDeletion.add(playing.keyAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
for (final Integer key : markedForDeletion) {
|
||||
playing.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisplay(@NonNull RecyclerView recyclerView, @NonNull GiphyMp4ProjectionPlayerHolder holder, @NonNull GiphyMp4Playable giphyMp4Playable) {
|
||||
GiphyMp4Projection projection = giphyMp4Playable.getProjection(recyclerView);
|
||||
|
||||
holder.getContainer().setX(projection.getX());
|
||||
holder.getContainer().setY(projection.getY());
|
||||
|
||||
ViewGroup.LayoutParams params = holder.getContainer().getLayoutParams();
|
||||
if (params.width != projection.getWidth() || params.height != projection.getHeight()) {
|
||||
params.width = projection.getWidth();
|
||||
params.height = projection.getHeight();
|
||||
holder.getContainer().setLayoutParams(params);
|
||||
}
|
||||
|
||||
holder.setCornerMask(projection.getCornerMask());
|
||||
}
|
||||
|
||||
private void startPlayback(@NonNull GiphyMp4ProjectionPlayerHolder holder, @NonNull GiphyMp4Playable giphyMp4Playable) {
|
||||
if (!Objects.equals(holder.getMediaSource(), giphyMp4Playable.getMediaSource())) {
|
||||
holder.setOnPlaybackReady(null);
|
||||
giphyMp4Playable.showProjectionArea();
|
||||
|
||||
holder.setOnPlaybackReady(giphyMp4Playable::hideProjectionArea);
|
||||
holder.playContent(giphyMp4Playable.getMediaSource(), giphyMp4Playable.getPlaybackPolicyEnforcer());
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable GiphyMp4ProjectionPlayerHolder getCurrentHolder(int adapterPosition) {
|
||||
if (playing.get(adapterPosition) != null) {
|
||||
return playing.get(adapterPosition);
|
||||
} else if (notPlaying.get(adapterPosition) != null) {
|
||||
return notPlaying.get(adapterPosition);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull GiphyMp4ProjectionPlayerHolder acquireHolderForPosition(int adapterPosition) {
|
||||
GiphyMp4ProjectionPlayerHolder holder = playing.get(adapterPosition);
|
||||
if (holder == null) {
|
||||
if (notPlaying.size() != 0) {
|
||||
holder = notPlaying.get(adapterPosition);
|
||||
if (holder == null) {
|
||||
int key = notPlaying.keyAt(0);
|
||||
holder = Objects.requireNonNull(notPlaying.get(key));
|
||||
notPlaying.remove(key);
|
||||
} else {
|
||||
notPlaying.remove(adapterPosition);
|
||||
}
|
||||
} else {
|
||||
holder = holders.remove(0);
|
||||
}
|
||||
playing.put(adapterPosition, holder);
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
@@ -16,6 +19,7 @@ import com.google.android.exoplayer2.ui.PlayerView;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.CornerMask;
|
||||
|
||||
/**
|
||||
* Video Player class specifically created for the GiphyMp4Fragment.
|
||||
@@ -27,6 +31,7 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||
|
||||
private final PlayerView exoView;
|
||||
private ExoPlayer exoPlayer;
|
||||
private CornerMask cornerMask;
|
||||
|
||||
public GiphyMp4VideoPlayer(Context context) {
|
||||
this(context, null);
|
||||
@@ -49,7 +54,15 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||
Log.d(TAG, "onDetachedFromWindow");
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
|
||||
@Override protected void dispatchDraw(Canvas canvas) {
|
||||
super.dispatchDraw(canvas);
|
||||
|
||||
if (cornerMask != null) {
|
||||
cornerMask.mask(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
void setExoPlayer(@NonNull ExoPlayer exoPlayer) {
|
||||
exoView.setPlayer(exoPlayer);
|
||||
this.exoPlayer = exoPlayer;
|
||||
@@ -58,6 +71,11 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||
void setVideoSource(@NonNull MediaSource mediaSource) {
|
||||
exoPlayer.prepare(mediaSource);
|
||||
}
|
||||
|
||||
void setCornerMask(@Nullable CornerMask cornerMask) {
|
||||
this.cornerMask = new CornerMask(this, cornerMask);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
void play() {
|
||||
if (exoPlayer != null) {
|
||||
@@ -71,6 +89,14 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||
}
|
||||
}
|
||||
|
||||
long getDuration() {
|
||||
if (exoPlayer != null) {
|
||||
return exoPlayer.getDuration();
|
||||
} else {
|
||||
return C.LENGTH_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
|
||||
exoView.setResizeMode(resizeMode);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
/**
|
||||
* Holds a view which will either play back an MP4 gif or show its still.
|
||||
*/
|
||||
final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder {
|
||||
final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable {
|
||||
|
||||
private final AspectRatioFrameLayout container;
|
||||
private final ImageView stillImage;
|
||||
@@ -62,14 +62,31 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder {
|
||||
itemView.setOnClickListener(v -> listener.onClick(giphyImage));
|
||||
}
|
||||
|
||||
void show() {
|
||||
@Override
|
||||
public void showProjectionArea() {
|
||||
container.setAlpha(1f);
|
||||
}
|
||||
|
||||
void hide() {
|
||||
@Override
|
||||
public void hideProjectionArea() {
|
||||
container.setAlpha(0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MediaSource getMediaSource() {
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
|
||||
return GiphyMp4Projection.forView(recyclerView, itemView, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canPlayContent() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void loadPlaceholderImage(@NonNull GiphyImage giphyImage) {
|
||||
GlideApp.with(itemView)
|
||||
.load(new ChunkedImageUrl(giphyImage.getStillUrl()))
|
||||
@@ -78,8 +95,4 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder {
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(stillImage);
|
||||
}
|
||||
|
||||
@NonNull MediaSource getMediaSource() {
|
||||
return mediaSource;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user