Android游戲教程:Bitmap惊窖、Canvas、Paint的那些事

在上一篇教程介紹了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>

繪制并緩存的流程如下:

  1. 創(chuàng)建一個(gè)Bitmap對(duì)象缘挽。
  2. 創(chuàng)建一個(gè)Canvas對(duì)象瞄崇,并把Bitmap對(duì)象作為參數(shù)傳給Canvas的構(gòu)造方法。
  3. 利用Canvas對(duì)象在Bitmap上畫出玩家飛船的樣子壕曼。
  4. 對(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ì)象畫到屏幕指定的位置上的流程:

  1. 從緩存中取出玩家的Bitmap瞳抓,如果沒有則繪制并緩存。
  2. 用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群聊:口袋里的安卓

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市瓷叫,隨后出現(xiàn)的幾起案子屯吊,更是在濱河造成了極大的恐慌送巡,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盒卸,死亡現(xiàn)場(chǎng)離奇詭異骗爆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蔽介,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門摘投,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人虹蓄,你說我怎么就攤上這事犀呼。” “怎么了薇组?”我有些...
    開封第一講書人閱讀 157,435評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵圆凰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我体箕,道長(zhǎng)专钉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,509評(píng)論 1 284
  • 正文 為了忘掉前任累铅,我火速辦了婚禮跃须,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘娃兽。我一直安慰自己菇民,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評(píng)論 6 386
  • 文/花漫 我一把揭開白布投储。 她就那樣靜靜地躺著第练,像睡著了一般。 火紅的嫁衣襯著肌膚如雪玛荞。 梳的紋絲不亂的頭發(fā)上娇掏,一...
    開封第一講書人閱讀 49,837評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音勋眯,去河邊找鬼婴梧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛客蹋,可吹牛的內(nèi)容都是我干的塞蹭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,987評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼讶坯,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼番电!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辆琅,我...
    開封第一講書人閱讀 37,730評(píng)論 0 267
  • 序言:老撾萬榮一對(duì)情侶失蹤漱办,失蹤者是張志新(化名)和其女友劉穎担汤,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體洼冻,經(jīng)...
    沈念sama閱讀 44,194評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡崭歧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了撞牢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片率碾。...
    茶點(diǎn)故事閱讀 38,664評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖屋彪,靈堂內(nèi)的尸體忽然破棺而出所宰,到底是詐尸還是另有隱情,我是刑警寧澤畜挥,帶...
    沈念sama閱讀 34,334評(píng)論 4 330
  • 正文 年R本政府宣布仔粥,位于F島的核電站,受9級(jí)特大地震影響蟹但,放射性物質(zhì)發(fā)生泄漏躯泰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評(píng)論 3 313
  • 文/蒙蒙 一华糖、第九天 我趴在偏房一處隱蔽的房頂上張望麦向。 院中可真熱鬧,春花似錦客叉、人聲如沸诵竭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)卵慰。三九已至,卻和暖如春佛呻,著一層夾襖步出監(jiān)牢的瞬間裳朋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工件相, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留再扭,地道東北人氧苍。 一個(gè)月前我還...
    沈念sama閱讀 46,389評(píng)論 2 360
  • 正文 我出身青樓夜矗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親让虐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子紊撕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容