【自定義View】抖音網紅文字時鐘-下篇

源碼地址

設置動態壁紙-TextClockWallpaperServicehtml

起源

填坑啦!填坑啦!java

其實關於「設置動態壁紙的實操」我是棄坑了的,由於我單方面以爲只是壁紙相關API的使用,價值不大。但不久前有「掘友」留言說及這事,並表示期待更新「下篇」,因而我又單方面以爲價值仍是有的,能幫一個是一個。android

目錄

如下是我列的本篇目錄,將按順序依次作解說git

  1. 如何快速上手設置壁紙?
  2. 相關API說明
  3. 文字時鐘動態壁紙實踐

如何快速上手設置壁紙?

NOTE: 這裏暫時不關心「爲何」,咱們只按照既定的步驟,快速上手實現一個「Hong Kong is part of China!」靜態壁紙github

  1. res -> xml目錄下新建一個壁紙描述文件,名字可自取(text_colck_wallpaper.xml),內容很簡單
    <wallpaper xmlns:android="http://schemas.android.com/apk/res/android" />
    複製代碼
  2. 繼承WallpaperService在內部處理咱們本身的繪製。(能夠關注下onVisibilityChanged方法)
    class TextClockWallpaperService : WallpaperService() {
    
        override fun onCreateEngine(): Engine {
            return MyEngine()
        }
    
        inner class MyEngine : Engine() {
            /** * 準備畫筆 */
            private val mPaint = Paint().apply {
                this.color = Color.RED
                this.isAntiAlias = true
                this.textSize = 60f
                this.textAlign = Paint.Align.CENTER
            }
    
            /** * Called to inform you of the wallpaper becoming visible or * hidden. <em>It is very important that a wallpaper only use * CPU while it is visible.</em>. * * 當壁紙顯示或隱藏時會回調該方法。 * 很重要的一點是,要只在壁紙顯示的時候作繪製操做(佔用CPU)。 */
            override fun onVisibilityChanged(visible: Boolean) {
                super.onVisibilityChanged(visible)
                Log.d("clock", "onVisibilityChanged >>> $visible")
    
                //只在壁紙顯示的作繪製操做,這很重要!
                if (visible) {
                    surfaceHolder.lockCanvas()?.let { canvas ->
                        //將原點移動到畫布中心
                        canvas.save()
                        canvas.translate((canvas.width / 2).toFloat(), (canvas.height / 2).toFloat())
    
                        //繪製文字
                        canvas.drawText("Hong Kong is part of China!", 0f, 0f, mPaint)
    
                        canvas.restore()
                        surfaceHolder.unlockCanvasAndPost(canvas)
                    }
                }
            }
        }
    }
    複製代碼
  3. AndroidManifest.xml文件中增長壁紙服務的聲明
    <!--動態壁紙服務-->
    <service
        android:name=".view.TextClockWallpaperService"
        android:permission="android.permission.BIND_WALLPAPER">
        <intent-filter>
            <action android:name="android.service.wallpaper.WallpaperService" />
        </intent-filter>
        <meta-data
            android:name="android.service.wallpaper"
            android:resource="@xml/text_clock_wallpaper" />
    </service>
    複製代碼
  4. 增長啓動壁紙設置服務的方法
    btnSet.setOnClickListener {
        val intent = Intent().apply {
            action = WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER
            putExtra(
                WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,
                ComponentName(
                    context,
                    TextClockWallpaperService::class.java
                )
            )
        }
        startActivity(intent)
    }
    複製代碼

通過前面四步,咱們成功的愛了一把國,效果如圖:canvas

相關API說明

壁紙描述文件

首先,這個描述文件是必須的,在聲明服務的時候必須在meta-data上配置上。知道爲何嗎?bash

  1. 這個描述文件中有三個可選屬性description(對壁紙服務的描述) settingsActivity(對此壁紙進行參數設置的Activity) thumbnail(壁紙服務縮略圖)
    //示例代碼
    <?xml version="1.0" encoding="utf-8"?>
    <wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
        android:description="@string/description"
        android:settingsActivity="me.erwa.xxx.SettingsActivity"
        android:thumbnail="@mipmap/ic_launcher">
    </wallpaper>
    複製代碼
  2. 經過源碼可知,在壁紙服務啓動以前會進行三個檢查,其中第三個檢查就是對meta-data中提供關於壁紙的描寫敘述信息的檢查用以建立一個叫WallpaperInfo的實例。(關於原理部分解析,我在拜讀的文章中貼出了連接,你們可自行食用)

壁紙服務的聲明

其中必需要加的是:app

  1. 權限:android:permission="android.permission.BIND_WALLPAPER"
  2. 處理Service的Action:<action android:name="android.service.wallpaper.WallpaperService"/>

對!這兩個也分別對應壁紙服務啓動前的第一個檢查第二個檢查ide

壁紙服務的實現

  1. 繼承WallpaperService並複寫抽象方法public abstract Engine onCreateEngine();
    /** * Must be implemented to return a new instance of the wallpaper's engine. * Note that multiple instances may be active at the same time, such as * when the wallpaper is currently set as the active wallpaper and the user * is in the wallpaper picker viewing a preview of it as well. * * 必須實現並返回一個壁紙引擎的新實例。 * 注意同一時間可能有多個實例在運行,好比當前壁紙正在運行時,用戶在挑選壁紙頁面瀏覽該壁紙的預覽畫面。 */
    public abstract Engine onCreateEngine();
    
    /** * The actual implementation of a wallpaper. A wallpaper service may * have multiple instances running (for example as a real wallpaper * and as a preview), each of which is represented by its own Engine * instance. You must implement {@link WallpaperService#onCreateEngine()} * to return your concrete Engine implementation. * * 壁紙的實際實現。一個壁紙服務可能有多個實例在運行(例如一個是真實的壁紙和一個處於預覽的壁紙), * 每一個壁紙都只能由其相應的引擎實例來作實現。 * 你必須實現{@link WallpaperService#onCreateEngine()}並返回你建立的引擎的實例。 */
    public class Engine {
        ...省略代碼
    }
    複製代碼
  2. Engine的關鍵生命週期,它們是從上到下依次執行的。咱們重點關注onVisibilityChanged onSurfaceDestroyed便可。
    inner class MyEngine : Engine() {
    
        override fun onCreate(surfaceHolder: SurfaceHolder?) {
            super.onCreate(surfaceHolder)
            Log.d("clock", "onCreate")
        }
    
        override fun onSurfaceCreated(holder: SurfaceHolder?) {
            super.onSurfaceCreated(holder)
            Log.d("clock", "onSurfaceCreated")
        }
    
        override fun onSurfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
            super.onSurfaceChanged(holder, format, width, height)
            Log.d("clock", "onSurfaceChanged")
        }
    
        /** * Called to inform you of the wallpaper becoming visible or * hidden. <em>It is very important that a wallpaper only use * CPU while it is visible.</em>. * * 當壁紙顯示或隱藏是會回調該方法。 * 很重要的一點是,要只在壁紙顯示的時候作繪製操做(佔用CPU)。 */
        override fun onVisibilityChanged(visible: Boolean) {
            super.onVisibilityChanged(visible)
            Log.d("clock", "onVisibilityChanged >>> $visible")
        }
    
        override fun onSurfaceDestroyed(holder: SurfaceHolder?) {
            super.onSurfaceDestroyed(holder)
            Log.d("clock", "onSurfaceDestroyed")
        }
    
        override fun onDestroy() {
            super.onDestroy()
            Log.d("clock", "onDestroy")
        }
        
    }
    複製代碼
  3. 繪製相關的API
    /** * Provides access to the surface in which this wallpaper is drawn. * 提供對繪製壁紙時實際Surface(表面)的訪問 */
    public SurfaceHolder getSurfaceHolder() {
        return mSurfaceHolder;
    }
    
    /** * Start editing the pixels in the surface. The returned Canvas can be used * to draw into the surface's bitmap. A null is returned if the surface has * not been created or otherwise cannot be edited. You will usually need * to implement {@link Callback#surfaceCreated Callback.surfaceCreated} * to find out when the Surface is available for use. * * 開始在Surface上編輯像素。這個返回的畫布能夠用來在Surface的位圖上繪製。若是Surface還沒建立或者不能被編輯會返回null。 * 通常狀況下,你須要經過實現{@link Callback#surfaceCreated Callback.surfaceCreated}這個方法, * 來得知surface何時可用。 * * <p>The content of the Surface is never preserved between unlockCanvas() and * lockCanvas(), for this reason, every pixel within the Surface area * must be written. The only exception to this rule is when a dirty * rectangle is specified, in which case, non-dirty pixels will be * preserved. * * Surface的內容在unlockCanvas()和lockCanvas()之間是不會保存的,所以,Surface區域必須寫入每一個像素。 * 這個規則有個例外就是指定一個特殊的髒矩形區域,這種狀況下,非髒區域的像素纔會被保存。 * * <p>If you call this repeatedly when the Surface is not ready (before * {@link Callback#surfaceCreated Callback.surfaceCreated} or after * {@link Callback#surfaceDestroyed Callback.surfaceDestroyed}), your calls * will be throttled to a slow rate in order to avoid consuming CPU. * * 若是你在Surface建立前或銷燬後重復調用該方法,爲了不佔用CPU,你的調用將被限制爲慢速率。 * * <p>If null is not returned, this function internally holds a lock until * the corresponding {@link #unlockCanvasAndPost} call, preventing * {@link SurfaceView} from creating, destroying, or modifying the surface * while it is being drawn. This can be more convenient than accessing * the Surface directly, as you do not need to do special synchronization * with a drawing thread in {@link Callback#surfaceDestroyed * Callback.surfaceDestroyed}. * * 若是返回值不爲null,該方法內部持有鎖,直到相應的{@link #unlockCanvasAndPost}方法被調用,並會防止Surface * 在繪製時被建立、銷燬或修改。這樣比直接訪問Surface更方便,由於你不須要在{@link Callback#surfaceDestroyed * Callback.surfaceDestroyed}和繪製線程中作特殊的同步。 * * @return Canvas Use to draw into the surface. * 返回一個用來繪製到Surface上的畫布 */
    public Canvas lockCanvas();
    
    /** * Finish editing pixels in the surface. After this call, the surface's * current pixels will be shown on the screen, but its content is lost, * in particular there is no guarantee that the content of the Surface * will remain unchanged when lockCanvas() is called again. * * 完成Surface上像素的編輯。該方法調用完,Surface上的像素將會展現到屏幕上,可是它的內容會丟失, * 尤爲再次調用lockCanvas()時也不能保證它的內容不會變更。 * * @see #lockCanvas() * * @param canvas The Canvas previously returned by lockCanvas(). * 參數需傳入以前lockCanvas()返回的畫布 */
    public void unlockCanvasAndPost(Canvas canvas);
    複製代碼

文字時鐘動態壁紙實踐

有了前面的鋪墊,這部分就相對簡單了。不過咱們依舊先思考🤔下思路:佈局

  1. 如何繪製文字時鐘?「上篇」中咱們已經有了「TextClockView」,咱們直接把它當作一個封裝好的對象,再擴展添加幾個咱們須要的方法便可使用。
  2. 如何讓壁紙動起來?跟「上篇」中同樣,開一個定時器,每秒鐘調用一次TextClockViewdoInvalidate()方法。

擴展TextClockView

  1. 首先要能在外部初始化繪製時依賴的寬高
    /** * 初始化寬高,供動態壁紙使用 */
    fun initWidthHeight(width: Float, height: Float) {
        if (this.mWidth < 0) {
            this.mWidth = width
            this.mHeight = height
    
            mHourR = mWidth * 0.143f
            mMinuteR = mWidth * 0.35f
            mSecondR = mWidth * 0.35f
        }
    }
    複製代碼
  2. 繪製方法中增長回調,用於將實際繪製調用放到壁紙服務中
    /** * 開始繪製 */
    fun doInvalidate(block: (() -> Unit)? = null) {
        this.mBlock = block
    
        Calendar.getInstance().run {
            ...省略代碼
            mAnimator.addUpdateListener {
                ...省略代碼
                if (this@TextClockView.mBlock != null) {
                    this@TextClockView.mBlock?.invoke()
                } else {
                    invalidate()
                }
            }
            mAnimator.start()
        }
    }
    複製代碼
  3. 中止後續繪製的方法
    /** * 中止後續繪製,供動態壁紙使用 */
    fun stopInvalidate() {
        mAnimator.removeAllUpdateListeners()
    }
    複製代碼

處理壁紙服務的具體繪製實現

  1. 相關初始化
    inner class MyEngine : Engine() {
        private val mClockView = TextClockView(this@TextClockWallpaperService.baseContext)
        private val mHandler = Handler()
        private var mTimer: Timer? = null
        ...省略代碼
    }
    複製代碼
  2. 核心繪製操做
    override fun onVisibilityChanged(visible: Boolean) {
        super.onVisibilityChanged(visible)
        Log.d("clock", "onVisibilityChanged >>> $visible")
        if (visible) {
            startClock()
        } else {
            stopClock()
        }
    }
    
    /** * 開始繪製 */
    private fun startClock() {
        if (mTimer != null) return
    
        mTimer = timer(period = 1000) {
            mHandler.post {
                mClockView.doInvalidate {
                    if (mTimer != null && surfaceHolder != null) {
                        surfaceHolder.lockCanvas()?.let { canvas ->
                            mClockView.initWidthHeight(canvas.width.toFloat(), canvas.height.toFloat())
                            mClockView.draw(canvas)
                            surfaceHolder.unlockCanvasAndPost(canvas)
                        }
                    }
                }
            }
        }
    }
    
    /** * 中止繪製 */
    private fun stopClock() {
        mTimer?.cancel()
        mTimer = null
        mClockView.stopInvalidate()
    }
    複製代碼

到這裏就完成啦!撒花!撒花!(歡迎食用源碼並實際體驗~)

文末

我的能力有限,若有不正之處歡迎你們批評指出,我會虛心接受並第一時間修改,以不誤導你們

拜讀的文章

個人其它文章

相關文章
相關標籤/搜索