设置系统壁纸这个功能,对于应用层App来说,场景其实并不多,但在一些场景的周边活动中,确也是一种提升品牌粘性的方式,就好比某个活动中创建的角色的壁纸美图,这些就可以新增一个设置壁纸的功能。
从原始的Android开始,系统就支持设置两种方式的壁纸,一种是静态壁纸,另一种是动态壁纸。
静态壁纸
静态壁纸没什么好说的,通过系统提供的API一行代码就完事了。
最简单代码如下所示。
val wallpaperManager = WallpaperManager.getInstance(this)try {val bitmap = ContextCompat.getDrawable(this, R.drawable.ic_launcher_background)?.toBitmap()wallpaperManager.setBitmap(bitmap)} catch (e: Exception) {e.printStackTrace()}val wallpaperManager = WallpaperManager.getInstance(this) try { val bitmap = ContextCompat.getDrawable(this, R.drawable.ic_launcher_background)?.toBitmap() wallpaperManager.setBitmap(bitmap) } catch (e: Exception) { e.printStackTrace() }val wallpaperManager = WallpaperManager.getInstance(this) try { val bitmap = ContextCompat.getDrawable(this, R.drawable.ic_launcher_background)?.toBitmap() wallpaperManager.setBitmap(bitmap) } catch (e: Exception) { e.printStackTrace() }
除了setBitmap之外,系统还提供了setResource、setStream,一共三种方式来设置静态壁纸。
三种方式殊途同归,都是设置一个Bitmap给系统API。
动态壁纸
动态壁纸就有点意思了,很多手机ROM也内置了一些动态壁纸,别以为这些是什么新功能,从Android 1.5开始,就已经支持这种方式了。只不过做的人比较少,为啥呢,主要是没有什么特别合适的场景,而且动态壁纸,会比静态壁纸更加耗电,所以大部分时候,我们都没用这种方式。
壁纸作为一个系统服务,在系统启动时,不管是动态壁纸还是静态壁纸,都会以一个Service的形式运行在后台——WallpaperService,它的Window类型为TYPE_WALLPAPER,WallpaperService提供了一个SurfaceHolder来暴露给外界来对画面进行渲染,这就是设置壁纸的基本原理。
创建一个动态壁纸,需要继承系统的WallpaperService,并提供一个WallpaperService.Engin来进行渲染,下面这个就是一个模板代码。
class MyWallpaperService : WallpaperService() {override fun onCreateEngine(): Engine = WallpaperEngine()inner class WallpaperEngine : WallpaperService.Engine() {lateinit var mediaPlayer: MediaPlayeroverride fun onSurfaceCreated(holder: SurfaceHolder?) {super.onSurfaceCreated(holder)}override fun onCommand(action: String?, x: Int, y: Int, z: Int, extras: Bundle?, resultRequested: Boolean): Bundle {try {Log.d("xys", "onCommand: $action----$x---$y---$z")if ("android.wallpaper.tap" == action) {}} catch (e: Exception) {e.printStackTrace()}return super.onCommand(action, x, y, z, extras, resultRequested)}override fun onVisibilityChanged(visible: Boolean) {if (visible) {} else {}}override fun onDestroy() {super.onDestroy()}}}class MyWallpaperService : WallpaperService() { override fun onCreateEngine(): Engine = WallpaperEngine() inner class WallpaperEngine : WallpaperService.Engine() { lateinit var mediaPlayer: MediaPlayer override fun onSurfaceCreated(holder: SurfaceHolder?) { super.onSurfaceCreated(holder) } override fun onCommand(action: String?, x: Int, y: Int, z: Int, extras: Bundle?, resultRequested: Boolean): Bundle { try { Log.d("xys", "onCommand: $action----$x---$y---$z") if ("android.wallpaper.tap" == action) { } } catch (e: Exception) { e.printStackTrace() } return super.onCommand(action, x, y, z, extras, resultRequested) } override fun onVisibilityChanged(visible: Boolean) { if (visible) { } else { } } override fun onDestroy() { super.onDestroy() } } }class MyWallpaperService : WallpaperService() { override fun onCreateEngine(): Engine = WallpaperEngine() inner class WallpaperEngine : WallpaperService.Engine() { lateinit var mediaPlayer: MediaPlayer override fun onSurfaceCreated(holder: SurfaceHolder?) { super.onSurfaceCreated(holder) } override fun onCommand(action: String?, x: Int, y: Int, z: Int, extras: Bundle?, resultRequested: Boolean): Bundle { try { Log.d("xys", "onCommand: $action----$x---$y---$z") if ("android.wallpaper.tap" == action) { } } catch (e: Exception) { e.printStackTrace() } return super.onCommand(action, x, y, z, extras, resultRequested) } override fun onVisibilityChanged(visible: Boolean) { if (visible) { } else { } } override fun onDestroy() { super.onDestroy() } } }
然后在manifest中注册这个Service。
<serviceandroid:name=".MyWallpaperService"android:exported="true"android:label="Wallpaper"android:permission="android.permission.BIND_WALLPAPER"><intent-filter><action android:name="android.service.wallpaper.WallpaperService" /></intent-filter><meta-dataandroid:name="android.service.wallpaper"android:resource="@xml/my_wallpaper" /></service><service android:name=".MyWallpaperService" 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/my_wallpaper" /> </service><service android:name=".MyWallpaperService" 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/my_wallpaper" /> </service>
另外,还需要申请相应的权限。
<uses-permission android:name="android.permission.SET_WALLPAPER" /><uses-permission android:name="android.permission.SET_WALLPAPER" /><uses-permission android:name="android.permission.SET_WALLPAPER" />
最后,在xml文件夹中新增一个描述文件,对应上面resource标签的文件。
<?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" /><?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" /><?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" />
动态壁纸只能通过系统的壁纸预览界面来进行设置。
val localIntent = Intent()localIntent.action = WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPERlocalIntent.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,ComponentName(applicationContext.packageName, MyWallpaperService::class.java.name))startActivity(localIntent)val localIntent = Intent() localIntent.action = WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER localIntent.putExtra( WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, ComponentName(applicationContext.packageName, MyWallpaperService::class.java.name)) startActivity(localIntent)val localIntent = Intent() localIntent.action = WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER localIntent.putExtra( WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, ComponentName(applicationContext.packageName, MyWallpaperService::class.java.name)) startActivity(localIntent)
这样我们就可以设置一个动态壁纸了。
玩点花
既然是使用提供的SurfaceHolder来进行渲染,那么我们所有能够使用到SurfaceHolder的场景,都可以来进行动态壁纸的创建了。
一般来说,有三种比较常见的使用场景。
- MediaPlayer
- Camera
- SurfaceView
这三种也是SurfaceHolder的常用使用场景。
首先来看下MediaPlayer,这是最简单的方式,可以设置一个视频,在桌面上循环播放。
inner class WallpaperEngine : WallpaperService.Engine() {lateinit var mediaPlayer: MediaPlayeroverride fun onSurfaceCreated(holder: SurfaceHolder?) {super.onSurfaceCreated(holder)mediaPlayer = MediaPlayer.create(applicationContext, R.raw.testwallpaper).also {it.setSurface(holder!!.surface)it.isLooping = true}}override fun onVisibilityChanged(visible: Boolean) {if (visible) {mediaPlayer.start()} else {mediaPlayer.pause()}}override fun onDestroy() {super.onDestroy()if (mediaPlayer.isPlaying) {mediaPlayer.stop()}mediaPlayer.release()}}inner class WallpaperEngine : WallpaperService.Engine() { lateinit var mediaPlayer: MediaPlayer override fun onSurfaceCreated(holder: SurfaceHolder?) { super.onSurfaceCreated(holder) mediaPlayer = MediaPlayer.create(applicationContext, R.raw.testwallpaper).also { it.setSurface(holder!!.surface) it.isLooping = true } } override fun onVisibilityChanged(visible: Boolean) { if (visible) { mediaPlayer.start() } else { mediaPlayer.pause() } } override fun onDestroy() { super.onDestroy() if (mediaPlayer.isPlaying) { mediaPlayer.stop() } mediaPlayer.release() } }inner class WallpaperEngine : WallpaperService.Engine() { lateinit var mediaPlayer: MediaPlayer override fun onSurfaceCreated(holder: SurfaceHolder?) { super.onSurfaceCreated(holder) mediaPlayer = MediaPlayer.create(applicationContext, R.raw.testwallpaper).also { it.setSurface(holder!!.surface) it.isLooping = true } } override fun onVisibilityChanged(visible: Boolean) { if (visible) { mediaPlayer.start() } else { mediaPlayer.pause() } } override fun onDestroy() { super.onDestroy() if (mediaPlayer.isPlaying) { mediaPlayer.stop() } mediaPlayer.release() } }
接下来,再来看下使用Camera来刷新Surface的。
inner class WallpaperEngine : WallpaperService.Engine() {lateinit var camera: Cameraoverride fun onVisibilityChanged(visible: Boolean) {if (visible) {startPreview()} else {stopPreview()}}override fun onDestroy() {super.onDestroy()stopPreview()}private fun startPreview() {camera = Camera.open()camera.setDisplayOrientation(90)try {camera.setPreviewDisplay(surfaceHolder)camera.startPreview()} catch (e: IOException) {e.printStackTrace()}}private fun stopPreview() {try {camera.stopPreview()camera.release()} catch (e: Exception) {e.printStackTrace()}}}inner class WallpaperEngine : WallpaperService.Engine() { lateinit var camera: Camera override fun onVisibilityChanged(visible: Boolean) { if (visible) { startPreview() } else { stopPreview() } } override fun onDestroy() { super.onDestroy() stopPreview() } private fun startPreview() { camera = Camera.open() camera.setDisplayOrientation(90) try { camera.setPreviewDisplay(surfaceHolder) camera.startPreview() } catch (e: IOException) { e.printStackTrace() } } private fun stopPreview() { try { camera.stopPreview() camera.release() } catch (e: Exception) { e.printStackTrace() } } }inner class WallpaperEngine : WallpaperService.Engine() { lateinit var camera: Camera override fun onVisibilityChanged(visible: Boolean) { if (visible) { startPreview() } else { stopPreview() } } override fun onDestroy() { super.onDestroy() stopPreview() } private fun startPreview() { camera = Camera.open() camera.setDisplayOrientation(90) try { camera.setPreviewDisplay(surfaceHolder) camera.startPreview() } catch (e: IOException) { e.printStackTrace() } } private fun stopPreview() { try { camera.stopPreview() camera.release() } catch (e: Exception) { e.printStackTrace() } } }
同时需要添加下Camera的权限。
<uses-permission android:name="android.permission.CAMERA" /><uses-permission android:name="android.permission.SET_WALLPAPER" /><uses-feature android:name="android.hardware.camera" /><uses-feature android:name="android.hardware.camera.autofocus" /><uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.SET_WALLPAPER" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" /><uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.SET_WALLPAPER" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" />
由于这里偷懒,没有使用最新的CameraAPI,也没有动态申请权限,所以你需要自己手动去授权。
最后一种,通过Surface来进行自绘渲染。
val holder = surfaceHoldervar canvas: Canvas? = nulltry {canvas = holder.lockCanvas()if (canvas != null) {canvas.save()// Draw Something}} finally {if (canvas != null) holder.unlockCanvasAndPost(canvas)}val holder = surfaceHolder var canvas: Canvas? = null try { canvas = holder.lockCanvas() if (canvas != null) { canvas.save() // Draw Something } } finally { if (canvas != null) holder.unlockCanvasAndPost(canvas) }val holder = surfaceHolder var canvas: Canvas? = null try { canvas = holder.lockCanvas() if (canvas != null) { canvas.save() // Draw Something } } finally { if (canvas != null) holder.unlockCanvasAndPost(canvas) }
这里就可以完全使用Canvas的API来进行绘制了。
这里有一个比较复杂的绘制Demo,可以给大家参考。
www.developer.com/design/buil…
有意思的方法
虽然WallpaperService是一个系统服务,但它也提供了一些比较有用的回调函数来帮助我们做一些有意思的东西。
onOffsetsChanged
当用户在手机桌面滑动时,有的壁纸图片会跟着左右移动,这个功能就是通过这个回调来实现的,在手势滑动的每一帧都会回调这个方法。
xOffset:x轴滑动的百分比
yOffset:y轴滑动百分比
xOffsetStep:x轴桌面Page数进度
yOffsetStep:y轴桌面Page数进度
xPixelOffset:x轴像素偏移量
通过这个函数,就可以拿到手势的移动惯量,从而对图片做出一些修改。
onTouchEvent、onCommand
这两个方法,都可以获取用户的点击行为,通过判断点击类型,就可以针对用户的特殊点击行为来做一些逻辑处理,例如点击某些特定的地方时,唤起App,或者打开某个界面等等。
class MyWallpaperService : WallpaperService() {override fun onCreateEngine(): Engine = WallpaperEngine()private inner class WallpaperEngine : WallpaperService.Engine() {override fun onTouchEvent(event: MotionEvent?) {// on finder press eventsif (event?.action == MotionEvent.ACTION_DOWN) {// get the canvas from the Engine or leaveval canvas = surfaceHolder?.lockCanvas() ?: return// TODO// update the surfacesurfaceHolder.unlockCanvasAndPost(canvas)}}}}class MyWallpaperService : WallpaperService() { override fun onCreateEngine(): Engine = WallpaperEngine() private inner class WallpaperEngine : WallpaperService.Engine() { override fun onTouchEvent(event: MotionEvent?) { // on finder press events if (event?.action == MotionEvent.ACTION_DOWN) { // get the canvas from the Engine or leave val canvas = surfaceHolder?.lockCanvas() ?: return // TODO // update the surface surfaceHolder.unlockCanvasAndPost(canvas) } } } }class MyWallpaperService : WallpaperService() { override fun onCreateEngine(): Engine = WallpaperEngine() private inner class WallpaperEngine : WallpaperService.Engine() { override fun onTouchEvent(event: MotionEvent?) { // on finder press events if (event?.action == MotionEvent.ACTION_DOWN) { // get the canvas from the Engine or leave val canvas = surfaceHolder?.lockCanvas() ?: return // TODO // update the surface surfaceHolder.unlockCanvasAndPost(canvas) } } } }
B站怎么玩的呢
不得不说,B站在这方面玩的是真的花,最近B站里面新加了一个异想少女系列,你可以设置一个动态壁纸,同时还带交互,有点意思。
其实类似这样的交互,基本上都是通过OpenGL或者是RenderScript来实现的,通过GLSurfaceView来进行渲染,从而实现了一些复杂的交互,下面这些例子,就是一些实践。
code.tutsplus.com/tutorials/c…
但是B站的这个效果,显然比上面的方案更加成熟和完整,所以,通过调研可以发现,它们使用的是Live2D的方案。
动态壁纸的Demo如下。
这个东西是小日子的一个SDK,专业做2D可交互纸片人,这个东西已经出来很久了,前端之前用它来做网页的看板娘,现在客户端又拿来做动态壁纸,风水轮流换啊,想要使用的,可以参考它们官方的Demo。
但是官方的动态壁纸Demo在客户端是有Bug的,会存在各种闪的问题,由于我本身不懂OpenGL,所以也无法解决,通过回退Commit,发现可以直接使用这个CommitID : Merge pull request #2 from Live2D/create-new-function ,就没有闪的问题。
a9040ddbf99d9a130495e4a6190592068f2f7a77
好了,B站YYDS,但我觉得这东西的使用场景太有限了,而且特别卡,极端影响功耗,所以,要不要这么卷呢,你看着办吧。