[go: nahoru, domu]

blob: 90bf63ac5b77d66531c9f74dd1cef65ffce53877 [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.animation.core.AnimationClockObservable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.FloatPropKey
import androidx.compose.animation.core.InterruptionHandling
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.TransitionAnimation
import androidx.compose.animation.core.createAnimation
import androidx.compose.animation.core.transitionDefinition
import androidx.compose.animation.core.tween
import androidx.compose.getValue
import androidx.compose.mutableStateOf
import androidx.compose.setValue
import androidx.compose.animation.OffsetPropKey
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.inMilliseconds
import androidx.compose.ui.unit.milliseconds
import kotlin.math.max
/**
* [RippleAnimation]s are drawn as part of [RippleIndication] as a visual indicator for an
* [androidx.compose.foundation.Interaction.Pressed] state.
*
* Use [androidx.compose.foundation.clickable] or [androidx.compose.foundation.indication] to add a
* [RippleIndication] to your component, which contains a RippleAnimation for pressed states, and
* a state layer for other states.
*
* This is a default implementation based on the Material Design specification.
*
* A circular ripple effect whose origin starts at the input touch point and
* whose radius expands from 60% of the final value. The ripple origin
* animates to the center of its target layout for the bounded version
* and stays in the center for the unbounded one.
*
* @param size The size of the target layout.
* @param startPosition The position the animation will start from.
* @param radius Effects grow up to this size.
* @param clipped If true the effect should be clipped by the target layout bounds.
* @param clock The animation clock observable that will drive this ripple effect
* @param onAnimationFinished Call when the effect animation has been finished.
*/
internal class RippleAnimation(
size: Size,
startPosition: Offset,
radius: Float,
private val clipped: Boolean,
clock: AnimationClockObservable,
private val onAnimationFinished: (RippleAnimation) -> Unit
) {
private val animation: TransitionAnimation<RippleTransition.State>
private var transitionState = RippleTransition.State.Initial
private var finishRequested = false
private var animationPulse by mutableStateOf(0L)
init {
val surfaceSize = size
val startRadius = getRippleStartRadius(surfaceSize)
val targetRadius = radius
val center = size.center()
animation = RippleTransition.definition(
startRadius = startRadius,
endRadius = targetRadius,
startCenter = startPosition,
endCenter = center
).createAnimation(clock)
animation.onUpdate = {
// TODO We shouldn't need this animationPulse hack b/152631516
animationPulse++
}
animation.onStateChangeFinished = { stage ->
transitionState = stage
if (transitionState == RippleTransition.State.Finished) {
onAnimationFinished(this)
}
}
// currently we are in Initial state, now we start the animation:
animation.toState(RippleTransition.State.Revealed)
}
fun finish() {
finishRequested = true
animation.toState(RippleTransition.State.Finished)
}
fun DrawScope.draw(color: Color) {
animationPulse // model read so we will be redrawn with the next animation values
val alpha = if (transitionState == RippleTransition.State.Initial && finishRequested) {
// if we still fading-in we should immediately switch to the final alpha.
1f
} else {
animation[RippleTransition.Alpha]
}
val centerOffset = animation[RippleTransition.Center]
val radius = animation[RippleTransition.Radius]
val modulatedColor = color.copy(alpha = color.alpha * alpha)
if (clipped) {
clipRect {
drawCircle(modulatedColor, radius, centerOffset)
}
} else {
drawCircle(modulatedColor, radius, centerOffset)
}
}
}
/**
* The Ripple transition specification.
*/
private object RippleTransition {
enum class State {
/** The starting state. */
Initial,
/** User is still touching the surface. */
Revealed,
/** User stopped touching the surface. */
Finished
}
private val FadeInDuration = 75.milliseconds
private val RadiusDuration = 225.milliseconds
private val FadeOutDuration = 150.milliseconds
val Alpha = FloatPropKey()
val Radius = FloatPropKey()
val Center = OffsetPropKey()
fun definition(
startRadius: Float,
endRadius: Float,
startCenter: Offset,
endCenter: Offset
) = transitionDefinition<State> {
state(State.Initial) {
this[Alpha] = 0f
this[Radius] = startRadius
this[Center] = startCenter
}
state(State.Revealed) {
this[Alpha] = 1f
this[Radius] = endRadius
this[Center] = endCenter
}
state(State.Finished) {
this[Alpha] = 0f
// the rest are the same as for Revealed
this[Radius] = endRadius
this[Center] = endCenter
}
transition(State.Initial to State.Revealed) {
Alpha using tween(
durationMillis = FadeInDuration.inMilliseconds().toInt(),
easing = LinearEasing
)
Radius using tween(
durationMillis = RadiusDuration.inMilliseconds().toInt(),
easing = FastOutSlowInEasing
)
Center using tween(
durationMillis = RadiusDuration.inMilliseconds().toInt(),
easing = LinearEasing
)
// we need to always finish the radius animation before starting fading out
interruptionHandling = InterruptionHandling.UNINTERRUPTIBLE
}
transition(State.Revealed to State.Finished) {
fun <T> toFinished() =
tween<T>(
durationMillis = FadeOutDuration.inMilliseconds().toInt(),
easing = LinearEasing
)
Alpha using toFinished()
Radius using toFinished()
Center using toFinished()
}
}
}
/**
* According to specs the starting radius is equal to 60% of the largest dimension of the
* surface it belongs to.
*/
internal fun getRippleStartRadius(size: Size) =
max(size.width, size.height) * 0.3f
/**
* According to specs the ending radius
* - expands to 10dp beyond the border of the surface it belongs to for bounded ripples
* - fits within the border of the surface it belongs to for unbounded ripples
*/
internal fun Density.getRippleEndRadius(bounded: Boolean, size: Size): Float {
val radiusCoveringBounds =
(Offset(size.width, size.height).getDistance() / 2f)
return if (bounded) {
radiusCoveringBounds + BoundedRippleExtraRadius.toPx()
} else {
radiusCoveringBounds
}
}
private val BoundedRippleExtraRadius = 10.dp