Compose – Modifier Modifier

1. Concept

Four major usage scenarios:

  • Modify appearance (size, style, layout, behavior).
  • Add extra information (such as an accessibility label).
  • Add interactivity (click, scroll, drag, zoom).
  • Handle user input.

1.1 Add Modifier parameter to combined function

  • Any combination function should have a Modifier parameter: for flexibility considerations, for example, the function of Image() is to display the avatar, it can control the shape border, etc., but it should not control the alignment, otherwise it will always be displayed on the left, the alignment should be Let the parent control decide when calling. At this time, add the Modifier parameter to Image(), and you can specify the alignment when calling.
  • Placed in the first non-mandatory parameter position: Since it is an optional parameter, it is placed after all mandatory parameters, so that the caller can choose whether to specify it, otherwise it must be mandatory to specify the Modifier first, and the use remains unchanged.
@Composable
fun ParentLayout(modifier: Modifier = Modifier) {
    //Specify the alignment when calling
    Avatar(Modifier. align(Alignment. Center Horizontally))
}

@Composable
fun Avatar(modifier: Modifier = Modifier) {
    Image(
        painter = painterResource(id = R. drawable. icon),
        contentDescription = "Icon Image",
        //When using, use the modifier passed in
        modifier = modifier
            .wrapContentSize()
            .background(Color.Gray)
            .padding(18.dp)
            .border(5.dp, Color. Magenta, CircleShape)
            .clip(CircleShape)
    )
}
@Composable
fun TestComposable(a: Int, b: String, modifier: Modifier = Modifier) {...}

2. Modify appearance

APIs are concatenated by chaining calls, so the order affects the final result (such as margin padding).

2.1 Dimensions

2.1.1 Specify a specific value

Sets the preferred value (if the specified size does not satisfy the parent layout’s constraints, the size will be invalid. If forced to set regardless of parent control constraints use requiredSize).

.width(width: Dp)

.width(intrinsicSize: IntrinsicSize) //The parameter is IntrinsicSize.Min or IntrinsicSize.Max

.height(height: Dp)

.height(intrinsicSize: IntrinsicSize)

.size(size: Dp) //Set width and height at the same time

.size(width: Dp, height: Dp) //set width and height separately

Modifier.width(5.dp).height(5.dp)
Modifier.size(5.dp) //Set width and height at the same time

@Composable
funDemo() {
    Box(
        Modifier
            .background(Color.Blue)
            .width(50.dp)
            .height(IntrinsicSize.Min) //height
    ){
        Column{//Whether it is Column or Row, whether it is MIn or Max above, the blue height is the wrapping effect
            Box(
                Modifier
                    .background(Color.Red)
                    .size(25.dp)
            )
            Box(
                Modifier
                    .background(Color.Green)
                    .size(10.dp)
            )
        }
    }
}

2.1.2 Mandatory use of specified values

.requiredWidth(width: Dp)
.requiredHeight(height: Dp)
.requiredSize(size: Dp)

@Preview(showBackground = true)
@Composable
fun Demo1() {
    Column(modifier = Modifier. size(100.dp)) {
        Image(
            painter = painterResource(id = R. drawable. logo_wechat_rectangle),
            contentDescription = null,
            modifier = Modifier.size(150.dp) // greater than the size of the parent control, invalid
        )
    }
}

@Preview(showBackground = true)
@Composable
fun Demo2() {
    Column(modifier = Modifier. size(100.dp)) {
        Image(
            painter = painterResource(id = R.drawable.logo_wechat_rectangle),
            contentDescription = null,
            modifier = Modifier.requiredSize(150.dp) //Force the specified value to be used
        )
    }
}

2.1.3 Percentage of free space

The value range is 0.0~1.0.

.fillMaxWidth(fraction: Float = 1f)

.fillMaxHeight(fraction: Float = 1f)

.fillMaxSize(fraction: Float = 1f)

Modifier.fillMaxWidth(0.5f).fillMaxHeight(0.5f)
Modifier.fillMaxSize(0.5f) //Set width and height at the same time

2.1.4 Setting range

bounded between the maximum and minimum values.

.widthIn(min: Dp = Dp.Unspecified, max: Dp = Dp.Unspecified)

.heightIn(min: Dp = Dp.Unspecified, max: Dp = Dp.Unspecified)

.sizetIn(min: Dp = Dp.Unspecified, max: Dp = Dp.Unspecified)

The width of the parent container and the child element are the same, only the height
Parent heightIn(min=10, max=40) Child< Parent: The child displays 3, the parent displays 10
Child = parent: the height display is consistent
Child> Degree: both are the parent’s maximum value of 40
Parent heightIn(min=50) Child< Parent: The child displays 30, the parent displays 50
Child= Parent: The height display is the same
Child> Degree: Both are child height 100
Parent height(max=50) Child< Parent: Both are child heights 30
Child = Parent: The height displays are consistent
Child> Degree: Both are the parent maximum value 50
@Composable
fun Demo() {
    Box(Modifier
        .background(Color.Blue)
        .width(50.dp)
        .heightIn(min = 10.dp, max = 40.dp)
    ){
        Box(Modifier
            .background(Color.Red)
            .width(25.dp)
// .height(30.dp) //The child element is within the range, blue and red are the same height
// .height(900.dp) //The child element is higher than the maximum value, and both red and blue display 40dp
            .height(3.dp) //The child element is lower than the minimum value, red displays 3dp, blue displays 10dp
        )
    }
}

2.1.5 weight weight

When setting weight, the difference between fill = true/false.

2.1.6 Determine the size according to its own content

For example, you can let an Image determine the size of the control based on its own content.

.wrapContentWidth(
align: Alignment.Horizontal = Alignment.CenterHorizontally, //Alignment
unbounded: Boolean = false
)
.wrapContentHeight(
align: Alignment.Vertical = Alignment.CenterVertically,
unbounded: Boolean = false
)
.wrapContentSize(
align: Alignment = Alignment. Center,
unbounded: Boolean = false
)
@Composable
fun Demo() {
    Column(modifier = Modifier. width(50.dp)) {
        Image(
            painter = painterResource(id = R.drawable.logo_baidu),
            contentDescription = null,
            modifier = Modifier. wrapContentSize()
        )
    }
}

2.2 style

2.2.1 Margin padding

Since the inner and outer margins can be achieved by adjusting the order of chain calls, there is no margin.

Setting respectively: up, down, left and right .padding(
start = 0.dp,
top = 0.dp,
end: Dp = 0.dp,
bottom: Dp = 0.dp
)
Set respectively: horizontal and vertical .padding(
horizontal: Dp = 0.dp,
vertical: Dp = 0.dp
)
Set simultaneously .padding(all: Dp)

val paddingValues = PaddingValues(10.dp,20.dp,30.dp,40.dp)

@Composable
fun Demo() {
    // Pass in a PaddingValues object
    Box(Modifier. padding(paddingValues)) {}
}

2.2.3 background background

.background(
color: Color, //color
shape: Shape = RectangleShape //shape
)

2.2.4 Clipping clip

.clip(shape: Shape) //CircleShape, RectangleShape

@Composable
fun Demo() {
    Image(
        painter = painterResource(id = R.drawable.logo_wechat_square),
        contentDescription = null,
        modifier = Modifier. clip(CircleShape)
    )
}

2.2.5 Border border

.border(width: Dp, brush: Brush, shape: Shape)

@Composable
fun Demo() {
    Image(
        painter = painterResource(id = R.drawable.logo_wechat_square),
        contentDescription = null,
        modifier = Modifier.border(
            width = 2.dp,
            color = Color. Blue,
            shape = CircleShape
        )
    )
}

2.2.6 shadow shadow

.shadow(
elevation: Dp,
shape: Shape = RectangleShape,
clip: Boolean = elevation > 0.dp,
ambientColor: Color = DefaultShadowColor,
spotColor: Color = DefaultShadowColor,
)

2.3 Layout

For details on options and effects, see

Compose can understand the scope of the current code. For example, if you set the alignment of sub-elements for a vertical layout, the option given by the IDE will automatically change to Alignment.Horizontal, indicating that the alignment can only be specified in the horizontal direction.

2.3.1 Alignment of child elements align

.align(alignment: Alignment)

2.4 Behavior

2.4.1 Offset

.offset(x: Dp = 0.dp, y: Dp = 0. dp)

@Composable
fun Demo() {
    Image(
        painter = painterResource(id = R.drawable.logo_wechat_square),
        contentDescription = null,
        modifier = Modifier.offset(x = 10.dp, y = 30.dp)
    )
}

2.4.2 Rotation

.rotate(degrees: Float)

@Composable
fun Demo() {
    Image(
        painter = painterResource(id = R.drawable.logo_wechat_square),
        contentDescription = null,
        modifier = Modifier.rotate(180F)
    )
}

3. Add additional information

Internally, Compose uses a tree structure to store each Composable function node during a reorganization process. One is the recombination tree we see now, and the other is the semantic tree we cannot see.

The semantic tree does not participate in drawing and rendering work at all, so it is completely invisible. It only serves Accessibility and Test. Accessibility needs to be pronounced based on the node content of the semantic tree, and Test needs to find the node you want to test based on the semantic tree to execute the test logic.

In most cases, there is no need to do anything specifically for the semantic tree. The standard combination item has already handled these tasks internally (Button nests a Text, which are independent controls, and Talkback will make a sound independently, but as long as the control can be Click to automatically merge all child nodes). If you use some low-level APIs to draw the interface yourself (selecting No. 8 on the calendar will only pronounce the selected calendar), you have to do these tasks yourself.

.semantics(
mergeDescendants: Boolean = false,
properties: (SemanticsPropertyReceiver.() -> Unit)
)

Allows adding additional information in the form of key-value pairs to the current Compose control, but cannot overwrite it.

.clearAndSetSemantics(
properties: (SemanticsPropertyReceiver.() -> Unit)
)

Relatively used more often, it will clear some of the extra information previously carried by the Compsoe control.

4. Add interactive functions

4.1 Clickable

Allows the app to detect clicks on this element.

.clickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
)

@Composable
fun ClickableSample() {
    val count = remember { mutableStateOf(0) }
    Text(
        text = count.value.toString(),
        modifier = Modifier.clickable { count.value + = 1 }
    )
}

4.2 Scrolling

4.2.1 verticalScroll, horizontalScroll

Similar to ScrollView, you can scroll the elements inside when the content boundary is larger than the maximum size constraint. With ScrollState, you can also change the scroll position or get the current state.

@Composable
fun ScrollBoxes() {
    val scrollState = rememberScrollState()
    LaunchedEffect(Unit) { scrollState. animateScrollTo(100) }
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
// .verticalScroll(rememberScrollState()) //Use default parameters
            .verticalScroll(scrollState) //It will automatically scroll 100px once displayed
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier. padding(2. dp))
        }
    }
}

4.2.2 scrollable

Only gestures are detected and content is not offset. The constructor needs to provide a consumeScrollDelta( ) function, which is called at each scroll step and returns the consumed distance in pixels.

@Composable
funScrollableSample() {
    var offset by remember { mutableStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                state = rememberScrollableState { delta ->
                    //Get the offset delta of each slide
                    offset + = delta
                    delta
                }
            )
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}

4.2.3 Nested scrolling

4.2.3.1 Automatic nested scrolling

Simple nested scrolling requires no additional operations. When the child element cannot scroll further, the gesture will be handled by the parent element, and the gesture will automatically propagate from the child element to the parent element.

//The parent Box nests 10 sub-Boxes, the sub-Box scrolls to the boundary and the parent Box will be scrolled
@Composable
funScrollableSample() {
    //Set the gradient color to facilitate the observation of sub-Box scrolling (blue→yellow 1000 levels)
    val gradient = Brush.verticalGradient(0f to Color.Blue, 1000f to Color.Yellow)
    Box(
        modifier = Modifier
            .background(Color.LightGray)
            .verticalScroll(rememberScrollState())
            .padding(32.dp)
    ) {
        Column {
            repeat(10) {
                Box(
                    modifier = Modifier
                        .height(128.dp)
                        .verticalScroll(rememberScrollState())
                ) {
                    Text(
                        text = "Scroll here",
                        color = Color.Red,
                        modifier = Modifier
                            .border(12.dp, Color.DarkGray)
                            .background(brush = gradient)
                            .padding(24.dp)
                            .height(150.dp)
                    )
                }
            }
        }
    }
}

4.2.3.2 nestedScroll

4.2.3.3 Nested scrolling interoperability (v1.2.0)

4.3 Drag

Only detects gestures without offsetting content (requires saving state and representing it on screen, e.g. moving elements via offset modifier), in pixels.

4.3.1 Linear drag (one-dimensional) draggable

Make the element drag in the horizontal or vertical direction, and can monitor the dragging distance.

.draggable(
state: DraggableState,
orientation: Orientation, //drag direction
enabled: Boolean = true, //Whether it is enabled
interactionSource: MutableInteractionSource? = null,
startDragImmediately: Boolean = false,
onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {}, //Callback when dragging starts
onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {}, //Callback when dragging ends
reverseDirection: Boolean = false //Reverse direction
)
var offsetX by remember { mutableStateOf(0f) }
Text(
    modifier = Modifier
        .offset { IntOffset(offsetX.roundToInt(), 0) }
        .draggable(
            orientation = Orientation.Horizontal,
            state = rememberDraggableState { delta ->
                offsetX + = delta
            }
        ),
    text = "Drag me!"
)

4.3.2 Plane drag (two-dimensional) pointerInput

Use the lower level pointerInput() instead.

//Drag the blue child Box in the parent Box
@Composable
funScrollableSample() {
    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) {
                    //Listen to the user's drag gesture
                    detectDragGestures { change, dragAmount ->
                        change.consume() //Since it is the underlying API, many things need to be done by yourself, so you need to consume them here.
                        offsetX + = dragAmount.x //horizontal distance
                        offsetY + = dragAmount.y //Vertical distance
                    }
                }
        )
    }
}

4.4 swipeable

Only gestures are detected and content is not offset (state needs to be saved and represented on the screen, e.g. moving elements via the offset modifier). It has inertia and will animate towards the anchor point after being released. A common use is to slide to close.

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeableSample() {
    val squareSize = 48.dp //Size of sub-Box
    val swipeableState = rememberSwipeableState(0)
    val sizePx = with(LocalDensity.current) { squareSize.toPx() } //DP to PX
    //Set the anchor point (key is pixel, value is index)
    val anchors = mapOf(0f to 0, sizePx to 1)
    Box(
        modifier = Modifier
            .width(96.dp)
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                //Threshold (if it exceeds it, it will slide to the bottom, if it cannot be reached, it will slide back)
                thresholds = { _, _ -> FractionalThreshold(0.3f) },
                orientation = Orientation.Horizontal
            )
            .background(Color.LightGray)
    ) {
        Box(
            Modifier
                .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
                .size(squareSize)
                .background(Color.DarkGray)
        )
    }
}

4.5 Multi-touch transformable

Only detects gestures and does not convert elements. Pan, zoom, rotate.

@Composable
fun TransformableSample() {
    var scale by remember { mutableStateOf(1f) } //scale
    var rotation by remember { mutableStateOf(0f) } //rotation
    var offset by remember { mutableStateOf(Offset.Zero) } //translation
    val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
        scale *= zoomChange
        rotation + = rotationChange
        offset + = offsetChange
    }
    Box(
        Modifier
            .graphicsLayer(
                scaleX = scale, //Equal scaling
                scaleY = scale, //Equal scaling
                rotationZ = rotation,
                translationX = offset.x,
                translationY = offset.y
            )
            .transformable(state = state)
            .background(Color.Blue)
            .fillMaxSize()
    )
}

5. Process user input pointerInput

The input here is not text, but the user’s finger sliding and clicking on the screen (gesture). When the upper-layer API cannot be satisfied (the interactive function provided by the fourth part of the system), it is necessary to call the lower-layer API for customization.

.pointerInput(
key1: Any?, //At least one key must be passed (other overloads pass more keys), and the function will be re-executed when it changes (pass Unit if not needed).
block: suspend PointerInputScope.() -> Unit
)

5.1 Click and drag

Both functions are blocking and cannot be written in the same pointerInput( ) at the same time.

Listen to click events

detectTapGestures(
onDoubleTap: ((Offset) -> Unit)? = null, //Double-click
onLongPress: ((Offset) -> Unit)? = null, //long press
onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture, //short press (the other three will trigger once)
onTap: ((Offset) -> Unit)? = null //click
)

Listen to drag events

detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit //Get the drag distance in each direction through dragAmount.x and dragAmount.y.
)

Box(modifier = Modifier
        .size(200.dp)
        .background(Color.Blue)
        .pointerInput(Unit) {
            detectTapGestures { offset ->
                Log.d("PointerInputEvent", "Tap")
            }
            //Note: The detection function is blocked, and the code here is unreachable
        }.pointerInput(Unit) {
            detectDragGestures { change, dragAmount ->
                Log.d("PointerInputEvent", "Dragging")
            }
            //Note: The detection function is blocked, and the code here is unreachable
        }
)

5.2 Lower level processing (custom)

The writing method is too low-level, and there are basically not many scenarios we need to use.

Start coroutine scope

suspend fun awaitPointerEventScope(

block: suspend AwaitPointerEventScope.() -> R

): R

Wait for user input event

suspend fun awaitPointerEvent(
pass: PointerEventPass = PointerEventPass.Main

): PointerEvent