diff --git a/app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java index aef705c1..a23b5d15 100644 --- a/app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/SliderItemsAdapter.java @@ -22,7 +22,7 @@ public final class SliderItemsAdapter extends ListAdapter DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java index 325aa720..613c2250 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/SliderVideoViewHolder.java @@ -135,6 +135,16 @@ public class SliderVideoViewHolder extends SliderItemViewHolder { videoPlayerViewHelper.releasePlayer(); } + public void resetPlayerTimeline() { + if (videoPlayerViewHelper == null) return; + videoPlayerViewHelper.resetTimeline(); + } + + public void removeCallbacks() { + if (videoPlayerViewHelper == null) return; + videoPlayerViewHelper.removeCallbacks(); + } + // private void setDimensions(final FeedModel feedModel, final int spanCount, final boolean animate) { // final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams(); // final int deviceWidth = Utils.displayMetrics.widthPixels; diff --git a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java index 04e07854..987f9dd5 100644 --- a/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java +++ b/app/src/main/java/awais/instagrabber/customviews/VideoPlayerViewHelper.java @@ -10,6 +10,7 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.appcompat.view.ContextThemeWrapper; +import androidx.appcompat.widget.AppCompatTextView; import androidx.appcompat.widget.PopupMenu; import com.facebook.drawee.backends.pipeline.Fresco; @@ -26,6 +27,8 @@ import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.material.slider.LabelFormatter; +import com.google.android.material.slider.Slider; import awais.instagrabber.R; import awais.instagrabber.databinding.LayoutExoCustomControlsBinding; @@ -39,6 +42,8 @@ import static com.google.android.exoplayer2.Player.STATE_READY; public class VideoPlayerViewHelper implements Player.EventListener { private static final String TAG = "VideoPlayerViewHelper"; + private static final long INITIAL_DELAY = 0; + private static final long RECURRING_DELAY = 60; private final Context context; private final awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding binding; @@ -52,6 +57,62 @@ public class VideoPlayerViewHelper implements Player.EventListener { private final DefaultDataSourceFactory dataSourceFactory; private SimpleExoPlayer player; private PopupMenu speedPopup; + private Runnable positionChecker; + + private final Handler positionUpdateHandler = new Handler(); + private final Player.EventListener listener = new Player.EventListener() { + @Override + public void onPlaybackStateChanged(final int state) { + switch (state) { + case Player.STATE_BUFFERING: + case STATE_IDLE: + case STATE_ENDED: + positionUpdateHandler.removeCallbacks(positionChecker); + return; + case STATE_READY: + setupTimeline(); + positionUpdateHandler.postDelayed(positionChecker, INITIAL_DELAY); + break; + } + } + + @Override + public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason) { + updatePlayPauseDrawable(playWhenReady); + } + }; + private final AudioListener audioListener = new AudioListener() { + @Override + public void onVolumeChanged(final float volume) { + updateMuteIcon(volume); + } + }; + private final Slider.OnChangeListener onChangeListener = (slider, value, fromUser) -> { + if (!fromUser) return; + long actualValue = (long) value; + if (actualValue < 0) { + actualValue = 0; + } else if (actualValue > player.getDuration()) { + actualValue = player.getDuration(); + } + player.seekTo(actualValue); + }; + private final View.OnClickListener onClickListener = v -> player.setPlayWhenReady(!player.getPlayWhenReady()); + private final LabelFormatter labelFormatter = value -> TextUtils.millisToTimeString((long) value); + private final View.OnClickListener muteOnClickListener = v -> toggleMute(); + private final View.OnClickListener rewOnClickListener = v -> { + final long positionMs = player.getCurrentPosition() - 5000; + player.seekTo(positionMs < 0 ? 0 : positionMs); + }; + private final View.OnClickListener ffOnClickListener = v -> { + long positionMs = player.getCurrentPosition() + 5000; + long duration = player.getDuration(); + if (duration == TIME_UNSET) { + duration = 0; + } + player.seekTo(Math.min(positionMs, duration)); + }; + private final View.OnClickListener showMenu = this::showMenu; public VideoPlayerViewHelper(@NonNull final Context context, @NonNull final LayoutVideoPlayerWithThumbnailBinding binding, @@ -130,6 +191,10 @@ public class VideoPlayerViewHelper implements Player.EventListener { player = new SimpleExoPlayer.Builder(context) .setLooper(Looper.getMainLooper()) .build(); + positionChecker = new PositionCheckRunnable(positionUpdateHandler, + player, + controlsBinding.timeline, + controlsBinding.fromTime); player.addListener(this); player.setVolume(initialVolume); player.setPlayWhenReady(true); @@ -148,87 +213,22 @@ public class VideoPlayerViewHelper implements Player.EventListener { binding.playerView.setUseController(false); if (player == null) { enableControls(false); - controlsBinding.playPause.setEnabled(true); - controlsBinding.playPause.setOnClickListener(v -> binding.thumbnailParent.performClick()); + // controlsBinding.playPause.setEnabled(true); + // controlsBinding.playPause.setOnClickListener(new NoPlayerPlayPauseClickListener(binding.thumbnailParent)); return; } enableControls(true); - final Handler handler = new Handler(); - final long initialDelay = 0; - final long recurringDelay = 60; - final Runnable positionChecker = new Runnable() { - @Override - public void run() { - handler.removeCallbacks(this); - if (player == null) return; - final long currentPosition = player.getCurrentPosition(); - final long duration = player.getDuration(); - if (duration == TIME_UNSET) { - controlsBinding.timeline.setValueFrom(0); - controlsBinding.timeline.setValueTo(0); - controlsBinding.timeline.setEnabled(false); - return; - } - controlsBinding.timeline.setValue(Math.min(currentPosition, duration)); - controlsBinding.fromTime.setText(TextUtils.millisToTimeString(currentPosition)); - handler.postDelayed(this, recurringDelay); - } - }; updatePlayPauseDrawable(player.getPlayWhenReady()); updateMuteIcon(player.getVolume()); - player.addListener(new Player.EventListener() { - @Override - public void onPlaybackStateChanged(final int state) { - switch (state) { - case Player.STATE_BUFFERING: - case STATE_IDLE: - case STATE_ENDED: - handler.removeCallbacks(positionChecker); - return; - case STATE_READY: - setupTimeline(); - handler.postDelayed(positionChecker, initialDelay); - break; - } - } - - @Override - public void onPlayWhenReadyChanged(final boolean playWhenReady, final int reason) { - updatePlayPauseDrawable(playWhenReady); - } - }); - player.addAudioListener(new AudioListener() { - @Override - public void onVolumeChanged(final float volume) { - updateMuteIcon(volume); - } - }); - controlsBinding.timeline.addOnChangeListener((slider, value, fromUser) -> { - if (!fromUser) return; - long actualValue = (long) value; - if (actualValue < 0) { - actualValue = 0; - } else if (actualValue > player.getDuration()) { - actualValue = player.getDuration(); - } - player.seekTo(actualValue); - }); - controlsBinding.timeline.setLabelFormatter(value -> TextUtils.millisToTimeString((long) value)); - controlsBinding.playPause.setOnClickListener(v -> player.setPlayWhenReady(!player.getPlayWhenReady())); - controlsBinding.mute.setOnClickListener(v -> toggleMute()); - controlsBinding.rewWithAmount.setOnClickListener(v -> { - final long positionMs = player.getCurrentPosition() - 5000; - player.seekTo(positionMs < 0 ? 0 : positionMs); - }); - controlsBinding.ffWithAmount.setOnClickListener(v -> { - long positionMs = player.getCurrentPosition() + 5000; - long duration = player.getDuration(); - if (duration == TIME_UNSET) { - duration = 0; - } - player.seekTo(Math.min(positionMs, duration)); - }); - controlsBinding.speed.setOnClickListener(this::showMenu); + player.addListener(listener); + player.addAudioListener(audioListener); + controlsBinding.timeline.addOnChangeListener(onChangeListener); + controlsBinding.timeline.setLabelFormatter(labelFormatter); + controlsBinding.playPause.setOnClickListener(onClickListener); + controlsBinding.mute.setOnClickListener(muteOnClickListener); + controlsBinding.rewWithAmount.setOnClickListener(rewOnClickListener); + controlsBinding.ffWithAmount.setOnClickListener(ffOnClickListener); + controlsBinding.speed.setOnClickListener(showMenu); } private void setupTimeline() { @@ -242,12 +242,18 @@ public class VideoPlayerViewHelper implements Player.EventListener { private void enableControls(final boolean enable) { controlsBinding.speed.setEnabled(enable); + controlsBinding.speed.setClickable(enable); controlsBinding.mute.setEnabled(enable); + controlsBinding.mute.setClickable(enable); controlsBinding.ffWithAmount.setEnabled(enable); + controlsBinding.ffWithAmount.setClickable(enable); controlsBinding.rewWithAmount.setEnabled(enable); + controlsBinding.rewWithAmount.setClickable(enable); controlsBinding.fromTime.setEnabled(enable); controlsBinding.toTime.setEnabled(enable); controlsBinding.playPause.setEnabled(enable); + controlsBinding.playPause.setClickable(enable); + controlsBinding.timeline.setEnabled(enable); } public void showMenu(View anchor) { @@ -302,10 +308,10 @@ public class VideoPlayerViewHelper implements Player.EventListener { private void updateMuteIcon(final float volume) { if (volume == 0) { - controlsBinding.mute.setIconResource(R.drawable.ic_volume_off_24); + controlsBinding.mute.setIconResource(R.drawable.ic_volume_off_24_states); return; } - controlsBinding.mute.setIconResource(R.drawable.ic_volume_up_24); + controlsBinding.mute.setIconResource(R.drawable.ic_volume_up_24_states); } private void updatePlayPauseDrawable(final boolean playWhenReady) { @@ -313,7 +319,7 @@ public class VideoPlayerViewHelper implements Player.EventListener { controlsBinding.playPause.setIconResource(R.drawable.ic_pause_24); return; } - controlsBinding.playPause.setIconResource(R.drawable.ic_play_arrow_24); + controlsBinding.playPause.setIconResource(R.drawable.ic_play_states); } @Override @@ -338,23 +344,88 @@ public class VideoPlayerViewHelper implements Player.EventListener { return vol; } - public void togglePlayback() { - if (player == null) return; - final int playbackState = player.getPlaybackState(); - if (playbackState == STATE_IDLE || playbackState == STATE_ENDED) return; - final boolean playWhenReady = player.getPlayWhenReady(); - player.setPlayWhenReady(!playWhenReady); - } + // public void togglePlayback() { + // if (player == null) return; + // final int playbackState = player.getPlaybackState(); + // if (playbackState == STATE_IDLE || playbackState == STATE_ENDED) return; + // final boolean playWhenReady = player.getPlayWhenReady(); + // player.setPlayWhenReady(!playWhenReady); + // } public void releasePlayer() { if (player == null) return; player.release(); player = null; + if (positionUpdateHandler != null && positionChecker != null) { + positionUpdateHandler.removeCallbacks(positionChecker); + } } public void pause() { if (player == null) return; player.pause(); + if (positionUpdateHandler != null && positionChecker != null) { + positionUpdateHandler.removeCallbacks(positionChecker); + } + } + + public void resetTimeline() { + if (player == null) { + enableControls(false); + return; + } + setupTimeline(); + final long currentPosition = player.getCurrentPosition(); + controlsBinding.timeline.setValue(Math.min(currentPosition, player.getDuration())); + setupControls(); + } + + public void removeCallbacks() { + if (player != null) { + player.removeListener(listener); + player.removeAudioListener(audioListener); + } + controlsBinding.timeline.removeOnChangeListener(onChangeListener); + controlsBinding.timeline.setLabelFormatter(null); + controlsBinding.playPause.setOnClickListener(null); + controlsBinding.mute.setOnClickListener(null); + controlsBinding.rewWithAmount.setOnClickListener(null); + controlsBinding.ffWithAmount.setOnClickListener(null); + controlsBinding.speed.setOnClickListener(null); + } + + private static class PositionCheckRunnable implements Runnable { + private final Handler positionUpdateHandler; + private final SimpleExoPlayer player; + private final Slider timeline; + private final AppCompatTextView fromTime; + + public PositionCheckRunnable(final Handler positionUpdateHandler, + final SimpleExoPlayer simpleExoPlayer, + final Slider slider, + final AppCompatTextView fromTime) { + this.positionUpdateHandler = positionUpdateHandler; + this.player = simpleExoPlayer; + this.timeline = slider; + this.fromTime = fromTime; + } + + @Override + public void run() { + positionUpdateHandler.removeCallbacks(this); + if (player == null) return; + final long currentPosition = player.getCurrentPosition(); + final long duration = player.getDuration(); + if (duration == TIME_UNSET) { + timeline.setValueFrom(0); + timeline.setValueTo(0); + timeline.setEnabled(false); + return; + } + timeline.setValue(Math.min(currentPosition, duration)); + fromTime.setText(TextUtils.millisToTimeString(currentPosition)); + positionUpdateHandler.postDelayed(this, RECURRING_DELAY); + } } public interface VideoPlayerCallback { diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 5482eb0a..5564d0e1 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -113,6 +113,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { private int sliderPosition = -1; private DialogInterface.OnShowListener onShowListener; private boolean isLoggedIn; + private boolean hasBeenToggled = false; private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener = new VerticalDragHelper.OnVerticalDragListener() { @@ -303,8 +304,8 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { } @Override - public void onDestroy() { - super.onDestroy(); + public void onDestroyView() { + super.onDestroyView(); switch (feedModel.getItemType()) { case MEDIA_TYPE_VIDEO: if (videoPlayerViewHelper != null) { @@ -870,6 +871,20 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { if (!wasPaused && sharedMainPostElement != null) { addSharedElement(sharedMainPostElement, binding.sliderParent); } + final boolean hasVideo = feedModel.getSliderItems() + .stream() + .anyMatch(postChild -> postChild.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO); + if (hasVideo) { + final View child = binding.sliderParent.getChildAt(0); + if (child instanceof RecyclerView) { + ((RecyclerView) child).setItemViewCacheSize(feedModel.getSliderItems().size()); + ((RecyclerView) child).addRecyclerListener(holder -> { + if (holder instanceof SliderVideoViewHolder) { + ((SliderVideoViewHolder) holder).releasePlayer(); + } + }); + } + } sliderItemsAdapter = new SliderItemsAdapter(onVerticalDragListener, binding.playerControls, true, new SliderCallbackAdapter() { @Override public void onThumbnailLoaded(final int position) { @@ -879,19 +894,17 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { @Override public void onItemClicked(final int position) { - toggleDetails(); } @Override public void onPlayerPlay(final int position) { - if (!detailsVisible) return; - toggleDetails(); + if (!detailsVisible || hasBeenToggled) return; showPlayerControls(); } @Override public void onPlayerPause(final int position) { - if (detailsVisible) return; + if (detailsVisible || hasBeenToggled) return; toggleDetails(); } }); @@ -924,7 +937,22 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { final String text = (position + 1) + "/" + size; binding.mediaCounter.setText(text); final PostChild postChild = feedModel.getSliderItems().get(position); + final View view = binding.sliderParent.getChildAt(0); + if (prevPosition != -1) { + if (view instanceof RecyclerView) { + final RecyclerView.ViewHolder viewHolder = ((RecyclerView) view).findViewHolderForAdapterPosition(prevPosition); + if (viewHolder instanceof SliderVideoViewHolder) { + ((SliderVideoViewHolder) viewHolder).removeCallbacks(); + } + } + } if (postChild.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) { + if (view instanceof RecyclerView) { + final RecyclerView.ViewHolder viewHolder = ((RecyclerView) view).findViewHolderForAdapterPosition(position); + if (viewHolder instanceof SliderVideoViewHolder) { + ((SliderVideoViewHolder) viewHolder).resetPlayerTimeline(); + } + } enablePlayerControls(true); return; } @@ -1052,6 +1080,9 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { hideCaption(); // previously invisible view View view = binding.playerControls.getRoot(); + if (view != null && view.getVisibility() == View.VISIBLE) { + return; + } if (!ViewCompat.isAttachedToWindow(view)) { view.setVisibility(View.VISIBLE); return; @@ -1076,6 +1107,9 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { private void hidePlayerControls() { // previously visible view final View view = binding.playerControls.getRoot(); + if (view != null && view.getVisibility() == View.GONE) { + return; + } if (!ViewCompat.isAttachedToWindow(view)) { view.setVisibility(View.GONE); return; @@ -1106,6 +1140,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { } private void toggleDetails() { + hasBeenToggled = true; binding.getRoot().post(() -> { TransitionManager.beginDelayedTransition(binding.getRoot()); if (detailsVisible) { diff --git a/app/src/main/res/drawable/ic_forward_5_24_a50.xml b/app/src/main/res/drawable/ic_forward_5_24_a50.xml new file mode 100644 index 00000000..1340c13f --- /dev/null +++ b/app/src/main/res/drawable/ic_forward_5_24_a50.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_forward_5_24_states.xml b/app/src/main/res/drawable/ic_forward_5_24_states.xml new file mode 100644 index 00000000..4c096286 --- /dev/null +++ b/app/src/main/res/drawable/ic_forward_5_24_states.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play_arrow_24_a50.xml b/app/src/main/res/drawable/ic_play_arrow_24_a50.xml new file mode 100644 index 00000000..249e0f6d --- /dev/null +++ b/app/src/main/res/drawable/ic_play_arrow_24_a50.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_states.xml b/app/src/main/res/drawable/ic_play_states.xml new file mode 100644 index 00000000..3f27a8c9 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_states.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_replay_5_24_a50.xml b/app/src/main/res/drawable/ic_replay_5_24_a50.xml new file mode 100644 index 00000000..a0904aca --- /dev/null +++ b/app/src/main/res/drawable/ic_replay_5_24_a50.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_replay_5_24_states.xml b/app/src/main/res/drawable/ic_replay_5_24_states.xml new file mode 100644 index 00000000..19bad1b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_replay_5_24_states.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_volume_off_24_a50.xml b/app/src/main/res/drawable/ic_volume_off_24_a50.xml new file mode 100644 index 00000000..5bbfbe9a --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_off_24_a50.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_off_24_states.xml b/app/src/main/res/drawable/ic_volume_off_24_states.xml new file mode 100644 index 00000000..71b5d07b --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_off_24_states.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_volume_up_24_a50.xml b/app/src/main/res/drawable/ic_volume_up_24_a50.xml new file mode 100644 index 00000000..9c518f60 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_up_24_a50.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_volume_up_24_states.xml b/app/src/main/res/drawable/ic_volume_up_24_states.xml new file mode 100644 index 00000000..3a947b21 --- /dev/null +++ b/app/src/main/res/drawable/ic_volume_up_24_states.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/speed_text_color_states.xml b/app/src/main/res/drawable/speed_text_color_states.xml new file mode 100644 index 00000000..2e62e4fd --- /dev/null +++ b/app/src/main/res/drawable/speed_text_color_states.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_exo_custom_controls.xml b/app/src/main/res/layout/layout_exo_custom_controls.xml index b8e90299..4292ebbc 100644 --- a/app/src/main/res/layout/layout_exo_custom_controls.xml +++ b/app/src/main/res/layout/layout_exo_custom_controls.xml @@ -53,13 +53,14 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:visibility="visible" - app:icon="@drawable/ic_replay_5_24" + app:icon="@drawable/ic_replay_5_24_states" app:iconSize="24dp" app:iconTint="@color/white" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/play_pause" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/timeline" + tools:enabled="false" tools:visibility="visible" /> \ No newline at end of file diff --git a/app/src/main/res/values/color.xml b/app/src/main/res/values/color.xml index df1db0b3..c59baf9a 100755 --- a/app/src/main/res/values/color.xml +++ b/app/src/main/res/values/color.xml @@ -33,6 +33,7 @@ #888888 #FFFFFF + #80FFFFFF #000000 #121212