/*
* Copyright 2022 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 androidx.tv.foundation
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.Orientation.Horizontal
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.debugInspectorInfo
/* Copied from
compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/
Scrollable.kt and modified */
/**
* Configure touch scrolling and flinging for the UI element in a single [Orientation].
*
* Users should update their state themselves using default [ScrollableState] and its
* `consumeScrollDelta` callback or by implementing [ScrollableState] interface manually and reflect
* their own state in UI when using this component.
*
* @param state [ScrollableState] state of the scrollable. Defines how scroll events will be
* interpreted by the user land logic and contains useful information about on-going events.
* @param orientation orientation of the scrolling
* @param pivotOffsets offsets of child element within the parent and starting edge of the child
* from the pivot defined by the parentOffset.
* @param enabled whether or not scrolling in enabled
* @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will
* behave like bottom to top and left to right will behave like right to left.
* drag events when this scrollable is being dragged.
*/
@OptIn(ExperimentalFoundationApi::class)
@ExperimentalTvFoundationApi
fun Modifier.scrollableWithPivot(
state: ScrollableState,
orientation: Orientation,
pivotOffsets: PivotOffsets,
enabled: Boolean = true,
reverseDirection: Boolean = false
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "scrollableWithPivot"
properties["orientation"] = orientation
properties["state"] = state
properties["enabled"] = enabled
properties["reverseDirection"] = reverseDirection
properties["pivotOffsets"] = pivotOffsets
},
factory = {
val coroutineScope = rememberCoroutineScope()
val keepFocusedChildInViewModifier =
remember(coroutineScope, orientation, state, reverseDirection) {
ContentInViewModifier(
coroutineScope, orientation, state, reverseDirection, pivotOffsets)
}
Modifier
.focusGroup()
.then(keepFocusedChildInViewModifier.modifier)
.pointerScrollable(
orientation,
reverseDirection,
state,
enabled
)
}
)
@Suppress("ComposableModifierFactory")
@Composable
private fun Modifier.pointerScrollable(
orientation: Orientation,
reverseDirection: Boolean,
controller: ScrollableState,
enabled: Boolean
): Modifier {
val nestedScrollDispatcher = remember { mutableStateOf(NestedScrollDispatcher()) }
val scrollLogic = rememberUpdatedState(
ScrollingLogic(
orientation,
reverseDirection,
controller
)
)
val nestedScrollConnection = remember(enabled) {
scrollableNestedScrollConnection(scrollLogic, enabled)
}
return this.nestedScroll(nestedScrollConnection, nestedScrollDispatcher.value)
}
private class ScrollingLogic(
val orientation: Orientation,
val reverseDirection: Boolean,
val scrollableState: ScrollableState,
) {
fun Float.toOffset(): Offset = when {
this == 0f -> Offset.Zero
orientation == Horizontal -> Offset(this, 0f)
else -> Offset(0f, this)
}
fun Offset.toFloat(): Float = if (orientation == Horizontal) this.x else this.y
fun Float.reverseIfNeeded(): Float = if (reverseDirection) this * -1 else this
fun performRawScroll(scroll: Offset): Offset {
return if (scrollableState.isScrollInProgress) {
Offset.Zero
} else {
scrollableState.dispatchRawDelta(scroll.toFloat().reverseIfNeeded())
.reverseIfNeeded().toOffset()
}
}
}
private fun scrollableNestedScrollConnection(
scrollLogic: State<ScrollingLogic>,
enabled: Boolean
): NestedScrollConnection = object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset = if (enabled) scrollLogic.value.performRawScroll(available) else Offset.Zero
}