ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Jetpack compose - Custom layout
    프로그래밍/Android 2022. 12. 12. 11:31
    반응형

    ----

     

    Jetpack Compose has been updated 1.3.2.

     

    Set up Compose for an existing app

    To start using Compose, you need to first add some build configurations to your project. Add the following definition to your app’s build.gradle file:

    // build.gradle
    android {
        buildFeatures {
            compose true
        }
    
        composeOptions {
            kotlinCompilerExtensionVersion = "1.3.2"
        }
    }
    • You need to your dependencies from the block below:
    • A BOM is a Maven module that declares a set of libraries with their versions. 
    def composeBom = platform('androidx.compose:compose-bom:2022.10.00')
        implementation composeBom
        androidTestImplementation composeBom
    
        implementation 'androidx.compose.foundation:foundation'
        implementation 'androidx.compose.ui:ui'
    }

     

    A simple composable function

    Using Compose, you can build your user interface by defining a set of composable functions that take in data and emit UI elements. A simple example is a Greeting widget, which takes in a String and emits a Text widget which displays a greeting message.

     

    Imperative UI vs Declarative UI

    • Imperative means describing how (서술적) and Declarative means describing what (직관적).
    • For example, 
    // Imperative Java UI
    JPanel main = new JPanel();
    JPanel toolbar = new JPanel();
    JButton button = new JButton();
    button.setIcon(…);
    button.setLabel(“Cut”);
    toolbar.add(button);
    main.add(toolbar);
    JTextArea editor = new JTextArea();
    main.add(editor);
    
    // Declarative HTML 
    <div id=“main”>
    <div id=“toolbar”>
    <button>
    <img src=“cut.png”></img>
    Cut
    </button>
    </div>
    <textarea id=“editor”></textarea>
    </div>
    • Jetpack Compose can draw UI in a declarative way to describe data like the Text("Hello world") composable.
    • Android XML can draw UI in an imperative way to describe data likes textView.setText("Hello world").

     

    State in Compose

    State in an app is any value that can change over time. All Android apps display the state to the user. A few examples of state in Android apps:

    • A Snackbar that shows when a network connection can't be established.
    • A blog post and associated comments.

    Compose transforms the state into UI.  

     

    How Compose transforms the state into UI

    Composition executes your composable functions which can emit UI, creating a UI tree. For example, executing this SearchResult composable yields a tree like this. 

    In the layout stage, this tree is worked and each piece of UI is measured and placed on the screen. That is each node determines their width and height and x, y coordinates. Each element is also located within its parent, specified as an (x, y) position, and a size, specified as a width and a height.

    In the drawing stage, the UI tree is worked again, and all elements are rendered.

     

    Layout stage

    Let's go to a deep dive into the layout stage and how it works.

    There are 2 phases in Layout.

    1. Measurement
      1. Measure any children
      2. Decide its own size (own width/height)
      3. = View#onMeasure
    2. Placement
      1. Place its children (x, y coordinates)
      2. = View#onLayout

    These are roughly equivalent to onMeasure and onLayout in the View. But in Compose, these phases are intertwined.

     

    Laying out each node in the UI tree is a three-step process. Each node must measure any children, then decide its own size, then place its children. 

    1. Measure any children
    2. Decide its own size
    3. Place its children

     

    In the SearchResult example, the UI tree layout follows this order:

    1. The root node Row is asked to measure.
    2. The root node Row asks its first child, Image, to measure.
    3. Image is a leaf node (that is, it has no children), so it reports a size and returns placement instructions.
    4. The root node Row asks its second child, Column, to measure.
    5. The Column node asks its first Text child to measure.
    6. The first Text node is a leaf node, so it reports a size and returns placement instructions.
    7. The Column node asks its second Text child to measure.
    8. The second Text node is a leaf node, so it reports a size and returns placement instructions.
    9. Now that the Column node has measured, sized, and, placed its children, it can determine its own size and placement.
    10. Now that the root node Row has measured, sized, and placed its children, it can determine its own size and placement.
    11. Once all elements are measured in size, the tree is walked again, and all placement instructions are executed in the placement phase. 

     

    더보기

    First, the root layout, the row is asked to measure. In turn, it asks its first child, the image to measure.

    The image is a leaf node with no children, so it measures itself and reports the size. It also returns instructions for how to place any children. The leaf node is usually empty. But all layouts return these placement instructions at the same time as setting their size. 

    placement instruction : how to place any children

    The row then measures its second child, the column. The column measures its children. So the first text measures and reports its size and placement instructions. Then the same for the second text. 

    Now the column has measured its children, it can determine its own size and placement logic. 

    Finally, now that all of its children are sized, the root row can then determine its size and placement instructions. 

    Once all elements are measured in size, the tree is walked again, and all placement instructions are executed in the placement phase. 

     

    Compose Modifier

    Modifiers allow you to add attributes to a composable. Modifiers let you do these sorts of things:

    • Change the composable's size, layout, behavior, and appearance
    • Add information, like accessibility labels
    • Process user input (ex. onClickListener)

    Modifiers are standard Kotlin objects. Create a modifier by calling one of the Modifier class functions:

    import androidx.compose.ui.Modifier
    
    @Composable
    private fun Greeting(name: String) {
      Column(modifier = Modifier.padding(24.dp)) {
        Text(text = "Hello,")
        Text(text = name)
      }
    }

     

    Compose Layout

    @Composable
    fun Layout(
        content: @Composable () -> Unit, 
        modifier: Modifier = Modifier,
        measurePolicy: MeasurePolicy
    ) {...}

    For the purposes of layout, content contains the child layouts. (= View#ViewGroup)

    • The content is a slot for any child composables.
    • A modifier parameter for applying modifiers to the layout.
    • MeasurePolicy is how the layout measures and places items.

    We can implement this function to implement custom layout's behavior like this: 

    Here is MyCustomLayout composable, we call the Layout function and provide measure policy as a trailing lambda, 

    @Composable
    fun MyCustomLayout(
        modifier: Modifier = Modifier,
        content: @Composable () -> Unit
    ) {
        Layout(
            modifier = modifier,
            content = content
        ) {
            measurables: List<Measureable>,
            constraints: Constraints -> 
            // TODO measure and place items
        }
    }

    It's parameters include measurable and constraints. We can measure composables by given measurable and constraints.

    • measurable the element you can measure.
    • constraints that composable's incoming constraints.

    Measurable represents the child elements passed in.

    Constraints is a simple class, modeling the minimum and maximum width and height that the layout can be.(?)

    class Constraints {
        val minWidth: Int,
        val maxWidth: Int,
        val minHeight: Int,
        val maxHeight: Int,
        ...
    }

    For example, Constraints can express that the layout can be as large as it likes,

    val bigAsYouLike = Constraints(
        minWidth = 0,
        maxWidth = Constraints.Infinity,
        minHeight = 0,
        maxHeight = Constraints.Infinity
    )

    Or can express that the layout should be an exact size.

    val exact = Constraints(
        minWidth = 50,
        maxWidth = 50,
        minHeight = 50,
        maxHeight = 50
    )

    We can change constraints and passing it when measuring like this:

     Layout(
            modifier = modifier,
            content = content,
        ) { measurables, constraints ->
            val newConstraints = constraints.copy(
                minWidth = 50,
                minHeight = 50
            )
            val placeables = measurables.map { measurable ->
                measurable.measure(newConstraints)
            }
            ...

     

    First, let's measure any children.

    @Composable
    fun Greeting(name: String) {
        MyCustomLayout(Modifier.padding(10.dp)) {
            Text(text = "H")
            Text(text = "E")
            Text(text = "L")
            Text(text = "L")
            Text(text = "O")
        }
    }
    
    @Composable
    fun MyCustomLayout(
        modifier: Modifier = Modifier,
        content: @Composable () -> Unit
    ) {
        Layout(
            modifier = modifier,
            content = content
        ) {
            measurables: List<Measureable>,
            constraints: Constraints -> 
            // placeables are the measured children and have a size.
            val placeables = measureables.map { measurable ->
                // we don't apply any cumstom measurement logic,
                // just map over the list of measurables, measuring each one.
                measureable.measure(constraints) // accepts size constraints.
                
                // we can use the placeables 
                // to calculate how big our layout should be.
                val width = // calculate from placeables
                val height = // calculate from placeables
                // parent's layout size
                layout(width, height) {
                    // place items
                    placeables.forEach { placeable ->
                        // place each item where you want it to be.
                        placeable.place(x = , y = )
                    }
                }
            }
        }
    }

    The place method is only available on placeables, which are returned from the measure method. The order is strongly enforced. 

     

    Custom Modifier

    Let's display a Text composable on the screen and control the distance from the top to the baseline of the first line of text. This is exactly what the Modifier.paddingFromBaseline() modifier does, we're implementing it here as an example. To do that, use the layout modifier to manually place the composable on the screen. Here's the desired behavior where the Text top padding is set 24.dp:

    You can use the layout modifier to modify how an element is measured and laid out.

    fun Modifier.customLayoutModifier(...) =
        this.layout { measurable, constraints ->
            ...
        })

    Here's the code to produce that spacing:

    @Preview
    @Composable
    fun TextWithPaddingToBaselinePreview() {
        Text("Hi there!", Modifier.firstBaselineToTop(24.dp))
    }
    
    @Preview
    @Composable
    fun TextWithNormalPaddingPreview() {
        Text("Hi there!", Modifier.padding(top = 24.dp))
    }
    
    fun Modifier.firstBaselineToTop(
        firstBaselineToTop: Dp
    ) = layout { measurable, constraints ->
        // Measure the composable
        val placeable = measurable.measure(constraints)
    
        // Check the composable has a first baseline
        check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
        // Get the baseline like textView.getBaseLine()
        val firstBaseline = placeable[FirstBaseline]
    
        // Height of the composable with padding - first baseline
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        val height = placeable.height + placeableY
        layout(placeable.width, height) {
            // Where the composable gets placed
            placeable.place(0, placeableY)
        }
    }
    • In the measurable lambda parameter, you measure the Text represented by the measurable parameter by calling measurable.measure(constraints).
    • You specify the size of the composable by calling the layout(width, height) method, which also gives a lambda used for placing the wrapped elements. In this case, it's the height between the last baseline and added top padding.
    • You position the wrapped elements on the screen by calling placeable.place(x, y). If the wrapped elements aren't placed, they won't be visible. The y position corresponds to the top padding - the position of the first baseline of the text.

     

    Custom Layout

    The layout modifier only changes the calling composable. To measure and layout multiple composables, use the Layout composable instead. This composable allows you to measure and layout children manually. All higher-level layouts like Column and Row are built with the Layout composable.

    Let's build a very basic version of Column. Most custom layouts follow this pattern:

    @Composable
    fun CallingComposable(modifier: Modifier = Modifier) {
        MyBasicColumn(modifier.padding(8.dp)) {
            Text("MyBasicColumn")
            Text("places items")
            Text("vertically.")
            Text("We've done it by hand!")
        }
    }
    
    @Composable
    fun MyBasicColumn(
        modifier: Modifier = Modifier,
        content: @Composable () -> Unit
    ) {
        Layout(
            modifier = modifier,
            content = content
        ) { measurables, constraints ->
            // measure and position children given constraints logic here
        }
    }

    Similarly to the layout modifier, measurables are the list of children that need to be measured and constraints are the constraints from the parent. Following the same logic as before, MyBasicColumn can be implemented like this:

    @Composable
    fun MyBasicColumn(
        modifier: Modifier = Modifier,
        content: @Composable () -> Unit
    ) {
        Layout(
            modifier = modifier,
            content = content
        ) { measurables, constraints ->
            // Don't constrain child views further, measure them with given constraints
            // List of measured children
            val placeables = measurables.map { measurable ->
                // Measure each children
                measurable.measure(constraints)
            }
            val width = placeables.sumOf { it.width }
            val height = placeables.sumOf { it.height }
    
            // Set the size of the layout with all sizes of placeables
            layout(width, height) {
                // Track the y co-ord we have placed children up to
                var yPosition = 0
    
                // Place children in the parent layout
                placeables.forEach { placeable ->
                    // Position item on the screen
                    placeable.place(x = 0, y = yPosition)
    
                    // Record the y co-ord placed up to
                    yPosition += placeable.height
                }
            }
        }
    }

    The Layout is sized by the total width and height of composables, and they're placed based on the yPosition of the previous composable.

     

    더보기

    ----

    이 포스트는 droidcon Think outside the Box 의 요약입니다.

    https://www.droidcon.com/2022/11/16/thinking-outside-the-box-custom-compose-layouts/

    https://www.youtube.com/watch?v=xcfEQO0k_gU&list=PLWz5rJ2EKKc_L3n1j4ajHjJ6QccFUvW1u&index=27 

    Goals

    Custom Layout

    Making a custom layout from scratch with tools provided by Compose.

     

    SubcomposeLayout

    What it is, when you need it and why you probably don’t,.

     

    LazyLayout

    Building a custom scrollable lazy list.

     

    Try to implement a sample app.

    A sample app called Jetlagged. 

     

    Why we can’t just use columns and roles here? Why do we need to implement any custom layout?

    Right can be just a column with multiple roles. It can work in some cases, but maybe we have more requirements. 

    We want to position this component we called bar and we can see that its position configured by the header because this bar is representing a slip we had which started 8 PM and ended at around 8 AM. It’s definitely more than I had of today. 

     

     

    It is configured by header and header will be providing some lines for you. Compose helps to do that. Compose has a feature called alignment lines. 

    In order to use it, you’ll need 2 custom layouts. First one, for the header where will be providing such alignment lens. The second one for the whole graph will be reading this alignment lines and apply them for the bars and position. Provide the size for all the elements here. 

     

    What do we need to know about layouts? You probably aware of this. It’s kind of what we always talk about when we talk about Compose. We talk on a free phases first as a composition, then there’s layout and then there’s draw phase where everything canvas. 

    There we focusing on a layout and it is separated into measure where we figure out what the size of elements are and placement and what’s the position element is? 

     

    The entry points all the custom layouts is a Layout composable. Most of things that you use in compose in the standard level UI library is implemented with this layout. 

    Here are few things to consider. 

    First one is the content. It’s the children of this layout that will be composed into it. Second one is the modifier. All the things that use modifier for applied here maybe interaction, like drawing background. 

    And the last thing is a measure policy and this is the logic of a layout the mid of it. 

    We get measurable that is the children that we compose from the content. We get the constraints. This is the size that parents expect from us. 

    And position it in the placement. So as you may see the placement is actually happening in a layout block. Compose uses to ensure that you can’t place items before it goes into placement into second phase. It’s really useful. 

    We’ll be using this layout. Let’s take a look at it.

    Let’s start with the header. It is straightforward layout. We’re just a few horizontal items positioned one by one. 

    We start off the composition. Just trying to figure out what children this layout has.  

    This is the API we decided to the whole area. So the first thing it isn’t here we represent at the time range with the local times. And there is an hour step in between those kind of showing how many hours will be spend in between each of the labels. 

    And the other thing is the content for each of the labels wanted to be customizable. So we decided to do a kind of slot-based API based on local time for each of the labels. 

    The first thing inside this row is to figure out which labels which local times correspond to the labels that you want to have and which composable will show this labels on screen.

    And we’re ready to go to measure which children apply layout are. We just need to measure them?

    So measurement here is pretty straightforward. We have the layout with we take the maximum of whatever parent allow us to have and then we just like separated the equal chunks. 

    That’s what we do here. We have the number of our measurable the number of our children. We have the maximum width of the layout we divided get the with and then force the constrains to be exact size that we want. 

    And that’s exact size. We measure the children and get the placeable out of it and they will be like each of them will be equal size. 

    We’re almost ready to go to the placement and position layout because we already know the children size, but we still need to report the layout size of the header itself. 

    And for that we can just calculate it during this loop where we measure the children before we just accumulate the width and take the maximum of the height of the placeables.

    We’re ready to position the children.

     

    반응형
Designed by Tistory.