kopia lustrzana https://github.com/ryukoposting/Signal-Android
rodzic
52a5fb8ea2
commit
fb8e81cf50
|
@ -513,7 +513,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||||
mediaIds = state.sharedMediaIds,
|
mediaIds = state.sharedMediaIds,
|
||||||
onMediaRecordClick = { mediaRecord, isLtr ->
|
onMediaRecordClick = { mediaRecord, isLtr ->
|
||||||
startActivityForResult(
|
startActivityForResult(
|
||||||
MediaIntentFactory.intentFromMediaRecord(requireContext(), mediaRecord, isLtr),
|
MediaIntentFactory.intentFromMediaRecord(requireContext(), mediaRecord, isLtr, allMediaInRail = true),
|
||||||
REQUEST_CODE_RETURN_FROM_MEDIA
|
REQUEST_CODE_RETURN_FROM_MEDIA
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,8 @@ object MediaIntentFactory {
|
||||||
fun intentFromMediaRecord(
|
fun intentFromMediaRecord(
|
||||||
context: Context,
|
context: Context,
|
||||||
mediaRecord: MediaRecord,
|
mediaRecord: MediaRecord,
|
||||||
leftIsRecent: Boolean
|
leftIsRecent: Boolean,
|
||||||
|
allMediaInRail: Boolean
|
||||||
): Intent {
|
): Intent {
|
||||||
val attachment: DatabaseAttachment = mediaRecord.attachment!!
|
val attachment: DatabaseAttachment = mediaRecord.attachment!!
|
||||||
return create(
|
return create(
|
||||||
|
@ -65,7 +66,7 @@ object MediaIntentFactory {
|
||||||
attachment.size,
|
attachment.size,
|
||||||
attachment.caption,
|
attachment.caption,
|
||||||
leftIsRecent,
|
leftIsRecent,
|
||||||
allMediaInRail = true,
|
allMediaInRail = allMediaInRail,
|
||||||
sorting = MediaDatabase.Sorting.Newest,
|
sorting = MediaDatabase.Sorting.Newest,
|
||||||
isVideoGif = attachment.isVideoGif
|
isVideoGif = attachment.isVideoGif
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import androidx.core.os.bundleOf
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment
|
import org.thoughtcrime.securesms.attachments.Attachment
|
||||||
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
|
|
||||||
class MediaPreviewV2Adapter(val fragment: Fragment) : FragmentStateAdapter(fragment) {
|
class MediaPreviewV2Adapter(val fragment: Fragment) : FragmentStateAdapter(fragment) {
|
||||||
|
@ -37,6 +38,10 @@ class MediaPreviewV2Adapter(val fragment: Fragment) : FragmentStateAdapter(fragm
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun findItemPosition(media: Media): Int {
|
||||||
|
return items.indexOfFirst { it.uri == media.uri }
|
||||||
|
}
|
||||||
|
|
||||||
fun updateBackingItems(newItems: Collection<Attachment>) {
|
fun updateBackingItems(newItems: Collection<Attachment>) {
|
||||||
if (newItems != items) {
|
if (newItems != items) {
|
||||||
items = newItems.toList()
|
items = newItems.toList()
|
||||||
|
|
|
@ -24,7 +24,8 @@ import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.isVisible
|
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.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||||
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
|
||||||
|
@ -42,7 +43,9 @@ 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.mediapreview.mediarail.CenterDecoration
|
||||||
|
import org.thoughtcrime.securesms.mediapreview.mediarail.MediaRailAdapter
|
||||||
|
import org.thoughtcrime.securesms.mediapreview.mediarail.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
|
||||||
|
@ -137,20 +140,11 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
|
||||||
|
|
||||||
private fun initializeAlbumRail() {
|
private fun initializeAlbumRail() {
|
||||||
binding.mediaPreviewPlaybackControls.recyclerView.apply {
|
binding.mediaPreviewPlaybackControls.recyclerView.apply {
|
||||||
this.itemAnimator = null // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
|
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
|
||||||
PagerSnapHelper().attachToRecyclerView(this)
|
addItemDecoration(CenterDecoration(0))
|
||||||
albumRailAdapter = MediaRailAdapter(
|
albumRailAdapter = MediaRailAdapter(
|
||||||
GlideApp.with(this@MediaPreviewV2Fragment),
|
GlideApp.with(this@MediaPreviewV2Fragment),
|
||||||
object : MediaRailAdapter.RailItemListener {
|
{ media -> jumpViewPagerToMedia(media) },
|
||||||
override fun onRailItemClicked(distanceFromActive: Int) {
|
|
||||||
binding.mediaPager.currentItem += distanceFromActive
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRailItemDeleteClicked(distanceFromActive: Int) {
|
|
||||||
throw UnsupportedOperationException("Callback unsupported.")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
object : ImageLoadingListener() {
|
object : ImageLoadingListener() {
|
||||||
override fun onAllRequestsFinished() {
|
override fun onAllRequestsFinished() {
|
||||||
crossfadeViewIn(this@apply)
|
crossfadeViewIn(this@apply)
|
||||||
|
@ -281,14 +275,40 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
|
||||||
if (albumRail.visibility == GONE) {
|
if (albumRail.visibility == GONE) {
|
||||||
albumRail.visibility = View.INVISIBLE
|
albumRail.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
albumRailAdapter.setMedia(albumThumbnailMedia, albumPosition)
|
|
||||||
albumRail.smoothScrollToPosition(albumPosition)
|
albumRailAdapter.currentItemPosition = albumPosition
|
||||||
|
albumRailAdapter.submitList(albumThumbnailMedia)
|
||||||
|
scrollAlbumRailToCurrentAdapterPosition()
|
||||||
} else {
|
} else {
|
||||||
albumRail.visibility = View.GONE
|
albumRail.visibility = View.GONE
|
||||||
albumRailAdapter.setMedia(emptyList())
|
albumRailAdapter.submitList(emptyList())
|
||||||
|
albumRailAdapter.imageLoadingListener.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun scrollAlbumRailToCurrentAdapterPosition() {
|
||||||
|
val currentItemPosition = albumRailAdapter.currentItemPosition
|
||||||
|
val currentList = albumRailAdapter.currentList
|
||||||
|
val albumRail: RecyclerView = binding.mediaPreviewPlaybackControls.recyclerView
|
||||||
|
var selectedItemWidth = -1
|
||||||
|
for (i in currentList.indices) {
|
||||||
|
val isSelected = i == currentItemPosition
|
||||||
|
val stableId = albumRailAdapter.getItemId(i)
|
||||||
|
val viewHolder = albumRail.findViewHolderForItemId(stableId) as? MediaRailAdapter.MediaRailViewHolder
|
||||||
|
if (viewHolder != null) {
|
||||||
|
viewHolder.setSelectedItem(isSelected)
|
||||||
|
if (isSelected) {
|
||||||
|
selectedItemWidth = viewHolder.itemView.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val offsetFromStart = (albumRail.width - selectedItemWidth) / 2
|
||||||
|
val smoothScroller = OffsetSmoothScroller(requireContext(), offsetFromStart)
|
||||||
|
smoothScroller.targetPosition = currentItemPosition
|
||||||
|
val layoutManager = albumRail.layoutManager as LinearLayoutManager
|
||||||
|
layoutManager.startSmoothScroll(smoothScroller)
|
||||||
|
}
|
||||||
|
|
||||||
private fun crossfadeViewIn(view: View, duration: Long = 200) {
|
private fun crossfadeViewIn(view: View, duration: Long = 200) {
|
||||||
if (!view.isVisible) {
|
if (!view.isVisible) {
|
||||||
val viewPropertyAnimator = view.animate()
|
val viewPropertyAnimator = view.animate()
|
||||||
|
@ -297,6 +317,11 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
|
||||||
.withStartAction {
|
.withStartAction {
|
||||||
view.visibility = VISIBLE
|
view.visibility = VISIBLE
|
||||||
}
|
}
|
||||||
|
.withEndAction {
|
||||||
|
if (view == binding.mediaPreviewPlaybackControls.recyclerView) {
|
||||||
|
scrollAlbumRailToCurrentAdapterPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
if (Build.VERSION.SDK_INT >= 21) {
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
viewPropertyAnimator.interpolator = PathInterpolator(0.17f, 0.17f, 0f, 1f)
|
viewPropertyAnimator.interpolator = PathInterpolator(0.17f, 0.17f, 0f, 1f)
|
||||||
}
|
}
|
||||||
|
@ -306,6 +331,12 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
|
||||||
|
|
||||||
private fun getMediaPreviewFragmentFromChildFragmentManager(currentPosition: Int) = childFragmentManager.findFragmentByTag("f$currentPosition") as? MediaPreviewFragment
|
private fun getMediaPreviewFragmentFromChildFragmentManager(currentPosition: Int) = childFragmentManager.findFragmentByTag("f$currentPosition") as? MediaPreviewFragment
|
||||||
|
|
||||||
|
private fun jumpViewPagerToMedia(media: Media) {
|
||||||
|
val viewPagerAdapter = binding.mediaPager.adapter as MediaPreviewV2Adapter
|
||||||
|
val position = viewPagerAdapter.findItemPosition(media)
|
||||||
|
binding.mediaPager.setCurrentItem(position, true)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String {
|
private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String {
|
||||||
val recipient: Recipient = Recipient.live(mediaRecord.recipientId).get()
|
val recipient: Recipient = Recipient.live(mediaRecord.recipientId).get()
|
||||||
val defaultFromString: String = if (mediaRecord.isOutgoing) {
|
val defaultFromString: String = if (mediaRecord.isOutgoing) {
|
||||||
|
@ -482,6 +513,16 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
|
||||||
viewModel.onDestroyView()
|
viewModel.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class OffsetSmoothScroller(context: Context, val offset: Int) : LinearSmoothScroller(context) {
|
||||||
|
override fun getHorizontalSnapPreference(): Int {
|
||||||
|
return SNAP_TO_START
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun calculateDxToMakeVisible(view: View?, snapPreference: Int): Int {
|
||||||
|
return offset + super.calculateDxToMakeVisible(view, snapPreference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ARGS_KEY: String = "args"
|
const val ARGS_KEY: String = "args"
|
||||||
|
|
||||||
|
|
|
@ -1,249 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.mediapreview;
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
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.components.ThumbnailView;
|
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
|
||||||
import org.thoughtcrime.securesms.util.adapter.StableIdGenerator;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.MediaRailViewHolder> {
|
|
||||||
|
|
||||||
private static final int TYPE_MEDIA = 1;
|
|
||||||
private static final int TYPE_BUTTON = 2;
|
|
||||||
|
|
||||||
private final GlideRequests glideRequests;
|
|
||||||
private final List<Media> media;
|
|
||||||
private final RailItemListener listener;
|
|
||||||
private final StableIdGenerator<Media> stableIdGenerator;
|
|
||||||
private final ImageLoadingListener imageLoadingListener;
|
|
||||||
|
|
||||||
private RailItemAddListener addListener;
|
|
||||||
private int activePosition;
|
|
||||||
private boolean editable;
|
|
||||||
private boolean interactive;
|
|
||||||
|
|
||||||
public MediaRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemListener listener, boolean editable, ImageLoadingListener imageLoadingListener) {
|
|
||||||
this.glideRequests = glideRequests;
|
|
||||||
this.media = new ArrayList<>();
|
|
||||||
this.listener = listener;
|
|
||||||
this.editable = editable;
|
|
||||||
this.stableIdGenerator = new StableIdGenerator<>();
|
|
||||||
this.interactive = true;
|
|
||||||
this.imageLoadingListener = imageLoadingListener;
|
|
||||||
|
|
||||||
setHasStableIds(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public MediaRailViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int type) {
|
|
||||||
switch (type) {
|
|
||||||
case TYPE_MEDIA:
|
|
||||||
return new MediaViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.mediarail_media_item, viewGroup, false));
|
|
||||||
case TYPE_BUTTON:
|
|
||||||
return new ButtonViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.mediarail_button_item, viewGroup, false));
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException("Unsupported view type: " + type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull MediaRailViewHolder viewHolder, int i) {
|
|
||||||
switch (getItemViewType(i)) {
|
|
||||||
case TYPE_MEDIA:
|
|
||||||
((MediaViewHolder) viewHolder).bind(media.get(i), i == activePosition, glideRequests, listener, i - activePosition, editable, interactive, imageLoadingListener);
|
|
||||||
break;
|
|
||||||
case TYPE_BUTTON:
|
|
||||||
((ButtonViewHolder) viewHolder).bind(addListener);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException("Unsupported view type: " + getItemViewType(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
if (editable && position == getItemCount() - 1) {
|
|
||||||
return TYPE_BUTTON;
|
|
||||||
} else {
|
|
||||||
return TYPE_MEDIA;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewRecycled(@NonNull MediaRailViewHolder holder) {
|
|
||||||
holder.recycle();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return editable ? media.size() + 1 : media.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
switch (getItemViewType(position)) {
|
|
||||||
case TYPE_MEDIA:
|
|
||||||
return stableIdGenerator.getId(media.get(position));
|
|
||||||
case TYPE_BUTTON:
|
|
||||||
return Long.MAX_VALUE;
|
|
||||||
default:
|
|
||||||
throw new UnsupportedOperationException("Unsupported view type: " + getItemViewType(position));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMedia(@NonNull List<Media> media) {
|
|
||||||
setMedia(media, activePosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMedia(@NonNull List<Media> records, int activePosition) {
|
|
||||||
this.activePosition = activePosition;
|
|
||||||
|
|
||||||
this.media.clear();
|
|
||||||
this.media.addAll(records);
|
|
||||||
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setActivePosition(int activePosition) {
|
|
||||||
this.activePosition = activePosition;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAddButtonListener(@Nullable RailItemAddListener addListener) {
|
|
||||||
this.addListener = addListener;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEditable(boolean editable) {
|
|
||||||
this.editable = editable;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setInteractive(boolean interactive) {
|
|
||||||
this.interactive = interactive;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
static abstract class MediaRailViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
public MediaRailViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract void recycle();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class MediaViewHolder extends MediaRailViewHolder {
|
|
||||||
|
|
||||||
private final ThumbnailView image;
|
|
||||||
private final View outline;
|
|
||||||
private final View deleteButton;
|
|
||||||
private final View captionIndicator;
|
|
||||||
|
|
||||||
MediaViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
image = itemView.findViewById(R.id.rail_item_image);
|
|
||||||
outline = itemView.findViewById(R.id.rail_item_outline);
|
|
||||||
deleteButton = itemView.findViewById(R.id.rail_item_delete);
|
|
||||||
captionIndicator = itemView.findViewById(R.id.rail_item_caption);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bind(@NonNull Media media, boolean isActive, @NonNull GlideRequests glideRequests,
|
|
||||||
@NonNull RailItemListener railItemListener, int distanceFromActive, boolean editable,
|
|
||||||
boolean interactive, @NonNull ImageLoadingListener listener)
|
|
||||||
{
|
|
||||||
listener.onRequest();
|
|
||||||
image.setImageResource(glideRequests, media.getUri(), 0, 0, false, listener);
|
|
||||||
image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive));
|
|
||||||
|
|
||||||
outline.setVisibility(isActive && interactive ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
captionIndicator.setVisibility(media.getCaption().isPresent() ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
if (editable && isActive && interactive) {
|
|
||||||
deleteButton.setVisibility(View.VISIBLE);
|
|
||||||
deleteButton.setOnClickListener(v -> railItemListener.onRailItemDeleteClicked(distanceFromActive));
|
|
||||||
} else {
|
|
||||||
deleteButton.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void recycle() {
|
|
||||||
image.setOnClickListener(null);
|
|
||||||
deleteButton.setOnClickListener(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ButtonViewHolder extends MediaRailViewHolder {
|
|
||||||
|
|
||||||
public ButtonViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
}
|
|
||||||
|
|
||||||
void bind(@Nullable RailItemAddListener addListener) {
|
|
||||||
if (addListener != null) {
|
|
||||||
itemView.setOnClickListener(v -> addListener.onRailItemAddClicked());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void recycle() {
|
|
||||||
itemView.setOnClickListener(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface RailItemListener {
|
|
||||||
void onRailItemClicked(int distanceFromActive);
|
|
||||||
void onRailItemDeleteClicked(int distanceFromActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface RailItemAddListener {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package org.thoughtcrime.securesms.mediapreview.mediarail
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.Px
|
||||||
|
import androidx.core.view.doOnPreDraw
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From: <a href="https://stackoverflow.com/a/53510142">https://stackoverflow.com/a/53510142</a>
|
||||||
|
*/
|
||||||
|
class CenterDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
private var firstViewWidth = -1
|
||||||
|
private var lastViewWidth = -1
|
||||||
|
|
||||||
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
super.getItemOffsets(outRect, view, parent, state)
|
||||||
|
val adapterPosition = (view.layoutParams as RecyclerView.LayoutParams).absoluteAdapterPosition
|
||||||
|
val layoutManager = parent.layoutManager as LinearLayoutManager
|
||||||
|
if (adapterPosition == 0) {
|
||||||
|
if (view.width != firstViewWidth) {
|
||||||
|
view.doOnPreDraw { parent.invalidateItemDecorations() }
|
||||||
|
}
|
||||||
|
firstViewWidth = view.width
|
||||||
|
outRect.left = parent.width / 2 - view.width / 2
|
||||||
|
if (layoutManager.itemCount > 1) {
|
||||||
|
outRect.right = spacing / 2
|
||||||
|
} else {
|
||||||
|
outRect.right = outRect.left
|
||||||
|
}
|
||||||
|
} else if (adapterPosition == layoutManager.itemCount - 1) {
|
||||||
|
if (view.width != lastViewWidth) {
|
||||||
|
view.doOnPreDraw { parent.invalidateItemDecorations() }
|
||||||
|
}
|
||||||
|
lastViewWidth = view.width
|
||||||
|
outRect.right = parent.width / 2 - view.width / 2
|
||||||
|
outRect.left = spacing / 2
|
||||||
|
} else {
|
||||||
|
outRect.left = spacing / 2
|
||||||
|
outRect.right = spacing / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
package org.thoughtcrime.securesms.mediapreview.mediarail
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
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.components.ThumbnailView
|
||||||
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.StableIdGenerator
|
||||||
|
import org.thoughtcrime.securesms.util.visible
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the RecyclerView.Adapter for the row of thumbnails present in the media viewer screen.
|
||||||
|
*/
|
||||||
|
class MediaRailAdapter(private val glideRequests: GlideRequests, listener: RailItemListener, imageLoadingListener: ImageLoadingListener) : ListAdapter<Media, MediaRailAdapter.MediaRailViewHolder>(MediaDiffer()) {
|
||||||
|
val imageLoadingListener: ImageLoadingListener
|
||||||
|
|
||||||
|
var currentItemPosition: Int = -1
|
||||||
|
|
||||||
|
private val listener: RailItemListener
|
||||||
|
private val stableIdGenerator: StableIdGenerator<Media>
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.listener = listener
|
||||||
|
stableIdGenerator = StableIdGenerator()
|
||||||
|
this.imageLoadingListener = imageLoadingListener
|
||||||
|
setHasStableIds(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(viewGroup: ViewGroup, type: Int): MediaRailViewHolder {
|
||||||
|
return MediaRailViewHolder(LayoutInflater.from(viewGroup.context).inflate(R.layout.mediarail_media_item, viewGroup, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(viewHolder: MediaRailViewHolder, i: Int) {
|
||||||
|
viewHolder.bind(getItem(i), i == currentItemPosition, glideRequests, listener, imageLoadingListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewRecycled(holder: MediaRailViewHolder) {
|
||||||
|
holder.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return stableIdGenerator.getId(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaRailViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val image: ThumbnailView
|
||||||
|
private val outline: View
|
||||||
|
private val captionIndicator: View
|
||||||
|
|
||||||
|
init {
|
||||||
|
image = itemView.findViewById(R.id.rail_item_image)
|
||||||
|
outline = itemView.findViewById(R.id.rail_item_outline)
|
||||||
|
captionIndicator = itemView.findViewById(R.id.rail_item_caption)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(
|
||||||
|
media: Media,
|
||||||
|
isCurrentlySelected: Boolean,
|
||||||
|
glideRequests: GlideRequests,
|
||||||
|
railItemListener: RailItemListener,
|
||||||
|
listener: ImageLoadingListener
|
||||||
|
) {
|
||||||
|
listener.onRequest()
|
||||||
|
image.setImageResource(glideRequests, media.uri, 0, 0, false, listener)
|
||||||
|
image.setOnClickListener { railItemListener.onRailItemClicked(media) }
|
||||||
|
captionIndicator.visibility = if (media.caption.isPresent) View.VISIBLE else View.GONE
|
||||||
|
setSelectedItem(isCurrentlySelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun recycle() {
|
||||||
|
image.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelectedItem(isActive: Boolean) {
|
||||||
|
outline.visible = isActive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface RailItemListener {
|
||||||
|
fun onRailItemClicked(media: Media)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ImageLoadingListener : RequestListener<Drawable?> {
|
||||||
|
private val activeJobs = AtomicInteger()
|
||||||
|
fun onRequest() {
|
||||||
|
activeJobs.incrementAndGet()
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onLoadFailed(e: GlideException?, model: Any, target: Target<Drawable?>, isFirstResource: Boolean): Boolean {
|
||||||
|
val count = activeJobs.decrementAndGet()
|
||||||
|
if (count == 0) {
|
||||||
|
onAllRequestsFinished()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onResourceReady(resource: Drawable?, model: Any, target: Target<Drawable?>, dataSource: DataSource, isFirstResource: Boolean): Boolean {
|
||||||
|
val count = activeJobs.decrementAndGet()
|
||||||
|
if (count == 0) {
|
||||||
|
onAllRequestsFinished()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
activeJobs.set(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun onAllRequestsFinished()
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaDiffer : DiffUtil.ItemCallback<Media>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Media, newItem: Media): Boolean {
|
||||||
|
return oldItem.uri == newItem.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Media, newItem: Media): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,7 +50,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_marginBottom="11dp"
|
android:layout_marginBottom="11dp"
|
||||||
|
@ -58,7 +58,6 @@
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
tools:layout_height="64dp" />
|
tools:layout_height="64dp" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
tools:viewBindingIgnore="true"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@color/core_grey_95">
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.viewpager.HackyViewPager
|
|
||||||
android:id="@+id/media_pager"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clickable="true"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/media_preview_details_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
android:background="@drawable/image_preview_shade"
|
|
||||||
android:gravity="bottom"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.MaxHeightScrollView
|
|
||||||
android:id="@+id/media_preview_caption_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingTop="32dp"
|
|
||||||
android:animateLayoutChanges="true"
|
|
||||||
app:scrollView_maxHeight="120dp">
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
|
||||||
android:id="@+id/media_preview_caption"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="16dp"
|
|
||||||
android:paddingEnd="16dp"
|
|
||||||
android:paddingBottom="8dp"
|
|
||||||
style="@style/Signal.Text.Body"
|
|
||||||
android:textColor="@color/core_white"
|
|
||||||
android:gravity="bottom"
|
|
||||||
tools:text="With great power comes great responsibility." />
|
|
||||||
|
|
||||||
</org.thoughtcrime.securesms.components.MaxHeightScrollView>
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/media_preview_album_rail"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
tools:layout_height="64dp"/>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/media_preview_playback_controls_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:animateLayoutChanges="true"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
|
||||||
android:id="@+id/toolbar_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@color/media_preview_bar_background"
|
|
||||||
app:elevation="0dp">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/toolbar_cutout_spacer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:theme="?actionBarStyle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
|
||||||
android:background="@android:color/transparent" />
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
|
@ -4,8 +4,8 @@
|
||||||
tools:viewBindingIgnore="true"
|
tools:viewBindingIgnore="true"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="46dp"
|
android:layout_width="@dimen/media_rail_item_size"
|
||||||
android:layout_height="46dp"
|
android:layout_height="@dimen/media_rail_item_size"
|
||||||
android:layout_margin="4dp"
|
android:layout_margin="4dp"
|
||||||
android:animateLayoutChanges="true">
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
|
@ -42,14 +42,4 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible"/>
|
tools:visibility="visible"/>
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/rail_item_delete"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="end|top"
|
|
||||||
android:layout_margin="1dp"
|
|
||||||
android:src="@drawable/ic_x_28"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
|
@ -243,4 +243,5 @@
|
||||||
<dimen name="exo_media_preview_button_height">48dp</dimen>
|
<dimen name="exo_media_preview_button_height">48dp</dimen>
|
||||||
<dimen name="media_preview_button_horizontal_margin">8dp</dimen>
|
<dimen name="media_preview_button_horizontal_margin">8dp</dimen>
|
||||||
<dimen name="media_preview_lottie_button_dimen">36dp</dimen>
|
<dimen name="media_preview_lottie_button_dimen">36dp</dimen>
|
||||||
|
<dimen name="media_rail_item_size">46dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Ładowanie…
Reference in New Issue