From ddad9acef10d1a35576896eb4173c1d610337cee Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 7 Sep 2021 19:25:43 -0400 Subject: [PATCH] Add support for drag + drop in the media send flow. --- .../mediasend/v2/MediaSelectionViewModel.kt | 49 +++++++++++++++++ .../v2/gallery/MediaGalleryFragment.kt | 8 +++ .../gallery/MediaSelectionGalleryFragment.kt | 4 ++ .../v2/review/MediaReviewFragment.kt | 2 + .../review/MediaSelectionItemTouchHelper.java | 54 +++++++++++++++++++ .../securesms/recipients/Recipient.java | 6 +-- .../res/layout/v2_media_review_fragment.xml | 5 +- 7 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaSelectionItemTouchHelper.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt index cdaad5669..554737656 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionViewModel.kt @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.scribbles.ImageEditorFragment import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.livedata.Store +import java.util.Collections /** * ViewModel which maintains the list of selected media and other shared values. @@ -62,6 +63,8 @@ class MediaSelectionViewModel( } } + private var lastMediaDrag: Pair = Pair(0, 0) + init { val recipientId = destination.getRecipientId() if (recipientId != null) { @@ -123,6 +126,52 @@ class MediaSelectionViewModel( ) } + fun swapMedia(originalStart: Int, end: Int): Boolean { + var start: Int = originalStart + + if (lastMediaDrag.first == start && lastMediaDrag.second == end) { + return true + } else if (lastMediaDrag.first == start) { + start = lastMediaDrag.second + } + + val snapshot = store.state + + if (end >= snapshot.selectedMedia.size || end < 0 || start >= snapshot.selectedMedia.size || start < 0) { + return false + } + + lastMediaDrag = Pair(originalStart, end) + + val newMediaList = snapshot.selectedMedia.toMutableList() + + if (start < end) { + for (i in start until end) { + Collections.swap(newMediaList, i, i + 1) + } + } else { + for (i in start downTo end + 1) { + Collections.swap(newMediaList, i, i - 1) + } + } + + store.update { + it.copy( + selectedMedia = newMediaList + ) + } + + return true + } + + fun isValidMediaDragPosition(position: Int): Boolean { + return position >= 0 && position < store.state.selectedMedia.size + } + + fun onMediaDragFinished() { + lastMediaDrag = Pair(0, 0) + } + fun removeMedia(media: Media) { val snapshot = store.state val newMediaList = snapshot.selectedMedia - media diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt index 0670aaae4..cce0e744d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration @@ -39,6 +40,8 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) { private lateinit var bottomBarGroup: View private lateinit var selectedRecycler: RecyclerView + private var selectedMediaTouchHelper: ItemTouchHelper? = null + private val galleryAdapter = MappingAdapter() private val selectedAdapter = MappingAdapter() @@ -88,6 +91,7 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) { callbacks.onSelectedMediaClicked(media) } selectedRecycler.adapter = selectedAdapter + selectedMediaTouchHelper?.attachToRecyclerView(selectedRecycler) MediaGallerySelectableItem.registerAdapter( mappingAdapter = galleryAdapter, @@ -153,6 +157,10 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) { viewStateLiveData.value = state } + fun bindSelectedMediaItemDragHelper(helper: ItemTouchHelper) { + selectedMediaTouchHelper = helper + } + data class ViewState( val selectedMedia: List = listOf() ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaSelectionGalleryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaSelectionGalleryFragment.kt index f9aece2d4..4b5d3a095 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaSelectionGalleryFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaSelectionGalleryFragment.kt @@ -5,11 +5,13 @@ import android.view.View import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.ItemTouchHelper import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator.Companion.requestPermissionsForCamera import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel +import org.thoughtcrime.securesms.mediasend.v2.review.MediaSelectionItemTouchHelper import org.thoughtcrime.securesms.permissions.Permissions private const val MEDIA_GALLERY_TAG = "MEDIA_GALLERY" @@ -29,6 +31,8 @@ class MediaSelectionGalleryFragment : Fragment(R.layout.fragment_container), Med override fun onViewCreated(view: View, savedInstanceState: Bundle?) { mediaGalleryFragment = ensureMediaGalleryFragment() + mediaGalleryFragment.bindSelectedMediaItemDragHelper(ItemTouchHelper(MediaSelectionItemTouchHelper(sharedViewModel))) + sharedViewModel.state.observe(viewLifecycleOwner) { state -> mediaGalleryFragment.onViewStateUpdated(MediaGalleryFragment.ViewState(state.selectedMedia)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt index 84cedaa35..14265817c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt @@ -15,6 +15,7 @@ import androidx.core.graphics.drawable.DrawableCompat import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -181,6 +182,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) { } } selectionRecycler.adapter = selectionAdapter + ItemTouchHelper(MediaSelectionItemTouchHelper(sharedViewModel)).attachToRecyclerView(selectionRecycler) sharedViewModel.state.observe(viewLifecycleOwner) { state -> pagerAdapter.submitMedia(state.selectedMedia) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaSelectionItemTouchHelper.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaSelectionItemTouchHelper.java new file mode 100644 index 000000000..6839a3172 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaSelectionItemTouchHelper.java @@ -0,0 +1,54 @@ +package org.thoughtcrime.securesms.mediasend.v2.review; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel; + +/** + * A touch helper for handling drag + drop on the media rail in the media send flow. + */ +public class MediaSelectionItemTouchHelper extends ItemTouchHelper.Callback { + + private final MediaSelectionViewModel viewModel; + + public MediaSelectionItemTouchHelper(MediaSelectionViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public boolean isLongPressDragEnabled() { + return true; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return false; + } + + @Override + public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + if (viewModel.isValidMediaDragPosition(viewHolder.getAdapterPosition())) { + int dragFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT; + return makeMovementFlags(dragFlags, 0); + } else { + return 0; + } + } + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { + return viewModel.swapMedia(viewHolder.getAdapterPosition(), target.getAdapterPosition()); + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + viewModel.onMediaDragFinished(); + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 0c9548c02..7441c5c8b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.recipients; +import static org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier; + import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -32,7 +34,6 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting; import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode; import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; -import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor; import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; @@ -59,12 +60,9 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; -import static org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier; - public class Recipient { private static final String TAG = Log.tag(Recipient.class); diff --git a/app/src/main/res/layout/v2_media_review_fragment.xml b/app/src/main/res/layout/v2_media_review_fragment.xml index 14b731054..beeaa802c 100644 --- a/app/src/main/res/layout/v2_media_review_fragment.xml +++ b/app/src/main/res/layout/v2_media_review_fragment.xml @@ -55,12 +55,13 @@ android:id="@+id/selection_recycler" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginEnd="16dp" + android:paddingStart="8dp" + android:paddingEnd="8dp" android:layout_marginBottom="2dp" android:alpha="0" android:orientation="horizontal" android:visibility="gone" + android:clipToPadding="false" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toTopOf="@id/controls_shade" tools:alpha="1"