summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt16
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnState.kt58
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt86
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Haptics.kt292
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Rotary.kt1232
-rw-r--r--PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/RotaryVelocityTracker.kt47
6 files changed, 68 insertions, 1663 deletions
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt
index 6ce7df125..b84f840cf 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/ScrollableScreen.kt
@@ -36,6 +36,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
@@ -65,7 +66,6 @@ import androidx.wear.compose.material.Vignette
import androidx.wear.compose.material.VignettePosition
import androidx.wear.compose.material.scrollAway
import com.android.permissioncontroller.permission.ui.wear.elements.material3.WearPermissionScaffold
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rotaryWithScroll
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionTheme
@@ -86,7 +86,7 @@ fun ScrollableScreen(
isLoading: Boolean = false,
titleTestTag: String? = null,
subtitleTestTag: String? = null,
- content: ScalingLazyListScope.() -> Unit,
+ content: ListScopeWrapper.() -> Unit,
) {
var dismissed by remember { mutableStateOf(false) }
val activity = LocalContext.current.findActivity()
@@ -193,13 +193,7 @@ internal fun Wear2Scaffold(
}
WearPermissionTheme {
Scaffold(
- // TODO: Use a rotary modifier from Wear Compose once Wear Compose 1.4 is landed.
- // (b/325560444)
- modifier =
- Modifier.rotaryWithScroll(
- scrollableState = listState,
- focusRequester = focusRequester,
- ),
+ modifier = Modifier.focusRequester(focusRequester),
timeText = {
if (showTimeText && !isLoading) {
TimeText(
@@ -364,3 +358,7 @@ internal fun Context.findActivity(): Activity {
}
throw IllegalStateException("The screen should be called in the context of an Activity")
}
+
+interface ListScopeWrapper {
+ fun item(key: Any? = null, contentType: Any? = null, content: @Composable () -> Unit)
+}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnState.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnState.kt
index 0603647b1..b3698a60c 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnState.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/layout/ScalingLazyColumnState.kt
@@ -42,14 +42,8 @@ import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
import androidx.wear.compose.foundation.lazy.ScalingLazyListState
import androidx.wear.compose.foundation.lazy.ScalingParams
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnDefaults.responsiveScalingParams
import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState.RotaryMode
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rememberDisabledHaptic
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rememberRotaryHapticHandler
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rotaryWithScroll
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rotaryWithSnap
-import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.toRotaryScrollAdapter
// This file is a copy of ScalingLazyColumnState.kt from Horologist (go/horologist),
// remove it once after wear compose supports large screen dialogs.
@@ -61,10 +55,7 @@ import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.
class ScalingLazyColumnState(
val initialScrollPosition: ScrollPosition = ScrollPosition(1, 0),
val autoCentering: AutoCenteringParams? =
- AutoCenteringParams(
- initialScrollPosition.index,
- initialScrollPosition.offsetPx,
- ),
+ AutoCenteringParams(initialScrollPosition.index, initialScrollPosition.offsetPx),
val anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
val contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp),
val rotaryMode: RotaryMode? = RotaryMode.Scroll,
@@ -120,10 +111,7 @@ class ScalingLazyColumnState(
data object Scroll : RotaryMode
}
- data class ScrollPosition(
- val index: Int,
- val offsetPx: Int,
- )
+ data class ScrollPosition(val index: Int, val offsetPx: Int)
fun interface Factory {
@Composable fun create(): ScalingLazyColumnState
@@ -133,7 +121,7 @@ class ScalingLazyColumnState(
// @Deprecated("Replaced by rememberResponsiveColumnState")
@Composable
fun rememberColumnState(
- factory: ScalingLazyColumnState.Factory = ScalingLazyColumnDefaults.responsive(),
+ factory: ScalingLazyColumnState.Factory = ScalingLazyColumnDefaults.responsive()
): ScalingLazyColumnState {
val columnState = factory.create()
@@ -150,10 +138,7 @@ fun rememberResponsiveColumnState(
last = ScalingLazyColumnDefaults.ItemType.Unspecified,
),
verticalArrangement: Arrangement.Vertical =
- Arrangement.spacedBy(
- space = 4.dp,
- alignment = Alignment.Top,
- ),
+ Arrangement.spacedBy(space = 4.dp, alignment = Alignment.Top),
rotaryMode: RotaryMode? = RotaryMode.Scroll,
hapticsEnabled: Boolean = true,
reverseLayout: Boolean = false,
@@ -173,10 +158,7 @@ fun rememberResponsiveColumnState(
val topScreenOffsetPx = screenHeightPx / 2 - topPaddingPx
val initialScrollPosition =
- ScalingLazyColumnState.ScrollPosition(
- index = 0,
- offsetPx = topScreenOffsetPx,
- )
+ ScalingLazyColumnState.ScrollPosition(index = 0, offsetPx = topScreenOffsetPx)
val columnState =
ScalingLazyColumnState(
@@ -204,36 +186,8 @@ fun ScalingLazyColumn(
modifier: Modifier = Modifier,
content: ScalingLazyListScope.() -> Unit,
) {
- val focusRequester = rememberActiveFocusRequester()
-
- val rotaryHaptics =
- if (columnState.hapticsEnabled) {
- rememberRotaryHapticHandler(columnState.state)
- } else {
- rememberDisabledHaptic()
- }
-
- val modifierWithRotary =
- when (columnState.rotaryMode) {
- RotaryMode.Snap ->
- modifier.rotaryWithSnap(
- focusRequester = focusRequester,
- rotaryScrollAdapter = columnState.state.toRotaryScrollAdapter(),
- reverseDirection = columnState.reverseLayout,
- rotaryHaptics = rotaryHaptics,
- )
- RotaryMode.Scroll ->
- modifier.rotaryWithScroll(
- focusRequester = focusRequester,
- scrollableState = columnState.state,
- reverseDirection = columnState.reverseLayout,
- rotaryHaptics = rotaryHaptics,
- )
- else -> modifier
- }
-
ScalingLazyColumn(
- modifier = modifierWithRotary.fillMaxSize(),
+ modifier = modifier.fillMaxSize(),
state = columnState.state,
contentPadding = columnState.contentPadding,
reverseLayout = columnState.reverseLayout,
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt
index 9a926f5a3..babd7fb6f 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/material3/WearPermissionScaffold.kt
@@ -17,6 +17,7 @@ package com.android.permissioncontroller.permission.ui.wear.elements.material3
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
@@ -37,24 +38,41 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.ScrollInfoProvider
import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumnScope
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumnState
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
import androidx.wear.compose.material3.AppScaffold
import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.IconButtonDefaults
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.ScreenScaffold
import androidx.wear.compose.material3.ScrollIndicator
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.TimeText
+import androidx.wear.compose.material3.lazy.scrollTransform
import com.android.permissioncontroller.permission.ui.wear.elements.AnnotatedText
+import com.android.permissioncontroller.permission.ui.wear.elements.ListScopeWrapper
import com.android.permissioncontroller.permission.ui.wear.elements.Wear2Scaffold
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumn
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState
-import com.android.permissioncontroller.permission.ui.wear.elements.layout.rememberResponsiveColumnState
import com.android.permissioncontroller.permission.ui.wear.elements.rememberDrawablePainter
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionMaterialUIVersion.MATERIAL2_5
import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionTheme
+private class TransformingScopeConverter(private val scope: TransformingLazyColumnScope) :
+ ListScopeWrapper {
+ override fun item(key: Any?, contentType: Any?, content: @Composable () -> Unit) {
+ scope.item { Box(modifier = Modifier.scrollTransform(this)) { content() } }
+ }
+}
+
+private class ScalingScopeConverter(private val scope: ScalingLazyListScope) : ListScopeWrapper {
+ override fun item(key: Any?, contentType: Any?, content: @Composable () -> Unit) {
+ scope.item { content() }
+ }
+}
+
/**
* This component is wrapper on material scaffold component. It helps with time text, scroll
* indicator and standard list elements like title, icon and subtitle.
@@ -67,7 +85,7 @@ internal fun WearPermissionScaffold(
subtitle: CharSequence?,
image: Any?,
isLoading: Boolean,
- content: ScalingLazyListScope.() -> Unit,
+ content: ListScopeWrapper.() -> Unit,
titleTestTag: String? = null,
subtitleTestTag: String? = null,
) {
@@ -79,7 +97,7 @@ internal fun WearPermissionScaffold(
subtitle,
image,
isLoading,
- content,
+ { content.invoke(ScalingScopeConverter(this)) },
titleTestTag,
subtitleTestTag,
)
@@ -90,7 +108,7 @@ internal fun WearPermissionScaffold(
subtitle,
image,
isLoading,
- content,
+ { content.invoke(TransformingScopeConverter(this)) },
titleTestTag,
subtitleTestTag,
)
@@ -104,7 +122,7 @@ private fun WearPermissionScaffoldInternal(
subtitle: CharSequence?,
image: Any?,
isLoading: Boolean,
- content: ScalingLazyListScope.() -> Unit,
+ content: TransformingLazyColumnScope.() -> Unit,
titleTestTag: String? = null,
subtitleTestTag: String? = null,
) {
@@ -116,12 +134,11 @@ private fun WearPermissionScaffoldInternal(
screenHeight = screenHeight,
titleNeedsLargePadding = subtitle == null,
)
- val columnState =
- rememberResponsiveColumnState(contentPadding = { paddingDefaults.scrollContentPadding })
+ val columnState = rememberTransformingLazyColumnState()
WearPermissionTheme(version = WearPermissionMaterialUIVersion.MATERIAL3) {
AppScaffold(timeText = wearPermissionTimeText(showTimeText && !isLoading)) {
ScreenScaffold(
- scrollInfoProvider = ScrollInfoProvider(columnState.state),
+ scrollInfoProvider = ScrollInfoProvider(columnState),
scrollIndicator = wearPermissionScrollIndicator(!isLoading, columnState),
) {
Box(modifier = Modifier.fillMaxSize()) {
@@ -129,6 +146,7 @@ private fun WearPermissionScaffoldInternal(
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} else {
ScrollingView(
+ contentPadding = paddingDefaults.scrollContentPadding,
columnState = columnState,
icon = painterFromImage(image),
title = title,
@@ -184,7 +202,8 @@ private class WearPermissionScaffoldPaddingDefaults(
@Composable
private fun BoxScope.ScrollingView(
- columnState: ScalingLazyColumnState,
+ contentPadding: PaddingValues,
+ columnState: TransformingLazyColumnState,
icon: Painter?,
title: String?,
titleTestTag: String?,
@@ -192,16 +211,26 @@ private fun BoxScope.ScrollingView(
subtitleTestTag: String?,
titlePaddingValues: PaddingValues,
subTitlePaddingValues: PaddingValues,
- content: ScalingLazyListScope.() -> Unit,
+ content: TransformingLazyColumnScope.() -> Unit,
) {
- ScalingLazyColumn(columnState = columnState) {
- iconItem(icon, Modifier.size(24.dp))
- titleItem(text = title, testTag = titleTestTag, contentPaddingValues = titlePaddingValues)
- subtitleItem(
- text = subtitle,
- testTag = subtitleTestTag,
- modifier = Modifier.align(Alignment.Center).padding(subTitlePaddingValues),
- )
+ TransformingLazyColumn(
+ contentPadding = contentPadding,
+ state = columnState,
+ modifier = Modifier.background(MaterialTheme.colorScheme.background),
+ ) {
+ with(TransformingScopeConverter(this)) {
+ iconItem(icon, Modifier.size(IconButtonDefaults.LargeIconSize))
+ titleItem(
+ text = title,
+ testTag = titleTestTag,
+ contentPaddingValues = titlePaddingValues,
+ )
+ subtitleItem(
+ text = subtitle,
+ testTag = subtitleTestTag,
+ modifier = Modifier.align(Alignment.Center).padding(subTitlePaddingValues),
+ )
+ }
content()
}
}
@@ -216,15 +245,10 @@ private fun wearPermissionTimeText(showTime: Boolean): @Composable () -> Unit {
private fun wearPermissionScrollIndicator(
showIndicator: Boolean,
- columnState: ScalingLazyColumnState,
+ columnState: TransformingLazyColumnState,
): @Composable (BoxScope.() -> Unit)? {
return if (showIndicator) {
- {
- ScrollIndicator(
- modifier = Modifier.align(Alignment.CenterEnd),
- state = columnState.state,
- )
- }
+ { ScrollIndicator(modifier = Modifier.align(Alignment.CenterEnd), state = columnState) }
} else {
null
}
@@ -246,7 +270,7 @@ private fun Modifier.optionalTestTag(tag: String?): Modifier {
return this then testTag(tag)
}
-private fun ScalingLazyListScope.iconItem(painter: Painter?, modifier: Modifier = Modifier) =
+private fun ListScopeWrapper.iconItem(painter: Painter?, modifier: Modifier = Modifier) =
painter?.let {
item {
val iconColor = WearPermissionButtonStyle.Secondary.material3ButtonColors().iconColor
@@ -260,14 +284,14 @@ private fun ScalingLazyListScope.iconItem(painter: Painter?, modifier: Modifier
}
}
-private fun ScalingLazyListScope.titleItem(
+private fun ListScopeWrapper.titleItem(
text: String?,
testTag: String?,
contentPaddingValues: PaddingValues,
modifier: Modifier = Modifier,
) =
text?.let {
- item {
+ item(contentType = "header") {
ListHeader(
modifier = modifier.requiredHeightIn(1.dp), // We do not want default min height
contentPadding = contentPaddingValues,
@@ -281,7 +305,7 @@ private fun ScalingLazyListScope.titleItem(
}
}
-private fun ScalingLazyListScope.subtitleItem(
+private fun ListScopeWrapper.subtitleItem(
text: CharSequence?,
testTag: String?,
modifier: Modifier = Modifier,
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Haptics.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Haptics.kt
deleted file mode 100644
index 817bf7efe..000000000
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Haptics.kt
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * 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 com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput
-
-import android.os.Build
-import android.view.View
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalView
-import kotlin.math.abs
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.conflate
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.withContext
-
-// This file is a copy of Haptics.kt from Horologist (go/horologist),
-// remove it once Wear Compose 1.4 is landed (b/325560444).
-
-private const val DEBUG = false
-
-/** Debug logging that can be enabled. */
-private inline fun debugLog(generateMsg: () -> String) {
- if (DEBUG) {
- println("RotaryHaptics: ${generateMsg()}")
- }
-}
-
-/**
- * Throttling events within specified timeframe. Only first and last events will be received. For a
- * flow emitting elements 1 to 30, with a 100ms delay between them:
- * ```
- * val flow = flow {
- * for (i in 1..30) {
- * delay(100)
- * emit(i)
- * }
- * }
- * ```
- *
- * With timeframe=1000 only those integers will be received: 1, 10, 20, 30 .
- */
-internal fun <T> Flow<T>.throttleLatest(timeframe: Long): Flow<T> = flow {
- conflate().collect {
- emit(it)
- delay(timeframe)
- }
-}
-
-/** Handles haptics for rotary usage */
-interface RotaryHapticHandler {
-
- /** Handles haptics when scroll is used */
- fun handleScrollHaptic(scrollDelta: Float)
-
- /** Handles haptics when scroll with snap is used */
- fun handleSnapHaptic(scrollDelta: Float)
-}
-
-/**
- * Default implementation of [RotaryHapticHandler]. It handles haptic feedback based on the
- * [scrollableState], scrolled pixels and [hapticsThresholdPx]. Haptic is not fired in this class,
- * instead it's sent to [hapticsChannel] where it'll performed later.
- *
- * @param scrollableState Haptic performed based on this state
- * @param hapticsChannel Channel to which haptic events will be sent
- * @param hapticsThresholdPx A scroll threshold after which haptic is produced.
- */
-class DefaultRotaryHapticHandler(
- private val scrollableState: ScrollableState,
- private val hapticsChannel: Channel<RotaryHapticsType>,
- private val hapticsThresholdPx: Long = 50,
-) : RotaryHapticHandler {
-
- private var overscrollHapticTriggered = false
- private var currScrollPosition = 0f
- private var prevHapticsPosition = 0f
-
- override fun handleScrollHaptic(scrollDelta: Float) {
- if (
- (scrollDelta > 0 && !scrollableState.canScrollForward) ||
- (scrollDelta < 0 && !scrollableState.canScrollBackward)
- ) {
- if (!overscrollHapticTriggered) {
- trySendHaptic(RotaryHapticsType.ScrollLimit)
- overscrollHapticTriggered = true
- }
- } else {
- overscrollHapticTriggered = false
- currScrollPosition += scrollDelta
- val diff = abs(currScrollPosition - prevHapticsPosition)
-
- if (diff >= hapticsThresholdPx) {
- trySendHaptic(RotaryHapticsType.ScrollTick)
- prevHapticsPosition = currScrollPosition
- }
- }
- }
-
- override fun handleSnapHaptic(scrollDelta: Float) {
- if (
- (scrollDelta > 0 && !scrollableState.canScrollForward) ||
- (scrollDelta < 0 && !scrollableState.canScrollBackward)
- ) {
- if (!overscrollHapticTriggered) {
- trySendHaptic(RotaryHapticsType.ScrollLimit)
- overscrollHapticTriggered = true
- }
- } else {
- overscrollHapticTriggered = false
- trySendHaptic(RotaryHapticsType.ScrollItemFocus)
- }
- }
-
- private fun trySendHaptic(rotaryHapticsType: RotaryHapticsType) {
- // Ok to ignore the ChannelResult because we default to capacity = 2 and DROP_OLDEST
- @Suppress("UNUSED_VARIABLE") val unused = hapticsChannel.trySend(rotaryHapticsType)
- }
-}
-
-/** Interface for Rotary haptic feedback */
-interface RotaryHapticFeedback {
- fun performHapticFeedback(type: RotaryHapticsType)
-}
-
-/** Rotary haptic types */
-@JvmInline
-value class RotaryHapticsType(private val type: Int) {
- companion object {
- /**
- * A scroll ticking haptic. Similar to texture haptic - performed each time when a
- * scrollable content is scrolled by a certain distance
- */
- val ScrollTick: RotaryHapticsType = RotaryHapticsType(1)
-
- /**
- * An item focus (snap) haptic. Performed when a scrollable content is snapped to a specific
- * item.
- */
- val ScrollItemFocus: RotaryHapticsType = RotaryHapticsType(2)
-
- /**
- * A limit(overscroll) haptic. Performed when a list reaches the limit (start or end) and
- * can't scroll further
- */
- val ScrollLimit: RotaryHapticsType = RotaryHapticsType(3)
- }
-}
-
-/** Remember disabled haptics handler */
-@Composable
-fun rememberDisabledHaptic(): RotaryHapticHandler = remember {
- object : RotaryHapticHandler {
-
- override fun handleScrollHaptic(scrollDelta: Float) {
- // Do nothing
- }
-
- override fun handleSnapHaptic(scrollDelta: Float) {
- // Do nothing
- }
- }
-}
-
-/**
- * Remember rotary haptic handler.
- *
- * @param scrollableState A scrollableState, used to determine whether the end of the scrollable was
- * reached or not.
- * @param throttleThresholdMs Throttling events within specified timeframe. Only first and last
- * events will be received. Check [throttleLatest] for more info.
- * @param hapticsThresholdPx A scroll threshold after which haptic is produced.
- * @param hapticsChannel Channel to which haptic events will be sent
- * @param rotaryHaptics Interface for Rotary haptic feedback which performs haptics
- */
-@Composable
-fun rememberRotaryHapticHandler(
- scrollableState: ScrollableState,
- throttleThresholdMs: Long = 30,
- hapticsThresholdPx: Long = 50,
- hapticsChannel: Channel<RotaryHapticsType> = rememberHapticChannel(),
- rotaryHaptics: RotaryHapticFeedback = rememberDefaultRotaryHapticFeedback(),
-): RotaryHapticHandler {
- return remember(scrollableState, hapticsChannel, rotaryHaptics) {
- DefaultRotaryHapticHandler(scrollableState, hapticsChannel, hapticsThresholdPx)
- }
- .apply {
- LaunchedEffect(hapticsChannel) {
- hapticsChannel.receiveAsFlow().throttleLatest(throttleThresholdMs).collect {
- hapticType ->
- // 'withContext' launches performHapticFeedback in a separate thread,
- // as otherwise it produces a visible lag (b/219776664)
- val currentTime = System.currentTimeMillis()
- debugLog { "Haptics started" }
- withContext(Dispatchers.Default) {
- debugLog {
- "Performing haptics, delay: " +
- "${System.currentTimeMillis() - currentTime}"
- }
- rotaryHaptics.performHapticFeedback(hapticType)
- }
- }
- }
- }
-}
-
-@Composable
-private fun rememberHapticChannel() = remember {
- Channel<RotaryHapticsType>(
- capacity = 2,
- onBufferOverflow = BufferOverflow.DROP_OLDEST,
- )
-}
-
-@Composable
-public fun rememberDefaultRotaryHapticFeedback(): RotaryHapticFeedback =
- LocalView.current.let { view -> remember { findDeviceSpecificHapticFeedback(view) } }
-
-internal fun findDeviceSpecificHapticFeedback(view: View): RotaryHapticFeedback =
- if (isSamsungWatch()) {
- SamsungWatchHapticFeedback(view)
- } else {
- DefaultRotaryHapticFeedback(view)
- }
-
-/** Default Rotary implementation for [RotaryHapticFeedback] */
-class DefaultRotaryHapticFeedback(private val view: View) : RotaryHapticFeedback {
-
- override fun performHapticFeedback(
- type: RotaryHapticsType,
- ) {
- when (type) {
- RotaryHapticsType.ScrollItemFocus -> {
- view.performHapticFeedback(SCROLL_ITEM_FOCUS)
- }
- RotaryHapticsType.ScrollTick -> {
- view.performHapticFeedback(SCROLL_TICK)
- }
- RotaryHapticsType.ScrollLimit -> {
- view.performHapticFeedback(SCROLL_LIMIT)
- }
- }
- }
-
- private companion object {
- // Hidden constants from HapticFeedbackConstants
- const val SCROLL_TICK: Int = 18
- const val SCROLL_ITEM_FOCUS: Int = 19
- const val SCROLL_LIMIT: Int = 20
- }
-}
-
-/** Implementation of [RotaryHapticFeedback] for Samsung devices */
-private class SamsungWatchHapticFeedback(private val view: View) : RotaryHapticFeedback {
- override fun performHapticFeedback(
- type: RotaryHapticsType,
- ) {
- when (type) {
- RotaryHapticsType.ScrollItemFocus -> {
- view.performHapticFeedback(102)
- }
- RotaryHapticsType.ScrollTick -> {
- view.performHapticFeedback(102)
- }
- RotaryHapticsType.ScrollLimit -> {
- view.performHapticFeedback(50107)
- }
- }
- }
-}
-
-private fun isSamsungWatch(): Boolean = Build.MANUFACTURER.contains("Samsung", ignoreCase = true)
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Rotary.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Rotary.kt
deleted file mode 100644
index 19a6ea671..000000000
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/Rotary.kt
+++ /dev/null
@@ -1,1232 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * 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 com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput
-
-import android.view.ViewConfiguration
-import androidx.compose.animation.core.AnimationState
-import androidx.compose.animation.core.CubicBezierEasing
-import androidx.compose.animation.core.Easing
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.animation.core.animateTo
-import androidx.compose.animation.core.copy
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.MutatePriority
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.ScrollableDefaults
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.input.rotary.RotaryInputModifierNode
-import androidx.compose.ui.input.rotary.RotaryScrollEvent
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.util.fastSumBy
-import androidx.compose.ui.util.lerp
-import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
-import androidx.wear.compose.foundation.lazy.ScalingLazyListState
-import androidx.wear.compose.foundation.rememberActiveFocusRequester
-import kotlin.math.abs
-import kotlin.math.absoluteValue
-import kotlin.math.sign
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.async
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.flow.transformLatest
-import kotlinx.coroutines.launch
-
-// This file is a copy of Rotary.kt from Horologist (go/horologist),
-// remove it once Wear Compose 1.4 is landed (b/325560444).
-
-/**
- * A modifier which connects rotary events with scrollable. This modifier supports scroll with
- * fling.
- *
- * @param scrollableState Scrollable state which will be scrolled while receiving rotary events
- * @param focusRequester Requests the focus for rotary input. By default comes from
- * [rememberActiveFocusRequester], which is used with [HierarchicalFocusCoordinator]
- * @param flingBehavior Logic describing fling behavior. If null fling will not happen.
- * @param rotaryHaptics Class which will handle haptic feedback
- * @param reverseDirection Reverse the direction of scrolling. Should be aligned with Scrollable
- * `reverseDirection` parameter
- */
-@OptIn(ExperimentalWearFoundationApi::class)
-@Suppress("ComposableModifierFactory")
-@Composable
-fun Modifier.rotaryWithScroll(
- scrollableState: ScrollableState,
- focusRequester: FocusRequester = rememberActiveFocusRequester(),
- flingBehavior: FlingBehavior? = ScrollableDefaults.flingBehavior(),
- rotaryHaptics: RotaryHapticHandler = rememberRotaryHapticHandler(scrollableState),
- reverseDirection: Boolean = false,
-): Modifier =
- rotaryHandler(
- rotaryScrollHandler =
- RotaryDefaults.rememberFlingHandler(scrollableState, flingBehavior),
- reverseDirection = reverseDirection,
- rotaryHaptics = rotaryHaptics,
- inspectorInfo =
- debugInspectorInfo {
- name = "rotaryWithFling"
- properties["scrollableState"] = scrollableState
- properties["focusRequester"] = focusRequester
- properties["flingBehavior"] = flingBehavior
- properties["rotaryHaptics"] = rotaryHaptics
- properties["reverseDirection"] = reverseDirection
- },
- )
- .focusRequester(focusRequester)
- .focusable()
-
-/**
- * A modifier which connects rotary events with scrollable. This modifier supports snap.
- *
- * @param focusRequester Requests the focus for rotary input. By default comes from
- * [rememberActiveFocusRequester], which is used with [HierarchicalFocusCoordinator]
- * @param rotaryScrollAdapter A connection between scrollable objects and rotary events
- * @param rotaryHaptics Class which will handle haptic feedback
- * @param reverseDirection Reverse the direction of scrolling. Should be aligned with Scrollable
- * `reverseDirection` parameter
- */
-@OptIn(ExperimentalWearFoundationApi::class)
-@Suppress("ComposableModifierFactory")
-@Composable
-fun Modifier.rotaryWithSnap(
- rotaryScrollAdapter: RotaryScrollAdapter,
- focusRequester: FocusRequester = rememberActiveFocusRequester(),
- snapParameters: SnapParameters = RotaryDefaults.snapParametersDefault,
- rotaryHaptics: RotaryHapticHandler =
- rememberRotaryHapticHandler(rotaryScrollAdapter.scrollableState),
- reverseDirection: Boolean = false,
-): Modifier =
- rotaryHandler(
- rotaryScrollHandler =
- RotaryDefaults.rememberSnapHandler(rotaryScrollAdapter, snapParameters),
- reverseDirection = reverseDirection,
- rotaryHaptics = rotaryHaptics,
- inspectorInfo =
- debugInspectorInfo {
- name = "rotaryWithFling"
- properties["rotaryScrollAdapter"] = rotaryScrollAdapter
- properties["focusRequester"] = focusRequester
- properties["snapParameters"] = snapParameters
- properties["rotaryHaptics"] = rotaryHaptics
- properties["reverseDirection"] = reverseDirection
- },
- )
- .focusRequester(focusRequester)
- .focusable()
-
-/** An extension function for creating [RotaryScrollAdapter] from [ScalingLazyListState] */
-@Composable
-fun ScalingLazyListState.toRotaryScrollAdapter(): RotaryScrollAdapter =
- remember(this) { ScalingLazyColumnRotaryScrollAdapter(this) }
-
-/** An implementation of rotary scroll adapter for [ScalingLazyColumn] */
-class ScalingLazyColumnRotaryScrollAdapter(
- override val scrollableState: ScalingLazyListState,
-) : RotaryScrollAdapter {
-
- /** Calculates an average height of an item by taking an average from visible items height. */
- override fun averageItemSize(): Float {
- val visibleItems = scrollableState.layoutInfo.visibleItemsInfo
- return (visibleItems.fastSumBy { it.unadjustedSize } / visibleItems.size).toFloat()
- }
-
- /** Current (centred) item index */
- override fun currentItemIndex(): Int = scrollableState.centerItemIndex
-
- /** An offset from the item centre */
- override fun currentItemOffset(): Float = scrollableState.centerItemScrollOffset.toFloat()
-
- /** The total count of items in ScalingLazyColumn */
- override fun totalItemsCount(): Int = scrollableState.layoutInfo.totalItemsCount
-}
-
-/** An adapter which connects scrollableState to Rotary */
-interface RotaryScrollAdapter {
-
- /** A scrollable state. Used for performing scroll when Rotary events received */
- val scrollableState: ScrollableState
-
- /** Average size of an item. Used for estimating the scrollable distance */
- fun averageItemSize(): Float
-
- /** A current item index. Used for scrolling */
- fun currentItemIndex(): Int
-
- /** An offset from the centre or the border of the current item. */
- fun currentItemOffset(): Float
-
- /** The total count of items in [scrollableState] */
- fun totalItemsCount(): Int
-}
-
-/** Defaults for rotary modifiers */
-object RotaryDefaults {
-
- /** Returns default [SnapParameters] */
- val snapParametersDefault: SnapParameters =
- SnapParameters(
- snapOffset = 0,
- thresholdDivider = 1.5f,
- resistanceFactor = 3f,
- )
-
- /** Returns whether the input is Low-res (a bezel) or high-res(a crown/rsb). */
- @Composable
- fun isLowResInput(): Boolean =
- LocalContext.current.packageManager.hasSystemFeature(
- "android.hardware.rotaryencoder.lowres"
- )
-
- /**
- * Handles scroll with fling.
- *
- * @param scrollableState Scrollable state which will be scrolled while receiving rotary events
- * @param flingBehavior Logic describing Fling behavior. If null - fling will not happen
- * @param isLowRes Whether the input is Low-res (a bezel) or high-res(a crown/rsb)
- */
- @Composable
- internal fun rememberFlingHandler(
- scrollableState: ScrollableState,
- flingBehavior: FlingBehavior? = null,
- isLowRes: Boolean = isLowResInput(),
- ): RotaryScrollHandler {
- val viewConfiguration = ViewConfiguration.get(LocalContext.current)
-
- return remember(scrollableState, flingBehavior, isLowRes) {
- // Remove unnecessary recompositions by disabling tracking of changes inside of
- // this block. This algorithm properly reads all updated values and
- // don't need recomposition when those values change.
- Snapshot.withoutReadObservation {
- debugLog { "isLowRes : $isLowRes" }
- fun rotaryFlingBehavior() =
- flingBehavior?.run {
- RotaryFlingBehavior(
- scrollableState,
- flingBehavior,
- viewConfiguration,
- flingTimeframe =
- if (isLowRes) lowResFlingTimeframe else highResFlingTimeframe,
- )
- }
-
- fun scrollBehavior() = RotaryScrollBehavior(scrollableState)
-
- if (isLowRes) {
- LowResRotaryScrollHandler(
- rotaryFlingBehaviorFactory = { rotaryFlingBehavior() },
- scrollBehaviorFactory = { scrollBehavior() },
- )
- } else {
- HighResRotaryScrollHandler(
- rotaryFlingBehaviorFactory = { rotaryFlingBehavior() },
- scrollBehaviorFactory = { scrollBehavior() },
- )
- }
- }
- }
- }
-
- /**
- * Handles scroll with snap
- *
- * @param rotaryScrollAdapter A connection between scrollable objects and rotary events
- * @param snapParameters Snap parameters
- */
- @Composable
- internal fun rememberSnapHandler(
- rotaryScrollAdapter: RotaryScrollAdapter,
- snapParameters: SnapParameters = snapParametersDefault,
- isLowRes: Boolean = isLowResInput(),
- ): RotaryScrollHandler {
- return remember(rotaryScrollAdapter, snapParameters) {
- // Remove unnecessary recompositions by disabling tracking of changes inside of
- // this block. This algorithm properly reads all updated values and
- // don't need recomposition when those values change.
- Snapshot.withoutReadObservation {
- debugLog { "isLowRes : $isLowRes" }
- if (isLowRes) {
- LowResSnapHandler(
- snapBehaviourFactory = {
- RotarySnapBehavior(rotaryScrollAdapter, snapParameters)
- },
- )
- } else {
- HighResSnapHandler(
- resistanceFactor = snapParameters.resistanceFactor,
- thresholdBehaviorFactory = {
- ThresholdBehavior(
- rotaryScrollAdapter,
- snapParameters.thresholdDivider,
- )
- },
- snapBehaviourFactory = {
- RotarySnapBehavior(rotaryScrollAdapter, snapParameters)
- },
- scrollBehaviourFactory = {
- RotaryScrollBehavior(rotaryScrollAdapter.scrollableState)
- },
- )
- }
- }
- }
- }
-
- private val lowResFlingTimeframe: Long = 100L
- private val highResFlingTimeframe: Long = 30L
-}
-
-/**
- * Parameters used for snapping
- *
- * @param snapOffset an optional offset to be applied when snapping the item. After the snap the
- * snapped items offset will be [snapOffset].
- */
-class SnapParameters(
- val snapOffset: Int,
- val thresholdDivider: Float,
- val resistanceFactor: Float,
-) {
- /** Returns a snapping offset in [Dp] */
- @Composable
- fun snapOffsetDp(): Dp {
- return with(LocalDensity.current) { snapOffset.toDp() }
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as SnapParameters
-
- if (snapOffset != other.snapOffset) return false
- if (thresholdDivider != other.thresholdDivider) return false
- if (resistanceFactor != other.resistanceFactor) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = snapOffset
- result = 31 * result + thresholdDivider.hashCode()
- result = 31 * result + resistanceFactor.hashCode()
- return result
- }
-}
-
-/** An interface for handling scroll events */
-internal interface RotaryScrollHandler {
- /**
- * Handles scrolling events
- *
- * @param coroutineScope A scope for performing async actions
- * @param event A scrollable event from rotary input, containing scrollable delta and timestamp
- * @param rotaryHaptics
- */
- suspend fun handleScrollEvent(
- coroutineScope: CoroutineScope,
- event: TimestampedDelta,
- rotaryHaptics: RotaryHapticHandler,
- )
-}
-
-/**
- * Class responsible for Fling behaviour with rotary. It tracks and produces the fling when
- * necessary
- */
-internal class RotaryFlingBehavior(
- private val scrollableState: ScrollableState,
- private val flingBehavior: FlingBehavior,
- viewConfiguration: ViewConfiguration,
- private val flingTimeframe: Long,
-) {
-
- // A time range during which the fling is valid.
- // For simplicity it's twice as long as [flingTimeframe]
- private val timeRangeToFling = flingTimeframe * 2
-
- // A default fling factor for making fling slower
- private val flingScaleFactor = 0.7f
-
- private var previousVelocity = 0f
-
- private val rotaryVelocityTracker = RotaryVelocityTracker()
-
- private val minFlingSpeed = viewConfiguration.scaledMinimumFlingVelocity.toFloat()
- private val maxFlingSpeed = viewConfiguration.scaledMaximumFlingVelocity.toFloat()
- private var latestEventTimestamp: Long = 0
-
- private var flingVelocity: Float = 0f
- private var flingTimestamp: Long = 0
-
- /** Starts a new fling tracking session with specified timestamp */
- fun startFlingTracking(timestamp: Long) {
- rotaryVelocityTracker.start(timestamp)
- latestEventTimestamp = timestamp
- previousVelocity = 0f
- }
-
- /** Observing new event within a fling tracking session with new timestamp and delta */
- fun observeEvent(timestamp: Long, delta: Float) {
- rotaryVelocityTracker.move(timestamp, delta)
- latestEventTimestamp = timestamp
- }
-
- /** Performing fling if necessary and calling [beforeFling] lambda before it is triggered */
- suspend fun trackFling(beforeFling: () -> Unit) {
- val currentVelocity = rotaryVelocityTracker.velocity
- debugLog { "currentVelocity: $currentVelocity" }
-
- if (abs(currentVelocity) >= abs(previousVelocity)) {
- flingTimestamp = latestEventTimestamp
- flingVelocity = currentVelocity * flingScaleFactor
- }
- previousVelocity = currentVelocity
-
- // Waiting for a fixed amount of time before checking the fling
- delay(flingTimeframe)
-
- // For making a fling 2 criteria should be met:
- // 1) no more than
- // `rangeToFling` ms should pass between last fling detection
- // and the time of last motion event
- // 2) flingVelocity should exceed the minFlingSpeed
- debugLog {
- "Check fling: flingVelocity: $flingVelocity " +
- "minFlingSpeed: $minFlingSpeed, maxFlingSpeed: $maxFlingSpeed"
- }
- if (
- latestEventTimestamp - flingTimestamp < timeRangeToFling &&
- abs(flingVelocity) > minFlingSpeed
- ) {
- // Stops scrollAnimationCoroutine because a fling will be performed
- beforeFling()
- val velocity = flingVelocity.coerceIn(-maxFlingSpeed, maxFlingSpeed)
- scrollableState.scroll(MutatePriority.UserInput) {
- with(flingBehavior) {
- debugLog { "Flinging with velocity $velocity" }
- performFling(velocity)
- }
- }
- }
- }
-}
-
-/**
- * A rotary event object which contains a [timestamp] of the rotary event and a scrolled [delta].
- */
-internal data class TimestampedDelta(val timestamp: Long, val delta: Float)
-
-/**
- * This class does a smooth animation when the scroll by N pixels is done. This animation works well
- * on Rsb(high-res) and Bezel(low-res) devices.
- */
-internal class RotaryScrollBehavior(
- private val scrollableState: ScrollableState,
-) {
- private var sequentialAnimation = false
- private var scrollAnimation = AnimationState(0f)
- private var prevPosition = 0f
-
- /** Handles scroll event to [targetValue] */
- suspend fun handleEvent(targetValue: Float) {
- scrollableState.scroll(MutatePriority.UserInput) {
- debugLog { "ScrollAnimation value before start: ${scrollAnimation.value}" }
-
- scrollAnimation.animateTo(
- targetValue,
- animationSpec = spring(),
- sequentialAnimation = sequentialAnimation,
- ) {
- val delta = value - prevPosition
- debugLog { "Animated by $delta, value: $value" }
- scrollBy(delta)
- prevPosition = value
- sequentialAnimation = value != this.targetValue
- }
- }
- }
-}
-
-/**
- * A helper class for snapping with rotary. Uses animateScrollToItem method for snapping to the Nth
- * item.
- */
-internal class RotarySnapBehavior(
- private val rotaryScrollAdapter: RotaryScrollAdapter,
- private val snapParameters: SnapParameters,
-) {
- private var snapTarget: Int = rotaryScrollAdapter.currentItemIndex()
- private var sequentialSnap: Boolean = false
-
- private var anim = AnimationState(0f)
- private var expectedDistance = 0f
-
- private val defaultStiffness = 200f
- private var snapTargetUpdated = true
-
- /**
- * Preparing snapping. This method should be called before [snapToTargetItem] is called.
- *
- * Snapping is done for current + [moveForElements] items.
- *
- * If [sequentialSnap] is true, items are summed up together. For example, if
- * [prepareSnapForItems] is called with [moveForElements] = 2, 3, 5 -> then the snapping will
- * happen to current + 10 items
- *
- * If [sequentialSnap] is false, then [moveForElements] are not summed up together.
- */
- fun prepareSnapForItems(moveForElements: Int, sequentialSnap: Boolean) {
- this.sequentialSnap = sequentialSnap
- if (sequentialSnap) {
- snapTarget += moveForElements
- } else {
- snapTarget = rotaryScrollAdapter.currentItemIndex() + moveForElements
- }
- snapTargetUpdated = true
- snapTarget = snapTarget.coerceIn(0 until rotaryScrollAdapter.totalItemsCount())
- }
-
- /** Performs snapping to the closest item. */
- suspend fun snapToClosestItem() {
- // Snapping to the closest item by using performFling method with 0 speed
- rotaryScrollAdapter.scrollableState.scroll(MutatePriority.UserInput) {
- debugLog { "snap to closest item" }
- var prevPosition = 0f
- AnimationState(0f).animateTo(
- targetValue = -rotaryScrollAdapter.currentItemOffset(),
- animationSpec = tween(durationMillis = 100, easing = FastOutSlowInEasing),
- ) {
- val animDelta = value - prevPosition
- scrollBy(animDelta)
- prevPosition = value
- }
- snapTarget = rotaryScrollAdapter.currentItemIndex()
- }
- }
-
- /** Returns true if top edge was reached */
- fun topEdgeReached(): Boolean = snapTarget <= 0
-
- /** Returns true if bottom edge was reached */
- fun bottomEdgeReached(): Boolean = snapTarget >= rotaryScrollAdapter.totalItemsCount() - 1
-
- /** Performs snapping to the specified in [prepareSnapForItems] element */
- suspend fun snapToTargetItem() {
- if (sequentialSnap) {
- anim = anim.copy(0f)
- } else {
- anim = AnimationState(0f)
- }
- rotaryScrollAdapter.scrollableState.scroll(MutatePriority.UserInput) {
- // If snapTargetUpdated is true - then the target was updated so we
- // need to do snap again
- while (snapTargetUpdated) {
- snapTargetUpdated = false
- var latestCenterItem: Int
- var continueFirstScroll = true
- debugLog { "snapTarget $snapTarget" }
- while (continueFirstScroll) {
- latestCenterItem = rotaryScrollAdapter.currentItemIndex()
- anim = anim.copy(0f)
- expectedDistance = expectedDistanceTo(snapTarget, snapParameters.snapOffset)
- debugLog {
- "expectedDistance = $expectedDistance, " +
- "scrollableState.centerItemScrollOffset " +
- "${rotaryScrollAdapter.currentItemOffset()}"
- }
- continueFirstScroll = false
- var prevPosition = 0f
-
- anim.animateTo(
- expectedDistance,
- animationSpec =
- SpringSpec(
- stiffness = defaultStiffness,
- visibilityThreshold = 0.1f,
- ),
- sequentialAnimation = (anim.velocity != 0f),
- ) {
- val animDelta = value - prevPosition
- debugLog {
- "First animation, value:$value, velocity:$velocity, " +
- "animDelta:$animDelta"
- }
-
- // Exit animation if snap target was updated
- if (snapTargetUpdated) cancelAnimation()
-
- scrollBy(animDelta)
- prevPosition = value
-
- if (latestCenterItem != rotaryScrollAdapter.currentItemIndex()) {
- continueFirstScroll = true
- cancelAnimation()
- return@animateTo
- }
-
- debugLog { "centerItemIndex = ${rotaryScrollAdapter.currentItemIndex()}" }
- if (rotaryScrollAdapter.currentItemIndex() == snapTarget) {
- debugLog { "Target is visible. Cancelling first animation" }
- debugLog {
- "scrollableState.centerItemScrollOffset " +
- "${rotaryScrollAdapter.currentItemOffset()}"
- }
- expectedDistance = -rotaryScrollAdapter.currentItemOffset()
- continueFirstScroll = false
- cancelAnimation()
- return@animateTo
- }
- }
- }
- // Exit animation if snap target was updated
- if (snapTargetUpdated) continue
-
- anim = anim.copy(0f)
- var prevPosition = 0f
- anim.animateTo(
- expectedDistance,
- animationSpec =
- SpringSpec(
- stiffness = defaultStiffness,
- visibilityThreshold = 0.1f,
- ),
- sequentialAnimation = (anim.velocity != 0f),
- ) {
- // Exit animation if snap target was updated
- if (snapTargetUpdated) cancelAnimation()
-
- val animDelta = value - prevPosition
- debugLog { "Final animation. velocity:$velocity, animDelta:$animDelta" }
- scrollBy(animDelta)
- prevPosition = value
- }
- }
- }
- }
-
- private fun expectedDistanceTo(index: Int, targetScrollOffset: Int): Float {
- val averageSize = rotaryScrollAdapter.averageItemSize()
- val indexesDiff = index - rotaryScrollAdapter.currentItemIndex()
- debugLog { "Average size $averageSize" }
- return (averageSize * indexesDiff) + targetScrollOffset -
- rotaryScrollAdapter.currentItemOffset()
- }
-}
-
-/**
- * A modifier which handles rotary events. It accepts ScrollHandler as the input - a class where
- * main logic about how scroll should be handled is lying
- */
-internal fun Modifier.rotaryHandler(
- rotaryScrollHandler: RotaryScrollHandler,
- reverseDirection: Boolean,
- rotaryHaptics: RotaryHapticHandler,
- inspectorInfo: InspectorInfo.() -> Unit,
-): Modifier =
- this then
- RotaryHandlerElement(
- rotaryScrollHandler,
- reverseDirection,
- rotaryHaptics,
- inspectorInfo,
- )
-
-/**
- * Batching requests for scrolling events. This function combines all events together (except first)
- * within specified timeframe. Should help with performance on high-res devices.
- */
-@OptIn(ExperimentalCoroutinesApi::class)
-internal fun Flow<TimestampedDelta>.batchRequestsWithinTimeframe(
- timeframe: Long
-): Flow<TimestampedDelta> {
- var delta = 0f
- var lastTimestamp = -timeframe
- return if (timeframe == 0L) {
- this
- } else {
- this.transformLatest {
- delta += it.delta
- debugLog { "Batching requests. delta:$delta" }
- if (lastTimestamp + timeframe <= it.timestamp) {
- lastTimestamp = it.timestamp
- debugLog { "No events before, delta= $delta" }
- emit(TimestampedDelta(it.timestamp, delta))
- } else {
- delay(timeframe)
- debugLog { "After delay, delta= $delta" }
- if (delta > 0f) {
- emit(TimestampedDelta(it.timestamp, delta))
- }
- }
- delta = 0f
- }
- }
-}
-
-/**
- * A scroll handler for RSB(high-res) without snapping and with or without fling A list is scrolled
- * by the number of pixels received from the rotary device.
- *
- * This class is a little bit different from LowResScrollHandler class - it has a filtering for
- * events which are coming with wrong sign ( this happens to rsb devices, especially at the end of
- * the scroll)
- *
- * This scroll handler supports fling. It can be set with [RotaryFlingBehavior].
- */
-internal class HighResRotaryScrollHandler(
- private val rotaryFlingBehaviorFactory: () -> RotaryFlingBehavior?,
- private val scrollBehaviorFactory: () -> RotaryScrollBehavior,
- private val hapticsThreshold: Long = 50,
-) : RotaryScrollHandler {
-
- // This constant is specific for high-res devices. Because that input values
- // can sometimes come with different sign, we have to filter them in this threshold
- private val gestureThresholdTime = 200L
- private var scrollJob: Job = CompletableDeferred<Unit>()
- private var flingJob: Job = CompletableDeferred<Unit>()
-
- private var previousScrollEventTime = 0L
- private var rotaryScrollDistance = 0f
-
- private var rotaryFlingBehavior: RotaryFlingBehavior? = rotaryFlingBehaviorFactory()
- private var scrollBehavior: RotaryScrollBehavior = scrollBehaviorFactory()
-
- override suspend fun handleScrollEvent(
- coroutineScope: CoroutineScope,
- event: TimestampedDelta,
- rotaryHaptics: RotaryHapticHandler,
- ) {
- val time = event.timestamp
- val isOppositeScrollValue = isOppositeValueAfterScroll(event.delta)
-
- if (isNewScrollEvent(time)) {
- debugLog { "New scroll event" }
- resetTracking(time)
- rotaryScrollDistance = event.delta
- } else {
- // Due to the physics of Rotary side button, some events might come
- // with an opposite axis value - either at the start or at the end of the motion.
- // We don't want to use these values for fling calculations.
- if (!isOppositeScrollValue) {
- rotaryFlingBehavior?.observeEvent(event.timestamp, event.delta)
- } else {
- debugLog { "Opposite value after scroll :${event.delta}" }
- }
- rotaryScrollDistance += event.delta
- }
-
- scrollJob.cancel()
-
- rotaryHaptics.handleScrollHaptic(event.delta)
- debugLog { "Rotary scroll distance: $rotaryScrollDistance" }
-
- previousScrollEventTime = time
- scrollJob = coroutineScope.async { scrollBehavior.handleEvent(rotaryScrollDistance) }
-
- if (rotaryFlingBehavior != null) {
- flingJob.cancel()
- flingJob =
- coroutineScope.async {
- rotaryFlingBehavior?.trackFling(
- beforeFling = {
- debugLog { "Calling before fling section" }
- scrollJob.cancel()
- scrollBehavior = scrollBehaviorFactory()
- }
- )
- }
- }
- }
-
- private fun isOppositeValueAfterScroll(delta: Float): Boolean =
- rotaryScrollDistance * delta < 0f && (abs(delta) < abs(rotaryScrollDistance))
-
- private fun isNewScrollEvent(timestamp: Long): Boolean {
- val timeDelta = timestamp - previousScrollEventTime
- return previousScrollEventTime == 0L || timeDelta > gestureThresholdTime
- }
-
- private fun resetTracking(timestamp: Long) {
- scrollBehavior = scrollBehaviorFactory()
- rotaryFlingBehavior = rotaryFlingBehaviorFactory()
- rotaryFlingBehavior?.startFlingTracking(timestamp)
- }
-}
-
-/**
- * A scroll handler for Bezel(low-res) without snapping. This scroll handler supports fling. It can
- * be set with RotaryFlingBehavior.
- */
-internal class LowResRotaryScrollHandler(
- private val rotaryFlingBehaviorFactory: () -> RotaryFlingBehavior?,
- private val scrollBehaviorFactory: () -> RotaryScrollBehavior,
-) : RotaryScrollHandler {
-
- private val gestureThresholdTime = 200L
- private var previousScrollEventTime = 0L
- private var rotaryScrollDistance = 0f
-
- private var scrollJob: Job = CompletableDeferred<Unit>()
- private var flingJob: Job = CompletableDeferred<Unit>()
-
- private var rotaryFlingBehavior: RotaryFlingBehavior? = rotaryFlingBehaviorFactory()
- private var scrollBehavior: RotaryScrollBehavior = scrollBehaviorFactory()
-
- override suspend fun handleScrollEvent(
- coroutineScope: CoroutineScope,
- event: TimestampedDelta,
- rotaryHaptics: RotaryHapticHandler,
- ) {
- val time = event.timestamp
-
- if (isNewScrollEvent(time)) {
- resetTracking(time)
- rotaryScrollDistance = event.delta
- } else {
- rotaryFlingBehavior?.observeEvent(event.timestamp, event.delta)
- rotaryScrollDistance += event.delta
- }
-
- scrollJob.cancel()
- flingJob.cancel()
-
- rotaryHaptics.handleScrollHaptic(event.delta)
- debugLog { "Rotary scroll distance: $rotaryScrollDistance" }
-
- previousScrollEventTime = time
- scrollJob = coroutineScope.async { scrollBehavior.handleEvent(rotaryScrollDistance) }
-
- flingJob =
- coroutineScope.async {
- rotaryFlingBehavior?.trackFling(
- beforeFling = {
- debugLog { "Calling before fling section" }
- scrollJob.cancel()
- scrollBehavior = scrollBehaviorFactory()
- },
- )
- }
- }
-
- private fun isNewScrollEvent(timestamp: Long): Boolean {
- val timeDelta = timestamp - previousScrollEventTime
- return previousScrollEventTime == 0L || timeDelta > gestureThresholdTime
- }
-
- private fun resetTracking(timestamp: Long) {
- scrollBehavior = scrollBehaviorFactory()
- debugLog { "Velocity tracker reset" }
- rotaryFlingBehavior = rotaryFlingBehaviorFactory()
- rotaryFlingBehavior?.startFlingTracking(timestamp)
- }
-}
-
-/**
- * A scroll handler for RSB(high-res) with snapping and without fling Snapping happens after a
- * threshold is reached ( set in [RotarySnapBehavior])
- *
- * This scroll handler doesn't support fling.
- */
-internal class HighResSnapHandler(
- private val resistanceFactor: Float,
- private val thresholdBehaviorFactory: () -> ThresholdBehavior,
- private val snapBehaviourFactory: () -> RotarySnapBehavior,
- private val scrollBehaviourFactory: () -> RotaryScrollBehavior,
-) : RotaryScrollHandler {
- private val gestureThresholdTime = 200L
- private val snapDelay = 100L
- private val maxSnapsPerEvent = 2
-
- private var scrollJob: Job = CompletableDeferred<Unit>()
- private var snapJob: Job = CompletableDeferred<Unit>()
-
- private var previousScrollEventTime = 0L
- private var snapAccumulator = 0f
- private var rotaryScrollDistance = 0f
- private var scrollInProgress = false
-
- private var snapBehaviour = snapBehaviourFactory()
- private var scrollBehaviour = scrollBehaviourFactory()
- private var thresholdBehavior = thresholdBehaviorFactory()
-
- private val scrollEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.5f, 1.0f)
-
- override suspend fun handleScrollEvent(
- coroutineScope: CoroutineScope,
- event: TimestampedDelta,
- rotaryHaptics: RotaryHapticHandler,
- ) {
- val time = event.timestamp
-
- if (isNewScrollEvent(time)) {
- debugLog { "New scroll event" }
- resetTracking()
- snapJob.cancel()
- snapBehaviour = snapBehaviourFactory()
- scrollBehaviour = scrollBehaviourFactory()
- thresholdBehavior = thresholdBehaviorFactory()
- thresholdBehavior.startThresholdTracking(time)
- snapAccumulator = 0f
- rotaryScrollDistance = 0f
- }
-
- if (!isOppositeValueAfterScroll(event.delta)) {
- thresholdBehavior.observeEvent(event.timestamp, event.delta)
- } else {
- debugLog { "Opposite value after scroll :${event.delta}" }
- }
-
- thresholdBehavior.applySmoothing()
- val snapThreshold = thresholdBehavior.snapThreshold()
-
- snapAccumulator += event.delta
- if (!snapJob.isActive) {
- val resistanceCoeff =
- 1 - scrollEasing.transform(rotaryScrollDistance.absoluteValue / snapThreshold)
- rotaryScrollDistance += event.delta * resistanceCoeff
- }
-
- debugLog { "Snap accumulator: $snapAccumulator" }
- debugLog { "Rotary scroll distance: $rotaryScrollDistance" }
-
- debugLog { "snapThreshold: $snapThreshold" }
- previousScrollEventTime = time
-
- if (abs(snapAccumulator) > snapThreshold) {
- scrollInProgress = false
- scrollBehaviour = scrollBehaviourFactory()
- scrollJob.cancel()
-
- val snapDistance =
- (snapAccumulator / snapThreshold)
- .toInt()
- .coerceIn(-maxSnapsPerEvent..maxSnapsPerEvent)
- snapAccumulator -= snapThreshold * snapDistance
- val sequentialSnap = snapJob.isActive
-
- debugLog {
- "Snap threshold reached: snapDistance:$snapDistance, " +
- "sequentialSnap: $sequentialSnap, " +
- "snap accumulator remaining: $snapAccumulator"
- }
- if (
- (!snapBehaviour.topEdgeReached() && snapDistance < 0) ||
- (!snapBehaviour.bottomEdgeReached() && snapDistance > 0)
- ) {
- rotaryHaptics.handleSnapHaptic(event.delta)
- }
-
- snapBehaviour.prepareSnapForItems(snapDistance, sequentialSnap)
- if (!snapJob.isActive) {
- snapJob.cancel()
- snapJob =
- coroutineScope.async {
- debugLog { "Snap started" }
- try {
- snapBehaviour.snapToTargetItem()
- } finally {
- debugLog { "Snap called finally" }
- }
- }
- }
- rotaryScrollDistance = 0f
- } else {
- if (!snapJob.isActive) {
- scrollJob.cancel()
- debugLog { "Scrolling for $rotaryScrollDistance/$resistanceFactor px" }
- scrollJob =
- coroutineScope.async {
- scrollBehaviour.handleEvent(rotaryScrollDistance / resistanceFactor)
- }
- delay(snapDelay)
- scrollInProgress = false
- scrollBehaviour = scrollBehaviourFactory()
- rotaryScrollDistance = 0f
- snapAccumulator = 0f
- snapBehaviour.prepareSnapForItems(0, false)
-
- snapJob.cancel()
- snapJob = coroutineScope.async { snapBehaviour.snapToClosestItem() }
- }
- }
- }
-
- private fun isOppositeValueAfterScroll(delta: Float): Boolean =
- sign(rotaryScrollDistance) * sign(delta) == -1f && (abs(delta) < abs(rotaryScrollDistance))
-
- private fun isNewScrollEvent(timestamp: Long): Boolean {
- val timeDelta = timestamp - previousScrollEventTime
- return previousScrollEventTime == 0L || timeDelta > gestureThresholdTime
- }
-
- private fun resetTracking() {
- scrollInProgress = true
- }
-}
-
-/**
- * A scroll handler for RSB(high-res) with snapping and without fling Snapping happens after a
- * threshold is reached ( set in [RotarySnapBehavior])
- *
- * This scroll handler doesn't support fling.
- */
-internal class LowResSnapHandler(
- private val snapBehaviourFactory: () -> RotarySnapBehavior,
-) : RotaryScrollHandler {
- private val gestureThresholdTime = 200L
-
- private var snapJob: Job = CompletableDeferred<Unit>()
-
- private var previousScrollEventTime = 0L
- private var snapAccumulator = 0f
- private var scrollInProgress = false
-
- private var snapBehaviour = snapBehaviourFactory()
-
- override suspend fun handleScrollEvent(
- coroutineScope: CoroutineScope,
- event: TimestampedDelta,
- rotaryHaptics: RotaryHapticHandler,
- ) {
- val time = event.timestamp
-
- if (isNewScrollEvent(time)) {
- debugLog { "New scroll event" }
- resetTracking()
- snapJob.cancel()
- snapBehaviour = snapBehaviourFactory()
- snapAccumulator = 0f
- }
-
- snapAccumulator += event.delta
-
- debugLog { "Snap accumulator: $snapAccumulator" }
-
- previousScrollEventTime = time
-
- if (abs(snapAccumulator) > 1f) {
- scrollInProgress = false
-
- val snapDistance = sign(snapAccumulator).toInt()
- rotaryHaptics.handleSnapHaptic(event.delta)
- val sequentialSnap = snapJob.isActive
- debugLog {
- "Snap threshold reached: snapDistance:$snapDistance, " +
- "sequentialSnap: $sequentialSnap, " +
- "snap accumulator: $snapAccumulator"
- }
-
- snapBehaviour.prepareSnapForItems(snapDistance, sequentialSnap)
- if (!snapJob.isActive) {
- snapJob.cancel()
- snapJob =
- coroutineScope.async {
- debugLog { "Snap started" }
- try {
- snapBehaviour.snapToTargetItem()
- } finally {
- debugLog { "Snap called finally" }
- }
- }
- }
- snapAccumulator = 0f
- }
- }
-
- private fun isNewScrollEvent(timestamp: Long): Boolean {
- val timeDelta = timestamp - previousScrollEventTime
- return previousScrollEventTime == 0L || timeDelta > gestureThresholdTime
- }
-
- private fun resetTracking() {
- scrollInProgress = true
- }
-}
-
-internal class ThresholdBehavior(
- private val rotaryScrollAdapter: RotaryScrollAdapter,
- private val thresholdDivider: Float,
- private val minVelocity: Float = 300f,
- private val maxVelocity: Float = 3000f,
- private val smoothingConstant: Float = 0.4f,
-) {
- private val thresholdDividerEasing: Easing = CubicBezierEasing(0.5f, 0.0f, 0.5f, 1.0f)
-
- private val rotaryVelocityTracker = RotaryVelocityTracker()
-
- private var smoothedVelocity = 0f
-
- fun startThresholdTracking(time: Long) {
- rotaryVelocityTracker.start(time)
- smoothedVelocity = 0f
- }
-
- fun observeEvent(timestamp: Long, delta: Float) {
- rotaryVelocityTracker.move(timestamp, delta)
- }
-
- fun applySmoothing() {
- if (rotaryVelocityTracker.velocity != 0.0f) {
- // smooth the velocity
- smoothedVelocity =
- exponentialSmoothing(
- currentVelocity = rotaryVelocityTracker.velocity.absoluteValue,
- prevVelocity = smoothedVelocity,
- smoothingConstant = smoothingConstant,
- )
- }
- debugLog { "rotaryVelocityTracker velocity: ${rotaryVelocityTracker.velocity}" }
- debugLog { "SmoothedVelocity: $smoothedVelocity" }
- }
-
- fun snapThreshold(): Float {
- val thresholdDividerFraction =
- thresholdDividerEasing.transform(
- inverseLerp(
- minVelocity,
- maxVelocity,
- smoothedVelocity,
- ),
- )
- return rotaryScrollAdapter.averageItemSize() /
- lerp(
- 1f,
- thresholdDivider,
- thresholdDividerFraction,
- )
- }
-
- private fun exponentialSmoothing(
- currentVelocity: Float,
- prevVelocity: Float,
- smoothingConstant: Float,
- ): Float = smoothingConstant * currentVelocity + (1 - smoothingConstant) * prevVelocity
-}
-
-private data class RotaryHandlerElement(
- private val rotaryScrollHandler: RotaryScrollHandler,
- private val reverseDirection: Boolean,
- private val rotaryHaptics: RotaryHapticHandler,
- private val inspectorInfo: InspectorInfo.() -> Unit,
-) : ModifierNodeElement<RotaryInputNode>() {
- override fun create(): RotaryInputNode =
- RotaryInputNode(
- rotaryScrollHandler,
- reverseDirection,
- rotaryHaptics,
- )
-
- override fun update(node: RotaryInputNode) {
- debugLog { "Update launched!" }
- node.rotaryScrollHandler = rotaryScrollHandler
- node.reverseDirection = reverseDirection
- node.rotaryHaptics = rotaryHaptics
- }
-
- override fun InspectorInfo.inspectableProperties() {
- inspectorInfo()
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other == null || this::class != other::class) return false
-
- other as RotaryHandlerElement
-
- if (rotaryScrollHandler != other.rotaryScrollHandler) return false
- if (reverseDirection != other.reverseDirection) return false
- if (rotaryHaptics != other.rotaryHaptics) return false
- if (inspectorInfo != other.inspectorInfo) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = rotaryScrollHandler.hashCode()
- result = 31 * result + reverseDirection.hashCode()
- result = 31 * result + rotaryHaptics.hashCode()
- result = 31 * result + inspectorInfo.hashCode()
- return result
- }
-}
-
-private class RotaryInputNode(
- var rotaryScrollHandler: RotaryScrollHandler,
- var reverseDirection: Boolean,
- var rotaryHaptics: RotaryHapticHandler,
-) : RotaryInputModifierNode, Modifier.Node() {
-
- val channel = Channel<TimestampedDelta>(capacity = Channel.CONFLATED)
- val flow = channel.receiveAsFlow()
-
- override fun onAttach() {
- coroutineScope.launch {
- flow.collectLatest {
- debugLog {
- "Scroll event received: " + "delta:${it.delta}, timestamp:${it.timestamp}"
- }
- rotaryScrollHandler.handleScrollEvent(this, it, rotaryHaptics)
- }
- }
- }
-
- override fun onRotaryScrollEvent(event: RotaryScrollEvent): Boolean = false
-
- override fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
- debugLog { "onPreRotaryScrollEvent" }
- channel.trySend(
- TimestampedDelta(
- event.uptimeMillis,
- event.verticalScrollPixels * if (reverseDirection) -1f else 1f,
- ),
- )
- return true
- }
-}
-
-private fun inverseLerp(start: Float, stop: Float, value: Float): Float {
- return ((value - start) / (stop - start)).coerceIn(0f, 1f)
-}
-
-/** Debug logging that can be enabled. */
-private const val DEBUG = false
-
-private inline fun debugLog(generateMsg: () -> String) {
- if (DEBUG) {
- println("RotaryScroll: ${generateMsg()}")
- }
-}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/RotaryVelocityTracker.kt b/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/RotaryVelocityTracker.kt
deleted file mode 100644
index 1719ecef3..000000000
--- a/PermissionController/src/com/android/permissioncontroller/permission/ui/wear/elements/rotaryinput/RotaryVelocityTracker.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * 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 com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput
-
-import androidx.compose.ui.input.pointer.util.VelocityTracker1D
-
-// This file is a copy of RotaryVelocityTracker.kt from Horologist (go/horologist),
-// remove it once Wear Compose 1.4 is landed (b/325560444).
-
-/** A wrapper around VelocityTracker1D to provide support for rotary input. */
-class RotaryVelocityTracker {
- private var velocityTracker: VelocityTracker1D = VelocityTracker1D(true)
-
- /** Retrieve the last computed velocity. */
- val velocity: Float
- get() = velocityTracker.calculateVelocity()
-
- /** Start tracking motion. */
- fun start(currentTime: Long) {
- velocityTracker.resetTracking()
- velocityTracker.addDataPoint(currentTime, 0f)
- }
-
- /** Continue tracking motion as the input rotates. */
- fun move(currentTime: Long, delta: Float) {
- velocityTracker.addDataPoint(currentTime, delta)
- }
-
- /** Stop tracking motion. */
- fun end() {
- velocityTracker.resetTracking()
- }
-}