Android animated wallpaper actual combat: Make a starry sky live wallpaper (with random meteor animation)

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:

s1.gif

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:

s2.jpg

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:

s1.gif

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

  1. Android wallpaper is still fun at station B
  2. Building an Android Live Wallpaper