Media Preview V2 Visual Redesign.

This commit is contained in:
Nicholas
2022-10-25 15:22:38 -04:00
committed by Alex Hart
parent b8174c5e00
commit 7759ad283d
21 changed files with 339 additions and 230 deletions

View File

@@ -5,7 +5,6 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import androidx.annotation.Nullable;
@@ -16,7 +15,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.MediaUtil;
public final class ImageMediaPreviewFragment extends MediaPreviewFragment {
private View bottomBarControlView;
private MediaPreviewPlayerControlView bottomBarControlView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -41,26 +40,24 @@ public final class ImageMediaPreviewFragment extends MediaPreviewFragment {
zoomingImageView.setOnClickListener(v -> events.singleTapOnMedia());
bottomBarControlView = getLayoutInflater().inflate(R.layout.image_media_preview_bottom_bar, null);
return zoomingImageView;
}
@Override
public void setShareButtonListener(View.OnClickListener listener) {
ImageButton forwardButton = bottomBarControlView.findViewById(R.id.image_preview_forward);
forwardButton.setOnClickListener(listener);
public void cleanUp() {
bottomBarControlView = null;
}
@Override
public void setForwardButtonListener(View.OnClickListener listener) {
ImageButton shareButton = bottomBarControlView.findViewById(R.id.image_preview_share);
shareButton.setOnClickListener(listener);
}
public void pause() {}
@Nullable
@Override
public View getBottomBarControls() {
public ViewGroup getBottomBarControls() {
return bottomBarControlView;
}
@Override
public void setBottomButtonControls(MediaPreviewPlayerControlView playerControlView) {
bottomBarControlView = playerControlView;
}
}

View File

@@ -3,14 +3,12 @@ package org.thoughtcrime.securesms.mediapreview;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.google.android.exoplayer2.ui.PlayerControlView;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.database.SignalDatabase;
@@ -81,15 +79,10 @@ public abstract class MediaPreviewFragment extends Fragment {
checkMediaStillAvailable();
}
public void cleanUp() {
}
public void pause() {
}
abstract public void setShareButtonListener(View.OnClickListener listener);
abstract public void setForwardButtonListener(View.OnClickListener listener);
abstract public @Nullable View getBottomBarControls();
public abstract void cleanUp();
public abstract void pause();
public abstract ViewGroup getBottomBarControls();
public abstract void setBottomButtonControls(MediaPreviewPlayerControlView playerControlView);
private void checkMediaStillAvailable() {
if (attachmentId == null) {

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.mediapreview
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import com.google.android.exoplayer2.ui.PlayerControlView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.MediaUtil
/**
* The bottom bar for the media preview. This includes the standard seek bar as well as playback controls,
* but adds forward and share buttons as well as a recyclerview that can be populated with a rail of thumbnails.
*/
class MediaPreviewPlayerControlView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
playbackAttrs: AttributeSet? = null
) : PlayerControlView(context, attrs, defStyleAttr, playbackAttrs) {
val recyclerView: RecyclerView = findViewById(R.id.media_preview_album_rail)
private val durationBar: LinearLayout = findViewById(R.id.exo_duration_viewgroup)
private val videoControls: LinearLayout = findViewById(R.id.exo_button_viewgroup)
private val shareButton: ImageButton = findViewById(R.id.exo_share)
private val forwardButton: ImageButton = findViewById(R.id.exo_forward)
enum class MediaMode {
IMAGE, VIDEO;
companion object {
@JvmStatic
fun fromString(contentType: String): MediaMode {
if (MediaUtil.isVideo(contentType)) return VIDEO
if (MediaUtil.isImageType(contentType)) return IMAGE
throw IllegalArgumentException("Unknown content type: $contentType")
}
}
}
init {
setShowPreviousButton(false)
setShowNextButton(false)
showShuffleButton = false
showVrButton = false
showTimeoutMs = -1
}
fun setVisibility(mediaMode: MediaMode) {
durationBar.visibility = if (mediaMode == MediaMode.VIDEO) VISIBLE else GONE
videoControls.visibility = if (mediaMode == MediaMode.VIDEO) VISIBLE else INVISIBLE
}
fun setShareButtonListener(listener: OnClickListener?) = shareButton.setOnClickListener(listener)
fun setForwardButtonListener(listener: OnClickListener?) = forwardButton.setOnClickListener(listener)
}

View File

@@ -20,7 +20,7 @@ class MediaPreviewV2Activity : AppCompatActivity(R.layout.activity_mediapreview_
super.onCreate(savedInstanceState)
setTheme(R.style.TextSecure_MediaPreview)
if (Build.VERSION.SDK_INT >= 21) {
val systemBarColor = ContextCompat.getColor(this, R.color.media_preview_bar_background)
val systemBarColor = ContextCompat.getColor(this, R.color.signal_dark_colorSurface)
window.statusBarColor = systemBarColor
window.navigationBarColor = systemBarColor
}

View File

@@ -18,7 +18,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.appbar.MaterialToolbar
@@ -72,12 +72,10 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
super.onViewCreated(view, savedInstanceState)
val args = MediaIntentFactory.requireArguments(requireArguments())
initializeViewModel(args)
initializeToolbar(binding.toolbar)
initializeViewPager()
initializeFullScreenUi()
initializeAlbumRail()
anchorMarginsToBottomInsets(binding.mediaPreviewDetailsContainer)
lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe {
bindCurrentState(it)
@@ -92,10 +90,7 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
requireActivity().finish()
}.show()
}
viewModel.setShowThread(args.showThread)
viewModel.setAlwaysShowAlbumRail(args.allMediaInRail)
viewModel.setLeftIsRecent(args.leftIsRecent)
viewModel.initialize(args.showThread, args.allMediaInRail, args.leftIsRecent)
val sorting = MediaDatabase.Sorting.deserialize(args.sorting)
viewModel.fetchAttachments(PartAuthority.requireAttachmentId(args.initialMediaUri), args.threadId, sorting)
}
@@ -105,6 +100,8 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
requireActivity().onBackPressed()
}
toolbar.setTitleTextAppearance(requireContext(), R.style.Signal_Text_TitleMedium)
toolbar.setSubtitleTextAppearance(requireContext(), R.style.Signal_Text_BodyMedium)
binding.toolbar.inflateMenu(R.menu.media_preview)
}
@@ -123,10 +120,9 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
})
}
private fun initializeAlbumRail() {
binding.mediaPreviewAlbumRail.itemAnimator = null // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
binding.mediaPreviewAlbumRail.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
binding.mediaPreviewAlbumRail.adapter = MediaRailAdapter(
private fun initializeAlbumRail(recyclerView: RecyclerView, albumThumbnailMedia: List<Media?>, albumPosition: Int) {
recyclerView.itemAnimator = null // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
val mediaRailAdapter = MediaRailAdapter(
GlideApp.with(this),
object : MediaRailAdapter.RailItemListener {
override fun onRailItemClicked(distanceFromActive: Int) {
@@ -139,6 +135,9 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
},
false
)
mediaRailAdapter.setMedia(albumThumbnailMedia, albumPosition)
recyclerView.adapter = mediaRailAdapter
recyclerView.smoothScrollToPosition(albumPosition)
}
private fun initializeFullScreenUi() {
@@ -152,21 +151,43 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
return
}
when (currentState.loadState) {
MediaPreviewV2State.LoadState.READY -> bindReadyState(currentState)
MediaPreviewV2State.LoadState.LOADED -> {
bindReadyState(currentState)
bindLoadedState(currentState)
}
MediaPreviewV2State.LoadState.DATA_LOADED -> bindDataLoadedState(currentState)
MediaPreviewV2State.LoadState.MEDIA_READY -> bindMediaReadyState(currentState)
else -> null
}
}
private fun bindReadyState(currentState: MediaPreviewV2State) {
private fun bindDataLoadedState(currentState: MediaPreviewV2State) {
(binding.mediaPager.adapter as MediaPreviewV2Adapter).updateBackingItems(currentState.mediaRecords.mapNotNull { it.attachment })
if (binding.mediaPager.currentItem != currentState.position) {
binding.mediaPager.setCurrentItem(currentState.position, false)
val currentPosition = currentState.position
if (binding.mediaPager.currentItem != currentPosition) {
binding.mediaPager.setCurrentItem(currentPosition, false)
}
val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position]
}
/**
* These are binding steps that need a reference to the actual fragment within the pager.
* This is not available until after a page has been chosen by the ViewPager, and we receive the
* {@link OnPageChangeCallback}.
*/
private fun bindMediaReadyState(currentState: MediaPreviewV2State) {
val currentPosition = currentState.position
val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentPosition]
// pause all other fragments
childFragmentManager.fragments.map { fragment ->
if (fragment.tag != "f$currentPosition") {
(fragment as? MediaPreviewFragment)?.pause()
}
}
val mediaType: MediaPreviewPlayerControlView.MediaMode = if (currentItem.attachment?.isVideoGif == true) {
MediaPreviewPlayerControlView.MediaMode.IMAGE
} else {
MediaPreviewPlayerControlView.MediaMode.fromString(currentItem.contentType)
}
binding.mediaPreviewPlaybackControls.setVisibility(mediaType)
binding.toolbar.title = getTitleText(currentItem, currentState.showThread)
binding.toolbar.subtitle = getSubTitleText(currentItem)
@@ -184,16 +205,6 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
}
return@setOnMenuItemClickListener true
}
}
/**
* These are binding steps that need a reference to the actual fragment within the pager.
* This is not available until after a page has been chosen by the ViewPager, and we receive the
* {@link OnPageChangeCallback}.
*/
private fun bindLoadedState(currentState: MediaPreviewV2State) {
val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position]
val albumThumbnailMedia = if (currentState.allMediaInAlbumRail) {
currentState.mediaRecords.map { it.toMedia() }
} else {
@@ -201,11 +212,10 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
.filter { it.attachment != null && it.attachment!!.mmsId == currentItem.attachment?.mmsId }
.map { it.toMedia() }
}
val caption = currentItem.attachment?.caption
binding.mediaPreviewAlbumRail.visibility = if (albumThumbnailMedia.size <= 1) View.GONE else View.VISIBLE
(binding.mediaPreviewAlbumRail.adapter as MediaRailAdapter).setMedia(albumThumbnailMedia, currentState.position)
binding.mediaPreviewAlbumRail.smoothScrollToPosition(currentState.position)
val albumRailEnabled = albumThumbnailMedia.size > 1
if (caption != null) {
binding.mediaPreviewCaption.text = caption
@@ -214,24 +224,21 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
binding.mediaPreviewCaption.visibility = View.GONE
}
val fragmentTag = "f${currentState.position}"
val currentFragment: MediaPreviewFragment? = childFragmentManager.findFragmentByTag(fragmentTag) as? MediaPreviewFragment
val playbackControls: View? = currentFragment?.bottomBarControls
if (albumThumbnailMedia.size <= 1 && caption == null && playbackControls == null) {
binding.mediaPreviewDetailsContainer.visibility = View.GONE
} else {
binding.mediaPreviewDetailsContainer.visibility = View.VISIBLE
binding.mediaPreviewPlaybackControls.setShareButtonListener { share(currentItem) }
binding.mediaPreviewPlaybackControls.setForwardButtonListener { forward(currentItem) }
val albumRail: RecyclerView = binding.mediaPreviewPlaybackControls.findViewById(R.id.media_preview_album_rail)
if (albumRailEnabled) {
val albumPosition = albumThumbnailMedia.indexOfFirst { it?.uri == currentItem.attachment?.uri }
initializeAlbumRail(albumRail, albumThumbnailMedia, albumPosition)
}
binding.mediaPreviewPlaybackControlsContainer.removeAllViews()
if (playbackControls != null) {
val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
playbackControls.layoutParams = params
binding.mediaPreviewPlaybackControlsContainer.addView(playbackControls)
}
currentFragment?.setShareButtonListener { share(currentItem) }
currentFragment?.setForwardButtonListener { forward(currentItem) }
albumRail.visibility = if (albumRailEnabled) View.VISIBLE else View.GONE
val currentFragment: MediaPreviewFragment? = getMediaPreviewFragmentFromChildFragmentManager(currentPosition)
currentFragment?.setBottomButtonControls(binding.mediaPreviewPlaybackControls)
}
private fun getMediaPreviewFragmentFromChildFragmentManager(currentPosition: Int) = childFragmentManager.findFragmentByTag("f$currentPosition") as? MediaPreviewFragment
private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String {
val recipient: Recipient = Recipient.live(mediaRecord.recipientId).get()
val defaultFromString: String = if (mediaRecord.isOutgoing) {
@@ -315,7 +322,7 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
}
override fun onMediaReady() {
Log.d(TAG, "onMediaReady()")
viewModel.setMediaReady()
}
private fun forward(mediaItem: MediaDatabase.MediaRecord) {
@@ -400,6 +407,11 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
.show()
}
override fun onPause() {
super.onPause()
getMediaPreviewFragmentFromChildFragmentManager(binding.mediaPager.currentItem)?.pause()
}
companion object {
const val ARGS_KEY: String = "args"
}

View File

@@ -8,7 +8,7 @@ data class MediaPreviewV2State(
val position: Int = 0,
val showThread: Boolean = false,
val allMediaInAlbumRail: Boolean = false,
val leftIsRecent: Boolean = false
val leftIsRecent: Boolean = false,
) {
enum class LoadState { INIT, READY, LOADED }
enum class LoadState { INIT, DATA_LOADED, MEDIA_READY }
}

View File

@@ -24,46 +24,44 @@ class MediaPreviewV2ViewModel : ViewModel() {
val state: Flowable<MediaPreviewV2State> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
fun fetchAttachments(startingAttachmentId: AttachmentId, threadId: Long, sorting: MediaDatabase.Sorting) {
disposables += store.update(repository.getAttachments(startingAttachmentId, threadId, sorting)) {
result: MediaPreviewRepository.Result, oldState: MediaPreviewV2State ->
if (oldState.leftIsRecent) {
oldState.copy(
position = result.initialPosition,
mediaRecords = result.records,
loadState = MediaPreviewV2State.LoadState.READY,
)
} else {
oldState.copy(
position = result.records.size - result.initialPosition - 1,
mediaRecords = result.records.reversed(),
loadState = MediaPreviewV2State.LoadState.READY,
)
fun fetchAttachments(startingAttachmentId: AttachmentId, threadId: Long, sorting: MediaDatabase.Sorting, forceRefresh: Boolean = false) {
if (store.state.loadState == MediaPreviewV2State.LoadState.INIT || forceRefresh) {
disposables += store.update(repository.getAttachments(startingAttachmentId, threadId, sorting)) {
result: MediaPreviewRepository.Result, oldState: MediaPreviewV2State ->
if (oldState.leftIsRecent) {
oldState.copy(
position = result.initialPosition,
mediaRecords = result.records,
loadState = MediaPreviewV2State.LoadState.DATA_LOADED,
)
} else {
oldState.copy(
position = result.records.size - result.initialPosition - 1,
mediaRecords = result.records.reversed(),
loadState = MediaPreviewV2State.LoadState.DATA_LOADED,
)
}
}
}
}
fun setShowThread(value: Boolean) {
store.update { oldState ->
oldState.copy(showThread = value)
}
}
fun setAlwaysShowAlbumRail(value: Boolean) {
store.update { oldState ->
oldState.copy(allMediaInAlbumRail = value)
}
}
fun setLeftIsRecent(value: Boolean) {
store.update { oldState ->
oldState.copy(leftIsRecent = value)
fun initialize(showThread: Boolean, allMediaInAlbumRail: Boolean, leftIsRecent: Boolean) {
if (store.state.loadState == MediaPreviewV2State.LoadState.INIT) {
store.update { oldState ->
oldState.copy(showThread = showThread, allMediaInAlbumRail = allMediaInAlbumRail, leftIsRecent = leftIsRecent)
}
}
}
fun setCurrentPage(position: Int) {
store.update { oldState ->
oldState.copy(position = position, loadState = MediaPreviewV2State.LoadState.LOADED)
oldState.copy(position = position)
}
}
fun setMediaReady() {
store.update { oldState ->
oldState.copy(loadState = MediaPreviewV2State.LoadState.MEDIA_READY)
}
}

View File

@@ -6,7 +6,6 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -30,8 +29,6 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
private VideoPlayer videoView;
private boolean isVideoGif;
private ImageButton shareButton;
private ImageButton forwardButton;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -90,21 +87,15 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
});
if (isVideoGif) {
videoView.hideControls();
videoView.loopForever();
}
videoView.setOnClickListener(v -> events.singleTapOnMedia());
final PlayerControlView controlView = videoView.getControlView();
if (controlView != null) {
shareButton = controlView.findViewById(R.id.exo_share);
forwardButton = controlView.findViewById(R.id.exo_forward);
}
return itemView;
}
private void updateSkipButtonState() {
final PlayerControlView playbackControls = getBottomBarControls();
final PlayerControlView playbackControls = videoView.getControlView();
if (playbackControls != null) {
boolean shouldShowSkipButtons = videoView.getDuration() > MINIMUM_DURATION_FOR_SKIP_MS;
playbackControls.setShowFastForwardButton(shouldShowSkipButtons);
@@ -149,20 +140,16 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
}
}
@Override
public void setShareButtonListener(View.OnClickListener listener) {
shareButton.setOnClickListener(listener);
}
@Override
public void setForwardButtonListener(View.OnClickListener listener) {
forwardButton.setOnClickListener(listener);
}
@Nullable
@Override
public PlayerControlView getBottomBarControls() {
return videoView != null && !isVideoGif ? videoView.getControlView() : null;
public MediaPreviewPlayerControlView getBottomBarControls() {
return (MediaPreviewPlayerControlView) videoView.getControlView();
}
@Override
public void setBottomButtonControls(@NonNull MediaPreviewPlayerControlView playerControlView) {
videoView.setControlView(playerControlView);
updateSkipButtonState();
}
private @NonNull Uri getUri() {

View File

@@ -19,15 +19,12 @@ package org.thoughtcrime.securesms.video;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.res.TypedArrayKt;
import androidx.core.content.res.TypedArrayUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
@@ -45,6 +42,7 @@ import com.google.android.exoplayer2.ui.PlayerView;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewPlayerControlView;
import org.thoughtcrime.securesms.mms.VideoSlide;
import java.util.Objects;
@@ -56,10 +54,10 @@ public class VideoPlayer extends FrameLayout {
private static final String TAG = Log.tag(VideoPlayer.class);
private final PlayerView exoView;
private final PlayerControlView exoControls;
private final DefaultMediaSourceFactory mediaSourceFactory;
private ExoPlayer exoPlayer;
private PlayerControlView exoControls;
private Window window;
private PlayerStateCallback playerStateCallback;
private PlayerPositionDiscontinuityCallback playerPositionDiscontinuityCallback;
@@ -233,6 +231,11 @@ public class VideoPlayer extends FrameLayout {
return this.exoControls;
}
public void setControlView(MediaPreviewPlayerControlView controller) {
exoControls = controller;
exoControls.setPlayer(exoPlayer);
}
public void stop() {
if (this.exoPlayer != null) {
exoPlayer.stop();