前言

在我之前的文章 羡慕大劳星空顶?不如跟我一起使用 Jetpack compose 绘制一个星空背景(带流星动画) 中,我们使用 Compose 实现了星空背景效果。

并且调用非常方便,只需要一行代码就可以给任意 Compose 组件添加上这个星空背景效果。

但是,只是给 Compose 添加背景效果总觉得有点"小题大作"了,这么好看的效果,不用来做壁纸实在是太可惜了。

于是,我尝试将其移植到动态壁纸中。但是,尝试了很久都没有找到怎么在动态壁纸中使用 Compose 。

最终,我还是使用安卓原生 Canvas 重新绘制了一个同样的动画效果。

实现效果如下:

好在 Compose 的绘制和安卓绘制其实区别也不是很大,所以重写起来也几乎没有动多少代码。

下面我们将讲解如何实现一个动态壁纸。

仓库地址:starrySkyWallpaper

动态壁纸实现

其实安卓在很早很早的版本就已经支持了动态壁纸,只是一直都没多少人使用罢了。

今天我们就来看看动态壁纸要怎么实现吧。

WallpaperService

安卓中的动态壁纸以服务(Server)的形式来完成计算和绘制,并且这个服务需要继承自 WallpaperService

一个简单的动态壁纸模版代码如下:

class StarrySkyWallpaperServer : WallpaperService() {override fun onCreateEngine(): Engine = WallpaperEngine()inner class WallpaperEngine : WallpaperService.Engine() {override fun onSurfaceCreated(holder: SurfaceHolder?) {super.onSurfaceCreated(holder)// 可以在这里写绘制代码}override fun onVisibilityChanged(visible: Boolean) {// 当壁纸的可见行性改变时会调用这里if (visible) {} else {}}override fun onDestroy() {super.onDestroy()}}
}

可以看到,在这个服务可供我们渲染的是 SurfaceHolder

而从 SurfaceHolder 中我们可以通过多种方式进行渲染,常用的三种方式为:

  • MediaPlayer
  • Camera
  • SurfaceView

第一个即媒体播放器,可以用来播放视频;第二个可以用来实时预览相机界面;第三个就是我们常用的 SurfaceView ,可以从中取出 Canvas 来自己绘制内容。

因为我们这里使用的是第三种方式:自定义绘制。所以前面两种这里就不再赘述,感兴趣的可以看看文末参考链接中的介绍。

在开始绘制之前,我们还有一点准备工作,因为这是一个服务,所以自然是需要在清单文件(manifest)中注册一下的:

<serviceandroid:name=".server.StarrySkyWallpaperServer"android:exported="true"android:label="Wallpaper"android:permission="android.permission.BIND_WALLPAPER"><intent-filter><action android:name="android.service.wallpaper.WallpaperService" /></intent-filter><meta-dataandroid:name="android.service.wallpaper"android:resource="@xml/wallpaper" />
</service>

其中的 android:resource="@xml/wallpaper" wallpaper 文件,需要我们自己在 xml 文件夹新建一个:

<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"android:description="@string/app_name"android:thumbnail="@mipmap/ic_launcher" />

从字段名也很容易看出,这是我们动态壁纸的一些配置信息,比如上面就写了描述信息和缩略图。

设置壁纸

经过上面的步骤,我们的动态壁纸服务就注册完成了,我们在手机上的壁纸编辑界面中选择动态壁纸就能看到我们创建的这个动态壁纸了。

然而,事实上,正因为我们上面说的,虽然安卓你的动态壁纸很久以前就有了,但是用的人一直不多。

所以国内的定制系统基本上都把这个功能阉割或魔改了。比如我现在用的 MIUI ,虽然设置壁纸时还能选动态壁纸,但是却只会显示官方的动态壁纸,第三方的都被隐藏了。

不过不用担心,我们可以在我们自己的APP中"手动"调用并设置我们自己的壁纸:

val intent = Intent()
intent.action = WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER
intent.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,ComponentName(context.packageName, StarrySkyWallpaperServer::class.java.name)
)
context.startActivity(intent)

例如,这里我们的APP启动界面代码如下:

@Composable
fun MainScreen() {val context = LocalContext.currentColumn(Modifier.fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {Button(onClick = {onClickSetWallPaper(context)}) {Text(text = "设置")}}
}private fun onClickSetWallPaper(context: Context) {val intent = Intent()intent.action = WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPERintent.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,ComponentName(context.packageName, StarrySkyWallpaperServer::class.java.name))context.startActivity(intent)
}

代码很简单,就一个居中的设置按钮,点击后就会跳转到系统的壁纸设置界面,并会显示我们自定义壁纸的动态预览:

绘制图案时机

在上面的 WallpaperService 服务模版中,我们在注释中写了可以在 onSurfaceCreated 回调中写我们的绘制代码。

但是这里我们为了更好的控制绘制过程,就不在 onSurfaceCreated 写我们的绘制代码了,而是在 onVisibilityChanged 回调中写:

override fun onVisibilityChanged(visible: Boolean) {if (visible) {// 启动绘制continueDraw()} else {// 停止绘制stopDraw()}
}

当壁纸可见时调用 continueDraw 开始绘制;当壁纸不可见时调用 stopDraw 停止绘制。

同时为了能够更好的停止绘制代码,我们这里用了协程,其实这里有点多余,因为我们的绘制内容都是在服务中,不会存在阻塞的情况。

continueDrawstopDraw 定义如下:

private var coroutineScope = CoroutineScope(Dispatchers.IO)private var drawStarrySky = DrawStarrySky()private fun continueDraw() {coroutineScope.launch {drawStarrySky.startDraw(surfaceHolder)}
}private fun stopDraw() {drawStarrySky.stopDraw()coroutineScope.coroutineContext.cancelChildren()
}

上面的 DrawStarrySky 类即我们的绘制代码,这里它只公开了两个方法:startDrawstopDraw

其实一开始我只对外暴露了 startDraw 方法,没有暴露停止方法,但是我在测试时发现,仅靠 coroutineScope.coroutineContext.cancelChildren() 并不能及时的取消掉协程。

这会导致可能绘制对象已经被销毁了,但是由于我的协程不是立即被取消的,依旧会调用已被销毁的绘制对象,这就会导致闪退。

所以我额外加了一个停止方法,并且在内部自己维护一个停止标志 isRunning 避免上述情况的出现。

绘制实现类 DrawStarrySky

在开始之前,先介绍一下如何从 SurfaceHolder 中拿到 Canvas 用于绘制。

在上面的代码中我们可以看到,我们的开始绘制方法 drawStarrySky.startDraw(surfaceHolder) 接收了一个参数,就是 SurfaceHolder。

那么如何从 SurfaceHolder 中拿到 Canvas ,并且当我们绘制完成后如何将这个 Canvas 写回呢?

其实很简单,依旧是一个模版代码:

var canvas: Canvas? = nulltry {// 锁定并返回当前 Surface 中的 Canvascanvas = surfaceHolder.lockCanvas()if (canvas != null) {// 在这里对 Canvas 进行绘制}
} finally {if (canvas != null) {// 解锁 Canvas 并写回到 Surface 中holder.unlockCanvasAndPost(canvas)}
}

当然,我们的绘制代码有很多,总不能每次都写这一大堆模版代码吧?

所以,我们写了一个函数 getCanvas :

private fun getCanvas(holder: SurfaceHolder,drawContent: (canvas: Canvas) -> Unit
) {var canvas: Canvas? = nulltry {canvas = holder.lockCanvas()if (canvas != null) {drawContent(canvas)}} finally {if (canvas != null) {try {holder.unlockCanvasAndPost(canvas)} catch (tr: Throwable) {tr.printStackTrace()}}}
}

了解了怎么拿到 Canvas 以及怎么写回 Canvas ,下一步就是正式开始绘制:

suspend fun startDraw(holder: SurfaceHolder,randomSeed: Long = 1L
) {isRunning = true// 初始化参数val random = Random(randomSeed)val paint = Paint()var canvasWidth = 0var canvasHeight = 0// 这里仅仅是为了拿到画布大小,其实有点多余了,拿画布大小的方法很多,没必要这样拿。不过这里偷了个懒getCanvas(holder) { canvas ->canvasWidth = canvas.widthcanvasHeight = canvas.height}// 背景缓存val bitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888)// 绘制静态背景drawFixedContent(Canvas(bitmap), random)while (isRunning) {// 绘制动态流星val safeDistanceStandard = canvasWidth / 10val safeDistanceVertical = canvasHeight / 10val startX = random.nextInt(safeDistanceStandard, canvasWidth - safeDistanceStandard)val startY = random.nextInt(safeDistanceVertical, canvasHeight - safeDistanceVertical)for (time in 0..meteorTime) {if (!isRunning) breakgetCanvas(holder) { canvas ->// 清除画布canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)// 绘制背景paint.reset()canvas.drawBitmap(bitmap, 0f, 0f, paint)// 绘制流星drawMeteor(canvas,time.toFloat(),startX,startY,paint)}delay(1)}delay(meteorScaleTime)}
}

从上面的绘制代码可以看到,我们先调用 drawFixedContent 方法绘制了静态背景,这里的具体绘制代码就不贴了,因为和上次我们用 Compose 实现的几乎没有区别,有需要的可以看我的上篇文章或者直接看项目源码了解。

我们只需要知道这个方法最终绘制的是黑色背景和其中固定不变的星星即可。

但是,不知道你们有没有注意到,这里我并不是直接把内容绘制到从 Surface 中拿到的 Canvas 中,而是绘制到了一个 Bitmap 中。

这是因为我们从 Surface 中拿到的 Canvas 并不是空白的 Canvas 而是当前 Surface 显示内容的 Canvas。

换句话说,我们每次拿到的 Canvas 都是之前所有绘制叠加起来的 Canvas。

为了实现动画效果,我们会在每次绘制之前使用 canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) 将当前画布 “清空”。

而我们这里绘制的明明是固定不变的背景,却在每次被清空后都重新计算并绘制。

这显然不合理,我们需要循环绘制的明明只有流星相关内容就可以了。

所以,这里我们在循环之外将背景计算并绘制到 Bitmap 中缓存。

每次需要更新 Canvas 时都只需要将这个缓存 Bitmap 绘制上去就可以了。

了解了我们的固定背景之后,再往下看。

下面我们用了两层循环,一层 while 死循环,用于持续生成流星。

一层 for 循环,用于绘制一次流星的动画。

在 while 循环中我们初始化参数(主要是随机生成一个流星起点坐标)后,开启 for 循环开始绘制流星的每一帧。

for 循环的参数即为我们的模拟时间参数。

同样的,drawMeteor 方法用于绘制流星,具体绘制代码我们也不贴了,各位可以看我上篇文章的解析,也可以直接看源码。

自此,我们的所有代码就完成了。

最终实现效果如下:

总结

通过上面的代码可以看到,其实安卓的动态壁纸并没有想象中的那么困难,无非就是自定义绘制这一套,如果熟悉自定义绘制的话,写起来还是非常容易的。

不过我们这里只展示了使用 Canvas 的绘制,事实上,由 SurfaceHolder 我们可以有更多的"骚操作",例如调用第三方成熟的动画库直接刷新 Surface 等,感兴趣的可以去搜一搜。

下一步

虽然现在我们已经实现了我们的需求,即将星空背景做成动态壁纸,

但是从代码中也可以看到,我们所有的参数都是写死的。

这显然不符合常理。

所以我们下一步目标是将这些参数抽出,作为用户可配置的配置项。

参考资料

  1. Android壁纸还是B站玩得花
  2. Building an Android Live Wallpaper

安卓动画壁纸实战:制作一个星空动态壁纸(带随机流星动画)相关推荐

  1. 120帧手机动态壁纸_移动串串烧:用视频制作手机的动态壁纸

    手机的壁纸是用户美化手机最常见的一种方式,但我们常用的壁纸大都却是静态的图片.如果想让壁纸更酷,最好的方法就是使用动态壁纸.可惜现在网络中动态壁纸非常少,所以我们不妨发扬自己动手的精神,利用视频来制作 ...

  2. 小米手机的这个黑科技,让你免费制作精美的动态壁纸,果粉馋哭了

    手机动态壁纸作为手机的重要象征,不仅代表着一个人兴趣爱好,在某些层面还能反映出一个人的心理性格,不过如何制作出一个好看又独一无二的动态壁纸成为了不少人的难题. 一般人制作自己的动态壁纸都是从APP中直 ...

  3. dreamweaver php 动态教程,利用DreamweaverCS5制作一个含有动态标题的教程

    做一个网页就先要做一个标题,一个好标题会让网页让人印象深刻,有动态的标题会让网页更生动,下面我就介绍一下怎么制作一个含有动态的标题. 1.双击DreamweaverCS5软件快捷键,打开Dreamwe ...

  4. 【web前端特效源码】使用HTML5+CSS3制作一个会动的电脑桌面+昼夜变化动画效果~~适合初学者~超简单~ |前端开发

    b站视频演示效果: [web前端特效源码]使用HTML5+CSS3制作一个会动的电脑桌面+昼夜变化动画效果~~适合初学者~超简单~ |前端开发|IT软件 效果图: 完整代码: <!DOCTYPE ...

  5. 120帧手机动态壁纸_小英雄高清动态壁纸app下载-小英雄高清动态壁纸v2.6手机下载...

    小英雄高清动态壁纸,下面由小编给大家介绍一下这款软件,该软件是一款专业的手机壁纸大全,汇集了多种专业的壁纸样式,包含了风景.卡通.可爱.美女和热门的壁纸素材,包含了静态壁纸和动态壁纸,所有的壁纸素材像 ...

  6. Mac电脑动态壁纸怎么设置_mac设置动态壁纸

    Mac动态桌面,它可以仅设定一张高效图片文件「High Efficiency Image File Format(简称 HEIF)」后,随着时间的推移改变桌面显示的内容.那么后缀为 .heic动态桌面 ...

  7. 计算机动画制作简单动画视频教程,如何制作一个时钟转动动画视频?电脑制作动画的软件制作时钟转动的小视频的方法...

    今天小编要来介绍的是制作动画的软件,可以用于制作时钟转动的动画效果,之前小编就介绍过时钟动画制作的方法,但今天的方法更先进哦,这里的时钟的样式还有颜色都可以自定义的哦.制作动画的软件是什么?不是手机自 ...

  8. android 心形上漂动画,PowerPoint Viewer制作一个漂亮心形飞出动画的操作教程

    各位使用PowerPoint Viewer的同学们,你们知道怎么制作一个漂亮心形飞出动画吗?在这篇教程内小编就为各位呈现了PowerPoint Viewer制作一个漂亮心形飞出动画的操作教程. Pow ...

  9. 动态壁纸安卓_【安卓】能够虚化加缩放的动态壁纸

    这 是 简 介这其实就是自己做动态壁纸,可以实现壁纸虚化加缩放的功能.纯净无广告不联网,体验还算不错.整体界面非常干净,一进去就可以开始修改了.而且自定义动画曲线可以返回直接生效,不用点击保存.常见问 ...

最新文章

  1. jsp ajax动态添加数据,jquery Ajax实现Select动态添加数据
  2. 逆天神经网络绘制神器!还有暗黑模式
  3. 修改input file默认样式
  4. MINIGUI 开发指南---GDI
  5. c++求n次方_课时9一元二次方程及其应用
  6. vb 复制 剪贴板 html,VB把选中的内容复制到剪切板
  7. Atitit.ati dwr的原理and设计 attilax 总结 java php 版本
  8. DHI Mike 后处理工具——污染带面积、长度、宽度统计工具
  9. 艾宾浩斯记忆表格excel_艾宾浩斯打卡群第二期邀请函
  10. ROS2暑期学校 ROS2 Summer School 2022-转-
  11. html5 日历 仿ios,一款完整的蓝白风格HTML5日历应用程序
  12. 如何根据商品条码查询商品名称和价格信息?
  13. Telegram APIs中文介绍
  14. Python 将一个已知的 utc时间字符串 转换为东八区时间
  15. 某站弹幕抓取,视频,评论......
  16. Windows 10找回高性能模式和节能模式
  17. 如何做一个基于微信校园运动场地预约小程序系统毕业设计毕设作品
  18. 浏览器显示无法解析服务器的DNS地址,搜狗浏览器无法解析服务器的DNS地址怎么解决...
  19. 股票量化分析系统浅析之(三)归一化与标准化
  20. 标准机构发布物联网安全测试指南

热门文章

  1. FreeBSD+XP双系统
  2. 为什么我的cairo画出的直线不同角度宽度不同???
  3. 自大型人格分析,如何改变自大型性格?
  4. 最新的 iPad开发项目 - Fotoboard for iPad
  5. BWAI学习记录003_使用Chaoslauncher和AI(Stardust)人机对战
  6. 如何调用阿里云、百度云API接口
  7. 百度地图LBS应用开发代码
  8. 【5月比赛合集】80场可报名的数据挖掘大奖赛,任君挑选!
  9. ps教程:用PS和FLASH8做眨眼教材
  10. 微信小程序(1)新闻小应用代码以及总结小程序一些知识点