[go: nahoru, domu]

blob: 0cf1823a92366590547f7c27b7c8ab7ac139d0a7 [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.material.ripple
import androidx.compose.Composable
import androidx.compose.Recompose
import androidx.compose.ambient
import androidx.compose.memo
import androidx.compose.onDispose
import androidx.compose.unaryPlus
import androidx.ui.animation.transitionsEnabled
import androidx.ui.core.Density
import androidx.ui.core.Dp
import androidx.ui.core.Draw
import androidx.ui.core.LayoutCoordinates
import androidx.ui.core.OnChildPositioned
import androidx.ui.core.PxPosition
import androidx.ui.core.ambientDensity
import androidx.ui.core.center
import androidx.ui.core.gesture.PressIndicatorGestureDetector
import androidx.ui.graphics.Color
/**
* Ripple is a visual indicator for a pressed state.
*
* A [Ripple] component responds to a tap by starting a new [RippleEffect] animation.
* For creating an effect it uses the [RippleTheme.factory].
*
* @sample androidx.ui.material.samples.RippleSample
*
* @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded
* ripples always animate from the target layout center, bounded ripples animate from the touch
* position.
* @param radius Effects grow up to this size. If null is provided the size would be calculated
* based on the target layout size.
* @param color The Ripple color is usually the same color used by the text or iconography in the
* component. If null is provided the color will be calculated by [RippleTheme.defaultColor].
* @param enabled The ripple effect will not start if false is provided.
*/
@Composable
fun Ripple(
bounded: Boolean,
radius: Dp? = null,
color: Color? = null,
enabled: Boolean = true,
children: @Composable() () -> Unit
) {
val density = +ambientDensity()
val state = +memo { RippleState() }
val theme = +ambient(CurrentRippleTheme)
OnChildPositioned(onPositioned = { state.coordinates = it }) {
PressIndicatorGestureDetector(
onStart = { position ->
if (enabled && transitionsEnabled) {
state.handleStart(position, theme.factory, density, bounded, radius)
}
},
onStop = { state.handleFinish(false) },
onCancel = { state.handleFinish(true) },
children = children
)
}
Recompose { recompose ->
state.recompose = recompose
val finalColor = (color ?: +theme.defaultColor).copy(alpha = +theme.opacity)
Draw { canvas, _ ->
if (state.effects.isNotEmpty()) {
val position = state.coordinates!!.position
canvas.translate(position.x.value, position.y.value)
state.effects.forEach { it.draw(canvas, finalColor) }
canvas.translate(-position.x.value, -position.y.value)
}
}
}
+onDispose {
state.effects.forEach { it.dispose() }
state.effects.clear()
state.currentEffect = null
}
}
private class RippleState {
var coordinates: LayoutCoordinates? = null
var effects = mutableListOf<RippleEffect>()
var currentEffect: RippleEffect? = null
var recompose: () -> Unit = {}
fun handleStart(
touchPosition: PxPosition,
factory: RippleEffectFactory,
density: Density,
bounded: Boolean,
radius: Dp?
) {
val coordinates = checkNotNull(coordinates) {
"handleStart() called before the layout coordinates were provided!"
}
val position = if (bounded) touchPosition else coordinates.size.center()
val onAnimationFinished = { effect: RippleEffect ->
effects.remove(effect)
if (currentEffect == effect) {
currentEffect = null
}
}
val effect = factory.create(
coordinates,
position,
density,
radius,
bounded,
recompose,
onAnimationFinished
)
effects.add(effect)
currentEffect = effect
recompose()
}
fun handleFinish(canceled: Boolean) {
currentEffect?.finish(canceled)
currentEffect = null
}
}