[go: nahoru, domu]

blob: 8ddbc816957c96d49dc4a243c81d053b5b63c119 [file] [log] [blame]
/*
* Copyright 2019 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.ui.foundation
import androidx.animation.AnimationClockObservable
import androidx.animation.AnimationEndReason
import androidx.compose.Composable
import androidx.compose.Model
import androidx.compose.remember
import androidx.ui.core.Alignment
import androidx.ui.core.AnimationClockAmbient
import androidx.ui.core.Clip
import androidx.ui.core.Constraints
import androidx.ui.core.Layout
import androidx.ui.core.Modifier
import androidx.ui.core.RepaintBoundary
import androidx.ui.foundation.animation.FlingConfig
import androidx.ui.foundation.gestures.DragDirection
import androidx.ui.foundation.gestures.Scrollable
import androidx.ui.foundation.gestures.ScrollableState
import androidx.ui.foundation.shape.RectangleShape
import androidx.ui.layout.Constraints
import androidx.ui.layout.Container
import androidx.ui.semantics.ScrollTo
import androidx.ui.semantics.Semantics
import androidx.ui.unit.IntPx
import androidx.ui.unit.ipx
import androidx.ui.unit.min
import androidx.ui.unit.px
import androidx.ui.unit.toPx
import kotlin.math.roundToInt
/**
* Create and [remember] the state for a [VerticalScroller] or [HorizontalScroller] based on the
* currently appropriate scroll configuration to allow changing scroll position or observing
* scroll behavior.
*
* @param initial initial scroller position to start with
*/
@Composable
fun ScrollerPosition(
initial: Float = 0f
): ScrollerPosition {
val clock = AnimationClockAmbient.current
val config = FlingConfig()
return remember(clock, config) {
ScrollerPosition(flingConfig = config, initial = initial, animationClock = clock)
}
}
/**
* This is the state of a [VerticalScroller] and [HorizontalScroller] that
* allows the developer to change the scroll position by calling methods on this object.
*
* @param flingConfig configuration that specifies fling logic when scrolling ends with velocity
* @param initial initial scroller position to start with
* @param animationClock clock observable to run animation on. Consider querying
* [AnimationClockAmbient] to get current composition value
*/
@Model
class ScrollerPosition(
/** Configuration that specifies fling logic when scrolling ends with velocity. */
flingConfig: FlingConfig,
initial: Float = 0f,
animationClock: AnimationClockObservable
) {
private val consumeDelta: (Float) -> Float = {
val reverseDelta = -it
val newValue = value + reverseDelta
val max = maxPosition
val min = 0f
val consumed = when {
newValue > max -> max - value
newValue < min -> min - value
else -> reverseDelta
}
value += consumed
-consumed
}
internal val scrollableState =
ScrollableState(consumeDelta, flingConfig, animationClock)
/**
* current scroller position value
*/
var value = initial
private set
/**
* whether this [ScrollerPosition] is currently animating/flinging
*/
val isAnimating
get() = scrollableState.isAnimating
/**
* maxPosition this scroller that consume this ScrollerPosition can reach, or
* [Float.POSITIVE_INFINITY] if still unknown
*/
var maxPosition: Float = Float.POSITIVE_INFINITY
internal set
/**
* Smooth scroll to position in pixels
*
* @param value target value to smooth scroll to
*/
// TODO (malkov/tianliu) : think about allowing to scroll with custom animation timings/curves
fun smoothScrollTo(
value: Float,
onEnd: (endReason: AnimationEndReason, finishValue: Float) -> Unit = { _, _ -> }
) {
smoothScrollBy(value - this.value, onEnd)
}
/**
* Smooth scroll by some amount of pixels
*
* @param value delta to scroll by
*/
fun smoothScrollBy(
value: Float,
onEnd: (endReason: AnimationEndReason, finishValue: Float) -> Unit = { _, _ -> }
) {
scrollableState.smoothScrollBy(value, onEnd)
}
/**
* Instantly jump to position in pixels
*
* @param value target value to jump to
*/
fun scrollTo(value: Float) {
this.value = value
}
/**
* Instantly jump by some amount of pixels
*
* @param value delta to jump by
*/
fun scrollBy(value: Float) {
scrollTo(this.value + value)
}
}
// TODO(malkov): Test behavior during animation more extensively (including pressing on the scroller
// during an animation when b/144878730 is fixed.
/**
* A container that composes all of its contents and lays it out, fitting the width of the child.
* If the child's height is less than the [Constraints.maxHeight], the child's height is used,
* or the [Constraints.maxHeight] otherwise. If the contents don't fit the height, the drag gesture
* allows scrolling its content vertically. The contents of the VerticalScroller are clipped to
* the VerticalScroller's bounds.
*
* @sample androidx.ui.foundation.samples.VerticalScrollerSample
*
* @param scrollerPosition state of this Scroller that holds current scroll position and provides
* user with useful methods like smooth scrolling
* @param modifier Modifier to be applied to the Scroller content layout
* @param isScrollable param to enabled or disable touch input scrolling, default is true
*/
@Composable
fun VerticalScroller(
scrollerPosition: ScrollerPosition = ScrollerPosition(),
modifier: Modifier = Modifier.None,
isScrollable: Boolean = true,
child: @Composable() () -> Unit
) {
Scroller(scrollerPosition, modifier, true, isScrollable, child)
}
/**
* A container that composes all of its contents and lays it out, fitting the height of the child.
* If the child's width is less than the [Constraints.maxWidth], the child's width is used,
* or the [Constraints.maxWidth] otherwise. If the contents don't fit the width, the drag gesture
* allows scrolling its content horizontally. The contents of the HorizontalScroller are clipped to
* the HorizontalScroller's bounds.
*
* @sample androidx.ui.foundation.samples.SimpleHorizontalScrollerSample
*
* If you want to control scrolling position from the code, e.g smooth scroll to position,
* you must own memorized instance of [ScrollerPosition] and then use it to call `scrollTo...`
* functions on it. Same tactic can be applied to the [VerticalScroller]
*
* @sample androidx.ui.foundation.samples.ControlledHorizontalScrollerSample
*
* @param scrollerPosition state of this Scroller that holds current scroll position and provides
* user with useful methods like smooth scrolling
* @param modifier Modifier to be applied to the Scroller content layout
* @param isScrollable param to enabled or disable touch input scrolling, default is true
*/
@Composable
fun HorizontalScroller(
scrollerPosition: ScrollerPosition = ScrollerPosition(),
modifier: Modifier = Modifier.None,
isScrollable: Boolean = true,
child: @Composable() () -> Unit
) {
Scroller(scrollerPosition, modifier, false, isScrollable, child)
}
@Composable
private fun Scroller(
scrollerPosition: ScrollerPosition,
modifier: Modifier,
isVertical: Boolean,
isScrollable: Boolean,
child: @Composable() () -> Unit
) {
val direction =
if (isVertical) DragDirection.Vertical else DragDirection.Horizontal
Semantics(container = true, properties = {
if (isScrollable) {
ScrollTo(action = { x, y ->
if (isVertical) {
scrollerPosition.scrollBy(y.value)
} else {
scrollerPosition.scrollBy(x.value)
}
})
}
}) {
Scrollable(
scrollableState = scrollerPosition.scrollableState,
dragDirection = direction,
enabled = isScrollable
) {
ScrollerLayout(
scrollerPosition = scrollerPosition,
onMaxPositionChanged = { scrollerPosition.maxPosition = it },
modifier = modifier,
isVertical = isVertical,
child = child
)
}
}
}
@Composable
private fun ScrollerLayout(
scrollerPosition: ScrollerPosition,
modifier: Modifier,
onMaxPositionChanged: (Float) -> Unit,
isVertical: Boolean,
child: @Composable() () -> Unit
) {
Layout(
modifier = modifier,
children = {
Clip(RectangleShape) {
Container(alignment = Alignment.TopStart) {
RepaintBoundary(children = child)
}
}
},
measureBlock = { measurables, constraints, _ ->
if (measurables.size > 1) {
throw IllegalStateException("Only one child is allowed in a VerticalScroller")
}
val childConstraints = constraints.copy(
maxHeight = if (isVertical) IntPx.Infinity else constraints.maxHeight,
maxWidth = if (isVertical) constraints.maxWidth else IntPx.Infinity
)
val childMeasurable = measurables.firstOrNull()
val placeable = childMeasurable?.measure(childConstraints)
val width: IntPx
val height: IntPx
if (placeable == null) {
width = constraints.minWidth
height = constraints.minHeight
} else {
width = min(placeable.width, constraints.maxWidth)
height = min(placeable.height, constraints.maxHeight)
}
layout(width, height) {
val childHeight = placeable?.height?.toPx() ?: 0.px
val childWidth = placeable?.width?.toPx() ?: 0.px
val scrollHeight = childHeight - height.toPx()
val scrollWidth = childWidth - width.toPx()
val side = if (isVertical) scrollHeight else scrollWidth
if (side != scrollerPosition.maxPosition.px) {
onMaxPositionChanged(side.value)
}
val xOffset = if (isVertical) 0 else -scrollerPosition.value.roundToInt()
val yOffset = if (isVertical) -scrollerPosition.value.roundToInt() else 0
placeable?.place(xOffset.ipx, yOffset.ipx)
}
}
)
}