kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add support for drag + drop in the media send flow.
rodzic
1dbb6013cb
commit
ddad9acef1
|
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
|
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
|
||||||
import org.thoughtcrime.securesms.util.Util
|
import org.thoughtcrime.securesms.util.Util
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
import java.util.Collections
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel which maintains the list of selected media and other shared values.
|
* ViewModel which maintains the list of selected media and other shared values.
|
||||||
|
@ -62,6 +63,8 @@ class MediaSelectionViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var lastMediaDrag: Pair<Int, Int> = Pair(0, 0)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val recipientId = destination.getRecipientId()
|
val recipientId = destination.getRecipientId()
|
||||||
if (recipientId != null) {
|
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) {
|
fun removeMedia(media: Media) {
|
||||||
val snapshot = store.state
|
val snapshot = store.state
|
||||||
val newMediaList = snapshot.selectedMedia - media
|
val newMediaList = snapshot.selectedMedia - media
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
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 bottomBarGroup: View
|
||||||
private lateinit var selectedRecycler: RecyclerView
|
private lateinit var selectedRecycler: RecyclerView
|
||||||
|
|
||||||
|
private var selectedMediaTouchHelper: ItemTouchHelper? = null
|
||||||
|
|
||||||
private val galleryAdapter = MappingAdapter()
|
private val galleryAdapter = MappingAdapter()
|
||||||
private val selectedAdapter = MappingAdapter()
|
private val selectedAdapter = MappingAdapter()
|
||||||
|
|
||||||
|
@ -88,6 +91,7 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
|
||||||
callbacks.onSelectedMediaClicked(media)
|
callbacks.onSelectedMediaClicked(media)
|
||||||
}
|
}
|
||||||
selectedRecycler.adapter = selectedAdapter
|
selectedRecycler.adapter = selectedAdapter
|
||||||
|
selectedMediaTouchHelper?.attachToRecyclerView(selectedRecycler)
|
||||||
|
|
||||||
MediaGallerySelectableItem.registerAdapter(
|
MediaGallerySelectableItem.registerAdapter(
|
||||||
mappingAdapter = galleryAdapter,
|
mappingAdapter = galleryAdapter,
|
||||||
|
@ -153,6 +157,10 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
|
||||||
viewStateLiveData.value = state
|
viewStateLiveData.value = state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun bindSelectedMediaItemDragHelper(helper: ItemTouchHelper) {
|
||||||
|
selectedMediaTouchHelper = helper
|
||||||
|
}
|
||||||
|
|
||||||
data class ViewState(
|
data class ViewState(
|
||||||
val selectedMedia: List<Media> = listOf()
|
val selectedMedia: List<Media> = listOf()
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,11 +5,13 @@ import android.view.View
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.mediasend.Media
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator
|
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator.Companion.requestPermissionsForCamera
|
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator.Companion.requestPermissionsForCamera
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
|
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
|
||||||
|
import org.thoughtcrime.securesms.mediasend.v2.review.MediaSelectionItemTouchHelper
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
|
|
||||||
private const val MEDIA_GALLERY_TAG = "MEDIA_GALLERY"
|
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?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
mediaGalleryFragment = ensureMediaGalleryFragment()
|
mediaGalleryFragment = ensureMediaGalleryFragment()
|
||||||
|
|
||||||
|
mediaGalleryFragment.bindSelectedMediaItemDragHelper(ItemTouchHelper(MediaSelectionItemTouchHelper(sharedViewModel)))
|
||||||
|
|
||||||
sharedViewModel.state.observe(viewLifecycleOwner) { state ->
|
sharedViewModel.state.observe(viewLifecycleOwner) { state ->
|
||||||
mediaGalleryFragment.onViewStateUpdated(MediaGalleryFragment.ViewState(state.selectedMedia))
|
mediaGalleryFragment.onViewStateUpdated(MediaGalleryFragment.ViewState(state.selectedMedia))
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.setFragmentResultListener
|
import androidx.fragment.app.setFragmentResultListener
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
@ -181,6 +182,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectionRecycler.adapter = selectionAdapter
|
selectionRecycler.adapter = selectionAdapter
|
||||||
|
ItemTouchHelper(MediaSelectionItemTouchHelper(sharedViewModel)).attachToRecyclerView(selectionRecycler)
|
||||||
|
|
||||||
sharedViewModel.state.observe(viewLifecycleOwner) { state ->
|
sharedViewModel.state.observe(viewLifecycleOwner) { state ->
|
||||||
pagerAdapter.submitMedia(state.selectedMedia)
|
pagerAdapter.submitMedia(state.selectedMedia)
|
||||||
|
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package org.thoughtcrime.securesms.recipients;
|
package org.thoughtcrime.securesms.recipients;
|
||||||
|
|
||||||
|
import static org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
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.RegisteredState;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
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.database.model.databaseprotos.RecipientExtras;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
@ -59,12 +60,9 @@ import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
|
||||||
|
|
||||||
public class Recipient {
|
public class Recipient {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(Recipient.class);
|
private static final String TAG = Log.tag(Recipient.class);
|
||||||
|
|
|
@ -55,12 +55,13 @@
|
||||||
android:id="@+id/selection_recycler"
|
android:id="@+id/selection_recycler"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:paddingStart="8dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:paddingEnd="8dp"
|
||||||
android:layout_marginBottom="2dp"
|
android:layout_marginBottom="2dp"
|
||||||
android:alpha="0"
|
android:alpha="0"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
android:clipToPadding="false"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:layout_constraintBottom_toTopOf="@id/controls_shade"
|
app:layout_constraintBottom_toTopOf="@id/controls_shade"
|
||||||
tools:alpha="1"
|
tools:alpha="1"
|
||||||
|
|
Ładowanie…
Reference in New Issue