利用Jetpack Compose實現(xiàn)經(jīng)典俄羅斯方塊游戲
你的童年是否有俄羅斯方塊呢,本文就來介紹如何通過 Jetpack Compose 實現(xiàn)一個俄羅斯方塊 ~~
先看下效果圖,功能還是挺完善的

就我自己的體驗來說,使用 Compose 開發(fā)的應(yīng)用我感受不到和 Android 原生開發(fā)之間有什么性能差異,但 Compose 在開發(fā)難度上會低很多
Google 官網(wǎng)上是這么介紹 Compose 的:Jetpack Compose 是用于構(gòu)建原生界面的新款 Android 工具包,它可簡化并加快 Android 上的界面開發(fā),使用更少的代碼、強(qiáng)大的工具和直觀的 Kotlin API,快速讓應(yīng)用生動而精彩
長期以來,Android 的視圖層次結(jié)構(gòu)可以表示為一個視圖樹,視圖樹中包含著若干個 View 和 ViewGroup。當(dāng)應(yīng)用的數(shù)據(jù)由于用戶交互等原因而發(fā)生變化時,界面的層次結(jié)構(gòu)就需要進(jìn)行更新以顯示最新數(shù)據(jù)。最常見的界面更新方式就是使用findViewById()等函數(shù)遍歷視圖樹,并通過調(diào)用 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法來改變特定節(jié)點,而這些方法就會改變 View 的內(nèi)部狀態(tài)。但這種手動操縱視圖的方式提高了出錯的可能性。如果一條數(shù)據(jù)需要在多個位置呈現(xiàn),開發(fā)者可能一不小心就會忘記更新某個顯示它的視圖。此外,當(dāng)兩項更新以意外的方式發(fā)生沖突時,也很容易造成異常狀態(tài)。例如,某項更新可能會嘗試修改剛剛從界面中移除的節(jié)點。一般來說,軟件維護(hù)復(fù)雜性會隨著需要更新的視圖數(shù)量增多而增長
在過去的幾年中,整個行業(yè)已開始轉(zhuǎn)向聲明性界面模型,該模型大大簡化了與構(gòu)建和更新界面關(guān)聯(lián)的工程設(shè)計。該技術(shù)的工作原理是在概念上從頭開始重新生成整個屏幕,然后僅執(zhí)行必要的更改。此方法可避免手動更新有狀態(tài)視圖層次結(jié)構(gòu)的復(fù)雜性。Compose 就是一個適用于 Android 的新式聲明性界面工具包,提供了聲明性 API,讓開發(fā)者可在不以命令方式改變前端視圖的情況下呈現(xiàn)應(yīng)用界面,從而使編寫和維護(hù)應(yīng)用界面變得更加容易
可組合函數(shù)
Compose 的重點就在于 @Composable函數(shù),即可組合函數(shù),每個可組合函數(shù)可以接收若干入?yún)?shù)用于參與視圖結(jié)構(gòu)的繪制說明,但函數(shù)不返回任何值??山M合函數(shù)只用于描述視圖結(jié)構(gòu)如何繪制以及如何與用戶進(jìn)行交互,但不需要返回視圖對象,而是由 Compose 根據(jù)開發(fā)者的描述來生成具體的視圖對象
本游戲的 icon 就是通過這種方式來生成的??梢钥吹?PreviewTetrisIcon() 函數(shù)并不包含返回值,當(dāng)然這種情況下也不需要入?yún)?shù)。此外,Compose 的一個優(yōu)點就是所見即所得,通過添加 @Preview 注解就可以預(yù)覽實現(xiàn)效果,每次修改過后無需編譯,只要刷新一下就可以看到修改結(jié)果

Compose 是一個聲明性界面框架,這本身也帶有一點組合的意味。每個視圖結(jié)點均通過函數(shù)的形式來進(jìn)行聲明,那么我們自然也可以將每個視圖結(jié)點均聲明為一個個函數(shù),然后將每個函數(shù)作為最終視圖樹函數(shù)的入?yún)?shù)來進(jìn)行組合
以本游戲為例,整個游戲只包含一個頁面,頁面可以再細(xì)分為三個節(jié)點:游戲機(jī)身(TetrisBody)、游戲屏幕(TetrisScreen)、游戲按鈕(TetrisButton)
TetrisBody 函數(shù)就包含兩個入?yún)?shù)用于容納 TetrisScreen 和 TetrisButton
@Composable
fun TetrisBody(
tetrisScreen: @Composable (() -> Unit),
tetrisButton: @Composable (() -> Unit),
)游戲機(jī)身 - TetrisBody

TetrisBody 比較簡單,需要實現(xiàn)的功能有三個:
- 繪制背景色
- 為 TetrisScreen 和 TetrisButton 預(yù)留位置
- 為 TetrisScreen 繪制陰影邊框
@Composable
fun TetrisBody(
tetrisScreen: @Composable (() -> Unit),
tetrisButton: @Composable (() -> Unit),
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = BodyBackground)
.padding(bottom = 30.dp)
) {
Box(
Modifier
.align(alignment = Alignment.CenterHorizontally)
.fillMaxWidth()
.weight(weight = 1f)
.padding(start = 40.dp, top = 50.dp, end = 40.dp, bottom = 10.dp),
) {
//繪制游戲屏幕的邊框
val borderPadding = 8.dp
Canvas(modifier = Modifier.fillMaxSize()) {
drawScreenBorder(
leftTop = Offset(x = 0f, y = 0f),
width = size.width,
height = size.height,
borderPadding = borderPadding,
)
}
//游戲屏幕
Row(
modifier = Modifier
.matchParentSize()
.padding(all = borderPadding)
) {
tetrisScreen()
}
}
//游戲按鈕
tetrisButton()
}
}游戲按鈕 - TetrisButton

TetrisButton 也很簡單,需要實現(xiàn)的功能有兩個:
- 繪制九個操作按鈕
- 向外透傳用戶的點擊操作,對事件類型進(jìn)行區(qū)分
因此 TetrisButton 函數(shù)就需要包含一個入?yún)?shù) PlayListener 對象,TetrisButton 需要根據(jù)用戶點擊了哪個按鈕來回調(diào) PlayListener 相應(yīng)的方法,向外透傳點擊事件
enum class TransformationType {
Left, Right, Rotate, Down, FastDown, Fall
}
data class PlayListener constructor(
val onStart: () -> Unit,
val onPause: () -> Unit,
val onReset: () -> Unit,
val onSound: () -> Unit,
val onTransformation: (TransformationType) -> Unit
)
@Preview(backgroundColor = 0xffefcc19, showBackground = true)
@Composable
fun TetrisButton(
playListener: PlayListener = combinedPlayListener()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalArrangement = Arrangement.Center
) {
val controlPadding = 20.dp
ControlButton(hint = "Start", modifier = Modifier.padding(end = controlPadding)) {
playListener.onStart()
}
ControlButton(
hint = "Pause",
modifier = Modifier.padding(start = controlPadding, end = controlPadding)
) {
playListener.onPause()
}
ControlButton(
hint = "Reset",
modifier = Modifier.padding(start = controlPadding, end = controlPadding)
) {
playListener.onReset()
}
ControlButton(hint = "Sound", modifier = Modifier.padding(start = controlPadding)) {
playListener.onSound()
}
}
ConstraintLayout(
modifier = Modifier
.padding(top = 20.dp)
.fillMaxWidth()
.wrapContentWidth(align = Alignment.CenterHorizontally)
) {
val (leftBtn, rightBtn, fastDownBtn, rotateBtn, fallBtn) = createRefs()
val innerMargin = 24.dp
PlayButton(icon = "?", modifier = Modifier.constrainAs(leftBtn) {
start.linkTo(anchor = parent.start)
top.linkTo(anchor = parent.top)
end.linkTo(anchor = rightBtn.start, margin = innerMargin)
}) {
playListener.onTransformation(Left)
}
PlayButton(icon = "?", modifier = Modifier.constrainAs(rightBtn) {
start.linkTo(anchor = leftBtn.end, margin = innerMargin)
top.linkTo(anchor = leftBtn.top)
bottom.linkTo(anchor = leftBtn.bottom)
}) {
playListener.onTransformation(Right)
}
PlayButton(
icon = "Rotate",
fontSize = 18.sp,
modifier = Modifier.constrainAs(rotateBtn) {
top.linkTo(anchor = rightBtn.top)
start.linkTo(anchor = rightBtn.end, margin = innerMargin)
}) {
playListener.onTransformation(Rotate)
}
PlayButton(icon = "▼", modifier = Modifier.constrainAs(fastDownBtn) {
top.linkTo(anchor = leftBtn.bottom)
start.linkTo(anchor = leftBtn.start)
end.linkTo(anchor = rightBtn.end)
}) {
playListener.onTransformation(FastDown)
}
PlayButton(
icon = "▼\n▼",
modifier = Modifier.constrainAs(fallBtn) {
top.linkTo(anchor = fastDownBtn.top)
start.linkTo(anchor = rightBtn.end)
end.linkTo(anchor = rotateBtn.start)
}) {
playListener.onTransformation(Fall)
}
}
}
}游戲屏幕 - TetrisScreen

TetrisScreen 比較復(fù)雜,需要實現(xiàn)的功能點主要有五個:
- 繪制游戲屏幕背景
- 繪制不斷下落的方塊
- 為方塊提供左移、右移、勻速下降、加速下降、旋轉(zhuǎn)等功能
- 當(dāng)方塊無法再下落時,根據(jù)需要決定是否進(jìn)行消行,然后保存該方塊的坐標(biāo)信息到屏幕背景中,根據(jù)坐標(biāo)信息繪制實心方塊,然后生成新的方塊,重復(fù)第二個步驟
- 當(dāng)方塊無法再下落時,如果方塊超出當(dāng)前屏幕,則結(jié)束游戲,執(zhí)行清屏動畫
Compose 是根據(jù)函數(shù)的入?yún)?shù)是否發(fā)生了變化來決定是否需要進(jìn)行界面更新的,所以我們在繪制下落的方塊時可以將整個頁面視為靜態(tài)的,僅需要根據(jù)當(dāng)前的坐標(biāo)值進(jìn)行繪制即可,然后每隔幾百毫秒就改變方塊的坐標(biāo)信息,由此生成新的入?yún)?shù),通知 Compose 進(jìn)行頁面更新即可
整個游戲的所有狀態(tài)信息都保存在一個 TetrisState 對象中,Compose 就通過監(jiān)聽State<TetrisState>中值的變化來決定是否需要進(jìn)行界面更新。整個游戲屏幕就被定義為一個 10 x 24 的二維數(shù)組,即 brickArray,當(dāng)數(shù)組值等于一時,就對應(yīng)實心方塊,否則就是空心方塊。Tetris 就對應(yīng)處于下落狀態(tài)中的方塊
data class TetrisState(
val brickArray: Array<IntArray>, //屏幕坐標(biāo)系
val tetris: Tetris, //下落的方塊
val gameStatus: GameStatus = GameStatus.Welcome, //游戲狀態(tài)
val soundEnable: Boolean = true, //是否開啟音效
val nextTetris: Tetris = Tetris(), //下一個方塊
)方塊類型一共可以分為七種,用字母表示就分別是:I、S、Z、L、O、J、T。每種類型都可以容納在一個 4 x 4 的二維數(shù)組里,不管其如何旋轉(zhuǎn),都不會超出這個范圍??梢杂靡韵聰?shù)組來方便記憶每種可能的旋轉(zhuǎn)結(jié)果
val mockData = arrayOf(
arrayOf(//I
intArrayOf(
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
1, 1, 1, 1
),
intArrayOf(
0, 1, 0, 0,
0, 1, 0, 0,
0, 1, 0, 0,
0, 1, 0, 0
)
),
arrayOf(//S
intArrayOf(
0, 0, 0, 0,
0, 0, 0, 0,
0, 1, 1, 0,
1, 1, 0, 0
),
intArrayOf(
0, 0, 0, 0,
1, 0, 0, 0,
1, 1, 0, 0,
0, 1, 0, 0
)
),
arrayOf(//Z
intArrayOf(
0, 0, 0, 0,
0, 0, 0, 0,
1, 1, 0, 0,
0, 1, 1, 0
),
intArrayOf(
0, 0, 0, 0,
0, 1, 0, 0,
1, 1, 0, 0,
1, 0, 0, 0
)
),
···
)每個處于下落狀態(tài)的方塊都被定義為 Tetris 對象。初始狀態(tài)下 brickArray 的值都等于 0,而 Tetris 的初始位置是在屏幕之外的,方塊每次下落時都將方塊在 brickArray 中的位置的坐標(biāo)值改變?yōu)?1,從而決定了需要在屏幕的哪個位置繪制實心方塊;再通過改變方塊相對屏幕左上角的偏移量 Offset 的值,以此改變方塊相對屏幕的位置,從而實現(xiàn)方塊的左右移動和下落
data class Location(val x: Int, val y: Int)
data class Tetris constructor(
val shapes: List<List<Location>>, //此方塊所有可能的旋轉(zhuǎn)結(jié)果
val type: Int, //用于標(biāo)記當(dāng)前處于哪種旋轉(zhuǎn)狀態(tài)
val offset: Location, //方塊相對屏幕左上角的偏移量
) {
//此方塊當(dāng)前的形狀
val shape: List<Location>
get() = shapes[type]
}簡單起見,可以事先就定義好 Tetris 各種可能的方塊類型,以及該方塊的各種旋轉(zhuǎn)結(jié)果
private val allShapes = listOf(
//I
listOf(
listOf(Location(0, 3), Location(1, 3), Location(2, 3), Location(3, 3)),
listOf(Location(1, 0), Location(1, 1), Location(1, 2), Location(1, 3)),
),
//S
listOf(
listOf(Location(0, 3), Location(1, 2), Location(1, 3), Location(2, 2)),
listOf(Location(0, 1), Location(0, 2), Location(1, 2), Location(1, 3)),
),
//Z
listOf(
listOf(Location(0, 2), Location(1, 2), Location(1, 3), Location(2, 3)),
listOf(Location(0, 2), Location(0, 3), Location(1, 1), Location(1, 2)),
),
//L
listOf(
listOf(Location(0, 1), Location(0, 2), Location(0, 3), Location(1, 3)),
listOf(Location(0, 2), Location(0, 3), Location(1, 2), Location(2, 2)),
listOf(Location(0, 1), Location(1, 1), Location(1, 2), Location(1, 3)),
listOf(Location(0, 3), Location(1, 3), Location(2, 3), Location(2, 2)),
),
//O
listOf(
listOf(Location(0, 2), Location(0, 3), Location(1, 2), Location(1, 3)),
),
//J
listOf(
listOf(Location(0, 3), Location(1, 1), Location(1, 2), Location(1, 3)),
listOf(Location(0, 2), Location(0, 3), Location(1, 3), Location(2, 3)),
listOf(Location(0, 1), Location(0, 2), Location(0, 3), Location(1, 1)),
listOf(Location(0, 2), Location(1, 2), Location(2, 2), Location(2, 3)),
),
//T
listOf(
listOf(Location(0, 2), Location(1, 2), Location(2, 2), Location(1, 3)),
listOf(Location(1, 1), Location(0, 2), Location(1, 2), Location(1, 3)),
listOf(Location(1, 2), Location(0, 3), Location(1, 3), Location(2, 3)),
listOf(Location(0, 1), Location(0, 2), Location(0, 3), Location(1, 2)),
),
)之后在每次生成 Tetris 對象時,都隨機(jī)從 allShapes 中取值。并且每個 Tetris 對象的初始偏移量 offset 的 Y 值固定是 -4,即默認(rèn)處于屏幕之外,當(dāng)方塊不斷移動時,其 Offset 就會變成 Location(0, -3)、Location(1, -2) .... Location(2, 10)等各種值,通過改變 X 值來實現(xiàn)左右移動、改變 Y 值來實現(xiàn)下移
operator fun invoke(): Tetris {
val shapes = allShapes.random()
val type = Random.nextInt(0, shapes.size)
return Tetris(
shapes = shapes,
type = type,
offset = Location(
Random.nextInt(
0,
BRICK_WIDTH - 3
), -4
)
)
}每個方塊就可以通過 Canvas 來進(jìn)行繪制,方便起見就將其定義為擴(kuò)展函數(shù),通過 color 來控制是要繪制實心方塊還是虛心方塊

fun DrawScope.drawBrick(brickSize: Float, color: Color) {
drawRect(color = color, size = Size(brickSize, brickSize))
val strokeWidth = brickSize / 9f
translate(left = strokeWidth, top = strokeWidth) {
drawRect(
color = ScreenBackground,
size = Size(
width = brickSize - 2 * strokeWidth,
height = brickSize - 2 * strokeWidth
)
)
}
val brickInnerSize = brickSize / 2.0f
val translateLeft = (brickSize - brickInnerSize) / 2
translate(left = translateLeft, top = translateLeft) {
drawRect(
color = color,
size = Size(brickInnerSize, brickInnerSize)
)
}
}之后只需要遍歷代表整個屏幕坐標(biāo)值的 screenMatrix 進(jìn)行繪制就可以繪制出屏幕背景以及下落的方塊,如果值等于一就使用 BrickFill 顏色,否則就使用 BrickAlpha。每當(dāng)有方塊無法繼續(xù)下落時,該方塊所在的坐標(biāo)值就都會被寫入到 screenMatrix 中,以此來保存各個固定的實心方塊
Canvas(
modifier = Modifier
.fillMaxSize()
.background(color = ScreenBackground)
.padding(
start = screenPadding, top = screenPadding,
end = screenPadding, bottom = screenPadding
)
) {
val width = size.width
val height = size.height
val screenPaddingPx = screenPadding.toPx()
val spiritPaddingPx = spiritPadding.toPx()
val brickSize = (height - spiritPaddingPx * (matrixHeight - 1)) / matrixHeight
kotlin.run {
screenMatrix.forEachIndexed { y, ints ->
ints.forEachIndexed { x, isFill ->
translate(
left = x * (brickSize + spiritPaddingPx),
top = y * (brickSize + spiritPaddingPx)
) {
drawBrick(
brickSize = brickSize,
color = if (isFill == 1) BrickFill else BrickAlpha
)
}
}
}
}
···
}調(diào)度器 - TetrisViewModel
TetrisViewModel 是整個游戲的調(diào)度器,其大體結(jié)構(gòu)如下所示。dispatch 方法負(fù)責(zé)接收外部的各個事件,事件類型就對應(yīng)密封類 Action
class TetrisViewModel : ViewModel() {
companion object {
private const val DOWN_SPEED = 500L
private const val CLEAR_SCREEN_SPEED = 30L
}
private val _tetrisStateLD: MutableStateFlow<TetrisState> = MutableStateFlow(TetrisState())
val tetrisStateLD = _tetrisStateLD.asStateFlow()
private val tetrisState: TetrisState
get() = _tetrisStateLD.value
private var downJob: Job? = null
private var clearScreenJob: Job? = null
fun dispatch(action: Action) {
playSound(action)
val unit = when (action) {
Action.Welcome, Action.Reset -> {
···
}
Action.Start -> {
···
}
Action.Background, Action.Pause -> {
···
}
Action.Resume -> {
}
Action.Sound -> {
···
}
is Action.Transformation -> {
···
}
}
}
···
}
sealed class Action {
object Welcome : Action()
object Start : Action()
object Pause : Action()
object Reset : Action()
object Sound : Action()
object Background : Action()
object Resume : Action()
data class Transformation(val transformationType: TransformationType) : Action()
}
enum class TransformationType {
Left, Right, Rotate, Down, FastDown, Fall
}游戲第一次啟動時,由 MainActivity 來主動下發(fā) Action.Welcome 事件,執(zhí)行歡迎動畫。當(dāng)后續(xù)用戶點擊 Start 按鈕啟動游戲時,則會下發(fā) Action.Start 事件,從而啟動一個執(zhí)行延時任務(wù)的協(xié)程任務(wù) downJob,downJob 負(fù)責(zé)下發(fā) TransformationType.Down 事件,即方塊下落事件,當(dāng)消耗了該事件后,又會重復(fù)調(diào)用 startDownJob() 方法,從而實現(xiàn)自我驅(qū)動方塊勻速下降
private var downJob: Job? = null
private fun onStartGame() {
dispatchState(TetrisState().copy(gameStatus = GameStatus.Running))
startDownJob()
}
private fun startDownJob() {
cancelDownJob()
cancelClearScreenJob()
downJob = viewModelScope.launch {
delay(DOWN_SPEED)
dispatch(Action.Transformation(TransformationType.Down))
}
}Action.Transformation 代表的是多種操作行為,例如左右移動、旋轉(zhuǎn)等。但并不是每種操作都能生效,因為執(zhí)行該操作可能會導(dǎo)致方塊超出屏幕。所以如果 onTransformation 方法返回 null 的話,說明此次行為無效,無需更新界面
fun TetrisState.onTransformation(transformationType: TransformationType): TetrisState {
return when (transformationType) {
TransformationType.Left -> {
onLeft()
}
TransformationType.Right -> {
onRight()
}
TransformationType.Down -> {
onDown()
}
TransformationType.FastDown -> {
onFastDown()
}
TransformationType.Fall -> {
onFall()
}
TransformationType.Rotate -> {
onRotate()
}
}?.finalize() ?: this.finalize()
}對于 Left、Right、Down、FastDown、Fall 這幾種事件,都是在對 offset 做操作,通過改變 offset 的 X 坐標(biāo)和 Y 坐標(biāo)來移動方塊的位置
private fun TetrisState.onLeft(): TetrisState? {
return copy(
tetris = tetris.copy(offset = Location(tetris.offset.x - 1, tetris.offset.y))
).takeIf { it.isValidInMatrix() }
}
private fun TetrisState.onRight(): TetrisState? {
return copy(
tetris = tetris.copy(offset = Location(tetris.offset.x + 1, tetris.offset.y))
).takeIf { it.isValidInMatrix() }
}
private fun TetrisState.onDown(): TetrisState? {
return copy(
tetris = tetris.copy(
offset = Location(tetris.offset.x, tetris.offset.y + 1)
)
).takeIf { it.isValidInMatrix() }
}
private fun TetrisState.onFastDown(): TetrisState? {
return copy(
tetris = tetris.copy(
offset = Location(tetris.offset.x, tetris.offset.y + 2)
)
).takeIf { it.isValidInMatrix() }
}
private fun TetrisState.onFall(): TetrisState? {
while (true) {
val result = onDown() ?: return this
return result.onFall()
}
}前文說了,每種方塊類型都包含有多種旋轉(zhuǎn)結(jié)果,所以 Rotate 事件就需要將方塊改變?yōu)槠渌D(zhuǎn)形狀。而由于當(dāng)旋轉(zhuǎn)過后方塊的坐標(biāo)系可能會超出當(dāng)前屏幕的范圍,所以還需要依靠 adjustOffset()方法將方塊的坐標(biāo)系遷移回屏幕內(nèi)
private fun TetrisState.onRotate(): TetrisState? {
if (tetris.shapes.size == 1) {
return null
}
val nextType = if (tetris.type + 1 < tetris.shapes.size) {
tetris.type + 1
} else {
0
}
return copy(
tetris = tetris.copy(
type = nextType,
)
).adjustOffset().takeIf { it.isValidInMatrix() }
}當(dāng)方塊無法再下落,或者是已經(jīng)超出了屏幕時,則需要依靠 finalize()方法將方塊的坐標(biāo)值寫入到屏幕背景 brickArray 中,并重置游戲狀態(tài)
private fun TetrisState.finalize(): TetrisState {
if (canDown()) {
return this
} else {
var gameOver = false
for (location in tetris.shape) {
val x = location.x + tetris.offset.x
val y = location.y + tetris.offset.y
if (x in 0 until width && y in 0 until height) {
brickArray[y][x] = 1
} else {
gameOver = true
}
}
return if (gameOver) {
copy(gameStatus = GameStatus.GameOver)
} else {
val clearRes = clearIfNeed()
if (clearRes == null) {
copy(
gameStatus = GameStatus.Running,
tetris = nextTetris,
nextTetris = Tetris()
)
} else {
copy(
gameStatus = GameStatus.LineClearing,
tetris = nextTetris,
nextTetris = Tetris()
)
}
}
}
}項目地址
游戲的大體實現(xiàn)思路就如上所述,表達(dá)能力所限,有些地方?jīng)]法講得太清楚,實現(xiàn)細(xì)節(jié)歡迎查閱源碼了解
到此這篇關(guān)于利用Jetpack Compose實現(xiàn)經(jīng)典俄羅斯方塊游戲的文章就介紹到這了,更多相關(guān)Jetpack Compose俄羅斯方塊內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android音頻開發(fā)之音頻采集的實現(xiàn)示例
本篇文章主要介紹了Android音頻開發(fā)之音頻采集的實現(xiàn)示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04
Android中AlertDialog的六種創(chuàng)建方式
這篇文章主要介紹了Android中AlertDialog的六種創(chuàng)建方式的相關(guān)資料,需要的朋友可以參考下2016-07-07
Android編程實現(xiàn)將應(yīng)用強(qiáng)制安裝到手機(jī)內(nèi)存的方法
這篇文章主要介紹了Android編程實現(xiàn)將應(yīng)用強(qiáng)制安裝到手機(jī)內(nèi)存的方法,分析了Android程序安裝的相關(guān)屬性設(shè)置技巧及注意事項,需要的朋友可以參考下2015-12-12
Android實現(xiàn)多次閃退清除數(shù)據(jù)
這篇文章主要介紹了Android實現(xiàn)多次閃退清除數(shù)據(jù)的相關(guān)資料,感興趣的小伙伴們可以參考一下2016-04-04
Android實現(xiàn)網(wǎng)易云音樂高仿版流程
這篇文章主要介紹了Android實現(xiàn)網(wǎng)易云音樂高仿版,包含了首頁復(fù)雜發(fā)現(xiàn)界面布局和功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-08-08
Android打印機(jī)--小票打印格式及模板設(shè)置實例代碼
這篇文章主要介紹了Android打印機(jī)--小票打印格式及模板設(shè)置實例代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-04-04

