From c0a006f2381f449b471ab677a7bf75c89cbc1bb2 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Tue, 3 Sep 2024 13:34:55 +0200 Subject: [PATCH] make dragging be smooth --- .../newplayer/model/VideoPlayerViewModel.kt | 1 + .../model/VideoPlayerViewModelImpl.kt | 27 +++++++- .../model/ViewoPlayerViewModelDummy.kt | 4 ++ .../ui/videoplayer/StreamSelectUI.kt | 10 ++- .../ui/videoplayer/streamselect/StreamItem.kt | 26 ++++++- .../newplayer/utils/RememberHapticFeedback.kt | 68 +++++++++++++++++++ 6 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 new-player/src/main/java/net/newpipe/newplayer/utils/RememberHapticFeedback.kt diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt index ea46523..12aa255 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt @@ -65,4 +65,5 @@ interface VideoPlayerViewModel { fun onStorePlaylist() fun movePlaylistItem(from: Int, to: Int) fun removePlaylistItem(index: Int) + fun onStreamItemDragFinished() } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt index 2cabb5b..265d832 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt @@ -46,6 +46,7 @@ import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.utils.VideoSize import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.ui.ContentScale +import java.util.LinkedList val VIDEOPLAYER_UI_STATE = "video_player_ui_state" @@ -62,6 +63,9 @@ class VideoPlayerViewModelImpl @Inject constructor( private val mutableUiState = MutableStateFlow(VideoPlayerUIState.DEFAULT) private var currentContentRatio = 1F + private var playlistItemToBeMoved: Int? = null + private var playlistItemNewPosition: Int = 0 + private var uiVisibilityJob: Job? = null private var progressUpdaterJob: Job? = null @@ -422,13 +426,34 @@ class VideoPlayerViewModelImpl @Inject constructor( } override fun movePlaylistItem(from: Int, to: Int) { - newPlayer?.movePlaylistItem(from, to) + if(playlistItemToBeMoved == null) { + playlistItemToBeMoved = from + } + playlistItemNewPosition = to + val tempList = LinkedList(uiState.value.playList) + val item = uiState.value.playList[from] + tempList.removeAt(from) + tempList.add(to, item) + mutableUiState.update { + it.copy( + playList = tempList + ) + } + } + + override fun onStreamItemDragFinished() { + playlistItemToBeMoved?.let { + newPlayer?.movePlaylistItem(it, playlistItemNewPosition) + } + playlistItemToBeMoved = null } override fun removePlaylistItem(index: Int) { newPlayer?.removePlaylistItem(index) } + + private fun updateUiMode(newState: UIModeState) { val newPlayMode = newState.toPlayMode() val currentPlayMode = mutableUiState.value.uiMode.toPlayMode() diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt b/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt index 28f8e69..2ccee1c 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/ViewoPlayerViewModelDummy.kt @@ -111,6 +111,10 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel { println("dummy impl") } + override fun onStreamItemDragFinished() { + println("dummy impl") + } + override fun pause() { println("dummy pause") } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt index d671877..64e77da 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/StreamSelectUI.kt @@ -51,7 +51,9 @@ import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar import net.newpipe.newplayer.ui.videoplayer.streamselect.StreamItem import net.newpipe.newplayer.ui.videoplayer.streamselect.StreamSelectTopBar +import net.newpipe.newplayer.utils.ReorderHapticFeedbackType import net.newpipe.newplayer.utils.getInsets +import net.newpipe.newplayer.utils.rememberReorderHapticFeedback import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState @@ -122,15 +124,15 @@ fun ReorderableStreamItemsList( viewModel: VideoPlayerViewModel, uiState: VideoPlayerUIState ) { + val haptic = rememberReorderHapticFeedback() val lazyListState = rememberLazyListState() val reorderableLazyListState = rememberReorderableLazyListState(lazyListState = lazyListState) { from, to -> + haptic.performHapticFeedback(ReorderHapticFeedbackType.MOVE) viewModel.movePlaylistItem(from.index, to.index) } - - LazyColumn( modifier = Modifier .padding(innerPadding) @@ -149,7 +151,9 @@ fun ReorderableStreamItemsList( thumbnail = playlistItem.thumbnail, lengthInMs = playlistItem.lengthInS.toLong() * 1000, onClicked = { viewModel.streamSelected(it) }, - reorderableScope = this@ReorderableItem + reorderableScope = this@ReorderableItem, + haptic = haptic, + onDragFinished = viewModel::onStreamItemDragFinished ) } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt index 1bd0a83..b8add40 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/streamselect/StreamItem.kt @@ -22,6 +22,7 @@ package net.newpipe.newplayer.ui.videoplayer.streamselect import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -39,6 +40,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -55,6 +57,8 @@ import net.newpipe.newplayer.ui.CONTROLLER_UI_BACKGROUND_COLOR import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.utils.BitmapThumbnail import net.newpipe.newplayer.utils.OnlineThumbnail +import net.newpipe.newplayer.utils.ReorderHapticFeedback +import net.newpipe.newplayer.utils.ReorderHapticFeedbackType import net.newpipe.newplayer.utils.Thumbnail import net.newpipe.newplayer.utils.VectorThumbnail import net.newpipe.newplayer.utils.getLocale @@ -100,9 +104,14 @@ fun StreamItem( thumbnail: Thumbnail?, lengthInMs: Long, onClicked: (Int) -> Unit, - reorderableScope: ReorderableCollectionItemScope? + onDragFinished: () -> Unit, + reorderableScope: ReorderableCollectionItemScope?, + haptic: ReorderHapticFeedback? ) { val locale = getLocale()!! + + val interactionSource = remember { MutableInteractionSource() } + Row( modifier = modifier .padding(5.dp) @@ -159,7 +168,16 @@ fun StreamItem( Modifier .aspectRatio(1f) .fillMaxSize() - .draggableHandle() + .draggableHandle( + onDragStarted = { + haptic?.performHapticFeedback(ReorderHapticFeedbackType.START) + }, + onDragStopped = { + haptic?.performHapticFeedback(ReorderHapticFeedbackType.END) + onDragFinished() + }, + interactionSource = interactionSource, + ) } } else { Modifier @@ -190,7 +208,9 @@ fun StreamItemPreview() { thumbnail = null, lengthInMs = 15 * 60 * 1000, onClicked = {}, - reorderableScope = null + reorderableScope = null, + haptic = null, + onDragFinished = {} ) } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/utils/RememberHapticFeedback.kt b/new-player/src/main/java/net/newpipe/newplayer/utils/RememberHapticFeedback.kt new file mode 100644 index 0000000..91f2080 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/utils/RememberHapticFeedback.kt @@ -0,0 +1,68 @@ + /* + * Copyright 2023 Calvin Liang + * + * @Author Calvin Liang + * @Author Christian Schabesberger + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.newpipe.newplayer.utils + +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalView + +enum class ReorderHapticFeedbackType { + START, + MOVE, + END, +} + +open class ReorderHapticFeedback { + open fun performHapticFeedback(type: ReorderHapticFeedbackType) { + // no-op + } +} + +@Composable +fun rememberReorderHapticFeedback(): ReorderHapticFeedback { + val view = LocalView.current + + val reorderHapticFeedback = remember { + object : ReorderHapticFeedback() { + override fun performHapticFeedback(type: ReorderHapticFeedbackType) { + when (type) { + ReorderHapticFeedbackType.START -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + view.performHapticFeedback(android.view.HapticFeedbackConstants.DRAG_START) + } + + ReorderHapticFeedbackType.MOVE -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + view.performHapticFeedback(android.view.HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) + } + + ReorderHapticFeedbackType.END -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + view.performHapticFeedback(android.view.HapticFeedbackConstants.GESTURE_END) + } + } + } + } + } + + return reorderHapticFeedback +}