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:
- Define the state of the game
- State-based UI drawing
- 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 cao
Located in theGuan yu
On top ofGuan yu
Located in thePawn 1
Huang zhong
On top ofPawn 1
Located in theHe 2
Single 3
On 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:
- Its Composable children share data in CompositionLocal, avoiding layers of parameter passing.
- when
CompositionLocal
When 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…