Jetpack Compose development guide

A new Android UI framework, developed using Kotlin. DemoApp download address: Portal, can not open please scan code download:

Gitee: Portal Demo

advantages

Less code, higher performance, faster development, and reduced application package size.

disadvantages

It is not easy to understand, there are few wheels, some basic components are not complete, migration is difficult, it is still in beta, there are bugs.

Declarative programming

Compared to traditional development methods, the data is bidirectional bound without obtaining nodes, and the UI is updated directly by changing the state. Traditional: Like a tree Declarative programming: like building blocks Data rendering is a bit like DataBinding

The development of preparation

  • Android Studio Preview Portal
  • Kotlin
  • JDK
  • Official document portal

configuration

  • gradle
android {
    defaultConfig {
        ...
        minSdkVersion(21)
    }

    buildFeatures {
        // Enables Jetpack Compose for this module
        compose = true}...// Set both the Java and Kotlin compilers to target Java 8.

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
        useIR = true
    }

    composeOptions {
        kotlinCompilerVersion = "1.4.32"
        kotlinCompilerExtensionVersion = "$latest_version"}}Copy the code
  • dependency
dependencies {
    implementation("androidx.compose.ui:ui:$latest_version")
    // Tool support, use @preview annotation to Preview the interface when import
    implementation("androidx.compose.ui:ui-tooling:$latest_version")
    // Import when using (Border, Background, Box, Image, Scroll, shapes, animations)
    implementation("androidx.compose.foundation:foundation:$latest_version")
    // Material Design
    implementation("androidx.compose.material:material:$latest_version")
    // Material design icons
    implementation("androidx.compose.material:material-icons-core:$latest_version")
    implementation("androidx.compose.material:material-icons-extended:$latest_version")
    // Import when passing data using Livedata
    implementation("androidx.compose.runtime:runtime-livedata:$latest_version")
    // Import data when passing data using RxJava
    implementation("androidx.compose.runtime:runtime-rxjava2:$latest_version")
    // Import when using ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$latest_version")
    // Import when using Navigation
    implementation("Androidx. Navigation: navigation - compose: 2.4.0 - alpha04")
    // Import the image when loading with Coil
    implementation("Com. Google. Accompanist: accompanist - coil: 0.13.0")
    // Import the image when it is loaded using Glide
    implementation("Com. Google. Accompanist: accompanist - glide: 0.14.0")
    
    / / UI test
    androidTestImplementation("androidx.compose.ui:ui-test-junit4:$latest_version")}Copy the code

Hello World

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        // ComponentActivity extends method to create ComposeView and set ContentView to ComposeView
        setContent {
            Text(text="Hello World")}}}Copy the code

So that’s the effect

Status updates

Combination and state

  • combination

Describe the arrangement of the interface, for example:

Box(){
    Text("Hello World")
    Row(){
       Text("Row row row")}}Copy the code

Similar to writing an XML construct interface, but in kotlin’s way. Note that the components in Compose are not rendered in order; instead, the system renders the components based on their priority.

Box(){
    Text("111111")
    Text("222222")
    Text("333333")}Copy the code

The above three texts are not rendered in order. In addition, the layers are not necessarily rendered in order, such as

Box(){
    Text("111111")
    val isRender3 = false
    Box(){
        Text("222222")
        isRender3 = true
    }
    if(isRender3){
        Text("333333")}}Copy the code

The third Text above may not render.

  • state

In Compose, all UI updates are dependent on the state, and the interface updates are implemented by binding the interface to the state. You can think of it as a mediation of data and interface bindings. State is implemented based on MutableState. There are three ways to create a state:

// Default is the default value
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
Copy the code

Note that to use remember, you need to import:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
Copy the code

Currently, the IDE will not automatically import.

State can be assigned to a component like a normal value, 🌰

// Immutable state
val text by remember{ mutableStateOf("Hello World")}
Box(){
    Text(text = text)
}
Copy the code

When the value of text changes, the text displayed by the text component also changes, such as 🌰 in the input box

 // Variable state
 var text by remember { mutableStateOf("I'm the default")}/ / input box
 TextField(value = text,
    onValueChange = { text = it }, 
    label = { Text("I am an input field.")})Copy the code

You need to manually create a text state, set the value of the TextField to text, then listen for the value of the TextField to change, and assign the new value to text. When the value of text changes, the content of the input box will also change. If you do not set the value of text in the onValueChange callback, the input box will appear as if it cannot be entered because the state has not changed.

States sink, events rise

For example, Compose follows the rule that states go down, events go up, meaning states go down one layer and events go up one layer.

A minor 🌰

@Composable
fun HelloScreen(a) {
    var clickCountState by remember { mutableStateOf(0) }
    HelloWorldContent(clickCountState) {
        clickCountState++
    }
}

@Composable
fun HelloWorldContent(clickCount: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("I am the text content button currently clicked on the first${clickCount}Time. "")}}Copy the code

When a button is clicked, the click count state is passed to HelloWorldContent and the click event callback is passed to HelloScreen.

State transition

val name = MutableLiveData<String>()
// The LiveData status is changed
val nameState = name.observeAsState("")

// Flow Indicates the Flow state
val number = (1.2.3).asFlow().collectAsState()
Copy the code

Use the ViewModel

You need to import

implementation("androidx.compose.runtime:runtime-livedata:$latest_version")
Copy the code

Use the ViewModel method in the component

@Composable
fun HelloWorld(a) {
    // Create the same viewModel from the same Activity
    val viewModel: MainViewModel = viewModel()
    val text by remember { viewModel.textState }
    Text(text = text)
}
Copy the code

If you need to pass parameters to the ViewModel, you may need to use ViewModelProvider. The Factory

class MainViewModel(val text: String) : ViewModel() 

val viewModel: MainViewModel = viewModel(factory = object : ViewModelProvider.Factory {
    override fun 
        create(modelClass: Class<T>): T {
        return MainViewModel("hello") as T
    }
})
Copy the code

Use the Context

val context = LocalContext.current
Copy the code

State of preservation

When the Activity is recreated, you can use rememberSaveable to save the state, for example 🌰

var text = rememberSaveable { mutableStateOf("Hello World"))}Copy the code

If you want to save serialized objects, you need to annotate the class @parcelize, and the class needs to inherit Parcelable, for example 🌰

@Parcelize
data class User(val name:String,val age:Int): Parcelable
var user = rememberSaveable { mutableStateOf(User("Wang er Gou".3))}Copy the code

layout

Code portal

Compose consists of three basic layouts: Row,Column, and Box. In addition, scaffolds can be used to build interfaces.

Row

Similar to the horizontal Linearlayout, parameters are described:

@Composable
inline fun Row(
    // Basic style configuration
    modifier: Modifier = Modifier,
    // Configure horizontal alignment and item spacing
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    // Configure vertical alignment
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    / / content
    content: @Composable RowScope. () - >Unit
) 
Copy the code

Column

Similar to the vertical Linearlayout, the parameters are described:

@Composable
inline fun Column(
    // Basic style configuration
    modifier: Modifier = Modifier,
    // Configure vertical alignment and spacing
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    // Configure horizontal alignment
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    / / content
    content: @Composable ColumnScope. () - >Unit
) 
Copy the code

Box

The interface is constructed layer by layer. The parameters are described as follows:

@Composable
inline fun Box(
    // Basic style configuration
    modifier: Modifier = Modifier,
    // Content alignment
    contentAlignment: Alignment = Alignment.TopStart,
    // Whether the child component uses the minimum width and height of the parent layout
    propagateMinConstraints: Boolean = false./ / content
    content: @Composable BoxScope. () - >Unit
) 
Copy the code

Scaffold

Scaffold layout, positioning a general framework of interface, contains the topBar, content, bottomBar, floatingActionButtom, drawer, etc. Parameter description:

@Composable
fun Scaffold(
    // Basic style configuration
    modifier: Modifier = Modifier,
    // Drawer, snackbar status control
    scaffoldState: ScaffoldState = rememberScaffoldState().// The top title bar
    topBar: @Composable() - >Unit = {},
    // Bottom navigation bar
    bottomBar: @Composable() - >Unit = {},
    // Host the components of snackBar
    snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
    // Bottom right button
    floatingActionButton: @Composable() - >Unit = {},
    // Button position in the lower right corner
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    // Determine whether there is a bottomBar in the lower right corner
    isFloatingActionButtonDocked: Boolean = false.// Slide the contents of the pulled drawer right
    drawerContent: @Composable (ColumnScope.() -> Unit)? = null.// Right slide the pulled drawer to see if gestures are supported
    drawerGesturesEnabled: Boolean = true.// Drawer shape
    drawerShape: Shape = MaterialTheme.shapes.large,
    // The drawer level
    drawerElevation: Dp = DrawerDefaults.Elevation,
    // Drawer background color
    drawerBackgroundColor: Color = MaterialTheme.colors.surface,
    // The color of the contents displayed in the drawer
    drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
    // Block the color of the contents when the drawer is opened
    drawerScrimColor: Color = DrawerDefaults.scrimColor,
    / / the background color
    backgroundColor: Color = MaterialTheme.colors.background,
    // The color of the theme content
    contentColor: Color = contentColorFor(backgroundColor),
    / / content
    content: @Composable (PaddingValues) -> Unit
) 
Copy the code

The theme

Code portal

Compose theme follows the Material design specification, and the theme can be configured according to the Material specification. An 🌰

@Composable
// Dark mode color configuration
val darkColors = darkColors(
    primary = Default200,
    primaryVariant = Default700,
    secondary = DefaultSecondary,
    background = Color.Black,
    onPrimary = DefaultOnPrimary,
    surface = Color.Black,
)

// Color configuration in normal mode
val lightColors = lightColors(
    primary = Default500,
    primaryVariant = Default700,
    secondary = DefaultSecondary,
    background = Color.White,
    onPrimary = DefaultOnPrimary,
    surface = DefaultOnPrimary
)

// Self-encapsulated functions
@Composable
fun WithTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable() - >Unit
) {
    val colors = if (darkTheme) darkColors else lightColors
    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}
Copy the code

When using a theme, simply pass the content to the MaterialTheme:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        // ComponentActivity extends method to create ComposeView and set ContentView to ComposeView
        setContent {
           // Control the dark mode status. Set this to true and the screen will automatically update to the dark mode
           var darkThemeState by remember{ mutableStateOf(isSystemInDarkTheme())}
           WithTheme(darkThemeState){
               Text("Hello World")}}}}Copy the code

If you want to change a theme or add a theme, you just need to configure the colors. How to use colors from the theme:

Text("Hello World", color = MaterialTheme.colors.primary)
Copy the code

Just call materialTheme.colors.* to get the colors under the current theme.

Compose Color: Android Color

MaterialTheme.colors.primary.toArgb()
Copy the code

The list of

Code portal

ScroableRow

Row(modifier = Modifier
        .fillMaxWidth()
        .horizontalScroll(rememberScrollState())
) {
   // ...
}
Copy the code

A Row has a horizontalScroll, and a rememberScrollState can be used to retrieve parameters such as the scroll distance.

ScroableColumn

 Column(modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
 ) {
     // ...
 }
Copy the code

Just like ScrollView, you just add a verticalScroll for a Column.

LazyRow

This parameter is similar to the RecyclerView using the horizontal LinearLayoutManager

@Composable
fun LazyRow(
    // Basic style configuration
    modifier: Modifier = Modifier,
    // Scroll status listening
    state: LazyListState = rememberLazyListState()./ / content padding
    contentPadding: PaddingValues = PaddingValues(0.dp),
    // Whether to reverse the content
    reverseLayout: Boolean = false.// The horizontal alignment is spaced with the item
    horizontalArrangement: Arrangement.Horizontal =
        if(! reverseLayout) Arrangement.Startelse Arrangement.End,
        
    // Vertical alignment
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    // Scroll animation control
    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
    / / content
    content: LazyListScope.() -> Unit
)
Copy the code

Instructions:

 LazyColumn(verticalArrangement = Arrangement.spacedBy(15.dp)) {
        // Create an item
        item {
           Text("I am an item")}// Create multiple items. Viewmodel. data is a list of items
        items(viewModel.data) { item ->
            Text("I am an item")}}Copy the code

LazyColumn

Similar to the RecyclerView using the vertical LinearLayoutManager, parameter description

@Composable
fun LazyColumn(
    // Basic style configuration
    modifier: Modifier = Modifier,
    // Can be used to listen for scrolling status
    state: LazyListState = rememberLazyListState()./ / the content of padding
    contentPadding: PaddingValues = PaddingValues(0.dp),
    // Whether the content is in reverse order
    reverseLayout: Boolean = false.// Vertical alignment and item spacing
    verticalArrangement: Arrangement.Vertical =
        if(! reverseLayout) Arrangement.Topelse Arrangement.Bottom,
    // Horizontal alignment
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    // Scroll animation control
    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
    / / content
    content: LazyListScope.() -> Unit
) 
Copy the code

Use the same way as LazyRow.

Viscous title

Similar to the contact interface A,B,C,D, rolling will automatically absorb the top, take 🌰

 LazyColumn(verticalArrangement = Arrangement.spacedBy(15.dp)) {
        // Create an item
        item {
           Text("I am an item")
        }
        stickyHeader {
             Text("I'm a sticky title that automatically tops when you scroll to the top of the screen.")}// Create multiple items. Viewmodel. data is a list of items
        items(viewModel.data) { item ->
            Text("I am an item")}}Copy the code

Vertical GridList

RecyclerView is similar to the RecyclerView using GridLayoutManager.

@composable Fun LazyVerticalGrid(// Describe how to form columns by setting count or minSize to cells: GridCells, // Base style modifier: // Content padding-content padding-content padding-content padding-content: PaddingValues = PaddingValues(0.dp), // Content: LazyGridScope.() -> Unit)Copy the code

Use the same way as LazyRow.

Important

Item animations and waterfall streams are not currently supported.

The text

Code portal

Text

Compose reconstructs a set of methods for rendering Text. The original TextView corresponds to the Text component in Compose

@Composable
fun Text(
    // Text content
    text: String.// Basic style configuration
    modifier: Modifier = Modifier,
    // Text color
    color: Color = Color.Unspecified,
    // Font size
    fontSize: TextUnit = TextUnit.Unspecified,
    // Font style, italics, etc
    fontStyle: FontStyle? = null.// Font size
    fontWeight: FontWeight? = null./ / font
    fontFamily: FontFamily? = null.// The interval between each text
    letterSpacing: TextUnit = TextUnit.Unspecified,
    // Underline etc
    textDecoration: TextDecoration? = null.// Text alignment
    textAlign: TextAlign? = null./ / line height
    lineHeight: TextUnit = TextUnit.Unspecified,
    // Beyond the content handling mode
    overflow: TextOverflow = TextOverflow.Clip,
    // Whether to wrap lines
    softWrap: Boolean = true.// Maximum number of rows
    maxLines: Int = Int.MAX_VALUE,
    // Callback during text measurement
    onTextLayout: (TextLayoutResult) - >Unit = {},
    // Use style to set the text style, which has a lower priority than the above
    style: TextStyle = LocalTextStyle.current
) 
Copy the code

The rich text

Rich text implementation 🌰

Text(buildAnnotatedString {
        withStyle(style = SpanStyle(MaterialTheme.colors.primary)) {
            append("Try rich text.")
        }
        append("He")
        // Change the font color
        withStyle(style = SpanStyle(MaterialTheme.colors.secondary)) {
            append("ll")
        }
        append("o World")
        append("Rich text")
        / / bold
        withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
            append("Rich text with a bold")}})Copy the code

The text selected

  • Set support selected

Just wrap the Text in the SelectionContainer, 🌰

SelectionContainer {
    Text(text = "Selected text")}Copy the code
  • Ban on selected

You can disable some text, 🌰

SelectionContainer {
    Column {
        Text(text = "Part of the text that can be selected.")
        Text(text = "Part of the text that can be selected.")
        DisableSelection {
            Text(text = "Disallow selected portions of text")
        }
        Text(text = "Part of the text that can be selected.")}}Copy the code

Click on the text

  • Click on the monitor

ClickableText allows you to listen for the click of the text, which is similar to the effect of clicking to expand the text

ClickableText(
        text = buildAnnotatedString { append("Clickable text") },
        style = TextStyle(color = MaterialTheme.colors.onBackground)
    ) {
        Toast.makeText(context, "The first${it}The characters were clicked.", Toast.LENGTH_SHORT).show()
}
Copy the code
  • Deep link click

If you need to realize functions such as clicking the user name or topic name in the text content to jump to the corresponding page, you can also use 🌰

val annotatedText = buildAnnotatedString {
    withStyle(
        style = SpanStyle(
            color = MaterialTheme.colors.onSurface
        )
    ) {
        append("Try the link, click on it.")
    }
    pushStringAnnotation(
        tag = "URL".annotation = "https://developer.android.com"
    )
    withStyle(
        style = SpanStyle(
            color = MaterialTheme.colors.primary,
            fontWeight = FontWeight.Bold
        )
    ) {
        append("Here")
    }
    pop()
}
ClickableText(
    text = annotatedText,
    onClick = { offset ->
        annotatedText.getStringAnnotations(
            tag = "URL", start = offset, end = offset ).firstOrNull()? .let {annotation ->
                Toast.makeText(context, "Click the link${annotation.item}", Toast.LENGTH_SHORT).show()
            }
    }
)
Copy the code

Input box

Compose provides two Material style input fields: a background input field and a border input field.

  • With background input box

Parameters that

@Composable
fun TextField(
    // Input box content
    value: String.// The content change callback
    onValueChange: (String) - >Unit.// Base style
    modifier: Modifier = Modifier,
    // Whether editable and focused
    enabled: Boolean = true.// Read-only or not. In the read-only state, you cannot edit, but you can focus on the copied content
    readOnly: Boolean = false.// Text style
    textStyle: TextStyle = LocalTextStyle.current,
    // Material style Label
    label: @Composable(() - >Unit)? = null.// Placeholder content when the text is empty
    placeholder: @Composable(() - >Unit)? = null.// The icon before the text
    leadingIcon: @Composable(() - >Unit)? = null.// The icon after the text
    trailingIcon: @Composable(() - >Unit)? = null.// Whether the current status is error
    isError: Boolean = false.// The application of text effect can realize the effect of input password
    visualTransformation: VisualTransformation = VisualTransformation.None,
    // Can be used to configure the type of input box
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    // Can be used to configure the keyboard type of the input box Enter
    keyboardActions: KeyboardActions = KeyboardActions().// Single line
    singleLine: Boolean = false./ / the biggest
    maxLines: Int = Int.MAX_VALUE,
    // Listen for component interaction changes
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    // Input box background shape
    shape: Shape =
        MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
    // Input field color
    colors: TextFieldColors = TextFieldDefaults.textFieldColors()
)
Copy the code
  • Input box with border

Parameters that

@Composable
fun OutlinedTextField(
    // Input box content
    value: String.// Content change callback
    onValueChange: (String) - >Unit.// Basic style configuration
    modifier: Modifier = Modifier,
    // Whether editable and focused
    enabled: Boolean = true.// Read-only or not
    readOnly: Boolean = false.// Text style
    textStyle: TextStyle = LocalTextStyle.current,
    // Lable
    label: @Composable(() - >Unit)? = null.// Placeholder content when the text content is empty
    placeholder: @Composable(() - >Unit)? = null.// Icon before the text
    leadingIcon: @Composable(() - >Unit)? = null.// After the text icon
    trailingIcon: @Composable(() - >Unit)? = null.// Whether the current status is error
    isError: Boolean = false.// The application of text effect can realize the effect of input password
    visualTransformation: VisualTransformation = VisualTransformation.None,
    // Can be used to configure the type of input box
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    // Can be used to configure the keyboard type of the input box Enter
    keyboardActions: KeyboardActions = KeyboardActions.Default,
    // Single line
    singleLine: Boolean = false./ / the biggest
    maxLines: Int = Int.MAX_VALUE,
    // Listen for component interaction changes
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    // Input box background shape
    shape: Shape = MaterialTheme.shapes.small,
    // Input field color
    colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
) 
Copy the code
  • Basic input box

In addition to the Material style input field, Compose also provides a basic input field: BasicTextField

@Composable
fun BasicTextField(
    // Input box content
    value: String.// The content changes callback
    onValueChange: (String) - >Unit.// Base style matching values
    modifier: Modifier = Modifier,
    // Whether the focus can be edited
    enabled: Boolean = true.// Read-only or not
    readOnly: Boolean = false.// Text style
    textStyle: TextStyle = TextStyle.Default,
    // Can be used to configure the type of input box
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    // Can be used to configure the keyboard type of the input box Enter
    keyboardActions: KeyboardActions = KeyboardActions.Default,
    // Single line
    singleLine: Boolean = false./ / the biggest
    maxLines: Int = Int.MAX_VALUE,
    // The application of text effect can realize the effect of input password
    visualTransformation: VisualTransformation = VisualTransformation.None,
    / / Layout callback
    onTextLayout: (TextLayoutResult) - >Unit = {},
    // Listen for component interaction changes
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    // Cursor configuration
    cursorBrush: Brush = SolidColor(Color.Black),
    // The input box decorates the content. The content must finally be called innerTextField().
    decorationBox: @Composable (innerTextField: @Composable() - >Unit) - >Unit =
        @Composable { innerTextField -> innerTextField() }
)
Copy the code

Give you an 🌰

var text5 by remember { mutableStateOf("") }
BasicTextField(
    value = text5, onValueChange = { text5 = it },
    decorationBox = { innerTextField ->
        if (text5.isEmpty()) {
            Box(contentAlignment = Alignment.CenterStart) {
                Text(
                    text = "Custom Style input box",
                    color = MaterialTheme.colors.primary
                )
            }
        }
        // Must call back
        innerTextField()
    },
    modifier = Modifier
        .background(Color.LightGray, CircleShape)
        .padding(5.dp, 20.dp)
        .fillMaxWidth()
)
Copy the code

And it looks something like this

Importante

There does not appear to be any parameters that limit the number of words entered, but you can do so by judging the number of words in the onValueChange callback

The picture

Code portal

For example, for example, Compose: For example, Compose is a component for loading images, including Bitmap, Vector, and Painter.

Load the Bitmap

Parameters that

@Composable
fun Image(
    // Bitmap, note that bitmap is different
    bitmap: ImageBitmap.// ContentDescription
    contentDescription: String? .// Base style
    modifier: Modifier = Modifier,
    // Align the image
    alignment: Alignment = Alignment.Center
    // Image cropping mode
    contentScale: ContentScale = ContentScale.Fit,
    // Image transparency
    alpha: Float = DefaultAlpha,
    / / screen
    colorFilter: ColorFilter? = null
) 
Copy the code

Turn Bitmap ImageBitmap

bitmap.asImageBitmap()
Copy the code

Turn ImageBitmap Bitmap

bitmap.asAndroidBitmap()
Copy the code

Cutting way to support Crop, Fit, FillHeight FillWidth, Inside, None, FillBounds, specific effects refer to the Demo App.

The load Vector

Parameters that

@Composable
fun Image(
    // Vector
    imageVector: ImageVector,
    contentDescription: String? , modifier:Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null
)
Copy the code

Load the Painter

Parameters that

@Composable
fun Image(
    // painter
    painter: Painter,
    contentDescription: String? , modifier:Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null
) 
Copy the code

How to load resource images 🌰

Image(painter = painterResource(id = R.drawable.logo), contentDescription = null)
Copy the code

Coil loading image

Need to import libraries

implementation("Com. Google. Accompanist: accompanist - coil: 0.13.0")
Copy the code

Website in Google. Making. IO/accompanist… An 🌰

val painter = rememberCoilPainter("URL")
Image(
    painter = painter,
    contentDescription = null,
    modifier = Modifier
        .fillMaxWidth()
        .height(300.dp),
    contentScale = ContentScale.FillWidth
)
Copy the code

Glide loading images

Need to import libraries

implementation("Com. Google. Accompanist: accompanist - glide: 0.14.0")
Copy the code

Website in Google. Making. IO/accompanist… An 🌰

val painter = rememberGlidePainter("URL")
Image(
    painter = painter,
    contentDescription = null,
    modifier = Modifier
        .fillMaxWidth()
        .height(300.dp),
    contentScale = ContentScale.FillWidth
)
Copy the code

Monitor loading status

Currently, Compose provides a callback to the loading status, but it seems to be undetected by monitoring the loading progress, 🌰

val painter = rememberCoilPainter("URL")
Image(
    painter = painter,
    contentDescription = null,
    modifier = Modifier
        .fillMaxWidth()
        .height(300.dp),
    contentScale = ContentScale.FillWidth
)

when (painter.loadState) {
    is ImageLoadState.Loading -> {
        // In Loading, show Loading
        CircularProgressIndicator(Modifier.align(Alignment.Center))
    }
    is ImageLoadState.Error -> {
        // Failed to load}}Copy the code

Rounded corners and Cirle

For example: Compose Image does not provide the fillet parameter directly, so you need to use another component for the fillet

Surface(
    // Round the corners by setting the shape on the outside layer
    shape = RoundedCornerShape(4.dp),
    modifier = Modifier.padding(10.dp)
) {
    Image(
        painter = rememberCoilPainter("URL", fadeIn = true),
        contentDescription = null,
        modifier = Modifier
            .background(MaterialTheme.colors.onBackground),
    )
}
Copy the code

Do the same with a circular picture

Surface(
    // indicates that the rounded corners are generated in 50% proportion
    shape = RoundedCornerShape(parent=50),
    modifier = Modifier.padding(10.dp)
) {
    Image(
        painter = rememberCoilPainter("URL", fadeIn = true),
        contentDescription = null,
        modifier = Modifier
            .background(MaterialTheme.colors.onBackground),
    )
}
Copy the code

If the UI needs images with other shapes, you can define your own Shape and assign it to the Surface.

Canvas

Code portal

Compose Canvas basically implements the functions of Android Canvas and is easier to use. You only need to use the Canvas component. Parameter description

fun Canvas(
// Basic style configuration
modifier: Modifier.// Draw a Scope that calls a series of draw methods
onDraw: DrawScope. () - >Unit
) 
Copy the code

The solid line

Draw a line using the drawLine method, for example 🌰

Canvas(modifier = Modifier.fillMaxSize()) {
    drawLine(color, Offset(0f.20f), Offset(400f.20f), strokeWidth = 5f)}Copy the code

Parameters that

fun drawLine(
    // Line color
    color: Color./ / starting point
    start: Offset./ / end points
    end: Offset./ / line width
    strokeWidth: Float = Stroke.HairlineWidth,
    // Endpoint style
    cap: StrokeCap = Stroke.DefaultCap,
    // The line effect can be used to draw dotted lines
    pathEffect: PathEffect? = null./*FloatRange(from = 0.0, to = 1.0)*/
    // Line transparency
    alpha: Float = 1.0f,
    // Filter style
    colorFilter: ColorFilter? = null.// Mixed mode, similar to xperfMode
    blendMode: BlendMode = DefaultBlendMode
)
Copy the code

Dotted line

The implementation of the dotted line is easy, just add a pathEffect

Canvas(modifier = Modifier.fillMaxSize()) {
    drawLine(color, 
    Offset(0f.20f),
    Offset(400f.20f), 
    strokeWidth = 5f,
    pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f.20f), 5f))}Copy the code

rectangular

It is easier to draw a rectangle, for example 🌰

Canvas(modifier = Modifier.size(100.dp)) {
    drawRect(color = color, size = size / 2f)}Copy the code

Parameters that

fun drawRect(
    // The color of the rectangle
    color: Color.// The distance from the top and left of the rectangle
    topLeft: Offset = Offset.Zero,
    // The size of the rectangle
    size: Size = this.size.offsetSize(topLeft)./* @floatrange (from = 0.0, to = 1.0)*/
    alpha: Float = 1.0 f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
Copy the code

round

Parameters that

fun drawCircle(
    color: Color./ / radius
    radius: Float = size.minDimension / 2.0f,
    / / center
    center: Offset = this.center,
    /* @floatrange (from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
Copy the code

Path

Parameters that

fun drawPath(
    // path
    path: Path,
    color: Color./* @floatrange (from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
Copy the code

Arc, sector

Parameters that

fun drawArc(
    color: Color.// Start Angle
    startAngle: Float.// The Angle of the sweep
    sweepAngle: Float.// Whether to connect the center point. If true, draw the fan
    useCenter: Boolean,
    topLeft: Offset = Offset.Zero,
    size: Size = this.size.offsetSize(topLeft)./* @floatrange (from = 0.0, to = 1.0)*/
    alpha: Float = 1.0 f.// Draw the style, Stroke/Fill
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
Copy the code

The ellipse

Parameters that

fun drawOval(
    color: Color,
    topLeft: Offset = Offset.Zero,
    / / size
    size: Size = this.size.offsetSize(topLeft)./* @floatrange (from = 0.0, to = 1.0)*/
    alpha: Float = 1.0 f.// Draw the style, Stroke/Fill
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
Copy the code

The rounded rectangle

Parameters that

fun drawRoundRect(
    color: Color,
    topLeft: Offset = Offset.Zero,
    size: Size = this.size.offsetSize(topLeft).// Fillet size
    cornerRadius: CornerRadius = CornerRadius.Zero,
    style: DrawStyle = Fill,
    /* @floatrange (from = 0.0, to = 1.0)*/
    alpha: Float = 1.0 f,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
Copy the code

Bessel curve

The Bezier curve is implemented using Path

Canvas(modifier = Modifier.size(100.dp)) {
    val path = Path()
    path.cubicTo(0f.100f.100f.0f.200f.100f)
    drawPath(path, color, style = Stroke(5f))}Copy the code

Rotation, displacement

You just wrap it around rotate

Canvas(modifier = Modifier.size(100.dp)) {
    rotate(45f) {
        drawRect(
            color = color,
            topLeft = Offset(100f.100f),
            size = size / 2f)}}Copy the code

As with the displacement, translate the package

A variety of transformation

What if you want to rotate and shift, and Compose provides an efficient way to avoid the multiple nesting problem

Canvas(modifier = Modifier.size(100.dp)) {
    withTransform({
        // Let me write the transformation
        translate(300f)
        rotate(45f)
    }) {
        drawRect(
            color = color,
            topLeft = Offset(0f.100f),
            size = size / 2f)}}Copy the code

Bitmap

Draw a Bitmap, for example 🌰

Canvas(modifier = Modifier.size(100.dp)) {
    val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.logo)
    drawImage(bitmap.asImageBitmap())
}
Copy the code

Parameters that

fun drawImage(
    // ImageBitmap
    image: ImageBitmap,
    topLeft: Offset = Offset.Zero,
    /* @floatrange (from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
Copy the code

Android Canvas

Compose supports drawing using an Android Canvas, for example 🌰

@Composable
fun AndroidCanvasDemo(color: Color) {
    Text("Android Canvas")
    Canvas(
        modifier = Modifier
            .size(100.dp)
    ) {
        // Get the Android Canvas
        drawIntoCanvas { canvas ->
            val paint = Paint()
            paint.color = color
            canvas.drawOval(0f.0f.100f.100f, paint)
        }
    }
    Spacer(modifier = Modifier.height(10.dp))
}
Copy the code

Mixed way

Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose: Compose

Custom layout

Code portal

Compose supports custom layouts, as well as custom layout modifiers. Measure ->layout->draw Compose helps us make the draw step, so we only need to care about measure and layout.

Measurable: Include a measure that measures the width and height of a layout. Constraints: Used to store the size configuration of the parent layout, including the maximum and minimum width and height parameters of the parent layout. Placeable: used to set the location of the layout and stores the measured width and height of the layout.

Measure + layout 🌰

// Measure the layout constraints and get placeable
val placeable = measurable.measure(constraints)
// Set the layout size
layout(placeable.width, placeable.height) {
    // Set the x and y positions in the upper-left corner of the layout
    placeable.placeRelative(xPosition, yPosition)
}
Copy the code

Custom layout modifier

For example, for example, 🌰 for PaddingHorizontal

fun Modifier.paddingHorizontal(padding: Dp) = layout { measurable, constraints ->
    // Measure the width and height of the layout
    val placeable = measurable.measure(constraints.offset(-padding.roundToPx() * 2.0))
    // Set the layout width and height
    layout(placeable.width, placeable.height) {
        // Set the x and y positions in the upper-left corner of the layout
        placeable.placeRelative(padding.roundToPx(), 0)}}Copy the code

use

Text(
    "Here's a paddingHorizontal effect with a custom modifier, let's try changing the padding.",
    modifier = Modifier
        .paddingHorizontal(paddingHorizontal.dp)
        .background(Color.Red)
)
Copy the code

And that’s what it looks like

Custom layout

Custom constraints can only apply to themselves. If you want to constrain child components, you need to use a custom layout. One of the most common requirements in production development is streaming layout. Given a ViewGroup, put content in it, and then wrap it up when it’s not in one line, try using Compose.

First we need to implement a custom component

// The default interval is 0
val DefaultFlowBoxGap = FlowBoxGap(0.dp)

// Define an Item interval data class that stores the upper, lower, left, and left intervals
data class FlowBoxGap(val start: Dp, val top: Dp, val end: Dp, val bottom: Dp) {
    constructor(gap: Dp) : this(gap, gap, gap, gap)
}

@Composable
fun FlowBox(
    // Basic style configuration
    modifier: Modifier = Modifier,
    // Up, down, left, right spacing
    itemGap: FlowBoxGap = DefaultFlowBoxGap,
    content: @Composable() - >Unit
) {
    // For custom components, pass in content and measurePolicy
    Layout(
        content = content,
        measurePolicy = flowBoxMeasurePolicy(itemGap),
        modifier = modifier
    )
}
Copy the code

Let’s see what flowBoxMeasurePolicy is

// Return a MeasurePolicy that tells the custom component how to layout it
fun flowBoxMeasurePolicy(itemGap: FlowBoxGap) = MeasurePolicy { measurables, constraints ->
    // Get the subcomponent information in batches
    val placeables = measurables.map { placeable ->
        placeable.measure(constraints = constraints)
    }

    // Store the location of the subcomponent
    val positions = arrayListOf<Point>()
    // Default to the X point of the current component
    var xPosition = 0
    // Default the Y point of the current component
    var yPosition = 0
    // The height of the component with the highest current line height, which is used to set the starting yPosition of the next line on a newline
    var currentLineMaxHeight = 0
    placeables.forEach { placeable ->
        // Calculate the interval
        val horizontalGap = itemGap.start.roundToPx() + itemGap.end.roundToPx()
        val verticalGap = itemGap.top.roundToPx() + itemGap.bottom.roundToPx()
        // If the width of the current component plus the left and right spacing plus the starting X position is greater than the maximum width of the parent layout, newline.
        if (placeable.width + horizontalGap + xPosition > constraints.maxWidth) {
            xPosition = 0
            yPosition += currentLineMaxHeight
        }
        // Add the child component layout location
        positions.add(
            Point(
                xPosition + itemGap.start.roundToPx(),
                yPosition + itemGap.top.roundToPx()
            )
        )
        // Record the starting X position of a component
        xPosition += placeable.width + horizontalGap
        // Record the maximum height of the current row
        currentLineMaxHeight = currentLineMaxHeight.coerceAtLeast(placeable.height + verticalGap)
    }
    // Get the total height of all subcomponents
    val height = yPosition + currentLineMaxHeight

    // Set the width and height of FlowBox
    layout(constraints.maxWidth, height) {
        // Iterate through the list of locations and set the locations of the child components
        positions.zip(placeables).forEach { (position, placeable) ->
            placeable.placeRelative(position.x, position.y)
        }
    }
}
Copy the code

Usage:

FlowBox(
    modifier = Modifier.background(Color.Red),
    itemGap = FlowBoxGap(start.dp, top.dp, end.dp, bottom.dp)
) {
    Button(onClick = { }) { Text("1111") }
    Button(onClick = { }) { Text("222") }
    Button(onClick = { }) { Text("33333") }
    Button(onClick = { }) { Text("444") }
    Button(onClick = { }) { Text("5555") }
    Button(onClick = { }) { Text("666666") }
    Button(onClick = { }) { Text("77777777") }
    Button(onClick = { }) { Text("88") }
    Button(onClick = { }) { Text("9999999")}}Copy the code

Finally, the effect of the GIF at the beginning of this section is realized. The annotations are very clear, and you can watch by yourself.

Note that there are multiple ways of placeable to set the child component location

  • placeRelative

Common method, support RTL (Right To Left).

  • place

As with placeRelative, RTL is not supported.

  • placeRelativeWithLayer

With placeRelative, it looks like the performance will be a little bit better if you introduce a graphical layer into the drawing.

  • placeWithLayer

As with placeRelativeWithLayer, RTL is not supported.

animation

Code portal

Compose provides two levels of animation: high and low.

High-level animation

High-level animations are used a bit like normal components, simply wrapping them around animation functions.

AnimatedVisibility

The AnimatedVisibility animation is used to show and hide components, such as 🌰

var visible by remember{ mutableStateOf(false)}
AnimatedVisibility(
    visible = visible,
    modifier = Modifier.padding(10.dp)
) {
    Box(
        modifier = Modifier
            .background(MaterialTheme.colors.primary)
            .size(100.dp)
    )
}
Copy the code

When the value of visible changes, the execution animation is automatically triggered. Parameters that

@Composable
fun AnimatedVisibility(
    // Whether to show and hide
    visible: Boolean.// Basic style configuration
    modifier: Modifier = Modifier,
    // Display animation, default fade + expand
    enter: EnterTransition = fadeIn() + expandIn(),
    // Hide when the animation default fade + fold
    exit: ExitTransition = shrinkOut() + fadeOut(),
    / / content
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) 
Copy the code

In this case, the animation configuration is a little bit more important. By default, the animation operator for Compose overloads, so if you want to display multiple animations at the same time, you can simply string them together with “+”. Entry animation inherits from EnterTransition, exit animation inherits from ExitTransition, if you need to customize animation own inheritance rewrite can be. With the animation provided by the official, the specific effect of their own practice.

  • EnterTransition -> EnterTransition

    • Fade in - > fadeIn
    • Horizontal + vertical slide into -> slideIn
    • Horizontal + Vertical expand into -> expandIn
    • Horizontally -> horizontally
    • Vertical Expansion -> expandVertically
    • Horizontally -> slideInHorizontally
    • SlideInVertically -> slideInVertically
  • Exit animation -> ExitTransition

    • Fade - > fadeOut
    • SlideOut -> slideOut
    • Shrink Exit -> shrinkOut
    • Horizontally shrinking exits -> shrinkHorizontally
    • Vertical Cut back -> Shrinkreflective
    • Slide horizontally to Exit -> slideOutHorizontally
    • Slide vertically to exit -> slideOutVertically

AnimateContentSize

AnimateContentSize is used to display animations when high component width change, only need to add a AnimateContentSize to components of Modifier (), an 🌰

Box(
    modifier = Modifier
        .background(MaterialTheme.colors.primary)
        .animateContentSize()
        .padding(10.dp)
        .size(size)
)
Copy the code

When the width and height of the Box change, an animation will appear.

CrossFade

CrossFade is used to show transition animations when multiple states are switched, and is usually used in scenarios such as page hops (🌰)

var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage) { screen ->
    when (screen) {
        "A" -> {
            Box(
                modifier = Modifier
                    .background(MaterialTheme.colors.primary)
                    .padding(10.dp)
                    .size(100.dp)
            )
        }
        "B" -> Box(
            modifier = Modifier
                .background(MaterialTheme.colors.onBackground)
                .padding(10.dp)
                .size(100.dp)
        )
    }
}
Copy the code

You can see the effect of the transition when currentPage switches from A to B. Parameters that

@Composable
fun <T> Crossfade(
    // The target state
    targetState: T.// Base style
    modifier: Modifier = Modifier,
    // Animation specification, set animation execution rate, interpolator, etc
    animationSpec: FiniteAnimationSpec<Float> = tween().// The content, with the targetState passed in, needs to render different components according to the targetState
    content: @Composable (T) -> Unit
)
Copy the code

Low level animation

High-level animations are implemented using low-level animations. Low level animation is more flexible and is achieved by directly setting the properties of the component.

AnimateFloatAsState

It is similar to ValueAnimator, given the corresponding value, such as 🌰

var enabled by remember { mutableStateOf(false)}val scale: Float by animateFloatAsState(if (enabled) 1f else 0.5 f)
Box(
    Modifier
        .size(100.dp)
        // Assign scale to Box
        .graphicsLayer(scaleX = scale, scaleY = scale)
        .background(MaterialTheme.colors.primary)
)
Copy the code

When the scale value changes, the zooming animation is triggered. Parameters that

@Composable
fun animateFloatAsState(
    // The target value, which will be changed from the current value to the target value
    targetValue: Float.// Animation configuration
    animationSpec: AnimationSpec<Float> = defaultAnimation,
    // The animation threshold, which determines when the animation is approaching the targetValue. For example, targetValue=1f. The animation is considered complete when the current value reaches 0.99f.
    visibilityThreshold: Float = 0.01f,
    // Listen at the end of the animation
    finishedListener: ((Float) - >Unit)? = null
)
Copy the code

In addition, Compose provides a series of animations similar to the animateFloatAsState:

  • Dp Value animation ->animateDpAsState
  • The Size of animation - > animateSizeAsState
  • Offset animation - > animateOffsetAsState
  • The Rect animation - > animateRectAsState
  • Int animation - > animateIntAsState
  • IntOffset animation - > animateIntOffsetAsState
  • IntSize animation - > animateIntSizeAsState
  • Custom type animation ->animateValueAsState

They are used similarly to the animateFloatAsState.

Animatable

Animatable is an animation that only sets the initial value and then dynamically sets the targetValue as the state changes. For example, 🌰

var enabledAnimatable by remember { mutableStateOf(false)}val translationX = remember { Animatable(0f)}// To change the value of Animatable, use the LaunchedEffect to change the state of the animation while it is playing, cancel the previous animation and execute the new one.
LaunchedEffect(enabledAnimatable) {
    if (enabledAnimatable) translationX.animateTo(100f) else translationX.animateTo(0f)
}
Box(
    Modifier
        .size(100.dp)
        .offset(translationX.value.dp, 0.dp)
        .background(MaterialTheme.colors.primary)
)
Copy the code

LaunchedEffect needs to pass in a key, and when the key changes, the effect is triggered. These effects are officially known as side effects, and Compose also provides some other side effects that can be used to assist with specific effects, as you can see here.

updateTransition

UpdateTransition is used to execute multiple animations simultaneously, 🌰 for example

var state by remember { mutableStateOf(false)}val transition = updateTransition(state, label = "")
val color by transition.animateColor {
    if (it) Color.Red else MaterialTheme.colors.primary
}
val offset by transition.animateIntOffset {
    if (it) IntOffset(100.100) else IntOffset(0.0)}// Change both the displacement and the background color when the state changes
Box(
    Modifier
        .size(100.dp)
        .offset { offset }
        .background(color)
)
Copy the code

Just define a transition and use it to construct the animation. Use the same method as animateFloatAsState.

RememberInfiniteTransition

RememberInfiniteTransition used to construct the repeat of the animation, an 🌰

val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
    / / initial value
    initialValue = Color.Red,
    / / the end value
    targetValue = Color.Green,
    // Animation effects
    animationSpec = infiniteRepeatable(
        // Linear, duration=1000
        animation = tween(1000, easing = LinearEasing),
        // Repeat the execution
        repeatMode = RepeatMode.Reverse
    )
)
val offset by infiniteTransition.animateFloat(
    initialValue = 0f,
    targetValue = 100f,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
)

Box(
    Modifier
        .size(100.dp)
        .offset(offset.dp, 0.dp)
        .background(color)
)
Copy the code

Animation effects

Compose provides a variety of animation effects, which can be divided into Spring and Tween types by setting up the animationSpec.

Spring

Spring type animation has elastic effect, the word is just like the name, the animation will have a bouncing effect, refer to the official renderings

Spring can set two parameters: dampingRatio and Bounces. If the Spring type animation is a Spring, then dampingRatio is the Spring’s elasticity and stiffness is the Spring’s stiffness. DampingRatio determines the animation elasticity, and Bounces determines the animation execution time.

Tween

In addition to Spring, Tween type animations are more common. Let’s take a look at the parameters:

@Stable
fun <T> tween(
    // Animation time
    durationMillis: Int = DefaultDurationMillis,
    // The animation delay time can be set to start the animation delay
    delayMillis: Int = 0./ / interpolation
    easing: Easing = FastOutSlowInEasing
)
Copy the code

Common interpolators are

  • Linear - > LinearEasing
  • Slow in fast out -> FastOutSlowInEasing
  • Slow in linear out -> LinearOutSlowInEasing
  • Fast linear in and out -> FastOutLinearInEasing
  • Custom interpolator -> CubicBezierEasing

Custom interpolators need to pass four anchor points, similar to Android’s PathInterpolator.

class CubicBezierEasing(
    private val a: Float.private val b: Float.private val c: Float.private val d: Float
) 
Copy the code

gestures

Code portal

Click on the monitor

There are two ways to tap listen. One is that the component has its own tap callback, 🌰

var clickCount by remember { mutableStateOf(0) }
Button(onClick = { clickCount++ }) {
    Text("Clicked $clickCount")}Copy the code

The other is to add a click listener via Modifier. Clickable, usually for components that do not have a click callback, 🌰

var clickCount by remember { mutableStateOf(0) }
Box(modifier = Modifier.clickable { clickCount++ }) {
    Text("Clicked $clickCount")}Copy the code

Compose also offers an advanced tap listener for tap, click, double tap, and long tap gestures, 🌰

Box(
    modifier = Modifier
        .size(100.dp)
        .background(MaterialTheme.colors.primary)
        .pointerInput(Unit) {
            // Listen for click events
            detectTapGestures(
                onPress = {
                    Toast
                        .makeText(context, "Press", Toast.LENGTH_SHORT)
                        .show()
                },
                onDoubleTap = {
                    Toast
                        .makeText(context, "Double click", Toast.LENGTH_SHORT)
                        .show()
                },
                onLongPress = {
                    Toast
                        .makeText(context, "Long press", Toast.LENGTH_SHORT)
                        .show()
                },
                onTap = {
                    Toast
                        .makeText(context, "Click", Toast.LENGTH_SHORT)
                        .show()
                }
            )
        }) 
Copy the code

The above pointerInput is used to listen for touch events. Different types of touch data can be obtained by performing different detections in Scope. There doesn’t seem to be a way to listen for multiple clicks, but it should be possible to tell by listening for the number of clicks.

Sliding to monitor

For example, Compose provides three ways to listen for horizontal, vertical, and arbitrary slides.

  • The horizontal sliding

Through the way of Modifier. Dragable, 🌰

Box(
    modifier = Modifier
        .size(100.dp)
        .offset { IntOffset(offsetX.roundToInt(), 0) }
        .background(MaterialTheme.colors.primary)
        .draggable(
            // Set orientation to horizontal
            orientation = Orientation.Horizontal,
            state = rememberDraggableState { delta ->
                // Get the displacement compared to the last change
                offsetX += delta
            })
)
Copy the code
  • The vertical sliding

The same is achieved through the way of Modifier. Dragable, 🌰

Box(
    modifier = Modifier
        .size(100.dp)
        .offset { IntOffset(offsetX.roundToInt(), 0) }
        .background(MaterialTheme.colors.primary)
        .draggable(
            // Set orientation vertical
            orientation = Orientation.Vertical,
            state = rememberDraggableState { delta ->
                // Get the displacement compared to the last change
                offsetX += delta
            })
)
Copy the code
  • Arbitrary sliding

Implemented through pointerInput, detecte is detectDragGestures, 🌰

Box(
    modifier = Modifier
        .fillMaxWidth()
        .height(500.dp),
    contentAlignment = Alignment.Center
) {
    Box(
        modifier = Modifier
            .size(100.dp)
            .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
            .background(MaterialTheme.colors.primary)
            .pointerInput(Unit) {
                detectDragGestures { change, dragAmount ->
                    // Commit the changes to get the location
                    change.consumeAllChanges()
                    // Get x,y compared to the last change
                    offsetX += dragAmount.x
                    offsetY += dragAmount.y
                }
            }
    )
}
Copy the code

Using the detectDragGestures you can also listen to the x, Y position of the current touch, 🌰

Box(
    modifier = Modifier
        .fillMaxWidth()
        .height(300.dp)
        .pointerInput(Unit) {
            detectDragGestures { change, dragAmount ->
                change.consumeAllChanges()
                
                // The x,y position of the current touch
                touchedX = change.position.x
                touchedY = change.position.y
            }
        }, contentAlignment = Alignment.Center
) 
Copy the code

swipeable

Swipeable is a swipeable monitor that works like a switch. When you slide to a certain threshold, you can automatically scroll to the corresponding position when you release your hand. This feature is very powerful and can be used for pull-down refreshes, viewpagers, and carousel graphs. No official wheels yet. Use swipable 🌰

val width = 96.dp
val squareSize = 48.dp

// Create a swipeable
val swipeableState = rememberSwipeableState(0)
val sizePx = with(LocalDensity.current) { squareSize.toPx() }
// Create a map of anchor locations
val anchors = mapOf(0f to 0, sizePx to 1) 

Box(
    modifier = Modifier
        .width(width)
        // Use swipeable to listen for slides
        .swipeable(
            // Slide state
            state = swipeableState,
            / / the anchor
            anchors = anchors,
            // Set the threshold
            thresholds = { _, _ ->
                // If you swipe right by 30%, you will automatically scroll to the right
                FractionalThreshold(0.3 f)},// Slide direction
            orientation = Orientation.Horizontal
        )
        .background(MaterialTheme.colors.secondary)
) {
    Box(
        Modifier
            .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
            .size(squareSize)
            .background(MaterialTheme.colors.primary)
    )
}
Copy the code

The result is a simple switch, as shown in the GIF at the beginning of this section.

Zoom, pan, rotate

The most common goal of listening for gestures is to zoom, pan, and rotate components, which are very common in things like image previews. Compose directly provides an advanced way to listen for these gestures, 🌰

Box(
    Modifier
        .graphicsLayer(
            scaleX = scale,
            scaleY = scale,
            rotationZ = rotation,
            translationX = offset.x,
            translationY = offset.y
        )
        .pointerInput(Unit) {
            detectTransformGestures { _, pan, zoom, rotationChanged ->
                / / zoom
                scale *= zoom
                / / rotation
                rotation += rotationChanged
                / / displacement
                offset += pan
            }
            detectDragGestures { change, dragAmount ->
                change.consumeAllChanges()
                // detectTransformGestures can only detect two-finger gestures, so here is the detectDragGestures to detect single finger dragging.
                offset = Offset(dragAmount.x, dragAmount.y)
            }
        }
        .background(MaterialTheme.colors.primary)
        .size(100.dp)
Copy the code

But it doesn’t look very good at the moment, and when you zoom in and drag the component, it gets dull.

conclusion

For example, when I ran through almost all of the components in Compose, I found that the amount of code developed was reduced by 40% and the size of the package was reduced by more than half because of the lack of XML and the cumbersome process of manually writing and updating views.

In addition, Compose has implemented its own management of component scope, which basically eliminates many memory leaks in the original development method. Of course, it may be that I did not listen to it. After all, the view drawing method is completely different from the original one, and it seems that the official has not mentioned this problem.

One of the most surprising things about Compose is that it comes with the ability to seamlessly switch between themes, which is a great contrast to the original way of restarting themes.

Of course, there are still many shortcomings in Compose, such as not supporting Item animation, not supporting waterfall flow, not having some commonly used components, cumbersome migration, and occasional weird compilation bugs.

Let’s look forward to it!