Fade in media detail view.

main
Nicholas 2022-11-04 13:13:14 -04:00 zatwierdzone przez Cody Henthorne
rodzic 2856697109
commit 7ad6d95b27
12 zmienionych plików z 197 dodań i 252 usunięć

Wyświetl plik

@ -21,11 +21,14 @@ import androidx.annotation.UiThread;
import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatImageView;
import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.TransitionOptions;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.RequestOptions;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
@ -418,13 +421,21 @@ public class ThumbnailView extends FrameLayout {
} }
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height) { public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height) {
return setImageResource(glideRequests, uri, width, height, true, null);
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height, boolean animate, @Nullable RequestListener<Drawable> listener) {
SettableFuture<Boolean> future = new SettableFuture<>(); SettableFuture<Boolean> future = new SettableFuture<>();
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE); if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
GlideRequest request = glideRequests.load(new DecryptableUri(uri)) GlideRequest<Drawable> request = glideRequests.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(withCrossFade()); .listener(listener);
if (animate) {
request = request.transition(withCrossFade());
}
if (width > 0 && height > 0) { if (width > 0 && height > 0) {
request = request.override(width, height); request = request.override(width, height);

Wyświetl plik

@ -51,11 +51,6 @@ public final class ImageMediaPreviewFragment extends MediaPreviewFragment {
@Override @Override
public void pause() {} public void pause() {}
@Override
public ViewGroup getBottomBarControls() {
return bottomBarControlView;
}
@Override @Override
public void setBottomButtonControls(MediaPreviewPlayerControlView playerControlView) { public void setBottomButtonControls(MediaPreviewPlayerControlView playerControlView) {
bottomBarControlView = playerControlView; bottomBarControlView = playerControlView;

Wyświetl plik

@ -81,7 +81,6 @@ public abstract class MediaPreviewFragment extends Fragment {
public abstract void cleanUp(); public abstract void cleanUp();
public abstract void pause(); public abstract void pause();
public abstract ViewGroup getBottomBarControls();
public abstract void setBottomButtonControls(MediaPreviewPlayerControlView playerControlView); public abstract void setBottomButtonControls(MediaPreviewPlayerControlView playerControlView);
private void checkMediaStillAvailable() { private void checkMediaStillAvailable() {

Wyświetl plik

@ -5,18 +5,24 @@ import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.GONE
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import android.view.ViewGroup.VISIBLE
import android.view.animation.PathInterpolator
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.MarginPageTransformer import androidx.viewpager2.widget.MarginPageTransformer
import androidx.viewpager2.widget.ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT import androidx.viewpager2.widget.ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT
@ -34,6 +40,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.database.MediaDatabase import org.thoughtcrime.securesms.database.MediaDatabase
import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter.ImageLoadingListener
import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
@ -48,8 +55,8 @@ import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.StorageUtil import org.thoughtcrime.securesms.util.StorageUtil
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
import java.util.Locale import java.util.Locale
import java.util.Optional
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events { class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events {
@ -60,8 +67,8 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
private val viewModel: MediaPreviewV2ViewModel by viewModels() private val viewModel: MediaPreviewV2ViewModel by viewModels()
private val debouncer = Debouncer(2, TimeUnit.SECONDS) private val debouncer = Debouncer(2, TimeUnit.SECONDS)
private lateinit var fullscreenHelper: FullscreenHelper private lateinit var fullscreenHelper: FullscreenHelper
private lateinit var albumRailAdapter: MediaRailAdapter
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@ -80,6 +87,7 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
initializeViewModel(args) initializeViewModel(args)
initializeToolbar(binding.toolbar) initializeToolbar(binding.toolbar)
initializeViewPager() initializeViewPager()
initializeAlbumRail()
initializeFullScreenUi() initializeFullScreenUi()
anchorMarginsToBottomInsets(binding.mediaPreviewDetailsContainer) anchorMarginsToBottomInsets(binding.mediaPreviewDetailsContainer)
lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe {
@ -123,24 +131,30 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
}) })
} }
private fun initializeAlbumRail(recyclerView: RecyclerView, albumThumbnailMedia: List<Media?>, albumPosition: Int) { private fun initializeAlbumRail() {
recyclerView.itemAnimator = null // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682 binding.mediaPreviewPlaybackControls.recyclerView.apply {
val mediaRailAdapter = MediaRailAdapter( this.itemAnimator = null // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
GlideApp.with(this), PagerSnapHelper().attachToRecyclerView(this)
object : MediaRailAdapter.RailItemListener { albumRailAdapter = MediaRailAdapter(
override fun onRailItemClicked(distanceFromActive: Int) { GlideApp.with(this@MediaPreviewV2Fragment),
binding.mediaPager.currentItem += distanceFromActive object : MediaRailAdapter.RailItemListener {
} override fun onRailItemClicked(distanceFromActive: Int) {
binding.mediaPager.currentItem += distanceFromActive
}
override fun onRailItemDeleteClicked(distanceFromActive: Int) { override fun onRailItemDeleteClicked(distanceFromActive: Int) {
throw UnsupportedOperationException("Callback unsupported.") throw UnsupportedOperationException("Callback unsupported.")
}
},
false,
object : ImageLoadingListener() {
override fun onAllRequestsFinished() {
crossfadeViewIn(this@apply)
}
} }
}, )
false this.adapter = albumRailAdapter
) }
mediaRailAdapter.setMedia(albumThumbnailMedia, albumPosition)
recyclerView.adapter = mediaRailAdapter
recyclerView.smoothScrollToPosition(albumPosition)
} }
private fun initializeFullScreenUi() { private fun initializeFullScreenUi() {
@ -187,6 +201,7 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
onMediaNotAvailable() onMediaNotAvailable()
return return
} }
val currentPosition = currentState.position val currentPosition = currentState.position
val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentPosition] val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentPosition]
@ -197,16 +212,29 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
} }
} }
val mediaType: MediaPreviewPlayerControlView.MediaMode = if (currentItem.attachment?.isVideoGif == true) { bindTextViews(currentItem, currentState.showThread)
MediaPreviewPlayerControlView.MediaMode.IMAGE bindMenuItems(currentItem)
} else { bindMediaPreviewPlaybackControls(currentItem, getMediaPreviewFragmentFromChildFragmentManager(currentPosition))
MediaPreviewPlayerControlView.MediaMode.fromString(currentItem.contentType)
}
binding.mediaPreviewPlaybackControls.setMediaMode(mediaType)
binding.toolbar.title = getTitleText(currentItem, currentState.showThread) val albumThumbnailMedia: List<Media> = if (currentState.allMediaInAlbumRail) {
currentState.mediaRecords.mapNotNull { it.toMedia() }
} else {
currentState.albums[currentItem.attachment?.mmsId] ?: emptyList()
}
bindAlbumRail(albumThumbnailMedia, currentItem)
crossfadeViewIn(binding.mediaPreviewDetailsContainer)
}
private fun bindTextViews(currentItem: MediaDatabase.MediaRecord, showThread: Boolean) {
binding.toolbar.title = getTitleText(currentItem, showThread)
binding.toolbar.subtitle = getSubTitleText(currentItem) binding.toolbar.subtitle = getSubTitleText(currentItem)
val caption = currentItem.attachment?.caption
binding.mediaPreviewCaption.text = caption
binding.mediaPreviewCaption.visible = caption != null
}
private fun bindMenuItems(currentItem: MediaDatabase.MediaRecord) {
val menu: Menu = binding.toolbar.menu val menu: Menu = binding.toolbar.menu
if (currentItem.threadId == MediaIntentFactory.NOT_IN_A_THREAD.toLong()) { if (currentItem.threadId == MediaIntentFactory.NOT_IN_A_THREAD.toLong()) {
menu.findItem(R.id.delete).isVisible = false menu.findItem(R.id.delete).isVisible = false
@ -222,38 +250,50 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
} }
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
val albumThumbnailMedia = if (currentState.allMediaInAlbumRail) { }
currentState.mediaRecords.map { it.toMedia() }
private fun bindMediaPreviewPlaybackControls(currentItem: MediaDatabase.MediaRecord, currentFragment: MediaPreviewFragment?) {
val mediaType: MediaPreviewPlayerControlView.MediaMode = if (currentItem.attachment?.isVideoGif == true) {
MediaPreviewPlayerControlView.MediaMode.IMAGE
} else { } else {
currentState.mediaRecords MediaPreviewPlayerControlView.MediaMode.fromString(currentItem.contentType)
.filter { it.attachment != null && it.attachment!!.mmsId == currentItem.attachment?.mmsId }
.map { it.toMedia() }
} }
binding.mediaPreviewPlaybackControls.setMediaMode(mediaType)
val caption = currentItem.attachment?.caption
val albumRailEnabled = albumThumbnailMedia.size > 1
if (caption != null) {
binding.mediaPreviewCaption.text = caption
binding.mediaPreviewCaption.visibility = View.VISIBLE
} else {
binding.mediaPreviewCaption.visibility = View.GONE
}
binding.mediaPreviewPlaybackControls.setShareButtonListener { share(currentItem) } binding.mediaPreviewPlaybackControls.setShareButtonListener { share(currentItem) }
binding.mediaPreviewPlaybackControls.setForwardButtonListener { forward(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)
}
albumRail.visibility = if (albumRailEnabled) View.VISIBLE else View.GONE
val currentFragment: MediaPreviewFragment? = getMediaPreviewFragmentFromChildFragmentManager(currentPosition)
currentFragment?.setBottomButtonControls(binding.mediaPreviewPlaybackControls) currentFragment?.setBottomButtonControls(binding.mediaPreviewPlaybackControls)
} }
private fun bindAlbumRail(albumThumbnailMedia: List<Media>, currentItem: MediaDatabase.MediaRecord) {
val albumRail: RecyclerView = binding.mediaPreviewPlaybackControls.recyclerView
if (albumThumbnailMedia.size > 1) {
val albumPosition = albumThumbnailMedia.indexOfFirst { it.uri == currentItem.attachment?.uri }
if (albumRail.visibility == GONE) {
albumRail.visibility = View.INVISIBLE
}
albumRailAdapter.setMedia(albumThumbnailMedia, albumPosition)
albumRail.smoothScrollToPosition(albumPosition)
} else {
albumRail.visibility = View.GONE
albumRailAdapter.setMedia(emptyList())
}
}
private fun crossfadeViewIn(view: View, duration: Long = 200) {
if (!view.isVisible) {
val viewPropertyAnimator = view.animate()
.alpha(1f)
.setDuration(duration)
.withStartAction {
view.visibility = VISIBLE
}
if (Build.VERSION.SDK_INT >= 21) {
viewPropertyAnimator.interpolator = PathInterpolator(0.17f, 0.17f, 0f, 1f)
}
viewPropertyAnimator.start()
}
}
private fun getMediaPreviewFragmentFromChildFragmentManager(currentPosition: Int) = childFragmentManager.findFragmentByTag("f$currentPosition") as? MediaPreviewFragment private fun getMediaPreviewFragmentFromChildFragmentManager(currentPosition: Int) = childFragmentManager.findFragmentByTag("f$currentPosition") as? MediaPreviewFragment
private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String { private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String {
@ -304,29 +344,6 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
} }
} }
private fun MediaDatabase.MediaRecord.toMedia(): Media? {
val attachment = this.attachment
val uri = attachment?.uri
if (attachment == null || uri == null) {
return null
}
return Media(
uri,
this.contentType,
this.date,
attachment.width,
attachment.height,
attachment.size,
0,
attachment.isBorderless,
attachment.isVideoGif,
Optional.empty(),
Optional.ofNullable(attachment.caption),
Optional.empty()
)
}
override fun singleTapOnMedia(): Boolean { override fun singleTapOnMedia(): Boolean {
fullscreenHelper.toggleUiVisibility() fullscreenHelper.toggleUiVisibility()
return true return true

Wyświetl plik

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.mediapreview package org.thoughtcrime.securesms.mediapreview
import org.thoughtcrime.securesms.database.MediaDatabase import org.thoughtcrime.securesms.database.MediaDatabase
import org.thoughtcrime.securesms.mediasend.Media
data class MediaPreviewV2State( data class MediaPreviewV2State(
val mediaRecords: List<MediaDatabase.MediaRecord> = emptyList(), val mediaRecords: List<MediaDatabase.MediaRecord> = emptyList(),
@ -9,6 +10,7 @@ data class MediaPreviewV2State(
val showThread: Boolean = false, val showThread: Boolean = false,
val allMediaInAlbumRail: Boolean = false, val allMediaInAlbumRail: Boolean = false,
val leftIsRecent: Boolean = false, val leftIsRecent: Boolean = false,
val albums: Map<Long, List<Media>> = mapOf(),
) { ) {
enum class LoadState { INIT, DATA_LOADED, MEDIA_READY } enum class LoadState { INIT, DATA_LOADED, MEDIA_READY }
} }

Wyświetl plik

@ -13,8 +13,10 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.MediaDatabase import org.thoughtcrime.securesms.database.MediaDatabase
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.util.AttachmentUtil import org.thoughtcrime.securesms.util.AttachmentUtil
import org.thoughtcrime.securesms.util.rx.RxStore import org.thoughtcrime.securesms.util.rx.RxStore
import java.util.Optional
class MediaPreviewV2ViewModel : ViewModel() { class MediaPreviewV2ViewModel : ViewModel() {
private val TAG = Log.tag(MediaPreviewV2ViewModel::class.java) private val TAG = Log.tag(MediaPreviewV2ViewModel::class.java)
@ -27,16 +29,26 @@ class MediaPreviewV2ViewModel : ViewModel() {
fun fetchAttachments(startingAttachmentId: AttachmentId, threadId: Long, sorting: MediaDatabase.Sorting, forceRefresh: Boolean = false) { fun fetchAttachments(startingAttachmentId: AttachmentId, threadId: Long, sorting: MediaDatabase.Sorting, forceRefresh: Boolean = false) {
if (store.state.loadState == MediaPreviewV2State.LoadState.INIT || forceRefresh) { if (store.state.loadState == MediaPreviewV2State.LoadState.INIT || forceRefresh) {
disposables += store.update(repository.getAttachments(startingAttachmentId, threadId, sorting)) { result: MediaPreviewRepository.Result, oldState: MediaPreviewV2State -> disposables += store.update(repository.getAttachments(startingAttachmentId, threadId, sorting)) { result: MediaPreviewRepository.Result, oldState: MediaPreviewV2State ->
val albums = result.records.fold(mutableMapOf()) { acc: MutableMap<Long, MutableList<Media>>, mediaRecord: MediaDatabase.MediaRecord ->
val attachment = mediaRecord.attachment
if (attachment != null) {
val convertedMedia = mediaRecord.toMedia() ?: return@fold acc
acc.getOrPut(attachment.mmsId) { mutableListOf() }.add(convertedMedia)
}
acc
}
if (oldState.leftIsRecent) { if (oldState.leftIsRecent) {
oldState.copy( oldState.copy(
position = result.initialPosition, position = result.initialPosition,
mediaRecords = result.records, mediaRecords = result.records,
albums = albums,
loadState = MediaPreviewV2State.LoadState.DATA_LOADED, loadState = MediaPreviewV2State.LoadState.DATA_LOADED,
) )
} else { } else {
oldState.copy( oldState.copy(
position = result.records.size - result.initialPosition - 1, position = result.records.size - result.initialPosition - 1,
mediaRecords = result.records.reversed(), mediaRecords = result.records.reversed(),
albums = albums.mapValues { it.value.reversed() },
loadState = MediaPreviewV2State.LoadState.DATA_LOADED, loadState = MediaPreviewV2State.LoadState.DATA_LOADED,
) )
} }
@ -82,3 +94,26 @@ class MediaPreviewV2ViewModel : ViewModel() {
} }
} }
} }
fun MediaDatabase.MediaRecord.toMedia(): Media? {
val attachment = this.attachment
val uri = attachment?.uri
if (attachment == null || uri == null) {
return null
}
return Media(
uri,
this.contentType,
this.date,
attachment.width,
attachment.height,
attachment.size,
0,
attachment.isBorderless,
attachment.isVideoGif,
Optional.empty(),
Optional.ofNullable(attachment.caption),
Optional.empty()
)
}

Wyświetl plik

@ -1,148 +0,0 @@
package org.thoughtcrime.securesms.mediapreview;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.mediasend.Media;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
public class MediaPreviewViewModel extends ViewModel {
private final MutableLiveData<PreviewData> previewData = new MutableLiveData<>();
private boolean leftIsRecent;
private @Nullable Cursor cursor;
public void setCursor(@NonNull Context context, @Nullable Cursor cursor, boolean leftIsRecent) {
boolean firstLoad = (this.cursor == null) && (cursor != null);
this.cursor = cursor;
this.leftIsRecent = leftIsRecent;
if (firstLoad) {
setActiveAlbumRailItem(context, 0);
}
}
public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) {
if (cursor == null) {
previewData.postValue(new PreviewData(Collections.emptyList(), null, 0));
return;
}
activePosition = getCursorPosition(activePosition);
cursor.moveToPosition(activePosition);
MediaRecord activeRecord = MediaRecord.from(cursor);
LinkedList<Media> rail = new LinkedList<>();
Media activeMedia = toMedia(activeRecord);
if (activeMedia != null) rail.add(activeMedia);
while (cursor.moveToPrevious()) {
MediaRecord record = MediaRecord.from(cursor);
if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) {
Media media = toMedia(record);
if (media != null) rail.addFirst(media);
} else {
break;
}
}
cursor.moveToPosition(activePosition);
while (cursor.moveToNext()) {
MediaRecord record = MediaRecord.from(cursor);
if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) {
Media media = toMedia(record);
if (media != null) rail.addLast(media);
} else {
break;
}
}
if (!leftIsRecent) {
Collections.reverse(rail);
}
previewData.postValue(new PreviewData(rail.size() > 1 ? rail : Collections.emptyList(),
activeRecord.getAttachment().getCaption(),
rail.indexOf(activeMedia)));
}
public void resubmitPreviewData() {
previewData.postValue(previewData.getValue());
}
private int getCursorPosition(int position) {
if (cursor == null) {
return 0;
}
if (leftIsRecent) return position;
else return cursor.getCount() - 1 - position;
}
private @Nullable Media toMedia(@NonNull MediaRecord mediaRecord) {
Uri uri = mediaRecord.getAttachment().getUri();
if (uri == null) {
return null;
}
return new Media(uri,
mediaRecord.getContentType(),
mediaRecord.getDate(),
mediaRecord.getAttachment().getWidth(),
mediaRecord.getAttachment().getHeight(),
mediaRecord.getAttachment().getSize(),
0,
mediaRecord.getAttachment().isBorderless(),
mediaRecord.getAttachment().isVideoGif(),
Optional.empty(),
Optional.ofNullable(mediaRecord.getAttachment().getCaption()),
Optional.empty());
}
public LiveData<PreviewData> getPreviewData() {
return previewData;
}
public static class PreviewData {
private final List<Media> albumThumbnails;
private final String caption;
private final int activePosition;
public PreviewData(@NonNull List<Media> albumThumbnails, @Nullable String caption, int activePosition) {
this.albumThumbnails = albumThumbnails;
this.caption = caption;
this.activePosition = activePosition;
}
public @NonNull List<Media> getAlbumThumbnails() {
return albumThumbnails;
}
public @Nullable String getCaption() {
return caption;
}
public int getActivePosition() {
return activePosition;
}
}
}

Wyświetl plik

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.mediapreview; package org.thoughtcrime.securesms.mediapreview;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -8,6 +9,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.Media;
@ -16,6 +22,7 @@ import org.thoughtcrime.securesms.util.adapter.StableIdGenerator;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.MediaRailViewHolder> { public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.MediaRailViewHolder> {
@ -26,19 +33,21 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
private final List<Media> media; private final List<Media> media;
private final RailItemListener listener; private final RailItemListener listener;
private final StableIdGenerator<Media> stableIdGenerator; private final StableIdGenerator<Media> stableIdGenerator;
private final ImageLoadingListener imageLoadingListener;
private RailItemAddListener addListener; private RailItemAddListener addListener;
private int activePosition; private int activePosition;
private boolean editable; private boolean editable;
private boolean interactive; private boolean interactive;
public MediaRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemListener listener, boolean editable) { public MediaRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemListener listener, boolean editable, ImageLoadingListener imageLoadingListener) {
this.glideRequests = glideRequests; this.glideRequests = glideRequests;
this.media = new ArrayList<>(); this.media = new ArrayList<>();
this.listener = listener; this.listener = listener;
this.editable = editable; this.editable = editable;
this.stableIdGenerator = new StableIdGenerator<>(); this.stableIdGenerator = new StableIdGenerator<>();
this.interactive = true; this.interactive = true;
this.imageLoadingListener = imageLoadingListener;
setHasStableIds(true); setHasStableIds(true);
} }
@ -60,7 +69,7 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
public void onBindViewHolder(@NonNull MediaRailViewHolder viewHolder, int i) { public void onBindViewHolder(@NonNull MediaRailViewHolder viewHolder, int i) {
switch (getItemViewType(i)) { switch (getItemViewType(i)) {
case TYPE_MEDIA: case TYPE_MEDIA:
((MediaViewHolder) viewHolder).bind(media.get(i), i == activePosition, glideRequests, listener, i - activePosition, editable, interactive); ((MediaViewHolder) viewHolder).bind(media.get(i), i == activePosition, glideRequests, listener, i - activePosition, editable, interactive, imageLoadingListener);
break; break;
case TYPE_BUTTON: case TYPE_BUTTON:
((ButtonViewHolder) viewHolder).bind(addListener); ((ButtonViewHolder) viewHolder).bind(addListener);
@ -159,9 +168,10 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
void bind(@NonNull Media media, boolean isActive, @NonNull GlideRequests glideRequests, void bind(@NonNull Media media, boolean isActive, @NonNull GlideRequests glideRequests,
@NonNull RailItemListener railItemListener, int distanceFromActive, boolean editable, @NonNull RailItemListener railItemListener, int distanceFromActive, boolean editable,
boolean interactive) boolean interactive, @NonNull ImageLoadingListener listener)
{ {
image.setImageResource(glideRequests, media.getUri()); listener.onRequest();
image.setImageResource(glideRequests, media.getUri(), 0, 0, false, listener);
image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive)); image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive));
outline.setVisibility(isActive && interactive ? View.VISIBLE : View.GONE); outline.setVisibility(isActive && interactive ? View.VISIBLE : View.GONE);
@ -208,4 +218,32 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
public interface RailItemAddListener { public interface RailItemAddListener {
void onRailItemAddClicked(); void onRailItemAddClicked();
} }
abstract static class ImageLoadingListener implements RequestListener<Drawable> {
final private AtomicInteger activeJobs = new AtomicInteger();
void onRequest() {
activeJobs.incrementAndGet();
}
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
int count = activeJobs.decrementAndGet();
if (count == 0) {
onAllRequestsFinished();
}
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
int count = activeJobs.decrementAndGet();
if (count == 0) {
onAllRequestsFinished();
}
return false;
}
abstract void onAllRequestsFinished();
}
} }

Wyświetl plik

@ -142,11 +142,6 @@ public final class VideoMediaPreviewFragment extends MediaPreviewFragment {
} }
} }
@Override
public @Nullable ViewGroup getBottomBarControls() {
return videoView.getControlView();
}
@Override @Override
public void setBottomButtonControls(@NonNull MediaPreviewPlayerControlView playerControlView) { public void setBottomButtonControls(@NonNull MediaPreviewPlayerControlView playerControlView) {
videoView.setControlView(playerControlView); videoView.setControlView(playerControlView);

Wyświetl plik

@ -6,7 +6,6 @@ import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.WindowManager
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback

Wyświetl plik

@ -51,7 +51,7 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/media_preview_album_rail" android:id="@+id/media_preview_album_rail"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginTop="11dp" android:layout_marginTop="11dp"

Wyświetl plik

@ -18,7 +18,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:animateLayoutChanges="true" android:visibility="invisible"
android:alpha="0"
android:animateLayoutChanges="false"
android:background="@color/signal_dark_colorSurface_87" android:background="@color/signal_dark_colorSurface_87"
android:gravity="bottom" android:gravity="bottom"
android:orientation="vertical" android:orientation="vertical"