Instantly share code, notes, and snippets.
Android Jetpack Compose caret animation
/* | |
* Copyright 2020 Prat Tana. All rights reserved. | |
* 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. | |
*/ | |
import androidx.animation.* | |
import androidx.compose.Composable | |
import androidx.compose.remember | |
import androidx.ui.animation.animate | |
import androidx.ui.core.Modifier | |
import androidx.ui.foundation.Box | |
import androidx.ui.foundation.Canvas | |
import androidx.ui.foundation.Clickable | |
import androidx.ui.foundation.ContentGravity | |
import androidx.ui.graphics.* | |
import androidx.ui.layout.Container | |
import androidx.ui.layout.LayoutSize | |
import androidx.ui.material.MaterialTheme | |
import androidx.ui.material.ripple.Ripple | |
import androidx.ui.unit.dp | |
const val CaretAnimationDuration = 200 | |
private val StrokeWidth = 3.dp | |
private val CaretSize = 20.dp | |
private val YOffset = FloatPropKey() | |
private val Fraction = FloatPropKey() | |
enum class CaretState { | |
Up, | |
Down | |
} | |
@Composable | |
fun ExpandCollapseButton( | |
buttonState: CaretState, | |
onStateChange: ((CaretState) -> Unit)?, | |
modifier: Modifier = Modifier.None, | |
animationStiffness: Float = Spring.StiffnessMedium, | |
color: Color = MaterialTheme.colors().secondary | |
) { | |
Container(modifier) { | |
Ripple(bounded = false) { | |
Clickable(onClick = { | |
onStateChange | |
?.let { | |
it( | |
if (buttonState == CaretState.Down) | |
CaretState.Up | |
else | |
CaretState.Down | |
) | |
} | |
}) { | |
Box( | |
modifier = LayoutSize(28.dp, 28.dp), | |
gravity = ContentGravity.Center | |
) { | |
DrawCaret(buttonState, color, animationStiffness) | |
} | |
} | |
} | |
} | |
} | |
private fun lerp(start: Float, stop: Float, fraction: Float): Float = | |
(1 - fraction) * start + fraction * stop | |
@Composable | |
private fun DrawCaret( | |
caretState: CaretState, | |
activeColor: Color, | |
animationStiffness: Float | |
) { | |
val strokeWidthDp = StrokeWidth | |
val paint = remember { | |
Paint().apply { | |
isAntiAlias = true | |
strokeCap = StrokeCap.round | |
color = activeColor | |
style = PaintingStyle.stroke | |
} | |
} | |
val animationBuilder = remember { | |
PhysicsBuilder<Float>(stiffness = animationStiffness) | |
} | |
val t = animate(if (caretState == CaretState.Up) 1f else 0f, animationBuilder) | |
val animatedYOffset = lerp(0f, 1f, t) | |
val fraction = lerp(-1f, 1f, t) | |
Canvas( | |
modifier = LayoutSize(CaretSize) | |
) { | |
val strokeWidth = strokeWidthDp.toPx().value | |
paint.strokeWidth = strokeWidth | |
val height = size.height.value / 4 | |
val x = size.height.value / 4 | |
val yOffset = (size.height.value - height) / 2 | |
val path = Path() | |
path.moveTo(x, yOffset + height * animatedYOffset) | |
path.relativeLineTo(x, -height * fraction) | |
path.relativeLineTo(x, height * fraction) | |
drawPath(path, paint) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.