Android壁纸还是B站玩得花

2022-12-12 11:48:00 浏览数 (1)

设置系统壁纸这个功能,对于应用层App来说,场景其实并不多,但在一些场景的周边活动中,确也是一种提升品牌粘性的方式,就好比某个活动中创建的角色的壁纸美图,这些就可以新增一个设置壁纸的功能。

从原始的Android开始,系统就支持设置两种方式的壁纸,一种是静态壁纸,另一种是动态壁纸。

静态壁纸

静态壁纸没什么好说的,通过系统提供的API一行代码就完事了。

最简单代码如下所示。

代码语言:javascript复制
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来进行渲染,下面这个就是一个模板代码。

代码语言:javascript复制
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。

代码语言:javascript复制
<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>

另外,还需要申请相应的权限。

代码语言:javascript复制
<uses-permission android:name="android.permission.SET_WALLPAPER" />

最后,在xml文件夹中新增一个描述文件,对应上面resource标签的文件。

代码语言:javascript复制
<?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" />

动态壁纸只能通过系统的壁纸预览界面来进行设置。

代码语言:javascript复制
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,这是最简单的方式,可以设置一个视频,在桌面上循环播放。

代码语言:javascript复制
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的。

代码语言:javascript复制
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的权限。

代码语言:javascript复制
<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来进行自绘渲染。

代码语言:javascript复制
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,可以给大家参考。

https://www.developer.com/design/building-an-android-live-wallpaper/

有意思的方法

虽然WallpaperService是一个系统服务,但它也提供了一些比较有用的回调函数来帮助我们做一些有意思的东西。

onOffsetsChanged

当用户在手机桌面滑动时,有的壁纸图片会跟着左右移动,这个功能就是通过这个回调来实现的,在手势滑动的每一帧都会回调这个方法。

xOffset:x轴滑动的百分比

yOffset:y轴滑动百分比

xOffsetStep:x轴桌面Page数进度

yOffsetStep:y轴桌面Page数进度

xPixelOffset:x轴像素偏移量

通过这个函数,就可以拿到手势的移动惯量,从而对图片做出一些修改。

onTouchEvent、onCommand

这两个方法,都可以获取用户的点击行为,通过判断点击类型,就可以针对用户的特殊点击行为来做一些逻辑处理,例如点击某些特定的地方时,唤起App,或者打开某个界面等等。

代码语言:javascript复制
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来进行渲染,从而实现了一些复杂的交互,下面这些例子,就是一些实践。

https://github.com/PavelDoGreat/Unity-Android-Live-Wallpaper

https://github.com/jinkg/live-wallpaper

https://www.cnblogs.com/YFEYI/category/1425066.html

https://code.tutsplus.com/tutorials/creating-live-wallpapers-on-android--mobile-9516

但是B站的这个效果,显然比上面的方案更加成熟和完整,所以,通过调研可以发现,它们使用的是Live2D的方案。

https://www.live2d.com/

动态壁纸的Demo如下。

https://github.com/Live2D/CubismAndroidLiveWallpaper

这个东西是小日子的一个SDK,专业做2D可交互纸片人,这个东西已经出来很久了,前端之前用它来做网页的看板娘,现在客户端又拿来做动态壁纸,风水轮流换啊,想要使用的,可以参考它们官方的Demo。

❝但是官方的动态壁纸Demo在客户端是有Bug的,会存在各种闪的问题,由于我本身不懂OpenGL,所以也无法解决,通过回退Commit,发现可以直接使用这个CommitID : Merge pull request #2 from Live2D/create-new-function ,就没有闪的问题。 a9040ddbf99d9a130495e4a6190592068f2f7a77 ❞

好了,B站YYDS,但我觉得这东西的使用场景太有限了,而且特别卡,极端影响功耗,所以,要不要这么卷呢,你看着办吧。

0 人点赞