An overview of

Jetpack Compose is a new tool for building the Android interface, which I’ve been trying out since the beta was released.

Previously, we assigned values to the controls manually after finding them with findViewById() using AN XML layout. The disadvantages of this are that first, parsing XML to generate view objects requires wasted performance through reflection, and then manually assigning values to controls is error-prone, such as controlling the display and hiding of a view in one place, which becomes more and more complex to maintain as more and more places control it.

In recent years, the industry has moved to a declarative interface model that greatly simplifies the process of building interfaces. When updating the interface, it is smart to find the part that should be updated and refresh only that part of the view.

Compose is a declarative UI framework

For a better way to write Jetpack Compose, download the Android Studio preview version of Android Studio’s latest Canary version. It can coexist with the release we’re using. When you use it to create a project, there is a built-in template for creating the Compose project.

A simple introduction to the Compose function

@Composable fun Greeting(name: String) { Column (modifier = Modifier.padding(16.dp)){ Text(text = "Hello $name!" $name = "$name!" )}}Copy the code

  • This function requires the @composable annotation, which all Compose related functions require, to tell the compiler that the function is used to generate the UI.
  • This function can take parameters that allow the application logic to describe the interface, such as the function above, which accepts a string name that can affect the display of the interface
  • In this function Column() corresponds to the previous LinearLayout in XML, and Text() corresponds to the previous TextVIew in XML

Be careful when using Compose

  • Composable functions can be executed in any order
  • Composable functions can be executed in parallel
  • Avoid composable functions that do not need to be updated when regrouping
  • Reorganization is optimistic operation can be cancelled at any time
  • Composable functions are executed as frequently as each frame of the animation

The state of the composer

The following function allows you to dynamically change the contents of a Text field as you enter Text

@Composable
fun HelloContent(a){
    Column(modifier = Modifier.padding(16.dp)) {
        var name = remember{ mutableStateOf("")}
        if(name.value.isNotEmpty()){
           Text(text = "Hello,${name.value}",
                modifier = Modifier.padding(8.dp),
               style = MaterialTheme.typography.h5)
        }
        OutlinedTextField(value = name.value, onValueChange = { name.value = it },label = {Text("Name")}}}Copy the code

Remember is a function that can hold the numbers in a Composable temporarily, but when the Composable is removed it is also removed or broken.

To save your status after an interruption, for example when a call comes in, use rememberSaveable

We can use MutableState to dynamically update the interface by observing the state of the data. In addition to using MutableState, you can also use LiveData, Flow, and Rxjava2, which need to be converted to the State interface for Compose to recognize.

For example, in LiveData conversion, setting an extension method to LiveData also uses Remember to create a state and return it.

@Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
    val lifecycleOwner = LocalLifecycleOwner.current
    val state = remember { mutableStateOf(initial) }
    DisposableEffect(this, lifecycleOwner) {
        val observer = Observer<T> { state.value = it }
        observe(lifecycleOwner, observer)
        onDispose { removeObserver(observer) }
    }
    return state
}
Copy the code

Typically, data is not placed directly in the Composable modifier method but is presented as shown in the following code, which makes it easier to reuse and manage code

@Composable
fun HelloScreen(a){
    var name = remember{ mutableStateOf("")}
    HelloContent(name = name.value, onNmeChange = { name.value = it })
}

@Composable
fun HelloContent(name: String,onNmeChange:(String) - >Unit){
    Column(modifier = Modifier.padding(16.dp)) {
        if(name.isNotEmpty()){
           Text(text = "Hello,${name}",
                modifier = Modifier.padding(8.dp),
               style = MaterialTheme.typography.h5)
        }
        OutlinedTextField(value = name, onValueChange = onNmeChange,label = {Text("Name")}}}Copy the code

HelloScreen is responsible for the data state change logic, and HelloContent is responsible for UI presentation and event delivery.

Use the ViewModel

Modify the above example by using ViewModel to manage data state

class HelloViewModel:ViewModel() {
    private val _name = MutableLiveData("")
    val name : LiveData<String> = _name

    fun onNameChanged(newName:String){
        _name.value = newName
    }
}
Copy the code

In the MainActivity

private val viewModel:HelloViewModel by viewModels()
 override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface(color = MaterialTheme.colors.background) {
                    Column {
                        HelloScreen(viewModel)
                    }
                }
            }
        }
    }
@Composable
fun HelloScreen(viewModel: HelloViewModel){
    val name:String by viewModel.name.observeAsState("")
    HelloContent(name = name, onNmeChange = { viewModel.onNameChanged(it) })
}

@Composable
fun HelloScreen(a){
    var name = remember{ mutableStateOf("")}
    HelloContent(name = name.value, onNmeChange = { name.value = it })
}

@Composable
fun HelloContent(name: String,onNmeChange:(String) - >Unit){
    Column(modifier = Modifier.padding(16.dp)) {
        if(name.isNotEmpty()){
           Text(text = "Hello,${name}",
                modifier = Modifier.padding(8.dp),
               style = MaterialTheme.typography.h5)
        }
        OutlinedTextField(value = name, onValueChange = onNmeChange,label = {Text("Name")}}}Copy the code

The observeAsState in the code above looks at LiveData and returns State, the observable type that State Jetpack Compose can use directly. ObserveAsState also encapsulates the remember function. The following dependencies need to be introduced when used

Implementation "androidx.com pose. The runtime: the runtime - livedata: 1.0.0 - beta01"Copy the code

Jetpack Compose describes a UI interface as a combination of components. When the state of the application changes, Jetpack Compose arranges a reorganization. The reorganization is the re-execution of the components that may have changed due to the state change. Reorganizing is the only way to update the interface.

That is, a composition can represent an interface whose internal composable items have a life cycle of entering a composition, performing zero or more recombinations, and exiting the composition.

The recombination in Compose is typically triggered by the State

interface. Compose keeps track of composable items that use State

data, and Compose only changes the parts that have changed when recombining.

Let’s take a look at the common layout, list, animation, gesture, and other operations used in Compose.

layout

The three most common layouts in Compose are Column, Row, and Box

  • A Column is equivalent to a vertical LinearLayout
  • Row is equivalent to a horizontal LinearLayout
  • Box is equivalent to FrameLayout
@Composable
fun LayoutTest(a){
    Column() {
        Row(modifier = Modifier.padding(10.dp),verticalAlignment = Alignment.CenterVertically) {
            Image(painter = painterResource(id = R.drawable.ic_launcher_background),
                contentDescription = "Avatar",
                Modifier
                    .width(60.dp)
                    .height(60.dp)
                    .clip(RoundedCornerShape(50)))
            Column(modifier = Modifier.padding(10.dp)) {
                Text(text = "Name")
                Text(text = "2 minute ago")}}}}Copy the code

The above code easily achieves the effect of two vertical lines of text on the left and right.

Modifier is used specifically to control view properties such as padding, offset, background, rounded corners, size, appearance, behavior, interaction, etc

Note: The order of modifier can also affect the result. For example, in the following code, if the padding is before the clickable, then the padding is not clickable. If the padding is behind the clickable, then the entire Column layout can be clickable

@Composable
fun ArtistCard(name: String,onClick: () -> Unit){
    Column(modifier = Modifier
        .padding(16.dp) // Set the 16dp padding
        .clickable(onClick = onClick) // Let the control have the click property and the click water ripple effect
        .fillMaxWidth() // The width fills the parent control
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Spacer(Modifier.size(20.dp)) // set a square with a width and height of 20dp
            Card(elevation = 4.dp) {
                Image(painter = painterResource(id = R.drawable.img_bg_2), contentDescription = "pic")}}}}Copy the code

If you want Column or Row to scroll, just add the following properties. Modifier. VerticalScroll (rememberScrollState ()) or horizontalScroll (rememberScrollState ())

Use the Modifier. RequiredSize method to set the width and height of a container’s child layout to be within its scope by default. If you want the child layout to be outside its scope, use the Modifier

  Box(
        Modifier
            .size(90.dp, 150.dp)
            .background(Color.Green)) {
        Box(
            Modifier
                .requiredSize(100.dp, 100.dp)
                .background(Color.Red)) {
        }
    }
Copy the code

You can also use the weight attribute to scale down the size of the office

@Composable
fun FlexibleComposable(a) {
    Row(Modifier.fillMaxWidth()) {
        Box(
            Modifier
                .weight(2f)
                .height(50.dp)
                .background(Color.Blue))
        Box(
            Modifier
                .weight(1f)
                .height(50.dp)
                .background(Color.Red))
    }
}
Copy the code

In addition to the previous three layout containers, if you want to use a relative layout in Compose, you can use ConstraintLayout, which is useful when implementing a layout that is more complex in its way.

ConstraintLayout is the preferred layout for composing, although it is recommended to use ConstraintLayout in order to reduce the layout hierarchy. In Compose, there is no need to worry about the layout hierarchy, so Column, Row, and Box are preferred

Constraintlayout needs to be introduced separately when using it

Implementation "androidx constraintlayout: constraintlayout - compose: 1.0.0 - alpha03"Copy the code

The following code implements a text under the button, 16dp away from the bottom of the button

@Composable
fun ConstraintLayoutContent(a) {
    ConstraintLayout {
        // Create a reference to a composable object
        val (button, text) = createRefs()
        Button(
            onClick = {  },
            // Constrain the button to 16dp from the top of the layout
            modifier = Modifier.constrainAs(button) { 
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button")}// Constrain the text to 16dp from the bottom of the button
        Text("Text", Modifier.constrainAs(text) { 
            top.linkTo(button.bottom, margin = 16.dp)
        })
    }
}
Copy the code

Custom layout

For custom layouts, we inherit the ViewGroup and rewrite the onMeasure and onLayout for XML layouts. In Compose, we just write a function using Layout composables

For example, the following custom implements an effect similar to Column

@Composable
fun MyColumn(modifier: Modifier = Modifier,content:@Composable() () - >Unit){
    Layout(content = content,modifier = modifier, measurePolicy = MeasurePolicy{
        measurables, constraints ->
        //
        val placeables = measurables.map { measurable ->
            // Measure each child view
            measurable.measure(constraints.copy(minWidth = 0))
        }

        layout(constraints.maxWidth,300) {// Trace the y coordinate of the child view
            var yPosition = 0
            // Place the child view in the parent view
            placeables.forEach{ placeable ->
                placeable.place(x=0,y=yPosition)
                yPosition+=placeable.height
            }
        }
    })
}
// Use it directly as follows
MyColumn (Modifier
      .padding(8.dp)
      .background(Color.Red)){
          Text("Ha ha ha ha first line")
          Text("Ha ha ha ha second line")
          Text("Ha ha ha ha third line")
          Text("Ha ha ha ha line four")}Copy the code

In addition to the custom container, you can also customize the Modifier if you feel that the Modifier is not enough. This is to add an extension function to the Modifier, such as the following to add a distance from the baseline of the text to the top of the text

fun Modifier.firstBaselineToTop(firstBaseLineToTop: Dp)=Modifier.layout{
    measurable, constraints ->
    // Measure measurable parameters
    val placeable = measurable.measure(constraints)
    // Check if there is a text baseline FirstBaselinecheck(placeable[FirstBaseline]! = AlignmentLine.Unspecified)val firstBaseLine = placeable[FirstBaseline]
    // Height minus firstBaseLine
    val placeableY = firstBaseLineToTop.toPx().toInt() - firstBaseLine
    val height = placeable.height + placeableY
    // Specify the size of the composable item with layout
    layout(placeable.width,height){
        placeable.place(0,placeableY)
    }
}
Copy the code

If you add firstBaselineToTop to Text, you can see that using firstBaselineToTop is smaller than using the padding distance

@Preview
@Composable
fun TextWithPaddingToBaselinePreview(a) {
    MyApplicationTheme {
        Text("firstBaselineToTop", Modifier.firstBaselineToTop(32.dp))
    }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview(a) {
    MyApplicationTheme {
        Text("Common padding!", Modifier.padding(top = 32.dp))
    }
}
Copy the code

The custom View

Customizing views in Compose is a lot easier than using XML before, such as drawing lines, circles, rectangles, and so on directly inside the Canvas

@Composable
fun CanvasTest(a) {
    Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
        drawLine(
            start = Offset(0f.0f),
            end = Offset(size.width, size.height),
            color = Color.Blue,
            strokeWidth = 5f
        )
        rotate(degrees = 45f){
            drawRect(
                color = Color.Green,
                size = size/4f,
                topLeft = Offset(size.width/3f,size.height/3f)
            )
        }
        drawCircle(
            color = Color.Blue,
            center = Offset(size.width/2,size.height/2),
            radius = 50f
        )
        // Multiple state combinations of rotation and translation
        withTransform({
            translate(left = size.width/5f)
            rotate(degrees = 45f)
        }){
            drawRect(
                color = Color.Yellow,
                size = size/5f,
                topLeft = Offset(size.width/3f,size.height/3f)}})}Copy the code

The list of

We tried adding a verticalScroll() to Column to scroll it. But at this point it’s just the equivalent of a ScrollView when we used XML layout, loading everything at a time. Too much data will affect performance.

Compose provides LazyColumn and LazyRow if you want to implement Recyclerview caching for XML layouts. For example,

@Composable
fun MessageList(messages:List<String>){
     LazyColumn{
        items(messages){ message ->
            Text(text = message)
        }
     }
}
Copy the code

A single list item can be loaded inside a LazyColumn via item() and multiple items via items(). There is also an itemsIndexed list item that can be implemented.

@Composable
fun MessageList(messages:List<String>){
     LazyColumn{
         itemsIndexed(messages){index,message ->
             Text(text = "$message= = =$index")}}}Copy the code

If you want to add a stickyHeader to a list, you can easily implement it using stickyHeader, but this is an experimental API that may be changed or removed in the future.

@Composable
fun MessageList(messages:List<String>){
     val listState = rememberLazyListState()
     LazyColumn(contentPadding = PaddingValues(horizontal = 16.dp,vertical = 8.dp),
          verticalArrangement = Arrangement.spacedBy(4.dp),state = listState){
         println("Rolling:${listState.firstVisibleItemIndex}= =${listState.firstVisibleItemScrollOffset}")
         stickyHeader(1) {
             Text(text = "I am the head",Modifier.fillMaxWidth().height(60.dp).background(Color.Green))
         }
         itemsIndexed(messages){index,message ->
             Text(text = "$message= = =$index", Modifier.background(Color.Yellow).height(60.dp))
             Spacer(modifier = Modifier
                 .fillMaxWidth()
                 .height(5.dp)
                 .background(Color.Gray))
         }
     }
}
Copy the code

The listState in the above code listens for the state of the list as it scrolls, such as the first visible position. ListState also provides methods to control list scrolling, such as scrollToItem, animateScrollToItem, and so on.

Grid lists can be implemented via LazyVerticalGrid, an API that is experimental at the moment and may change in the future

@ExperimentalFoundationApi
@Composable
fun GridList(messages:List<String>){
    LazyVerticalGrid(cells = GridCells.Adaptive(minSize = 128.dp), content = {
        items(messages){ message ->
            Text(text = message,Modifier
                .background(Color.Yellow)
                .height(60.dp))
        }
    })
}
Copy the code

animation

AnimatedVisibility

This API makes it easy to combine multiple animations. Currently, this API is experimental and may be changed or deleted in the future

 Row{
        AnimatedVisibility(
        visible = visible,
        enter = slideInVertically(initialOffsetY = {-40}) + expandVertically(expandFrom = Alignment.Top) +
                fadeIn(initialAlpha = 0.3 f),
        exit = slideOutVertically()+ shrinkVertically()+ fadeOut()) {
        Text(text = "text",fontSize =30.sp)
    }
        Spacer(modifier = Modifier.size(20.dp)) Button(onClick = { visible = ! visible }) { Text(text ="Click")}}Copy the code

Click the button in the above code to control a text page from bottom to top, and enter the interface from top to bottom with the effect of fading in and out. Multiple effects can be directly linked by a + sign.

animateContentSize

If you want to animate a control when it resizes, use animateContentSize, which is handy.

  var message by remember { mutableStateOf("Hello") }
        Row {
            Box(
                modifier = Modifier
                    .background(Color.Blue)
                    .animateContentSize()
            ) {
                Text(text = message)
            }
            Button(onClick = { message += message }) {
                Text(text = "Click")}}Copy the code

When the above code is clicked, it adds the Text of the Text control. You can see that the Text control has a transition effect when resized.

Crossfade

It is easy to use the Crossfade to wrap the control in and out when switching

var currentPage by remember { mutableStateOf("A") }
Row {
    Crossfade(targetState = currentPage) { screen ->
       when(screen){
           "A" -> Text(text = "A",Modifier.background(Color.Green),fontSize = 30.sp)
           "B" -> Text(text = "B",Modifier.background(Color.Blue),fontSize = 30.sp)
       }
    }
    Spacer(modifier = Modifier.size(20.dp))
    Button(onClick = { if(currentPage=="A") currentPage="B" else currentPage="A" }) {
        Text(text = "Click")}}Copy the code

In the code above, click the button, Text switch A, B will fade in and out of the effect

animate*AsState

* represents various data types, such as animateFloatAsState, animateDpAsState, animateSizeAsState, animateOffsetAsState, animateIntAsState, etc

 var enabled by remember{mutableStateOf(true)}
 val alpha = animateFloatAsState(targetValue = if (enabled) 1f else 0.5 f)
 Row {
     Box (
         Modifier
             .width(50.dp)
             .height(50.dp)
             .graphicsLayer(alpha = alpha.value)
             .background(Color.Red))
     Spacer(modifier = Modifier.size(20.dp)) Button(onClick = { enabled = ! enabled }) { Text(text ="Click")}}Copy the code

In the code above, the opacity of the control’s background changes from 1 to 0.5 when the button is clicked

Animatable

Animatable is a container that can be used to add effects to animations using the animateTo method. Much of Animatable’s functionality is provided in the form of suspended functions, so it generally runs within the scope of a coroutine. LaunchedEffect can be used to create a coroutine scope

var ok by remember{mutableStateOf(true)}
val color = remember{ Animatable(Color.Gray)}
LaunchedEffect(ok){
    color.animateTo(if (ok) Color.Green else Color.Red)
}
Row {
    Box (
        Modifier
            .width(50.dp)
            .height(50.dp)
            .background(color.value))
    Spacer(modifier = Modifier.size(20.dp)) Button(onClick = { ok = ! ok }) { Text(text ="Click")}}Copy the code

In the code above, the background of the click button control transitions from green to red

updateTransition

UpdateTransition is a method that returns a Transition object that manages multiple animations and runs them simultaneously.

  var currentState by remember{mutableStateOf(BoxState.Collapsed)}
        val transition = updateTransition(targetState = currentState)
        val size by transition.animateDp { state ->
            when (state) {
                BoxState.Collapsed -> 10.dp
                BoxState.Expanded -> 100.dp
            }
        }
        val coloranimate by transition.animateColor(
            transitionSpec = {
                when {
                    BoxState.Expanded isTransitioningTo BoxState.Collapsed ->
                        // Spring can create animations based on physical properties such as fast then slow, rebound, constant speed, etc
                        spring(stiffness = 50f)
                    else ->
                        tween(durationMillis = 500)
                }
            }
        ) { state ->
            when (state) {
                BoxState.Collapsed -> Color.Blue
                BoxState.Expanded -> Color.Yellow
            }
        }
        Row {
            Box(
                Modifier
                    .size(size)
                    .background(coloranimate)){
            }
            Button(onClick = {
                currentState = if(currentState == BoxState.Collapsed) BoxState.Expanded
                else BoxState.Collapsed
            }) {
                Text(text = "Click")}}Copy the code

The code transition above manages two animations, one from 10 to 100 in size and one from blue to yellow. The two animations are executed together when the button is clicked

InfiniteTransition

InfiniteTransition can also save multiple animations, which, unlike previous animations, run as soon as they are laid out

val infiniteTransition = rememberInfiniteTransition()
     val colorTran by infiniteTransition.animateColor(
         initialValue = Color.Red,
         targetValue = Color.Green,
         animationSpec = infiniteRepeatable(
             animation = tween(1000, easing = LinearEasing),
             repeatMode = RepeatMode.Reverse
         )
     )
     Row {
         Box(
             Modifier
                 .width(60.dp)
                 .height(60.dp)
                 .background(colorTran))
     }
Copy the code

In the code above, after the page loads, the background of the control switches between red and green.

gestures

Click on the operation

Just use the Modifier clickable

@Composable
fun ClickableSample(a) {
    val count = remember { mutableStateOf(0) }
    Text(
        text = count.value.toString(),
        modifier = Modifier
            .width(30.dp)
            .height(30.dp)
            .background(Color.Gray)
            .wrapContentSize(Alignment.Center)
            .clickable { count.value += 1 },
        textAlign = TextAlign.Center
    )
}
Copy the code

If you want more elaborate clicking, you can use the pointerInput method to press, long press, double click, or click

@Composable
fun PointerInputSample(a) {
    val count = remember { mutableStateOf(0) }
    Text(
        text = count.value.toString(),
        modifier = Modifier
            .width(30.dp)
            .height(30.dp)
            .background(Color.Gray)
            .wrapContentSize(Alignment.Center)
            .pointerInput (Unit){
               detectTapGestures (
                   onPress = {/* Press to operate */},
                   onLongPress = {/* Long press */},
                   onDoubleTap = {/ * double-click * /},
                   onTap = {/ * click * /}
               )
            },
        textAlign = TextAlign.Center
    )
}
Copy the code

Rolling operation

Just add verticalScroll or horizontalScroll to a page element to do vertical and horizontal scrolling, similar to the ScrollView we used when we used XML layout

@Composable
fun ScrollBoxes(a) {
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}
Copy the code

If you want to scrollTo a specified location, such as clicking the button to scrollTo 200 in the code below, you can use the scrollTo method of the rememberScrollState to scroll.

This operation needs to be run in the scope of a coroutine, which can be obtained using the rememberCoroutineScope method

@Composable
private fun ScrollBoxesSmooth(a) {
    val scrollState = rememberScrollState()
    val scope = rememberCoroutineScope()
    Column {
        Column(
            modifier = Modifier
                .background(Color.LightGray)
                .size(100.dp)
                .padding(horizontal = 8.dp)
                .verticalScroll(scrollState)
        ) {
            repeat(10) {
                Text("Item $it", modifier = Modifier.padding(2.dp))
            }
        }
        Button(onClick = {
            scope.launch { scrollState.scrollTo(200) }
        }) {
            Text(text = "Click")}}}Copy the code

If you want to record where your finger slides across the screen, you can use the Scrollable modifier to do so. For example, in the code below, state in scrollable can listen for how far the finger is scrolling.

@Composable
fun ScrollableDemo(a){
    var offset by remember{ mutableStateOf(0f) }
    Box(
        Modifier
            .size(100.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                state = rememberScrollableState { delta ->
                    offset += delta
                    delta
                }
            )
            .background(Color.Gray),
        contentAlignment = Alignment.Center
    ) {
        Text(text = offset.toString())
    }
}
Copy the code

Nested scroll simple nested scroll is very simple, which control needs to scroll the corresponding verticalScroll or horizontalScroll can be added.

Compose automatically handles the sliding, scrolling child control first, and then the parent control starts when the child control reaches the edge.

In this example, each item in the class table is still a list. As you slide, you can see that the inner list slides first, and the outer list slides when you reach the boundary.

@Composable
fun nestScrollDemo1(a){
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .width(100.dp)
            .height(200.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Column(
                Modifier
                    .border(6.dp, Color.Blue)
                    .background(Color.Green)
                    .padding(15.dp)
                    .height(150.dp)
                    .verticalScroll(rememberScrollState())) {
                repeat(20){
                    Text("Item $it", modifier = Modifier.padding(2.dp))
                }
            }
        }
    }
}
Copy the code

A drag operation can use the draggable modifier, which allows you to drag in a single direction such as horizontal or vertical.

For example, in the following example, set the direction of the drag in the draggable. After listening to the distance of the drag, set the offset method to itself to achieve the drag and slide.

@Composable
fun draggableDemo(a){
    var offsetX by remember{ mutableStateOf(0f) }
    Text(text = "Drag me across.",
        modifier = Modifier
            .background(Color.Green)
            .offset { IntOffset(offsetX.roundToInt(), 0) }
            .draggable(
                orientation = Orientation.Horizontal,
                state = rememberDraggableState(onDelta = { offsetX += it })
            ))
}
Copy the code

If you want to drag in more than one direction, you can use the pointerInput modifier, such as the following example to record offsets in the X and Y directions, and then set it to itself to allow you to drag freely.

@Composable
fun draggableDemo1(a){
    Box(modifier = Modifier.fillMaxSize()) {
        var offsetX by remember { mutableStateOf(0f)}var offsetY by remember { mutableStateOf(0f) }
        Box(
            Modifier
                .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                .background(Color.Blue)
                .size(50.dp)
                .pointerInput(Unit) {
                    detectDragGestures { change, dragAmount ->
                        change.consumeAllChanges()
                        offsetX += dragAmount.x
                        offsetY += dragAmount.y
                    }
                }
        )
    }
}
Copy the code

Swipeable can automatically slide the control in a custom direction after dragging it loose, such as the common slide switch effect

@ExperimentalMaterialApi
@Composable
fun SwipeableDemo(a) {
    val squareSize = 50.dp

    // Gesture sliding state
    val swipeableState = rememberSwipeableState(0)
    val sizePx = with(LocalDensity.current) { squareSize.toPx() }
    // Sliding anchor point
    val anchors = mapOf(0f to 0, sizePx to 1)

    Box(
        modifier = Modifier
            .width(100.dp)
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                // Whether to slide to the specified position automatically after release
                thresholds = { _, _ -> FractionalThreshold(0.3 f) },
                orientation = Orientation.Horizontal
            )
            .background(Color.LightGray)
    ) {
        Box(
            Modifier
                // Use offset to offset itself
                .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
                .size(squareSize)
                .background(Color.DarkGray)
        )
    }
}
Copy the code

The rememberSwipeableState API, currently experimental, may change or be deleted in the future

To achieve multi-finger touch, use the transformable operator. For example, the following lines of code can be used to zoom in and out, rotate and move two fingers.

@Composable
fun TransformableDemo(a){
    // Set up all gestures first
    var scale by remember{ mutableStateOf(1f)}
    var rotation by remember{ mutableStateOf(0f)}
    var offset by remember{ mutableStateOf(Offset.Zero)}
    val state = rememberTransformableState{zoomChange, panChange, rotationChange ->
        scale *= zoomChange
        rotation += rotationChange
        offset += panChange
    }
    Box(Modifier
        .graphicsLayer(
           scaleX = scale,
           scaleY = scale,
           rotationZ = rotation,
           translationX = offset.x,
           translationY = offset.y
        ).transformable(state = state)
        .background(Color.Blue)
        .fillMaxSize()
    )
}
Copy the code

While Jetpack Compose is easy to use, it’s not like the XML layout we’re used to, so it takes a lot of practice.

Refer to official documentation