前语
在我之前的文章 羡慕大劳星空顶?不如跟我一起运用 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)中注册一下的:
<service
android: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-data
android: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.current
Column(
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_WALLPAPER
intent.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
中止制作。
同时为了能够更好的中止制作代码,咱们这儿用了协程,其实这儿有点剩余,由于咱们的制作内容都是在服务中,不会存在堵塞的状况。
continueDraw
和 stopDraw
定义如下:
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
类即咱们的制作代码,这儿它只公开了两个办法:startDraw
和 stopDraw
。
其实一开端我只对外露出了 startDraw
办法,没有露出中止办法,可是我在测试时发现,仅靠 coroutineScope.coroutineContext.cancelChildren()
并不能及时的取消掉协程。
这会导致或许制作目标现已被毁掉了,可是由于我的协程不是立即被取消的,依旧会调用已被毁掉的制作目标,这就会导致闪退。
所以我额定加了一个中止办法,而且在内部自己维护一个中止标志 isRunning
避免上述状况的出现。
制作完结类 DrawStarrySky
在开端之前,先介绍一下怎样从 SurfaceHolder 中拿到 Canvas 用于制作。
在上面的代码中咱们能够看到,咱们的开端制作办法 drawStarrySky.startDraw(surfaceHolder)
接收了一个参数,便是 SurfaceHolder。
那么怎样从 SurfaceHolder 中拿到 Canvas ,而且当咱们制作完结后怎样将这个 Canvas 写回呢?
其实很简单,依旧是一个模版代码:
var canvas: Canvas? = null
try {
// 锁定并回来当时 Surface 中的 Canvas
canvas = 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? = null
try {
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 = 0
var canvasHeight = 0
// 这儿仅仅是为了拿到画布大小,其实有点剩余了,拿画布大小的办法很多,没必要这样拿。不过这儿偷了个懒
getCanvas(holder) { canvas ->
canvasWidth = canvas.width
canvasHeight = canvas.height
}
// 布景缓存
val bitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888)
// 制作静态布景
drawFixedContent(Canvas(bitmap), random)
while (isRunning) {
// 制作动态流星
val safeDistanceStandard = canvasWidth / 10
val safeDistanceVertical = canvasHeight / 10
val startX = random.nextInt(safeDistanceStandard, canvasWidth - safeDistanceStandard)
val startY = random.nextInt(safeDistanceVertical, canvasHeight - safeDistanceVertical)
for (time in 0..meteorTime) {
if (!isRunning) break
getCanvas(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 等,感兴趣的能够去搜一搜。
下一步
尽管现在咱们现已完结了咱们的需求,行将星空布景做成动态壁纸,
可是从代码中也能够看到,咱们一切的参数都是写死的。
这明显不符合常理。
所以咱们下一步目标是将这些参数抽出,作为用户可装备的装备项。
参阅资料
- Android壁纸仍是B站玩得花
- Building an Android Live Wallpaper
本文正在参与「金石计划 . 分割6万现金大奖」