Foreword
In my previous article Are you envious of Da Lao Starry Sky Top? Why not use Jetpack compose to draw a star background (with meteor animation) with me, we use Compose to achieve the star background effect.
And it is very convenient to call, only one line of code is needed to add this starry sky background effect to any Compose component.
However, just adding background effects to Compose always feels a bit “overkill”. It’s a pity that such a beautiful effect is not used as a wallpaper.
So, I tried to transplant it into a live wallpaper. However, after trying for a long time, I couldn’t find how to use Compose in the live wallpaper.
In the end, I redraw the same animation effect using Android native Canvas.
The effect is as follows:
Fortunately, the difference between Compose’s drawing and Android’s drawing is not very big, so there is almost no code change when rewriting.
Below we will explain how to implement a live wallpaper.
Warehouse address: starrySkyWallpaper
Live wallpaper implementation
In fact, Android has supported live wallpapers in very early versions, but not many people have used it.
Today we will take a look at how to achieve live wallpaper.
WallpaperService
The live wallpaper in Android is calculated and drawn in the form of a service (Server), and this service needs to be inherited from WallpaperService
.
A simple live wallpaper template code is as follows:
class StarrySkyWallpaperServer : WallpaperService() {<!-- --> override fun onCreateEngine(): Engine = WallpaperEngine() inner class WallpaperEngine : WallpaperService.Engine() {<!-- --> override fun onSurfaceCreated(holder: SurfaceHolder?) {<!-- --> super.onSurfaceCreated(holder) // You can write drawing code here } override fun onVisibilityChanged(visible: Boolean) {<!-- --> // This is called when the visibility of the wallpaper changes if (visible) {<!-- --> } else {<!-- --> } } override fun onDestroy() {<!-- --> super. onDestroy() } } }
As you can see, what we can render in this service is SurfaceHolder
.
From SurfaceHolder
, we can render in many ways, the three commonly used ways are:
- MediaPlayer
- camera
- SurfaceView
The first is the media player, which can be used to play videos; the second can be used to preview the camera interface in real time; the third is our commonly used SurfaceView, from which Canvas can be taken out to draw content by ourselves.
Because we are using the third way here: custom drawing. So the first two will not be repeated here. If you are interested, you can take a look at the introduction in the reference link at the end of the article.
Before we start drawing, we still have a little preparation work, because this is a service, so naturally we need to register it in the manifest file:
<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>
The android:resource="@xml/wallpaper"
wallpaper file requires us to create a new one in the xml folder:
<?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" />
It is also easy to see from the field name that this is some configuration information of our live wallpaper, such as description information and thumbnails written above.
Set wallpaper
After the above steps, the registration of our live wallpaper service is completed. We can see the live wallpaper we created by selecting the live wallpaper in the wallpaper editing interface on the mobile phone.
However, in fact, because of what we said above, although Android Live Wallpaper has been available for a long time, not many people have used it.
Therefore, domestic customization systems have basically castrated or modified this function. For example, on the MIUI I am using now, although you can still choose live wallpapers when setting the wallpaper, it only displays the official live wallpapers, and the third-party ones are hidden.
But don’t worry, we can “manually” call and set our own wallpaper in our own 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)
For example, here our APP startup interface code is as follows:
@Composable fun MainScreen() {<!-- --> val context = LocalContext. current Column( Modifier. fillMaxSize(), verticalArrangement = Arrangement. Center, horizontalAlignment = Alignment. Center Horizontally ) {<!-- --> Button(onClick = {<!-- --> onClickSetWallPaper(context) }) {<!-- --> Text(text = "Settings") } } } 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) }
The code is very simple, just a centered setting button, after clicking it will jump to the system wallpaper setting interface, and will display the dynamic preview of our custom wallpaper:
When to draw a pattern
In the above WallpaperService
service template, we wrote in the comments that we can write our drawing code in the onSurfaceCreated
callback.
But here we don’t write our drawing code in onSurfaceCreated
in order to better control the drawing process, but write in onVisibilityChanged
callback:
override fun onVisibilityChanged(visible: Boolean) {<!-- --> if (visible) {<!-- --> // start drawing continueDraw() } else {<!-- --> // stop drawing stopDraw() } }
Call continueDraw
to start drawing when the wallpaper is visible; call stopDraw
to stop drawing when the wallpaper is invisible.
At the same time, in order to better stop the drawing code, we use a coroutine here. In fact, this is a bit redundant, because our drawing content is all in the service, and there will be no blocking.
continueDraw
and stopDraw
are defined as follows:
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() }
The above DrawStarrySky
class is our drawing code, here it only exposes two methods: startDraw
and stopDraw
.
In fact, I only exposed the startDraw
method to the outside world at the beginning, and did not expose the stop method, but I found out during the test that only relying on coroutineScope.coroutineContext.cancelChildren()
is not timely Cancel the coroutine.
This will lead to the fact that the drawing object may have been destroyed, but since my coroutine is not canceled immediately, the destroyed drawing object will still be called, which will cause a crash.
So I added an additional stop method, and internally maintained a stop flag isRunning
to avoid the above situation.
Drawing implementation class DrawStarrySky
Before we start, let’s introduce how to get Canvas from SurfaceHolder for drawing.
In the above code, we can see that our start drawing method drawStarrySky.startDraw(surfaceHolder)
receives a parameter, which is SurfaceHolder.
So how to get Canvas from SurfaceHolder, and how to write this Canvas back when we finish drawing?
In fact, it is very simple, it is still a template code:
var canvas: Canvas? = null try {<!-- --> // Lock and return the Canvas in the current Surface canvas = surfaceHolder. lockCanvas() if (canvas != null) {<!-- --> // draw the Canvas here } } finally {<!-- --> if (canvas != null) {<!-- --> // Unlock Canvas and write back to Surface holder. unlockCanvasAndPost(canvas) } }
Of course, we have a lot of drawing codes, so we can’t write a lot of template codes every time, right?
So, we write a function 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() } } } }
Knowing how to get Canvas and how to write back to Canvas, the next step is to officially start drawing:
suspend fun startDraw( holder: SurfaceHolder, randomSeed: Long = 1L ) {<!-- --> isRunning = true // Initialization parameters val random = Random(randomSeed) val paint = Paint() var canvasWidth = 0 var canvasHeight = 0 // This is just to get the size of the canvas, which is actually a bit redundant. There are many ways to get the size of the canvas, so there is no need to get it like this. But here is a lazy getCanvas(holder) {<!-- --> canvas -> canvasWidth = canvas. width canvasHeight = canvas.height } // background cache val bitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888) // draw static background drawFixedContent(Canvas(bitmap), random) while (isRunning) {<!-- --> // draw dynamic meteor 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 -> // clear the canvas canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) // draw the background paint. reset() canvas. drawBitmap(bitmap, 0f, 0f, paint) // draw meteor drawMeteor( canvas, time.toFloat(), startX, startY, paint ) } delay(1) } delay(meteorScaleTime) } }
As can be seen from the above drawing code, we first call the drawFixedContent
method to draw the static background, and the specific drawing code here will not be posted, because there is almost no difference from what we implemented with Compose last time. If you need it, you can read my previous article or directly read the project source code to understand.
We only need to know that this method finally draws a black background and fixed stars in it.
However, I don’t know if you have noticed that here I am not directly drawing the content into the Canvas obtained from the Surface, but into a Bitmap.
This is because the Canvas we get from the Surface is not a blank Canvas but a Canvas that currently displays content on the Surface.
In other words, the Canvas we get each time is the Canvas that has been superimposed by all previous drawings.
To achieve the animation effect, we will use canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
to “clear” the current canvas before each drawing.
What we draw here is obviously a fixed background, but it is recalculated and drawn every time it is cleared.
This is obviously unreasonable. We only need to draw meteor-related content in a loop.
So, here we calculate and draw the background to the Bitmap cache outside the loop.
Every time you need to update the Canvas, you only need to draw this cached Bitmap.
After understanding our fixed background, look down.
Below we use two layers of loops, one layer of while infinite loop, which is used to continuously generate meteors.
A layer of for loop is used to draw a meteor animation.
After we initialize the parameters in the while loop (mainly to randomly generate the starting point coordinates of a meteor), we start the for loop and start drawing each frame of the meteor.
The parameter of the for loop is our simulation time parameter.
Similarly, the drawMeteor
method is used to draw meteors. We will not post the specific drawing code. You can read the analysis of my previous article, or you can directly read the source code.
From here, all our code is complete.
The final effect is as follows:
Summary
From the above code, we can see that the Android live wallpaper is not as difficult as imagined, it is nothing more than a set of custom drawing, if you are familiar with custom drawing, it is very easy to write.
However, we only show the drawing using Canvas here. In fact, we can have more “saucy operations” by SurfaceHolder, such as calling a third-party mature animation library to directly refresh Surface, etc. If you are interested, you can search it .
Next
Although now we have realized our needs, that is, to make the background of the starry sky into a live wallpaper,
But you can also see from the code that all our parameters are hard-coded.
This is obviously not in line with common sense.
So our next goal is to extract these parameters as user-configurable configuration items.
References
- Android wallpaper is still fun at station B
- Building an Android Live Wallpaper