5分钟实现小姐姐喜欢的,抖音文字时钟--下

码个蛋(codeegg)第 724 次推文

作者:二娃_

原文: https://juejin.im/post/5d52aea86fb9a06ae61aad5b

前言

源码地址

设置动态壁纸-TextClockWallpaperService

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


填坑啦!填坑啦!

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

目录

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

  1. 如何快速上手设置壁纸?

  2. 相关API说明

  3. 文字时钟动态壁纸实践


1. 如何快速上手设置壁纸?

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

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


2. 相关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();

/**
 * 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);

3. 文字时钟动态壁纸实践

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

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

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

近期文章:

日问题:

快拿上去征服产品小姐姐吧?

专属升级社区:《这件事情,我终于想明白了》 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值