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

news2024/11/28 8:41:50

前言

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

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

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

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

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

实现效果如下:

s1.gif

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

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

s2.jpg

绘制图案时机

在上面的 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? = 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 方法用于绘制流星,具体绘制代码我们也不贴了,各位可以看我上篇文章的解析,也可以直接看源码。

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

最终实现效果如下:

s1.gif

总结

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

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

下一步

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

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

这显然不符合常理。

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

参考资料

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/550899.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

30多家投递石沉大海,总算上岸了

大家好&#xff0c;我是帅地。 今年的行情&#xff0c;无论是暑假实习还是春招校招&#xff0c;都比往年要难一些&#xff0c;很多人在三月份要嘛简历石沉大海&#xff0c;要嘛面试一轮游&#xff0c;但也有部分人最后都拿到了不错的 Offer&#xff0c;包括我 训练营 里&#…

企业级信息系统开发——初探Spring-采用Spring配置文件管理Bean

初探Spring 一、Spring框架&#xff08;一&#xff09;Spring框架优点&#xff08;二&#xff09;Spring 框架因何而来&#xff08;三&#xff09;Spring框架核心概念 二、采用Spring配置文件管理Bean&#xff08;一&#xff09;创建Maven项目&#xff08;二&#xff09;添加Sp…

在C++中,怎么把string转换成char*?

2023年5月21日&#xff0c;周日中午&#xff1a; 今天在写项目的时候遇到了这个问题&#xff0c;也解决了&#xff0c;所以记录一下 通过string类的copy成员函数就可以解决这个问题 copy函数的函数原型&#xff1a; string& copy(char* s, size_t n, size_t pos 0); 其…

【框架源码】SpringBoot核心源码解读之启动类源码分析

首先我们要先带着我们的疑问&#xff0c;spring boot是如何启动应用程序&#xff1f;去分析SpringBoot的启动源码。 我们在新建SpringBoot项目时&#xff0c;核心方法就是主类的run方法。 SpringApplication.run(ArchWebApplication.class, args) 我们点击run方法进入到源码中…

A survey of Large Lanuage models

一.引言 语言建模的四个阶段&#xff0c;统计语言模型&#xff08;SLM&#xff09;&#xff1a;基于马尔科夫假设建立词预测模型&#xff0c;n-gram&#xff0c;神经语言模型&#xff08;NLM&#xff09;&#xff1a;word2vec&#xff0c;预训练语言模型&#xff08;PLM&#…

Godot引擎 4.0 文档 - 入门介绍 - 学习新功能

本文为Google Translate英译中结果&#xff0c;DrGraph在此基础上加了一些校正。英文原版页面&#xff1a; Learning new features — Godot Engine (stable) documentation in English 学习新功能 Godot 是一个功能丰富的游戏引擎。有很多关于它的知识。本页介绍了如何使用…

English Learning - L3 作业打卡 Lesson2 Day11 2023.5.15 周一

English Learning - L3 作业打卡 Lesson2 Day11 2023.5.15 周一 引言&#x1f349;句1: Sometimes a person may be upset because he does not have something as nice as a friend has, like a fast new car.成分划分弱读连读爆破语调 &#x1f349;句2: That person may say…

【wifi-app 任意泄露】

一、fofa 搜索 title“Wi-Fi APP Login” # Date: 2022-06-12 # Exploit Author: Ahmed Alroky # Author Company : AIactive # Version: M30HG4.V5030.191116 # Vendor home page : wavlink.com # Authentication Required: No # CVE : CVE-2022-34047 # Tested on: Windows…

day2 I/O多路复用select函数

目录 思考一个问题&#xff1a; I/O多路复用select函数 代码实现 net.h server.c: socket.c 思考一个问题&#xff1a; 我们还是把视角放到应用B从TCP缓冲区中读取数据这个环节来。如果在并发的环境下&#xff0c;可能会N个人向应用B发送消息&#xff0c;这种情况下我们的…

java+springboot留学生新闻资讯网的设计与实现

Spring框架是Java平台的一个开放源代码的Full-stack(全栈)应用程序框架&#xff0c;和控制翻转容器的实现。Spring框架的一些核心功能理论&#xff0c;可以用于所有Java应用&#xff0c;Spring还为Java EE构建的Web应用提供大量的扩展支持。Spring框架没有实现任何的编程模型&a…

nodejs进阶(5)—接收请求参数

1. get请求参数接收 我们简单举一个需要接收参数的例子 如果有个查找功能&#xff0c;查找关键词需要从url里接收&#xff0c;http://localhost:8000/search?keyword地球。通过前面的进阶3教程《nodejs进阶(3)—路由处理》重介绍的url模块&#xff0c;我们知道接收方法如下这…

cpp11实现线程池(六)——线程池任务返回值类型Result实现

介绍 提交任务函数submitTask中返回的Result类型应该是用Result类包装当前的task&#xff0c;因为出函数之后task即如下形式&#xff1a;return Result(task); Result和Task都要互相持有对方的指针&#xff0c;Task要将任务执行结果通过Result::setVal(run()) 调用传给其对应…

RestCloud新一代(智能)全域数据集成平台发布

5月18日&#xff0c;RestCloud在其成立六周年的当天&#xff0c;发布了“新一代&#xff08;智能&#xff09;全域数据集成平台”。 5月18日&#xff0c;RestCloud在其成立六周年的当天&#xff0c;发布了“新一代&#xff08;智能&#xff09;全域数据集成平台”。 根据业内专…

【Linux环境基础开发工具】软件包管理器-yum

写在前面 今天我打算介绍如何在Linux环境下载软件&#xff0c; Linux作为一个操作系统&#xff0c;就像windows一样&#xff0c;当然是存在软件的。 目录 写在前面 怎么在Linux环境安装软件 源代码安装 rpm安装包安装 yum安装 如何理解Linux的生态 如何使用yum安装软…

【LLM大模型】模型和指令微调方法

note Hugging Face 的 PEFT是一个库&#xff08;LoRA 是其支持的技术之一&#xff0c;除此之外还有Prefix Tuning、P-Tuning、Prompt Tuning&#xff09;&#xff0c;可以让你使用各种基于 Transformer 结构的语言模型进行高效微调。AIpaca羊驼&#xff1a;让 OpenAI 的 text-…

今年测试工程师正遭【革命】,“点点工”如何破局?

近几年来的特殊情况&#xff0c;综合过去的大形势变化&#xff0c;所有行业都会自下而上的进行一轮技术“大清洗”&#xff0c;技术停滞不前的“点工”或将被逐步取代。 软件测试现状 测试行业在十几年间发生了翻天覆地的变化&#xff0c;从早期站在风口上的快速发展&#xff…

fastapi基础篇

文章目录 简介环境搭建安装基础文件自动文档 基础使用POST请求传递参数返回定制信息jinja2返回html 简介 FastAPI 是一个用于构建 API 的现代、快速&#xff08;高性能&#xff09;的 web 框架&#xff0c;使用 Python 3.6 并基于标准的 Python 类型提示。 关键特性 快速&#…

【学习笔记】TCP/IP协议详解

1.A、B、C类网络号各有多少个&#xff1f; A类网络号&#xff1a;共有2^7 - 2个&#xff0c;即126个。这是因为A类网络号的第一个字节范围是1.0.0.0到126.0.0.0&#xff0c;其中0.0.0.0和127.0.0.0是特殊保留地址&#xff0c;不能用于网络划分。 B类网络号&#xff1a;共有2^…

Redis的主从复制,哨兵及群集

一、主从复制 1、主从复制-哨兵-集群 主从复制&#xff1a;主从复制是高可用Redis的基础&#xff0c;哨兵和集群都是在主从复制基础上实现高可用的。主从复制主要实现了数据的多机备份&#xff0c;以及对于读操作的负载均衡和简单的故障恢复。缺陷&#xff1a;故障恢复无法自…

免费通过微软Edge浏览器使用ChatGPT的手把手详细教程

ChatGPT是OpenAI推出的人工智能语言模型&#xff0c;能够通过理解和学习人类的语言来进行对话&#xff0c;像人类一样进行聊天交流&#xff0c;甚至还可以生成图片、编写代码。下面介绍如何通过在微软Edge浏览器的扩展程序中使用ChatGPT。 一、使用效果 ChatGPT的使用效果示例…