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

foreword

In my previous article, 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 the 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 dynamic 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)
            // 可以在这里写绘制代码
        }

        override fun onVisibilityChanged(visible: Boolean) {
    
    
            // 当壁纸的可见行性改变时会调用这里
            if (visible) {
    
    
                
            } else {
    
    
                
            }
        }

        override fun onDestroy() {
    
    
            super.onDestroy()
            
        }
    }
}

As you can see, what is available for us to render in this service is SurfaceHolder.

From SurfaceHolderwhich we can render in a variety of ways, the three commonly used ways are:

  • media player
  • 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>

For android:resource="@xml/wallpaper"the wallpaper file, we need 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 choose live wallpapers when setting the wallpaper, only the official live wallpapers will be displayed, and the third-party ones will be 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.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)
}

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 service template above WallpaperService, we wrote in the comments that we can onSurfaceCreatedwrite our drawing code in the callback.

onSurfaceCreatedBut here we don't write our drawing code in order to better control the drawing process , but onVisibilityChangedwrite in the callback:

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

Call to start drawing when the wallpaper is visible continueDraw; call to stop drawing when the wallpaper is invisible stopDraw.

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.

continueDrawand stopDraware 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 DrawStarrySkyclass is our drawing code, here it only exposes two methods: startDrawand stopDraw.

In fact, I only exposed startDrawthe method to the outside world at the beginning, and did not expose the stop method, but I found during the test that the coroutine coroutineScope.coroutineContext.cancelChildren()cannot be canceled in time by alone.

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 maintained a stop sign internally isRunningto 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 {
    
    
    // 锁定并返回当前 Surface 中的 Canvas
    canvas = surfaceHolder.lockCanvas()
    if (canvas != null) {
    
    
        // 在这里对 Canvas 进行绘制
    }
} finally {
    
    
    if (canvas != null) {
    
    
        // 解锁 Canvas 并写回到 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 wrote 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

    // 初始化参数
    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)
    }
}

As you can see from the drawing code above, we first call drawFixedContentthe method to draw the static background, and the specific drawing code here will not be posted, because it is almost the same as what we implemented with Compose last time. If you need it, you can read my previous article Read the article or read the project source code directly.

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 the Canvas currently displayed on the Surface.

In other words, the Canvas we get each time is the Canvas that has been superimposed by all previous drawings.

In order to achieve animation effects, we will use before each drawing canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)to "clear" the current canvas.

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 a meteor starting point coordinates), 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, drawMeteorthe method is used to draw meteors, and 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

Summarize

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, with SurfaceHolder, we can have more "show operations", such as calling a third-party mature animation library to directly refresh Surface, etc. If you are interested, you can search it.

Next step

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

Guess you like

Origin blog.csdn.net/sinat_17133389/article/details/130894436