Преглед на файлове

swipe gestures for big carousel & gallery

Milan Jurkulak преди 3 години
родител
ревизия
9b7607e001

+ 37 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ModifierExt.kt

@@ -10,13 +10,16 @@ package org.mjdev.tvlib.extensions
 
 import android.annotation.SuppressLint
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.detectDragGestures
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.relocation.BringIntoViewResponder
 import androidx.compose.foundation.relocation.bringIntoViewResponder
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableIntState
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableLongStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
@@ -34,6 +37,7 @@ import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.drawscope.Fill
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.debugInspectorInfo
@@ -41,6 +45,7 @@ import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.delay
 import timber.log.Timber
 import java.lang.Float.min
+import kotlin.math.abs
 
 @Suppress("MemberVisibilityCanBePrivate")
 object ModifierExt {
@@ -176,4 +181,36 @@ object ModifierExt {
         }
     )
 
+    suspend fun PointerInputScope.detectSwipe(
+        swipeState: MutableIntState = mutableIntStateOf(-1),
+        onSwipeLeft: () -> Unit = {},
+        onSwipeRight: () -> Unit = {},
+        onSwipeUp: () -> Unit = {},
+        onSwipeDown: () -> Unit = {},
+    ) = detectDragGestures(
+        onDrag = { change, dragAmount ->
+            change.consume()
+            val (x, y) = dragAmount
+            if (abs(x) > abs(y)) {
+                when {
+                    x > 0 -> swipeState.intValue = 0
+                    x < 0 -> swipeState.intValue = 1
+                }
+            } else {
+                when {
+                    y > 0 -> swipeState.intValue = 2
+                    y < 0 -> swipeState.intValue = 3
+                }
+            }
+        },
+        onDragEnd = {
+            when (swipeState.intValue) {
+                0 -> onSwipeRight()
+                1 -> onSwipeLeft()
+                2 -> onSwipeDown()
+                3 -> onSwipeUp()
+            }
+        }
+    )
+
 }

+ 34 - 48
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/BigCarousel.kt

@@ -10,18 +10,14 @@ package org.mjdev.tvlib.ui.components.carousel
 
 import android.annotation.SuppressLint
 import android.content.res.Configuration
-import androidx.compose.foundation.gestures.detectHorizontalDragGestures
-import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.ContentScale
@@ -32,9 +28,8 @@ import androidx.tv.material3.CardScale
 import androidx.tv.material3.Carousel
 import androidx.tv.material3.CarouselState
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
+import org.mjdev.tvlib.extensions.ModifierExt.detectSwipe
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
-import kotlin.math.abs
 
 @SuppressLint("AutoboxingStateValueProperty")
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -43,21 +38,16 @@ import kotlin.math.abs
 fun BigCarousel(
     modifier: Modifier = Modifier,
     items: List<Any?> = listOf(Unit, Unit, Unit),
-    autoScrollDurationMillis: Long = 500,
-    focusState: MutableState<FocusState?> = rememberFocusState(items),
+    autoScrollDurationMillis: Long = 1500,
     carouselState: MutableState<CarouselState> = remember { mutableStateOf(CarouselState()) },
     onItemSelected: (movie: Any?) -> Unit = {},
     onItemClicked: (movie: Any?) -> Unit = {},
 ) {
     val height = LocalConfiguration.current.let { config ->
-        if (config.orientation == Configuration.ORIENTATION_PORTRAIT)
-            config.screenWidthDp * 0.4f
-        else
-            config.screenHeightDp * 0.4f
+        if (config.orientation == Configuration.ORIENTATION_PORTRAIT) config.screenWidthDp * 0.4f
+        else config.screenHeightDp * 0.4f
     }
-    val selectedItem: () -> Any? = { items[carouselState.value.activeItemIndex] }
-    val scrollDelta = remember { mutableFloatStateOf(0f) }
-    BoxWithConstraints {
+    Box {
         Carousel(
             autoScrollDurationMillis = autoScrollDurationMillis,
             carouselState = carouselState.value,
@@ -65,54 +55,50 @@ fun BigCarousel(
             modifier = modifier
                 .recomposeHighlighter()
                 .fillMaxWidth()
-                .height(height.dp),
+                .height(height.dp)
+                .pointerInput(Unit) {
+                    detectSwipe(
+                        onSwipeLeft = {
+                            val nextItem = carouselState.value.activeItemIndex + 1
+                            if (nextItem <= items.size) {
+                                carouselState.value = CarouselState(nextItem).apply {
+                                    pauseAutoScroll(nextItem)
+                                }
+                            }
+                        },
+                        onSwipeRight = {
+                            val prevItem = carouselState.value.activeItemIndex - 1
+                            if (prevItem >= 0) {
+                                carouselState.value = CarouselState(prevItem).apply {
+                                    pauseAutoScroll(prevItem)
+                                }
+                            }
+                        },
+                    )
+                },
         ) { indexOfCarouselItem ->
             CarouselCard(
                 item = items[indexOfCarouselItem],
                 modifier = modifier
                     .fillMaxWidth()
                     .height(height.dp)
-                    .pointerInput(Unit) {
-                        detectHorizontalDragGestures { _, dragAmount ->
-                            scrollDelta.value = dragAmount
-                        }
-                    }
                     .onFocusChanged { state ->
+                        val selectedIdx = carouselState.value.activeItemIndex
+                        val selectedItem = items[selectedIdx]
                         if (state.isFocused || state.hasFocus) {
-                            onItemSelected(selectedItem())
+                            onItemSelected(selectedItem)
                         }
                     },
                 contentScale = ContentScale.Crop,
                 scale = CardScale.None,
-                focusState = focusState,
+                focused = (carouselState.value.activeItemIndex == indexOfCarouselItem),
                 onFocus = onItemSelected,
                 onClick = {
-                    onItemClicked(selectedItem())
+                    val selectedIdx = carouselState.value.activeItemIndex
+                    val selectedItem = items[selectedIdx]
+                    onItemClicked(selectedItem)
                 }
             )
         }
     }
-    // todo improve
-    LaunchedEffect(scrollDelta.value) {
-        val activeItemIndex = carouselState.value.activeItemIndex
-        carouselState.value.pauseAutoScroll(activeItemIndex)
-        if(abs(scrollDelta.value) > 250) {
-            if (scrollDelta.value < 0) {
-                val nextItem = carouselState.value.activeItemIndex + 1
-                if (nextItem <= items.size) {
-                    carouselState.value = CarouselState(nextItem).apply {
-                        pauseAutoScroll(nextItem)
-                    }
-                }
-            }
-            if (scrollDelta.value > 0) {
-                val prevItem = carouselState.value.activeItemIndex - 1
-                if (prevItem >= 0) {
-                    carouselState.value = CarouselState(prevItem).apply {
-                        pauseAutoScroll(prevItem)
-                    }
-                }
-            }
-        }
-    }
 }

+ 2 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/CarouselCard.kt

@@ -41,6 +41,7 @@ fun CarouselCard(
     contentScale: ContentScale = ContentScale.Crop,
     scale: CardScale = CardScale.None,
     focusState: MutableState<FocusState?> = rememberFocusState(item),
+    focused: Boolean = false,
     imageRenderer: @Composable () -> Unit = {
         val image = (item as? ItemWithImage<*>)?.image
         val background = (item as? ItemWithBackground<*>)?.background
@@ -66,7 +67,7 @@ fun CarouselCard(
         aspectRatio = 1f,
         contentScale = contentScale,
         scale = scale,
-        focusState = focusState,
+        focused = focused,
         imageRenderer = imageRenderer,
         titlePadding = titlePadding,
         onFocus = onFocus,

+ 16 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/Gallery.kt

@@ -54,6 +54,7 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api
 import kotlinx.coroutines.delay
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.GlobalExt.toggle
+import org.mjdev.tvlib.extensions.ModifierExt.detectSwipe
 import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemWithBackground
 import org.mjdev.tvlib.interfaces.ItemWithImage
@@ -131,6 +132,7 @@ fun Gallery(
     listState: TvLazyListState = rememberTvLazyListState(),
 ) {
     val initialized = remember { mutableStateOf(false) }
+
     Box(
         modifier = modifier.background(Color.Black, RectangleShape),
         contentAlignment = Alignment.BottomCenter,
@@ -160,6 +162,20 @@ fun Gallery(
                                     infoVisible.toggle()
                                 }
                             }
+                            .pointerInput(Unit) {
+                                detectSwipe(
+                                    onSwipeLeft = {
+                                        if (currentItemIndex.intValue < (list.size - 1)) {
+                                            currentItemIndex.intValue += 1
+                                        }
+                                    },
+                                    onSwipeRight = {
+                                        if (currentItemIndex.intValue > 0) {
+                                            currentItemIndex.intValue -= 1
+                                        }
+                                    },
+                                )
+                            }
                             .onKeyEvent { ev -> handleKey(ev) }
                     )
                     ItemInfo(