kopia lustrzana https://github.com/ryukoposting/Signal-Android
Fade in media detail view.
rodzic
2856697109
commit
7ad6d95b27
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Ładowanie…
Reference in New Issue