在上一篇教程介紹了SurfaceView的使用厘贼,也實(shí)現(xiàn)了一個(gè)動(dòng)畫循環(huán)界酒,接下來就可以在這個(gè)循環(huán)體內(nèi)繪制游戲的畫面,由于我們繪制的只是游戲的一幀嘴秸,所以用時(shí)不能太長(zhǎng)毁欣,要講究點(diǎn)效率庇谆,這里我們可以使用LruCache對(duì)已經(jīng)繪制過的圖像進(jìn)行緩存,LruCache其實(shí)就是一個(gè)包裝了LinkedHashMap的鍵值對(duì)集合凭疮,圖像被緩存后可以直接從集合中取出饭耳,免去了反復(fù)重繪的時(shí)間。為了便于使用我們封閉了一個(gè)工具類:
object BmpCache {
private val mapCache = LruCache<String, Bitmap>(120)
fun get(key: String) = mapCache[key]
fun put(key: String, bmp: Bitmap) {
mapCache.put(key, bmp)
}
const val BMP_PLAYER = "player"
const val BMP_ENEMY = "enemy1"
}
繪畫主要是通過SurfaceHolder的lockCanvas返回的Canvas對(duì)象來實(shí)現(xiàn)的哭尝。在繪畫完成后再用unlockCanvasAndPost方法渲染到屏幕上哥攘。在開始之前先說明幾個(gè)繪圖術(shù)語(yǔ):
- 原點(diǎn)坐標(biāo):位于SurfaceView的左上角,X軸為0材鹦,往右遞增逝淹,Y軸為0,往下遞增桶唐。如果SurfaceView的分辨率為1920*1080時(shí)栅葡,原點(diǎn)坐標(biāo)為(0,0),右下角坐標(biāo)為(1919,1079)尤泽。
- 旋轉(zhuǎn)角度:以三點(diǎn)鐘方向?yàn)?度欣簇,順時(shí)鐘遞增,六點(diǎn)為90度坯约,九點(diǎn)為180度熊咽,十二點(diǎn)為270度。
- 筆刷類Paint:Paint類的主要作用是為Canvas繪制的圖形設(shè)置樣式闹丐,例如:圖形的顏色横殴,邊框的粗細(xì)和密閉區(qū)的填充模式、顏色和著色卿拴。
- 多邊形路徑類Path:Path類即可以為圖形指定動(dòng)畫路徑也可以用來畫多邊形衫仑,如果是畫多邊形的話一定要將多邊形閉合
至此我們可以正式的開始繪畫了,為了便于講解就以《空間大戰(zhàn)》里的玩家飛船為例堕花,首先是創(chuàng)建一個(gè)Bitmap對(duì)象并在上面畫出飛船的模樣文狱,畫完后立馬緩存起來,然后在繪制動(dòng)畫幀的時(shí)候再?gòu)木彺嬷腥〕霾嫷狡聊簧稀?/p>
繪制并緩存的流程如下:
- 創(chuàng)建一個(gè)Bitmap對(duì)象缘挽。
- 創(chuàng)建一個(gè)Canvas對(duì)象瞄崇,并把Bitmap對(duì)象作為參數(shù)傳給Canvas的構(gòu)造方法。
- 利用Canvas對(duì)象在Bitmap上畫出玩家飛船的樣子壕曼。
- 對(duì)Bitmap對(duì)象進(jìn)行緩存
代碼如下:
private fun buildPlayerBitmap(width: Int, height: Int): Bitmap {
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
// 手繪玩家的圖像
Canvas(bmp).apply {
val paint = Paint()
paint.isAntiAlias = true // 反鋸齒
paint.style = Paint.Style.FILL // 實(shí)心填充
// 設(shè)置漸變著色
paint.shader = RadialGradient(
width / 2f, 0f, width.toFloat(),
intArrayOf(Color.WHITE, Color.DKGRAY), null,
Shader.TileMode.CLAMP
)
// 定義多邊形的路徑
val path = Path()
path.moveTo(width / 2f, 0f)
path.lineTo(width.toFloat(), height - (height / 3f))
path.lineTo(width / 2f, height.toFloat())
path.lineTo(0f, height - (height / 3f))
path.close()
this.drawPath(path, paint) // 繪制多邊形苏研,樣式為實(shí)心、漸變
// 再次設(shè)置筆刷樣式為空心窝稿,邊線為1像素楣富,取消之前的著色器
paint.style = Paint.Style.STROKE
paint.strokeWidth = 1f
paint.shader = null
paint.color = Color.WHITE
paint.strokeJoin = Paint.Join.ROUND
this.drawPath(path, paint) // 繪制多邊形的邊框
this.drawLine(width / 2f, 0f, width / 2f, height.toFloat(), paint)
}
BmpCache.put("player", bmp)
return bmp
}
為了適配不同分辨率,我們?nèi)urfaceView尺寸中最窄的一邊的二十分之一為依據(jù)伴榔,作為玩家Bitmap對(duì)象的尺寸纹蝴。接著用Path對(duì)象勾勒出飛船的多邊形輪廓庄萎,再用Paint對(duì)象填充多邊形的內(nèi)部,因?yàn)橐呀?jīng)給Paint對(duì)象設(shè)置了一個(gè)漸變著色器塘安,所以填充后的多邊形內(nèi)部就呈現(xiàn)出漸變的效果糠涛。
Paint對(duì)象有一個(gè)特點(diǎn);就是當(dāng)圖形被畫到Bitmap上后就和Paint無關(guān)了兼犯,此時(shí)對(duì)Paint再次設(shè)置顏色等屬性時(shí)不會(huì)影響到已經(jīng)畫到Bitmap上的圖形忍捡,這概念相當(dāng)于我們畫完一幅圖后不用換筆刷,只需要洗一下筆刷重新蘸著顏料就能畫畫一樣切黔。所以再次對(duì)Paint對(duì)象進(jìn)行設(shè)置砸脊,把填充模式改為空心,白色線條并取消著色器纬霞,然后用之前的Path對(duì)象勾出一個(gè)白色的邊框凌埂,并在中心畫一線白線,最后的效果如下圖:
上述代碼只是把玩家的飛船畫到了Bitmap對(duì)象并且緩存了起來诗芜,接著就是把Bitmap對(duì)象畫到屏幕指定的位置上的流程:
- 從緩存中取出玩家的Bitmap瞳抓,如果沒有則繪制并緩存。
- 用Canvas的drawBitmap方法繪制到Surface的指定位置上伏恐。
private fun drawPlayer(
canvas: Canvas,
surfaceWidth: Int,
surfaceHeight: Int,
degrees: Float
) {
val bmp = if (BmpCache.get("player") == null) {
val size = if (surfaceWidth > surfaceHeight) surfaceHeight / 20 else surfaceWidth / 20
buildPlayerBitmap(size, size)
} else {
BmpCache.get("player")
}
// 將玩家的Bitmap繪制到Surface的中心孩哑,坐標(biāo)需要根據(jù)Bitmap的寬度作出偏移
val centerX = (surfaceWidth - bmp.width) / 2f
val centerY = (surfaceHeight - bmp.height) / 2f
// withRotation用于對(duì)圖形進(jìn)行旋轉(zhuǎn),pivotX和pivotY指定的是旋轉(zhuǎn)的軸坐標(biāo)
canvas.withRotation(degrees, surfaceWidth/2f, surfaceHeight/2f) {
canvas.drawBitmap(bmp, centerX, centerY, null)
// 為了便于觀察翠桦,在Bitmap外面套了一層矩形
paint.style = Paint.Style.STROKE
paint.color = Color.parseColor("#99FFFF00")
canvas.drawRect(centerX, centerY, centerX + bmp.width, centerY + bmp.height, paint)
}
}
上述代碼中用withRotation和drawBitmap方法結(jié)合横蜒,即指定了Bitmap繪制在屏幕上的坐標(biāo),又指定了旋轉(zhuǎn)的角度秤掌。這里面drawBitmap是從Bitmap的左上角開始顯示的愁铺,所以對(duì)顯示Bitmap的坐標(biāo)要進(jìn)行轉(zhuǎn)換鹰霍。而withRotation的軸坐標(biāo)則是針對(duì)屏幕來定位的闻鉴。
再結(jié)合上一篇Android游戲教程:SurfaceView - 游戲開始的地方最后的動(dòng)畫框架,我們實(shí)現(xiàn)了一個(gè)在屏幕正中不斷旋轉(zhuǎn)的玩家飛船的效果茂洒。
class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
private lateinit var surfaceView: SurfaceView
private var isLooping = false // 控制動(dòng)畫循環(huán)的運(yùn)行和結(jié)束
private val paint = Paint()
private var degrees = 0f
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initSurfaceView()
}
override fun onDestroy() {
super.onDestroy()
cancel()
}
/**
* 初始化SurfaceView
*/
private fun initSurfaceView() {
surfaceView = findViewById(R.id.surfaceView)
surfaceView.holder.setKeepScreenOn(true) // 屏幕常亮
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
isLooping = true
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
launch(Dispatchers.Default) {
while (isLooping) {
val canvas = holder.lockCanvas()
// 記錄幀開始的時(shí)間
val startMillis = System.currentTimeMillis()
// 在每幀開始的時(shí)候都要黑色矩形作為背景覆蓋掉上次的畫面
paint.color = Color.BLACK
canvas.drawRect(0f, 0f, width - 1f, height - 1f, paint)
drawFrame(canvas, width, height)
// 記錄幀結(jié)束的時(shí)間
val endMillis = System.currentTimeMillis()
// 使畫面保持在每秒60幀以內(nèi)
val frameDelay = 1000 / 60 - (startMillis - endMillis)
if (frameDelay > 0) delay(frameDelay)
// 在屏幕上顯示FPS
drawFPS(canvas, startMillis, System.currentTimeMillis())
holder.unlockCanvasAndPost(canvas)
}
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
isLooping = false
}
})
}
/**
* 顯示FPS
*/
private fun drawFPS(canvas: Canvas, startMillis: Long, endMillis: Long) {
paint.let {
it.color = Color.WHITE
it.style = Paint.Style.FILL
it.textSize = dp2px(14f)
}
val fps = 1000 / (endMillis - startMillis)
canvas.drawText("FPS:$fps", 10f, dp2px(20f), paint)
}
private fun dp2px(dp: Float): Float {
return dp * resources.displayMetrics.density + 0.5f
}
private fun drawFrame(canvas: Canvas, surfaceWidth: Int, surfaceHeight: Int) {
paint.color = Color.BLUE
canvas.drawLine(0f, surfaceHeight / 2f, surfaceWidth - 1f, surfaceHeight / 2f, paint)
canvas.drawLine(surfaceWidth / 2f, 0f, surfaceWidth / 2f, surfaceHeight - 1f, paint)
drawPlayer(canvas, surfaceWidth, surfaceHeight, degrees++)
if (degrees > 360f) degrees = 0f
}
private fun drawPlayer(
canvas: Canvas,
surfaceWidth: Int,
surfaceHeight: Int,
degrees: Float
) {
val bmp = if (BmpCache.get("player") == null) {
val size = if (surfaceWidth > surfaceHeight) surfaceHeight / 20 else surfaceWidth / 20
buildPlayerBitmap(size, size)
} else {
BmpCache.get("player")
}
// 將玩家的Bitmap繪制到Surface的中心孟岛,坐標(biāo)需要根據(jù)Bitmap的寬度作出偏移
val centerX = (surfaceWidth - bmp.width) / 2f
val centerY = (surfaceHeight - bmp.height) / 2f
// withRotation用于對(duì)圖形進(jìn)行旋轉(zhuǎn),pivotX和pivotY指定的是旋轉(zhuǎn)的軸坐標(biāo)
canvas.withRotation(degrees, surfaceWidth/2f, surfaceHeight/2f) {
canvas.drawBitmap(bmp, centerX, centerY, null)
// 為了便于觀察督勺,在Bitmap外面套了一層矩形
paint.style = Paint.Style.STROKE
paint.color = Color.parseColor("#99FFFF00")
canvas.drawRect(centerX, centerY, centerX + bmp.width, centerY + bmp.height, paint)
}
}
private fun buildPlayerBitmap(width: Int, height: Int): Bitmap {
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
// 手繪玩家的圖像
Canvas(bmp).apply {
val paint = Paint()
paint.isAntiAlias = true // 反鋸齒
paint.style = Paint.Style.FILL // 實(shí)心填充
// 設(shè)置漸變著色
paint.shader = RadialGradient(
width / 2f, 0f, width.toFloat(),
intArrayOf(Color.WHITE, Color.DKGRAY), null,
Shader.TileMode.CLAMP
)
// 定義多邊形的路徑
val path = Path()
path.moveTo(width / 2f, 0f)
path.lineTo(width.toFloat(), height - (height / 3f))
path.lineTo(width / 2f, height.toFloat())
path.lineTo(0f, height - (height / 3f))
path.close()
this.drawPath(path, paint) // 繪制多邊形渠羞,樣式為實(shí)心、漸變
// 再次設(shè)置筆刷樣式為空心智哀,邊線為1像素次询,取消之前的著色器
paint.style = Paint.Style.STROKE
paint.strokeWidth = 1f
paint.shader = null
paint.color = Color.WHITE
paint.strokeJoin = Paint.Join.ROUND
this.drawPath(path, paint) // 繪制多邊形的邊框
this.drawLine(width / 2f, 0f, width / 2f, height.toFloat(), paint)
}
BmpCache.put("player", bmp)
return bmp
}
}
如果對(duì)我的文章感興趣的話可以搜索和關(guān)注微公眾號(hào)或Q群聊:口袋里的安卓