最近发现在有很好的例子,可以精简地介绍 MVC/ MVP / MVVM 架构的区别和关系。
所以也做记录一下,以便后续的学习。 总的类图参考: https://blog.csdn.net/whjk20/article/details/107226213
目录
1. Model (Player - Cell - Board)
2. View( activity_tictactoe_mvc.xml)
MVC 的实现,其中Model 是游戏的关键实现,在这里可以忽略,侧重MVC 之间的交互
1. Model (Player - Cell - Board)
enum class Player {
X,
O
}
class Cell {
var value: Player ?= null
}
class Board {
// 3*3 格,二维数组,并初始化为空
private val cells = Array(3) {
arrayOfNulls<Cell>(3)
}
// 当前轮到谁
private var currentTurnPlayer: Player? = null
var winner: Player? = null
// 游戏状态
private var state: GameState? = null
private enum class GameState { IN_PROGRESS, FINISHED }
constructor() {
restart()
}
//重置或者重启一个新的游戏,清除棋盘和赢输状态
fun restart() {
clearCells()
winner = null
currentTurnPlayer = Player.X
state = GameState.IN_PROGRESS
}
private fun clearCells() {
for (i in cells.indices) {
for (j in cells[i].indices) {
cells[i][j] = Cell()
}
}
}
// 游戏主要数据更新逻辑 - TODO: 无法分出胜负时提示
fun mark(row: Int, col: Int): Player? {
var playerThatMoved: Player? = null
if (isValid(row, col)) {
cells[row][col]!!.value = currentTurnPlayer
playerThatMoved = currentTurnPlayer
if (isWinningMoveByPlayer(currentTurnPlayer, row, col)) {
state = GameState.FINISHED
winner = currentTurnPlayer
} else {
flipCurrentTurnPlayer()
}
}
return playerThatMoved
}
// 切换到下一个玩家
private fun flipCurrentTurnPlayer() {
currentTurnPlayer = if (currentTurnPlayer == Player.X) {
Player.O
} else {
Player.X
}
}
private fun isWinningMoveByPlayer(
player: Player?,
currentRow: Int,
currentCol: Int
): Boolean {
return (cells[currentRow][0]!!.value === player // 3-in-the-row
&& cells[currentRow][1]!!.value === player && cells[currentRow][2]!!.value === player
) || (cells[0][currentCol]!!.value === player // 3-in-the-column
&& cells[1][currentCol]!!.value === player && cells[2][currentCol]!!.value === player
) || (currentRow == currentCol // 3-in-the-diagonal
&& cells[0][0]!!.value === player && cells[1][1]!!.value === player && cells[2][2]!!.value === player
) || (currentRow + currentCol == 2 // 3-in-the-opposite-diagonal
&& cells[0][2]!!.value === player && cells[1][1]!!.value === player && cells[2][0]!!.value === player)
}
private fun isValid(row: Int, col: Int): Boolean {
return if (state === GameState.FINISHED) {
false
} else if (isOutOfBounds(row) || isOutOfBounds(col)) {
false
} else {
!isCellValueAlreadySet(row, col)
}
}
private fun isCellValueAlreadySet(row: Int, col: Int): Boolean {
return cells[row][col]!!.value != null
}
private fun isOutOfBounds(index: Int): Boolean {
return index < 0 || index > 2
}
}
2. View( activity_tictactoe_mvc.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tictactoe"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:gravity="center_horizontal"
tools:context=".mvc.controller.TicTacToeMVCActivity">
<GridLayout
android:id="@+id/buttonGrid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:columnCount="3"
android:rowCount="3">
<Button
android:tag="00"
android:onClick="onCellClicked"
style="@style/tictactoebutton"
/>
<Button
android:tag="01"
android:onClick="onCellClicked"
style="@style/tictactoebutton"
/>
<Button
android:tag="02"
android:onClick="onCellClicked"
style="@style/tictactoebutton"
/>
<Button
android:tag="10"
android:onClick="onCellClicked"
style="@style/tictactoebutton"
/>
<Button
android:tag="11"
android:onClick="onCellClicked"
style="@style/tictactoebutton"
/>
<Button
android:tag="12"
android:onClick="onCellClicked"
style="@style/tictactoebutton"
/>
<Button
android:tag="20"
android:onClick="onCellClicked"
style="@style/tictactoebutton"
/>
<Button
android:tag="21"
android:onClick="onCellClicked"
style="@style/tictactoebutton"
/>
<Button
android:tag="22"
android:onClick="onCellClicked"
style="@style/tictactoebutton"
/>
</GridLayout>
<LinearLayout
android:id="@+id/winnerPlayerViewGroup"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible"
>
<TextView
android:id="@+id/winnerPlayerLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="40sp"
android:layout_margin="20dp"
tools:text="X" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
android:text="@string/winner" />
</LinearLayout>
</LinearLayout>
其中,用到的资源:
(1) dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">44dp</dimen>
</resources>
(2) styles.xml
<resources>
<style name="tictactoebutton">
<item name="android:layout_width">100dp</item>
<item name="android:layout_height">100dp</item>
<item name="android:textSize">30sp</item>
</style>
</resources>
(3) strings.xml
<resources>
<string name="winner">Winner</string>
</resources>
3. Controller (Activity)
class TicTacToeMVCActivity : AppCompatActivity() {
companion object {
const val TAG = "TicTacToeMVCActivity"
}
private var model: Board? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tictactoe_mvc)
model = Board()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.reset_menu, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.design_reset -> {
resetView()
resetModel()
}
}
return super.onOptionsItemSelected(item)
}
// 操作VIEW, 更新MODLE 和VIEW -- TODO 需求可能更改(VIEW更改)
fun onCellClicked(view: View) {
// 读取VIEW操作
var clickedButton = view as Button
var tag = clickedButton.tag.toString()
var clickedRow = Integer.valueOf(tag.substring(0, 1))
var clickedCol = Integer.valueOf(tag.substring(1, 2))
Log.d(TAG, "onCellClicked - clickedRow = $clickedRow, clickedCol=$clickedCol")
// 获取当前移动的玩家
var playerThatMoved = model?.mark(clickedRow, clickedCol)
playerThatMoved?.let { clickedButton.text = it.toString() }
model?.winner?.let {
winnerPlayerViewGroup.visibility = View.VISIBLE
winnerPlayerLabel.text = it.toString()
}
}
// 重置VIEW
private fun resetView() {
winnerPlayerViewGroup.visibility = View.GONE
winnerPlayerLabel.text = EMPTY_TEXT
for (index in 0 until buttonGrid.childCount) {
(buttonGrid.getChildAt(index) as Button).text = EMPTY_TEXT
}
}
// 重置MODEL
private fun resetModel() {
model?.let { it.restart() }
}
}
其中,菜单布局:reset_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/design_reset"
android:title="重置"></item>
</menu>