Has been involved in the nuggets creator’s training camp in the third period, this paper details view: digging force planning | creators camp 3) is under way, “write” a personal influence | this is August I joined more challenges on the sixth day, activity details view: August more challenges

I’ve written several Compose demos before, but haven’t used the Gesture, Theme, etc., so I wrote a small app that demonstrates these features. Once again, I was impressed by Compose’s productivity, and the application came up with less than a hundred lines of code, which would be unthinkable in traditional development.

Code address: github.com/vitaviva/co…

The basic idea

The game’s logic is simple, so it doesn’t use a framework like MVI, but it still follows the idea of data-driven UI design:

  1. Define the state of the game
  2. State-based UI drawing
  3. User input triggers a state change

1. Define the game state

The State of the game is simple: the current placement of the pieces (Chees), so you can use a piece’s List as a data structure to carry State

1.1 Definition of Chess Pieces

Let’s look at the definition of a single piece

data class Chess(
    val name: String, // Role name
    val drawable: Int // Character image
    val w: Int.// Chess width
    val h: Int.// The length of the piece
    val offset: IntOffset = IntOffset(0.0) / / the offset
)
Copy the code

W and H can be used to determine the shape of the piece, and offset can be used to determine the current position in the board

1.2 Placement of opening pieces

Next we define the pieces for each character and place them according to the opening state

val zhang = Chess("Zhang fei", R.drawable.zhangfei, 1.2)
val cao = Chess("Cao cao", R.drawable.caocao, 2.2)
val huang = Chess("Huang zhong", R.drawable.huangzhong, 1.2)
val zhao = Chess("Zhaoyun", R.drawable.zhaoyun, 1.2)
val ma = Chess("D", R.drawable.machao, 1.2)
val guan = Chess("Guan yu", R.drawable.guanyu, 2.1)
val zu = buildList {  repeat(4) { add(Chess("Single$it", R.drawable.zu, 1.1))}}Copy the code

For example, “Zhang Fei” has a 2:1 aspect ratio, and “Cao Cao” has a 2:2 aspect ratio.

Next, define a game opening:

val gameOpening: List<Triple<Chess, Int.Int>> = buildList {
    add(Triple(zhang, 0.0)); add(Triple(cao,   1.0))
    add(Triple(zhao,  3.0)); add(Triple(huang, 0.2))
    add(Triple(ma,    3.2)); add(Triple(guan,  1.2))
    add(Triple(zu[0].0.4)); add(Triple(zu[1].1.3))
    add(Triple(zu[2].2.3)); add(Triple(zu[3].3.4))}Copy the code

The three members of Triple represent the pieces and their offsets on the board. For example, Triple(cao, 1,0) indicates that Cao Cao’s opening position is in (1,0) coordinates.

Finally, convert gameOpening to the State we need, a List

, with the following code:

const val boardGridPx = 200 // Chess unit size
fun ChessOpening.toList(a) =
    map { (chess, x, y) ->
        chess.moveBy(IntOffset(x * boardGridPx, y * boardGridPx))
    }

Copy the code

2. UI rendering and chess game drawing

After having a List

, the Chess pieces are drawn in turn to complete the drawing of the entire game.

@Composable
fun ChessBoard (chessList: List<Chess>) {
    Box(
        Modifier
            .width(boardWidth.toDp())
            .height(boardHeight.toDp())
    ) {
        chessList.forEach { chess ->
             Image( // The chessman picture
                    Modifier
                        .offset { chess.offset } // The offset position
                        .width(chess.width.toDp()) // Chess width
                        .height(chess.height.toDp())) // The height of the piece
                    painter = painterResource(id = chess.drawable),
                    contentDescription = chess.name
             )
        }
    }
}
Copy the code

Box determines the scope of the board, Image draws the pieces, and uses the Modifier. Offset {} to place them in the correct position.

So far, we have drawn a static opening with Compose. The next step is to make the pieces follow the fingers, which involves the use of Compose Gesture

3. Drag the chess pieces to trigger the state change

For Compose, use modifiers, such as modifiers. Draggable (), modifiers. Swipeable (), etc. In the game scene of Huarong Road, you can use draggable to listen to drag

3.1 Monitor Gestures

1) Use draggable to listen for gestures

Chessmen can be dragged in both X-axis and Y-axis directions, so we set two draggables respectively:

@Composable
fun ChessBoard (
    chessList: List<Chess>,
    onMove: (chess: String.x: Int.y: Int) - >Unit
) {
    Image(
        modifier = Modifier
           ...
           .draggable(// Listen for horizontal drag
                 orientation = Orientation.Horizontal,
                 state = rememberDraggableState(onDelta = {
                     onMove(chess.name, it.roundToInt(), 0)
                 })
            )
            .draggable(// Listen for vertical drag
                 orientation = Orientation.Vertical,
                 state = rememberDraggableState(onDelta = {
                     onMove(chess.name, 0, it.roundToInt())
                 })
            ),
            ...
    )
}
Copy the code

Orientation specifies which gesture to listen for: horizontal or vertical. RememberDraggableState saves the drag state and onDelta specifies the callback for the gesture. We throw the displacement information of the drag gesture through our custom onMove.

If you want to detectDragGestures in any direction, you can use detectDragGestures

2) Use pointerInput to listen for gestures

Draggable, swipeable, etc., are internally implemented by calling Modifier. PointerInput (), based on pointerInput can achieve more complex custom gestures:

fun Modifier.pointerInput(
    key1: Any? , block:suspend PointerInputScope. () - >Unit
): Modifier = composed (...) {... }Copy the code

PointerInput provides a PointerInputScope in which you can listen for various gestures using the suspend function. For example, detectDragGestures can be used to detect drags in any direction:

suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) - >Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange.dragAmount: Offset) - >Unit
)
Copy the code

DetectDragGestures is also available in horizontal and vertical versions, so in the huarong scene, you can also use the following methods for horizontal and vertical listening:

@Composable
fun ChessBoard (
    chessList: List<Chess>,
    onMove: (chess: String.x: Int.y: Int) - >Unit
) {
    Image(
        modifier = Modifier
            ...
            .pointerInput(Unit) {
                scope.launch {// Listen for horizontal drag
                    detectHorizontalDragGestures { change, dragAmount ->
                        change.consumeAllChanges()
                        onMove(chess.name, 0, dragAmount.roundToInt())
                    }
                }
                scope.launch {// Listen for vertical drag
                    detectVerticalDragGestures { change, dragAmount ->
                        change.consumeAllChanges()
                        onMove(chess.name, 0, dragAmount.roundToInt())
                    }
                }
            },
            ...
    )
}
Copy the code

Note detectHorizontalDragGestures and detectVerticalDragGestures is suspended function, so you need to start intercepting coroutines respectively, multiple flow can be likened to collect.

3.2 Checker collision detection

Once you have the information about the movement of the pieces being dragged, you can update the chess state and finally refresh the UI. However, before updating the status, it is necessary to detect the collision of the pieces, and the dragging of the pieces is bounded.

The principle of collision detection is simple: a piece must not cross other pieces in the current direction of movement.

1) Determination of relative position

First, you need to determine the position of the pieces relative to each other. You can use the following method to determine that piece A is above piece B:

val Chess.left get() = offset.x
val Chess.right get() = left + width

infix fun Chess.isAboveOf(other: Chess) =
    (bottom <= other.top) && ((left until right) intersect (other.left until other.right)).isNotEmpty()
Copy the code

Disassemble the above condition expression, that is, the lower boundary of chess piece A is above the upper boundary of chess piece B and the areas of chess piece A and chess piece B intersect in the horizontal direction:

For example, in the chess game above, you can get the following decision:

  • Cao caoLocated in theGuan yuOn top of
  • Guan yuLocated in thePawn 1 Huang zhongOn top of
  • Pawn 1Located in theHe 2 Single 3On top of

Although Guan Yu is above pawn 2, from the perspective of collision detection, there is no intersection between Guan Yu and pawn 2 in the X-axis direction, so Guan Yu’s movement in the Y-axis direction will not collide with Pawn 2.

guan.isAboveOf(zu1) == false
Copy the code

Similarly, other positions are as follows:

infix fun Chess.isToRightOf(other: Chess) =
    (left >= other.right) && ((top until bottom) intersect (other.top until other.bottom)).isNotEmpty()

infix fun Chess.isToLeftOf(other: Chess) =
    (right <= other.left) && ((top until bottom) intersect (other.top until other.bottom)).isNotEmpty()

infix fun Chess.isBelowOf(other: Chess) =
    (top >= other.bottom) && ((left until right) intersect (other.left until other.right)).isNotEmpty()
Copy the code

2) Boundary-crossing detection

Next, determine if a piece is out of bounds when it moves, that is, over other pieces in its direction of movement or out of bounds

For example, if a piece moves in the X-axis to check if it is out of bounds:


// move in the X direction
fun Chess.moveByX(x: Int) = moveBy(IntOffset(x, 0)) 

// Detect collision and move
fun Chess.checkAndMoveX(x: Int, others: List<Chess>): Chess { others.filter { it.name ! = name }.forEach { other ->if (x > 0 && this isToLeftOf other && right + x >= other.left)
            return moveByX(other.left - right)
        else if (x < 0 && this isToRightOf other && left + x <= other.right)
            return moveByX(other.right - left)
    }
    return if (x > 0) moveByX(min(x, boardWidth - right)) else moveByX(max(x, 0 - left))

}
Copy the code

The logic is clear: when a piece is moving in the X-axis direction, it will stop moving if it hits the piece to its right. Otherwise, continue until you hit the board boundary, and do the same in other directions.

3.3 Update chess status

To sum up, after obtaining gesture displacement information, detect collision and move to the correct position, and finally update the state and refresh the UI:

val chessList: List<Chess> by remember {
    mutableStateOf(opening.toList())
}
                             
ChessBoard(chessList = chessState) { cur, x, y -> / / onMove callback
         chessState = chessState.map { //it: Chess
            if (it.name == cur) {
                if(x ! =0) it.checkAndMoveX(x, chessState)
                else it.checkAndMoveY(y, chessState)
            } else { it }
         }
}
Copy the code

4. Theme change and game skin change

Finally, take a look at how to implement multiple skins for your game, using the Compose Theme.

Compose’s Theme configuration is straightforward, thanks to its CompositionLocal implementation. CompositionLocal can be thought of as a Composable parent with two characteristics:

  1. Its Composable children share data in CompositionLocal, avoiding layers of parameter passing.
  2. whenCompositionLocalWhen the data of the Composable changes, the sub-composable is automatically reorganized to get the latest data.

CompositionLocal allows you to implement the dynamic skin of Compose:

4.1 Defining the skin

First, we define multiple sets of skins, that is, multiple sets of image resources for the pieces

object DarkChess : ChessAssets {
    override val huangzhong = R.drawable.huangzhong
    override val caocao = R.drawable.caocao
    override val zhaoyun = R.drawable.zhaoyun
    override val zhangfei = R.drawable.zhangfei
    override val guanyu = R.drawable.guanyu
    override val machao = R.drawable.machao
    override val zu = R.drawable.zu
}

object LightChess : ChessAssets {
    / /... Ditto, slightly
}

object WoodChess : ChessAssets {
    / /... Ditto, slightly
}
Copy the code

4.2 create CompositionLocal

Then create the skin’s CompositionLocal, which we create using the compositionLocalOf method

internal var LocalChessAssets = compositionLocalOf<ChessAssets> {
    DarkChess
}
Copy the code

DarkChess is the default value here, but is not usually used directly. Normally we create a Composable container for CompositionLocal via CompositionLocalProvider and set the current value:

CompositionLocalProvider(LocalChessAssets provides chess) {
     / /...
}
Copy the code

Its internal children Composable share the values of the current setting.

4.3 Follow the Theme change to switch skins

In this game, we want to add the skins of the pieces to the overall Theme of the game and switch with the Theme change:

@Composable
fun ComposehuarongdaoTheme(
    theme: Int = 0,
    content: @Composable() () - >Unit
) {

    val (colors, chess) = when (theme) {
        0 -> DarkColorPalette to DarkChess
        1 -> LightColorPalette to LightChess
        2 -> WoodColorPalette to WoodChess
        else -> error("")
    }

    CompositionLocalProvider(LocalChessAssets provides chess) {
        MaterialTheme(
            colors = colors,
            typography = Typography,
            shapes = Shapes,
            content = content
        )
    }

}
Copy the code

Define the enumeration value of theme, according to the enumeration get different colors and ChessAssets, place the MaterialTheme inside LocalChessAssets. All ComposalBes within the MaterialTheme can share the values of the MaterialTheme and LocalChessAssets.

Finally, define a MaterialTheme extension function for LocalChessAssets,

val MaterialTheme.chessAssets
    @Composable
    @ReadOnlyComposable
    get() = LocalChessAssets.current
Copy the code

ChessAssets can be accessed just like any other property of the MaterialTheme.

The last

For example, Compose is composed for Gesture and Theme. For example, Compose is composed for Gesture and Theme.

Use Jetpack Compose for custom gesture processing

Tedious topic configuration? Compose makes it easy for you!

Code address: github.com/vitaviva/co…