Four, graphics,

Compose provides a rich set of components for creating a nice, easy-to-use interface. However, a large, rich component library cannot contain everything. Sometimes we need components that the library does not provide a ready-made solution, so we may need to draw them ourselves.

In the View system, to draw a custom graph, you need to call the Canvas method in the onDraw() function through the maintenance state to draw the desired graph and image dynamically, and assist to set the color and stroke properties in Paint. Compose turns the Canvas into a composable control. All properties are passed and maintained by DrawScope, which is a lambda argument to the Canvas.

@Preview
@Composable
fun ComposeCanvas(a) {
    Canvas(modifier = Modifier.fillMaxSize()){
        val canvasWidth = size.width
        val canvasHeight = size.height

        drawLine(
            start = Offset(x=canvasWidth, y = 0f),
            end = Offset(x = 0f, y = canvasHeight),
            color = Color.Blue,
            strokeWidth = 5F)}}Copy the code

The above code draws a line, using the start and end properties to determine the start and end of the line, color to determine the color of the line, and strokeWidth to determine the line width. The latter two are determined in Paint in the View system.

DrawScope also supports transformation operations, such as rotating a drawn image:

@Preview
@Composable
fun ComposeCanvas(a) {
    Canvas(modifier = Modifier.fillMaxSize()){
        val canvasWidth = size.width
        val canvasHeight = size.height

        rotate(degrees = 45F){
            drawRect(
                color = Color.Gray,
                topLeft = Offset(x = canvasWidth / 3F , y = canvasHeight / 3F),
                size = size / 3F)}}}Copy the code

The rotate() function turns a drawing 45° clockwise to produce a skewed rectangle:

Other transformations include things like Translate and scale. For multiple transformations, nested transformations are not recommended. Instead, the withTransform() method can be used to combine transformations as follows:

withTransform({
    translate(left = canvasWidth/5F)
    rotate(degrees=45F)
}) {
    drawRect(
        color = Color.Gray,
        topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
        size = canvasSize / 3F)}Copy the code

Five, animation,

Animation, while less basic and less commonly used than the previous sections, still has a very important application in improving the interactive experience.

Drawing on the all-widget idea of Flutter, it makes sense that animations should be packaged into Composable components that are supported by the underlying apis built with Kotlin coroutines.

There is a table below to help us decide what kind of animation implementation to use in different scenarios, of course, this is just one

Graph TB st(start) --> op1{change content during layout} op1 -- yes --> op11{enter animation} op11 -- yes --> ani11[AnimationVisibility] op11 -- no --> op12{change content size} Op12 -- Yes > ani12[Modifier. ContentSize] OP12 -- No > ani13[Crossfade] OP1 -- No > OP2 {State based} OP2 -- Yes > OP21 {occurred during combination} OP21 -- - > op211} {endless animation op211 -- - > ani21 [rememberInfiniteTransition] op211 - no - > op212 {at the same time more value driver} op212 -- - > Ani22 [updateTransition] op212 -- no --> Ani23 [animate*AsState] op2 -- no --> op3 -- Yes --> ani3[Animation] op3 -- No --> Op4 -- yes --> ani4[Animatable] op4 --> Ani5 [AnimationState or animate]

These implementations are described below.

1. Introduction to animation API

  • AnimatedVisibility (experimental function [^ 1])

    LocalhostAnimatedVisibility composable items can appear and disappear to add the content of animation effects.

    var editable by remember { mutableStateOf(true) }
    AnimatedVisibility(visible = editable) {
        Text(text = "Edit")}Copy the code

    By default, content appears as fade in and expand, and disappears as fade out and shrink. You can customize this transition effect by specifying EnterTransition and ExitTransition.

    var visible by remember { mutableStateOf(true) }
    AnimatedVisibility(
        visible = visible,
        enter = slideInVertically(
            initialOffsetY = { -40 }
        ) + expandVertically(
            expandFrom = Alignment.Top
        ) + fadeIn(initialAlpha = 0.3 f),
        exit = slideOutVertically() + shrinkVertically() + fadeOut()
    ) {
        Text("Hello", Modifier.fillMaxWidth().height(200.dp))
    }
    Copy the code

    The interesting point here is that the transition effects corresponding to the Enter and exit attributes can be combined with the “+” operator. These transition animation functions include the following:

    • EnterTransition
      • fadeIn
      • slideIn
      • expandIn
      • expandHorizontally
      • expandVertically
      • slideInHorizontally
      • slideInVertically
    • ExitTransition
      • fadeOut
      • slideOut
      • shrinkOut
      • shrinkHorizontally
      • shrinkVertically
      • slideOutHorizontally
      • slideOutVertically
  • animateContentSize

    AnimateContentSize displays animation effects when size changes.

    var message by remember { mutableStateOf("Hello") }
    Box(
        modifier = Modifier.background(Color.Blue).animateContentSize()
    ) {
        Text(text = message)
    }
    Copy the code
  • Crossfade

    Crossfade can be used to animate between two layouts using a fade in and out animation. By toggling the value passed to the current parameter, you can animate the content using fade in and fade out.

    var currentPage by remember { mutableStateOf("A") }
    Crossfade(targetState = currentPage) { screen ->
        when (screen) {
            "A" -> Text("Page A")
            "B" -> Text("Page B")}}Copy the code
  • animate*AsState

    The above animations are all high-level animations, and from this animation onwards, all low-level animations. Lower levels mean more abstraction and more customizable effects.

    The animate*AsState function is the simplest API that renders real-time value changes as animated values. It is supported by Animatable, a coroutine based API for animating individual values.

    Similar to the value property animation in the View architecture, the API will play the animation from the current value to the specified value as long as you provide the end value.

    val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5 f)
    Box(
        Modifier.fillMaxSize()
            .graphicsLayer(alpha = alpha)
            .background(Color.Red)
    )
    Copy the code

    For this “*”, Compose already provides out-of-the-box properties such as Float, Color, Dp, Size, Bounds, Offset, Rect, Int, IntOffset, and IntSize, and of course, You can also support other data types by providing typeConverter: TwoWayConverter

    to the animateValueAsState function that accepts a generic type.
    ,>

  • Animatable

    Animatable is a value container that animates values when they are changed by the animateTo function.

    Many of Animatable’s functions, including animateTo, are provided as suspended functions. This means that they need to be encapsulated within the appropriate coroutine scope. For example, you can use the LaunchedEffect composable to create a scope for the duration of a specified key value.

    // Start out gray and animate to green/red based on `ok`
    val color = remember { Animatable(Color.Gray) }
    LaunchedEffect(ok) {
        color.animateTo(if (ok) Color.Green else Color.Red)
    }
    Box(Modifier.fillMaxSize().background(color.value))
    Copy the code

    Animatable provides out-of-the-box support for Float and Color, but it is also possible to customize any data type with the typeConverter: TwoWayConverter

    argument.
    ,>

  • updateTransition

    Transition manages one or more animations as its children and runs them simultaneously across multiple states.

    The state here can be any data type. In many cases, you can use custom enum types to ensure type-safety, as shown in the following example:

    private enum class BoxState {
        Collapsed,
        Expanded
    }
    Copy the code

    UpdateTransition creates and remembers an instance of Transition and updates its state.

    var currentState by remember { mutableStateOf(BoxState.Collapsed) }
    val transition = updateTransition(currentState)
    Copy the code

    You can then use an animate* extension function to define child animations in this transition effect. Specify target values for each state. These animate* functions return an animation value that is updated frame by frame during animation playback when the transition state is updated using updateTransition.

    val rect by transition.animateRect { state ->
        when (state) {
            BoxState.Collapsed -> Rect(0f.0f.100f.100f)
            BoxState.Expanded -> Rect(100f.100f.300f.300f)}}val borderWidth by transition.animateDp { state ->
        when (state) {
            BoxState.Collapsed -> 1.dp
            BoxState.Expanded -> 0.dp
        }
    }
    Copy the code

    You can also pass the transitionSpec parameter, specifying a different AnimationSpec for each combination of transition state changes.

    val color by transition.animateColor(
        transitionSpec = {
            when {
                BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                    spring(stiffness = 50f)
                else ->
                    tween(durationMillis = 500)
            }
        }
    ) { state ->
        when (state) {
            BoxState.Collapsed -> MaterialTheme.colors.primary
            BoxState.Expanded -> MaterialTheme.colors.background
        }
    }
    Copy the code

    After transitioning to the targetState, transition.currentstate will be the same as transition.targetstate. This can be used as a signal to indicate whether the transition has been completed.

    Sometimes we want the initial state to be different from the first target state. We can do this by using a combination of updateTransition and MutableTransitionState. For example, it allows us to start playing animations as soon as the code enters the composition phase.

    // Start in collapsed state and immediately animate to expanded
    var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
    currentState.targetState = BoxState.Expanded
    val transition = updateTransition(currentState)
    // ...
    Copy the code
  • rememberInfiniteTransition

    InfiniteTransition can save one or more subanimations like Transition, but they start running as soon as they enter the composition phase and do not stop until they are removed. You can use rememberInfiniteTransition create InfiniteTransition instance. Child animations can be added using animateColor, animatedFloat, or animatedValue. You also need to specify infiniteRepeatable to specify the animation specification.

    val infiniteTransition = rememberInfiniteTransition()
    val color by infiniteTransition.animateColor(
        initialValue = Color.Red,
        targetValue = Color.Green,
        animationSpec = infiniteRepeatable(
            animation = tween(1000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        )
    )
    
    Box(Modifier.fillMaxSize().background(color))
    Copy the code
  • TargetBasedAnimation

    TargetBasedAnimation is the lowest level of animation API we have seen so far. Other apis suffice for most use cases, but using TargetBasedAnimation lets you directly control the playback time of your animation. In the following example, TargetAnimation’s playback time is manually controlled based on the frame time provided by withFrameMillis.

    val anim = remember {
        TargetBasedAnimation(
            animationSpec = tween(200),
            typeConverter = Float.VectorConverter,
            initialValue = 200f,
            targetValue = 1000f)}var playTime by remember { mutableStateOf(0L) }
    
    LaunchedEffect(anim) {
        val startTime = withFrameNanos { it }
    
        do {
            playTime = withFrameNanos { it } - startTime
            val animationValue = anim.getValueFromNanos(playTime)
        } while (someCustomCondition())
    }
    Copy the code

2. Custom animations

The animations above are simple uses of Compose default animations, but some of them support deeper customization. Many animation functions support specifying an AnimationSpec object as a parameter.

AnimationSpec is an interface, which means we can implement it to customize the animations we need. But don’t worry, there are some built-in implementation classes.

For the above animation, I have drawn a table that visually shows which apis receive AnimationSpec objects as parameters.

API The receivedAnimationSpecparameter
animateContentSize FiniteAnimationSpec
Crossfade FiniteAnimationSpec
animate*AsState AnimationSpec
TargetBasedAnimation AnimationSpec

For subclasses of FiniteAnimationSpec, each class has a corresponding build method that directly builds the corresponding object.

  • keyframes()

    Frame animation is based on the values corresponding to any timestamp in the process. At any given time, the value of the animation is calculated by the difference algorithm from the values of the two most recent frames. Of course, you can also specify the animation curve on a particular frame to change its linear progression.

    val value by animateFloatAsState(
        targetValue = 1f,
        animationSpec = keyframes {
            durationMillis = 375
            0.0 f at 0 with LinearOutSlowInEasing // for 0-15 ms
            0.2 f at 15 with FastOutLinearInEasing // for 15-75 ms
            0.4 f at 75 // ms
            0.4 f at 225 // ms})Copy the code
  • snap()

    Click! This animation can be transformed from one value to another in an instant, without process, all of a sudden. However, you can set delays and give yourself a reprieve!

    val value by animateFloatAsState(
        targetValue = 1f,
        animationSpec = snap(delayMillis = 50))Copy the code
  • tween()

    Tween animation can be understood as a keyframes with only the first and last frames. The animation completes its ordinary but great life by calmly moving from the beginning to the end with a curve.

    val value by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = 300,
            delayMillis = 50,
            easing = LinearOutSlowInEasing
        )
    )
    Copy the code
  • spring()

    The spring animation can be understood as an elastic animation, simulating the change in speed driven by the acceleration caused by the spring coefficient. It’s not a linear motion trend, it makes the animation process more physical.

Spring () function takes three parameters, including dampingRatio parameters define the spring elasticity, the default value is spring. The DampingRatioNoBouncy. Stiffness Defines how fast the Spring should move toward the end value. The default value is Spring.StiffnessMedium.

val value by animateFloatAsState(
    targetValue = 1f,
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioHighBouncy,
        stiffness = Spring.StiffnessMedium
    )
)
Copy the code
  • repeatable()

    Repeatable animation is like a buff that is applied to other ADC animations to complete assists. The repeatable () function to receive a DurationBasedAnimationSpec types of animation object description model, make continuous animation can achieve the result that after repeated.

Repetition effects support the following two modes:

You can specify the number of iterations through the Iterations parameter

  • infiniteRepeatable()

    Repeatable animation infiniteRepeatable animation is similar to repeatable animation, but it can not specify the number of repetition. it will repeat the animation indefinitely.

In the introduction of keyframes, we talked about changing the curve of the animation, and used LinearOutSlowInEasing and FastOutLinearInEasing.

Time-based AnimationSpec operations such as Tween or Keyframes use Easing to adjust the small values of the animation. This allows the animation to speed up and slow down rather than move at a constant rate. A decimal is a value between 0 (the starting value) and 1.0 (the ending value) and represents the current point in the animation.

Easing is actually a function that takes a small number between 0 and 1.0 and returns a floating point number. The returned value may be outside the boundary and represent overshoot or downshoot. You can create a custom Easing using the code shown below.

val CustomEasing = Easing { fraction -> fraction * fraction }

@Composable
fun EasingUsage(a) {
    val value by animateFloatAsState(
        targetValue = 1f,
        animationSpec = tween(
            durationMillis = 300,
            easing = CustomEasing
        )
    )
    / /... ...
}
Copy the code

Six, gestures

1. Click on the

Everything starts with the magic Modifier, which can not only control the position, spacing and color of the graphical properties, but also control click and scroll interactive events, is really a little clever.

The simplest interaction event is a click event. In Compose, if you want to use click events easily, you can do so simply by using the Button component.

Button(onClick = {
    Log.d(TAG, "Oh, you rascal!)
}) {
    Text("Don't move!)}Copy the code

But, there’s always a but, what if we just want to add click events to an image? That is not without a way, you can use the IconButtonModifier.

Image(
    painter = painterResource(id = R.mipmap.model),
    contentDescription = null,
    modifier = Modifier.clickable {
		Log.d(TAG, "Look only, don't touch.")})Copy the code

In fact, Button onClick is also the secret to steal the Modifier to learn.

2. Scroll

One day, when I was writing the Compose practice demo, I ran into a problem. I wanted to add a right slide menu to a list item, so I set the Row layout for the item, and the item on the left took up the entire screen. Hidden away from the screen on the right is the menu that is displayed after swiping. With everything in place, when I ran it, I realized that there was no way for the Row to slide, and I didn’t want to bother with LazyRow, so I had to do something different.

@Composable
fun TestScroll(a) {
    val scrollState = rememberScrollState()
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Row(
            modifier = Modifier.horizontalScroll(
                scrollState
            )
        ) {
            Box(
                modifier = Modifier
                .width(360.dp)
                .height(64.dp)
                .background(color = Color.Blue)
            )

            Text("You can't see me")}}}Copy the code

This is what happens when it runs:

If you need precise control over how far and how much to scroll, use the Modifier. Scrollable () attribute.

@Composable
fun TestScroll(a) {
    var offsetX by remember {
        mutableStateOf(0f)}val scrollableState = rememberScrollableState { delta ->
        offsetX += delta
        delta
    }
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Row(
            modifier = Modifier
                .scrollable(
                    scrollableState, Orientation.Horizontal
                )
                .offset {
                    IntOffset(offsetX.roundToInt(), 0)
                }
        ) {
            Box(
                modifier = Modifier
                    .width(360.dp)
                    .height(64.dp)
                    .background(color = Color.Blue)
            )
        }
    }
}
Copy the code

Put on the effect:

Of course, nested scrolling is definitely a problem with scrolling, because controls don’t play with themselves forever. Simple nested scrolling requires no additional work at all, and the gesture that initiates the scroll operation is automatically propagated from child to parent, so that when the child can’t scroll any further, the gesture is handled by the parent element. Of course, more complex and more personalized nestedScroll can be done through the Modifier.nestedscroll () property, similar to the use of NestedScrollView in View architecture.

You can always choose to believe in Modifier.

3. The other

Other more complex gestures include dragging, sliding, and multi-fingering.

Before we continue, we need to meet a new friend from the Modifier family — Modifier.Pointerinput (). This method takes two arguments, a key of any type and an extended method parameter of type suspend PointerInputScope.() -> Unit, which means that in this method we have the internal scope of PointerInputScope.

The PointerInputScope is an interface that inherits from the Density interface, which indicates that in its scope, we can directly obtain some Density related attributes. In addition, it declares two properties and a suspend function itself. Val size: IntSize gets the area that can be touched. The val viewConfiguration: viewConfiguration property identifies some touch configuration items. A suspended function blocks until the event is delivered and responds as soon as possible.

suspend fun <R> awaitPointerEventScope(
    block: suspend AwaitPointerEventScope. () - >R
): R
Copy the code

In this function, we can take the raw Touch event and customize personalized or more complex gestures based on the scene.

In addition, it has some very important and useful extension functions.

These suspend extension functions, prefixed with “detect,” encapsulate some of the higher-order gestures that are commonly used, with callback methods as arguments, and the appropriate function to use if needed.

With that in mind, let’s continue with our gestures.

1) drag and drop

In addition to the Clickable () and scrollable() methods described above, the Modifier class also has some *able() methods that are gesture specific. The Modifier. Draggable () method, for example, is an advanced encapsulation of the drag gesture. But it only supports a single direction, either horizontal or vertical. But you can use it twice to set up the horizontal and vertical drag gestures.

@Composable
fun TestGesture(a) {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        var offsetX by remember { mutableStateOf(0f)}var offsetY by remember { mutableStateOf(0f) }
        Box(modifier = Modifier
            .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
            .size(128.dp)
            .background(Color.Red)
            .draggable(
                orientation = Orientation.Horizontal,
                state = rememberDraggableState { delta ->
                    offsetX += delta
                }
            )
            .draggable(
                orientation = Orientation.Vertical,
                state = rememberDraggableState { delta ->
                    offsetY += delta
                }
            )
        )
    }
}
Copy the code

If you want to achieve the freedom to drag and drop, please use the Modifier. PointerInput (), with the help of PointerInputScope. DetectDragGestures () method.

2) the sliding

Sliding is similar to scrolling, but the difference is that scrolling is infinite. You can stop at any place and start again at any place. However, the slide is polar, it can not stop at any place, but must dock in a proprietary “port”. These ports are defined as anchors, beyond which they are, after all, just a passing passenger.

@Composable
fun TestGesture(a) {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        val width = 96.dp
        val squareSize = 48.dp

        val swipeableState = rememberSwipeableState(0)
        val sizePx = with(LocalDensity.current) { squareSize.toPx() }
        val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states

        Box(
            modifier = Modifier
                .width(width)
                .swipeable(
                    state = swipeableState,
                    anchors = anchors,
                    thresholds = { _, _ -> FractionalThreshold(0.3 f)}, orientation = Orientation.Horizontal ) .background(Color.LightGray) ) { Box( Modifier .offset { IntOffset(swipeableState.offset.value.roundToInt(),0) }
                    .size(squareSize)
                    .background(Color.DarkGray)
            )
        }
    }
}
Copy the code

Of course, the number of anchor points and the order is irrelevant, as long as the situation is endless, everywhere can stay! So you can also set three val anchors = mapOf(0F to 0, sizePx to 1, sizePx / 2 to 2).

The thresholds parameter is used to set the threshold for the switch to the next anchor. It can be a percentage (FractionalThreshold) or a FixedThreshold.

It’s important to note that until compose 1.0.3, this was an experimental feature.

3) Multi-finger touch control

The next one is the Modifier family’s Transformable () method. Without further ado, go straight to the demo on the official website.

@Composable
fun TransformableSample(a) {
    // set up all transformation states
    var scale by remember { mutableStateOf(1f)}var rotation by remember { mutableStateOf(0f)}var offset by remember { mutableStateOf(Offset.Zero) }
    val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
        scale *= zoomChange
        rotation += rotationChange
        offset += offsetChange
    }
    Box(
        Modifier
            // apply other transformations like rotation and zoom
            // on the pizza slice emoji
            .graphicsLayer(
                scaleX = scale,
                scaleY = scale,
                rotationZ = rotation,
                translationX = offset.x,
                translationY = offset.y
            )
            // add transformable to listen to multitouch transformation events
            // after offset
            .transformable(state = state)
            .background(Color.Blue)
            .fillMaxSize()
    )
}
Copy the code

As you can see, the transformable() method is a combination of “scaling,” “displacement,” and rotation.

Of course, more complex and subtle controls can still be configured in pointerInput().