+-
这次要把抖音网红文字时钟设置为壁纸了~

作者:二娃_

链接:https://juejin.im/post/5d52aea86fb9a06ae61aad5b

还记得上篇吗?我们先实现了抖音网红文字时钟的自定义 View 实现:


  • 抖音上炫酷的网红文字时钟

  • 概述


    源码地址


    https://github.com/drawf/SourceSet/blob/master/app/src/main/java/me/erwa/sourceset/view/TextClockWallpaperService.kt

    起源

    填坑啦!填坑啦!


    其实关于「设置动态壁纸的实操」我是弃坑了的,因为我单方面觉得只是壁纸相关API的使用,价值不大。但不久前有「掘友」留言说及这事,并表示期待更新「下篇」,于是我又单方面觉得价值还是有的,能帮一个是一个。


    目录


    以下是我列的本篇目录,将按顺序依次做解说


  • 如何快速上手设置壁纸?

  • 相关API说明

  • 文字时钟动态壁纸实践


  • 如何快速上手设置壁纸?


    NOTE: 这里暂时不关心「为什么」,我们只按照既定的步骤,快速上手实现一个「Hong Kong is part of China!」静态壁纸


    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)
    }

    经过前面四步,我们成功的爱了一把国,效果如图:


    相关API说明
    壁纸描述文件


    首先,这个描述文件是必须的,在声明服务的时候必须在meta-data上配置上。知道为什么吗?


    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的实例。(关于原理部分解析,我在拜读的文章中贴出了链接,大家可自行食用)


    壁纸服务的声明


    其中必须要加的是:


    1. 权限:android:permission="android.permission.BIND_WALLPAPER"

    2. 处理Service的Action:<action android:name="android.service.wallpaper.WallpaperService"/>


    对!这两个也分别对应壁纸服务启动前的第一个检查第二个检查


    壁纸服务的实现


    1. 继承WallpaperService并复写抽象方法public abstract Engine onCreateEngine();


     
    
    /**
     * 必须实现并返回一个壁纸引擎的新实例。
     * 注意同一时间可能有多个实例在运行,比如当前壁纸正在运行时,用户在挑选壁纸页面浏览该壁纸的预览画面。
     */
    public abstract Engine onCreateEngine();

    /**
     * 壁纸的实际实现。一个壁纸服务可能有多个实例在运行(例如一个是真实的壁纸和一个处于预览的壁纸),
     * 每个壁纸都只能由其相应的引擎实例来做实现。
     * 你必须实现{@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


    /**
     * 提供对绘制壁纸时实际Surface(表面)的访问
     */
    public SurfaceHolder getSurfaceHolder() {
        return mSurfaceHolder;
    }

    /**
     * 开始在Surface上编辑像素。这个返回的画布可以用来在Surface的位图上绘制。如果Surface还没创建或者不能被编辑会返回null。
     * 一般情况下,你需要通过实现{@link Callback#surfaceCreated Callback.surfaceCreated}这个方法,
     * 来得知surface什么时候可用。
     *
     * Surface的内容在unlockCanvas()和lockCanvas()之间是不会保存的,因此,Surface区域必须写入每个像素。
     * 这个规则有个例外就是指定一个特殊的脏矩形区域,这种情况下,非脏区域的像素才会被保存。
     *
     * 如果你在Surface创建前或销毁后重复调用该方法,为了避免占用CPU,你的调用将被限制为慢速率。

     * 如果返回值不为null,该方法内部持有锁,直到相应的{@link #unlockCanvasAndPost}方法被调用,并会防止Surface
     * 在绘制时被创建、销毁或修改。这样比直接访问Surface更方便,因为你不需要在{@link Callback#surfaceDestroyed
     * Callback.surfaceDestroyed}和绘制线程中做特殊的同步。
      * 返回一个用来绘制到Surface上的画布
     */
    public Canvas lockCanvas();

    /**
     * 完成Surface上像素的编辑。该方法调用完,Surface上的像素将会展示到屏幕上,但是它的内容会丢失,
     * 尤其再次调用lockCanvas()时也不能保证它的内容不会变动。
     *
     * 参数需传入之前lockCanvas()返回的画布
     */
    public void unlockCanvasAndPost(Canvas canvas);
    文字时钟动态壁纸实践

    有了前面的铺垫,这部分就相对简单了。不过我们依旧先思考🤔下思路:


    1. 如何绘制文字时钟?「上篇」中我们已经有了「TextClockView」,我们直接把它当做一个封装好的对象,再扩展添加几个我们需要的方法即可使用。


    2. 如何让壁纸动起来?跟「上篇」中一样,开一个定时器,每秒钟调用一次TextClockView的doInvalidate()方法。


    扩展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 ([email protected] != null) {
                    [email protected]?.invoke()
                } else {
                    invalidate()
                }
            }
            mAnimator.start()
        }
    }


    3. 停止后续绘制的方法


    /**
     * 停止后续绘制,供动态壁纸使用
     */
    fun stopInvalidate() {
        mAnimator.removeAllUpdateListeners()
    }


    处理壁纸服务的具体绘制实现


    1. 相关初始化


     
    
    inner class MyEngine : Engine() {
        private val mClockView = TextClockView([email protected])
        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()
    }

    到这里就完成啦!撒花!撒花!(欢迎食用源码并实际体验~)


    文末


    个人能力有限,如有不正之处欢迎大家批评指出,我会虚心接受并第一时间修改,以不误导大家。


    拜读的文章


    第8章 深入理解Android壁纸(节选)

    https://www.cnblogs.com/cynchanpin/p/6927516.html


    源码地址


    https://github.com/drawf/SourceSet/blob/master/app/src/main/java/me/erwa/sourceset/view/TextClockWallpaperService.kt


    ●编号597,输入编号直达本文

    ●输入m获取到文章目录

    推荐↓↓↓

    Java编程

    更多推荐25个技术类公众微信

    涵盖:程序人生、算法与数据结构、黑客技术与网络安全、大数据技术、前端开发、Java、Python、Web开发、安卓开发、iOS开发、C/C++、.NET、Linux、数据库、运维等。