mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 04:06:14 +00:00
Support drag multi-selection for media gallery.
This commit is contained in:
@@ -175,7 +175,7 @@ class MediaSelectionViewModel(
|
||||
return store.state.storySendRequirements
|
||||
}
|
||||
|
||||
private fun addMedia(media: Set<Media>) {
|
||||
fun addMedia(media: Set<Media>) {
|
||||
val newSelectionList: List<Media> = linkedSetOf<Media>().apply {
|
||||
addAll(store.state.selectedMedia)
|
||||
addAll(media)
|
||||
@@ -269,9 +269,13 @@ class MediaSelectionViewModel(
|
||||
}
|
||||
|
||||
fun removeMedia(media: Media) {
|
||||
removeMedia(setOf(media))
|
||||
}
|
||||
|
||||
fun removeMedia(media: Set<Media>) {
|
||||
val snapshot = store.state
|
||||
val newMediaList = snapshot.selectedMedia - media
|
||||
val oldFocusIndex = snapshot.selectedMedia.indexOf(media)
|
||||
val oldFocusIndex = snapshot.selectedMedia.indexOf(media.first())
|
||||
val newFocus = when {
|
||||
newMediaList.isEmpty() -> null
|
||||
media == snapshot.focusedMedia -> newMediaList[Util.clamp(oldFocusIndex, 0, newMediaList.size - 1)]
|
||||
@@ -282,7 +286,7 @@ class MediaSelectionViewModel(
|
||||
it.copy(
|
||||
selectedMedia = newMediaList,
|
||||
focusedMedia = newFocus,
|
||||
editorStateMap = it.editorStateMap - media.uri,
|
||||
editorStateMap = it.editorStateMap - media.map { it.uri },
|
||||
cameraFirstCapture = if (media == it.cameraFirstCapture) null else it.cameraFirstCapture
|
||||
)
|
||||
}
|
||||
@@ -292,9 +296,9 @@ class MediaSelectionViewModel(
|
||||
}
|
||||
|
||||
selectedMediaSubject.onNext(newMediaList)
|
||||
repository.deleteBlobs(listOf(media))
|
||||
repository.deleteBlobs(media.toList())
|
||||
|
||||
Log.d(TAG, "User removed ${media.uri} from message.")
|
||||
Log.d(TAG, "User removed ${media.forEach { it.uri }} from message.")
|
||||
cancelUpload(media)
|
||||
}
|
||||
|
||||
@@ -450,6 +454,10 @@ class MediaSelectionViewModel(
|
||||
}
|
||||
|
||||
private fun cancelUpload(media: Media) {
|
||||
cancelUpload(setOf(media))
|
||||
}
|
||||
|
||||
private fun cancelUpload(media: Set<Media>) {
|
||||
repository.uploadRepository.cancelUpload(media)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.map
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
||||
import org.thoughtcrime.securesms.conversation.ManageContextMenu
|
||||
@@ -23,6 +25,8 @@ import org.thoughtcrime.securesms.databinding.V2MediaGalleryFragmentBinding
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaRepository
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
|
||||
import org.thoughtcrime.securesms.mediasend.v2.review.MediaGalleryGridItemTouchListener
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
@@ -44,6 +48,10 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
|
||||
factoryProducer = { MediaGalleryViewModel.Factory(null, null, MediaGalleryRepository(requireContext(), MediaRepository())) }
|
||||
)
|
||||
|
||||
private val sharedViewModel: MediaSelectionViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() }
|
||||
)
|
||||
|
||||
private lateinit var callbacks: Callbacks
|
||||
|
||||
private var selectedMediaTouchHelper: ItemTouchHelper? = null
|
||||
@@ -54,6 +62,8 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
|
||||
|
||||
private val viewStateLiveData = MutableLiveData(ViewState())
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
onBack()
|
||||
@@ -64,6 +74,8 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
|
||||
callbacks = requireListener()
|
||||
val binding = V2MediaGalleryFragmentBinding.bind(view)
|
||||
|
||||
lifecycleDisposable.bindTo(this)
|
||||
|
||||
SystemWindowInsetsSetter.attach(view, viewLifecycleOwner, WindowInsetsCompat.Type.navigationBars())
|
||||
|
||||
binding.mediaGalleryToolbar.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
@@ -130,8 +142,45 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
|
||||
binding.mediaGallerySelected.adapter = selectedAdapter
|
||||
selectedMediaTouchHelper?.attachToRecyclerView(binding.mediaGallerySelected)
|
||||
|
||||
val mediaGalleryGridItemTouchListener = MediaGalleryGridItemTouchListener()
|
||||
val onDragSelectListener = object : MediaGalleryGridItemTouchListener.OnDragSelectListener {
|
||||
override fun onSelectionStarted(start: Int) {
|
||||
galleryAdapter.getModel(start).ifPresent {
|
||||
val fileModel = it as MediaGallerySelectableItem.FileModel
|
||||
val media = fileModel.media
|
||||
if (fileModel.isSelected) {
|
||||
callbacks.onMediaUnselected(media)
|
||||
mediaGalleryGridItemTouchListener.setIsActive(false)
|
||||
} else {
|
||||
callbacks.onMediaSelected(media)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSelectChange(start: Int, end: Int, shouldSelect: Boolean) {
|
||||
val mediaSet = (start..end)
|
||||
.mapNotNull { i ->
|
||||
galleryAdapter.getModel(i).orElse(null) as? MediaGallerySelectableItem.FileModel
|
||||
}
|
||||
.map { fileModel ->
|
||||
fileModel.media
|
||||
}
|
||||
.toSet()
|
||||
|
||||
if (mediaSet.isNotEmpty()) {
|
||||
if (shouldSelect) {
|
||||
callbacks.onMediaSelected(mediaSet)
|
||||
} else {
|
||||
callbacks.onMediaUnselected(mediaSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mediaGalleryGridItemTouchListener.withSelectListener(onDragSelectListener)
|
||||
|
||||
MediaGallerySelectableItem.registerAdapter(
|
||||
mappingAdapter = galleryAdapter,
|
||||
mediaGalleryGridItemTouchListener = mediaGalleryGridItemTouchListener,
|
||||
onMediaFolderClicked = {
|
||||
onBackPressedCallback.isEnabled = true
|
||||
viewModel.setMediaFolder(it)
|
||||
@@ -148,6 +197,7 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
|
||||
|
||||
binding.mediaGalleryGrid.adapter = galleryAdapter
|
||||
binding.mediaGalleryGrid.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(2)))
|
||||
binding.mediaGalleryGrid.addOnItemTouchListener(mediaGalleryGridItemTouchListener)
|
||||
|
||||
viewStateLiveData.observe(viewLifecycleOwner) { state ->
|
||||
binding.mediaGalleryBottomBarGroup.visible = state.selectedMedia.isNotEmpty()
|
||||
@@ -212,6 +262,13 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackPressedCallback)
|
||||
|
||||
lifecycleDisposable += sharedViewModel.mediaErrors
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
mediaGalleryGridItemTouchListener.stopAutoScroll()
|
||||
mediaGalleryGridItemTouchListener.setIsActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -285,6 +342,8 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
|
||||
fun isCameraEnabled(): Boolean = true
|
||||
fun isMultiselectEnabled(): Boolean = false
|
||||
fun onMediaSelected(media: Media)
|
||||
fun onMediaSelected(media: Set<Media>) = Unit
|
||||
fun onMediaUnselected(media: Set<Media>) = Unit
|
||||
fun onMediaUnselected(media: Media): Unit = throw UnsupportedOperationException()
|
||||
fun onSelectedMediaClicked(media: Media): Unit = throw UnsupportedOperationException()
|
||||
fun onNavigateToCamera(): Unit = throw UnsupportedOperationException()
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaFolder
|
||||
import org.thoughtcrime.securesms.mediasend.v2.review.MediaGalleryGridItemTouchListener
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
@@ -40,12 +41,13 @@ object MediaGallerySelectableItem {
|
||||
|
||||
fun registerAdapter(
|
||||
mappingAdapter: MappingAdapter,
|
||||
mediaGalleryGridItemTouchListener: MediaGalleryGridItemTouchListener,
|
||||
onMediaFolderClicked: OnMediaFolderClicked,
|
||||
onMediaClicked: OnMediaClicked,
|
||||
isMultiselectEnabled: Boolean
|
||||
) {
|
||||
mappingAdapter.registerFactory(FolderModel::class.java, LayoutFactory({ FolderViewHolder(it, onMediaFolderClicked) }, R.layout.v2_media_gallery_folder_item))
|
||||
mappingAdapter.registerFactory(FileModel::class.java, LayoutFactory({ FileViewHolder(it, onMediaClicked) }, if (isMultiselectEnabled) R.layout.v2_media_gallery_item else R.layout.v2_media_gallery_item_no_check))
|
||||
mappingAdapter.registerFactory(FileModel::class.java, LayoutFactory({ FileViewHolder(it, onMediaClicked, mediaGalleryGridItemTouchListener) }, if (isMultiselectEnabled) R.layout.v2_media_gallery_item else R.layout.v2_media_gallery_item_no_check))
|
||||
mappingAdapter.registerFactory(PlaceholderModel::class.java, LayoutFactory({ PlaceholderViewHolder(it) }, R.layout.v2_media_gallery_placeholder_item))
|
||||
}
|
||||
|
||||
@@ -116,7 +118,7 @@ object MediaGallerySelectableItem {
|
||||
}
|
||||
}
|
||||
|
||||
class FileViewHolder(itemView: View, private val onMediaClicked: OnMediaClicked) : BaseViewHolder<FileModel>(itemView) {
|
||||
class FileViewHolder(itemView: View, private val onMediaClicked: OnMediaClicked, private val mediaGalleryGridItemTouchListener: MediaGalleryGridItemTouchListener) : BaseViewHolder<FileModel>(itemView) {
|
||||
|
||||
private val selectedPadding = DimensionUnit.DP.toPixels(12f)
|
||||
private val selectedRadius = DimensionUnit.DP.toPixels(12f)
|
||||
@@ -126,6 +128,10 @@ object MediaGallerySelectableItem {
|
||||
checkView?.visible = model.isSelected
|
||||
checkView?.text = "${model.selectionOneBasedIndex}"
|
||||
itemView.setOnClickListener { onMediaClicked(model.media, model.isSelected) }
|
||||
itemView.setOnLongClickListener {
|
||||
mediaGalleryGridItemTouchListener.startDragSelection(bindingAdapterPosition)
|
||||
true
|
||||
}
|
||||
playOverlay?.visible = MediaUtil.isVideo(model.media.contentType) && !model.media.isVideoGif
|
||||
title?.visible = false
|
||||
|
||||
|
||||
@@ -93,6 +93,14 @@ class MediaSelectionGalleryFragment : Fragment(R.layout.fragment_container), Med
|
||||
sharedViewModel.removeMedia(media)
|
||||
}
|
||||
|
||||
override fun onMediaSelected(media: Set<Media>) {
|
||||
sharedViewModel.addMedia(media)
|
||||
}
|
||||
|
||||
override fun onMediaUnselected(media: Set<Media>) {
|
||||
sharedViewModel.removeMedia(media)
|
||||
}
|
||||
|
||||
override fun onSelectedMediaClicked(media: Media) {
|
||||
sharedViewModel.onPageChanged(media)
|
||||
navigator.goToReview(findNavController())
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.mediasend.v2.review
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.view.MotionEvent
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.OverScroller
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class MediaGalleryGridItemTouchListener : RecyclerView.OnItemTouchListener {
|
||||
private var isActive = false
|
||||
private var start = RecyclerView.NO_POSITION
|
||||
private var end = RecyclerView.NO_POSITION
|
||||
private var inTopSpot = false
|
||||
private var inBottomSpot = false
|
||||
private var scrollDistance = 0
|
||||
private var lastX = Float.MIN_VALUE
|
||||
private var lastY = Float.MIN_VALUE
|
||||
private var lastStart = RecyclerView.NO_POSITION
|
||||
private var lastEnd = RecyclerView.NO_POSITION
|
||||
|
||||
private var selectListener: OnDragSelectListener? = null
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private var scroller: OverScroller? = null
|
||||
|
||||
private var topBoundFrom = 0
|
||||
private var topBoundTo = 0
|
||||
private var bottomBoundFrom = 0
|
||||
private var bottomBoundTo = 0
|
||||
private var maxScrollDistance = 16
|
||||
private var autoScrollDistance = (Resources.getSystem().displayMetrics.density * 56).toInt()
|
||||
private var touchRegionTopOffset = 0
|
||||
private var touchRegionBottomOffset = 0
|
||||
private var scrollAboveTopRegion = true
|
||||
private var scrollBelowTopRegion = true
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
isActive = false
|
||||
start = RecyclerView.NO_POSITION
|
||||
end = RecyclerView.NO_POSITION
|
||||
lastStart = RecyclerView.NO_POSITION
|
||||
lastEnd = RecyclerView.NO_POSITION
|
||||
inTopSpot = false
|
||||
inBottomSpot = false
|
||||
lastX = Float.MIN_VALUE
|
||||
lastY = Float.MIN_VALUE
|
||||
stopAutoScroll()
|
||||
}
|
||||
|
||||
fun stopAutoScroll() {
|
||||
if (scroller != null && !scroller!!.isFinished) {
|
||||
recyclerView?.removeCallbacks(scrollRunnable)
|
||||
scroller?.abortAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
fun withSelectListener(selectListener: OnDragSelectListener): MediaGalleryGridItemTouchListener {
|
||||
this.selectListener = selectListener
|
||||
return this
|
||||
}
|
||||
|
||||
private val scrollRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (scroller != null && scroller!!.computeScrollOffset()) {
|
||||
scrollBy(scrollDistance)
|
||||
recyclerView?.postOnAnimation(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startDragSelection(position: Int) {
|
||||
isActive = true
|
||||
start = position
|
||||
end = position
|
||||
lastStart = position
|
||||
lastEnd = position
|
||||
selectListener?.onSelectionStarted(position)
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
|
||||
if (!isActive || rv.adapter?.itemCount == 0) return false
|
||||
|
||||
when (e.action) {
|
||||
MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_DOWN -> reset()
|
||||
}
|
||||
|
||||
recyclerView = rv
|
||||
val height = rv.height
|
||||
topBoundFrom = 0 + touchRegionTopOffset
|
||||
topBoundTo = topBoundFrom + autoScrollDistance
|
||||
bottomBoundFrom = height + touchRegionBottomOffset - autoScrollDistance
|
||||
bottomBoundTo = height + touchRegionBottomOffset
|
||||
return true
|
||||
}
|
||||
|
||||
fun setIsActive(isActive: Boolean) {
|
||||
this.isActive = isActive
|
||||
}
|
||||
|
||||
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
|
||||
if (!isActive) return
|
||||
|
||||
when (e.action) {
|
||||
MotionEvent.ACTION_DOWN -> updateSelectedRange(rv, e)
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (!inTopSpot && !inBottomSpot) updateSelectedRange(rv, e)
|
||||
processAutoScroll(e)
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> reset()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectedRange(rv: RecyclerView, e: MotionEvent) {
|
||||
updateSelectedRange(rv, e.x, e.y)
|
||||
}
|
||||
|
||||
private fun processAutoScroll(event: MotionEvent) {
|
||||
val y = event.y.toInt()
|
||||
val scrollSpeedFactor: Float
|
||||
when {
|
||||
y in topBoundFrom..topBoundTo -> {
|
||||
lastX = event.x
|
||||
lastY = event.y
|
||||
scrollSpeedFactor = (topBoundTo - topBoundFrom - (y - topBoundFrom)).toFloat() / (topBoundTo - topBoundFrom)
|
||||
scrollDistance = (maxScrollDistance * scrollSpeedFactor * -1f).toInt()
|
||||
if (!inTopSpot) {
|
||||
inTopSpot = true
|
||||
startAutoScroll()
|
||||
}
|
||||
}
|
||||
scrollAboveTopRegion && y < topBoundFrom -> {
|
||||
lastX = event.x
|
||||
lastY = event.y
|
||||
scrollDistance = -maxScrollDistance
|
||||
if (!inTopSpot) {
|
||||
inTopSpot = true
|
||||
startAutoScroll()
|
||||
}
|
||||
}
|
||||
y in bottomBoundFrom..bottomBoundTo -> {
|
||||
lastX = event.x
|
||||
lastY = event.y
|
||||
scrollSpeedFactor = (y - bottomBoundFrom).toFloat() / (bottomBoundTo - bottomBoundFrom)
|
||||
scrollDistance = (maxScrollDistance * scrollSpeedFactor).toInt()
|
||||
if (!inBottomSpot) {
|
||||
inBottomSpot = true
|
||||
startAutoScroll()
|
||||
}
|
||||
}
|
||||
scrollBelowTopRegion && y > bottomBoundTo -> {
|
||||
lastX = event.x
|
||||
lastY = event.y
|
||||
scrollDistance = maxScrollDistance
|
||||
if (!inTopSpot) {
|
||||
inTopSpot = true
|
||||
startAutoScroll()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
inBottomSpot = false
|
||||
inTopSpot = false
|
||||
lastX = Float.MIN_VALUE
|
||||
lastY = Float.MIN_VALUE
|
||||
stopAutoScroll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectedRange(rv: RecyclerView, x: Float, y: Float) {
|
||||
val child = rv.findChildViewUnder(x, y)
|
||||
if (child != null) {
|
||||
val position = rv.getChildAdapterPosition(child)
|
||||
if (position != RecyclerView.NO_POSITION && end != position) {
|
||||
end = position
|
||||
notifySelectRangeChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startAutoScroll() {
|
||||
val context = recyclerView?.context ?: return
|
||||
initScroller(context)
|
||||
if (scroller?.isFinished == true) {
|
||||
recyclerView?.removeCallbacks(scrollRunnable)
|
||||
scroller?.startScroll(0, scroller!!.currY, 0, 5000, 100000)
|
||||
recyclerView!!.postOnAnimation(scrollRunnable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifySelectRangeChange() {
|
||||
if (selectListener == null || start == RecyclerView.NO_POSITION || end == RecyclerView.NO_POSITION) return
|
||||
|
||||
val newStart = minOf(start, end)
|
||||
val newEnd = maxOf(start, end)
|
||||
when {
|
||||
lastStart == RecyclerView.NO_POSITION || lastEnd == RecyclerView.NO_POSITION -> {
|
||||
if (newEnd - newStart == 1) selectListener?.onSelectChange(newStart, newStart, true)
|
||||
else selectListener?.onSelectChange(newStart, newEnd, true)
|
||||
}
|
||||
newStart > lastStart -> selectListener?.onSelectChange(lastStart, newStart - 1, false)
|
||||
newStart < lastStart -> selectListener?.onSelectChange(newStart, lastStart - 1, true)
|
||||
}
|
||||
|
||||
when {
|
||||
newEnd > lastEnd -> selectListener?.onSelectChange(lastEnd + 1, newEnd, true)
|
||||
newEnd < lastEnd -> selectListener?.onSelectChange(newEnd + 1, lastEnd, false)
|
||||
}
|
||||
|
||||
lastStart = newStart
|
||||
lastEnd = newEnd
|
||||
}
|
||||
|
||||
private fun initScroller(context: Context) {
|
||||
if (scroller == null) scroller = OverScroller(context, LinearInterpolator())
|
||||
}
|
||||
|
||||
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { }
|
||||
|
||||
private fun scrollBy(distance: Int) {
|
||||
val scrollDist = if (distance > 0) minOf(distance, maxScrollDistance) else maxOf(distance, -maxScrollDistance)
|
||||
recyclerView?.scrollBy(0, scrollDist)
|
||||
if (lastX != Float.MIN_VALUE && lastY != Float.MIN_VALUE) {
|
||||
updateSelectedRange(recyclerView!!, lastX, lastY)
|
||||
}
|
||||
}
|
||||
|
||||
interface OnDragSelectListener {
|
||||
fun onSelectionStarted(start: Int)
|
||||
fun onSelectChange(start: Int, end: Int, shouldSelect: Boolean)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user