用 Slice 来做 Android Live Wallpaper 的快捷设置项
好久没写技术实现了,水一篇好了。
Slice 是 Google 做的一套模版化 UI,主要用于 Assistant 相关的界面展示,但系统设置顶部偶尔也会出现相关的应用。B-Reel 和 Google 在 Pixel 4 自带的 Pixel Live Wallpaper 里用 Slice 来做了一组快捷设定项,效果如下:
作为专业 Pixel Live Wallpaper 高仿者(误,做新软件 Diorama 的时候我也试了一把 Slice,效果还是可以的:
至于原理其实很简单,在 API 29 上,wallpaper.xml
可以添加一个新字段 android:settingsSliceUri
,如果提供的 Uri 是正确的,动态壁纸 Preview 的底部 panel 会新增一个 customize 的 tab。Diorama 目前的 wallpaper.xml
如下:
<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsSliceUri="content://com.justzht.lwp.diorama/settings"
android:showMetadataInPreview="true"
android:description="@string/app_wallpaper_description"
android:author="@string/app_author"
android:contextUri="@string/app_wallpaper_context_uri"
android:contextDescription="@string/app_wallpaper_context_description"
android:label="@string/app_name"
android:settingsActivity="com.justzht.lwp.diorama.view.activity.MainActivity"
android:thumbnail="@mipmap/wallpaper_preview"/>
Slice 的实现也很简单,首先 Android Studio 的新建菜单里 New/Other/Slice Provider 这个路径下就已经提供了一个基础的模板,只需要自己拓展 public Slice onBindSlice(Uri sliceUri)
方法返回一个自定义的 Slice 即可。
ListBuilder builder = new ListBuilder(context, sliceUri, ListBuilder.INFINITY);
if ("/settings".equals(sliceUri.getPath())) {
// add your own implementation here
return builder.build();
} else {
return builder.addRow(
new RowBuilder()
.setTitle("ERROR: URI " + sliceUri.getPath() + " not found.")
).build();
}
Slice 的 Template 里有一些可以交互的 UI 元素,比如 Toggle 或者 Slider。这些交互的触发事件可以通过设定各自元素 Builder 里的 Action 来实现,比如 ListBuilder.InputRangeBuilder
就可以调用 setInputAction(PendingIntent action)
来触发滑动事件,以及 setPrimaryAction(SliceAction action)
来触发整个 row 的点击事件。若是要做一个时间拖动的 Slider:
// init your ListBuilder before this code snippet
int hour = (int) Math.floor(24 * Utils.getVal(SettingsManager.getInstance().getModel().dateTimeManualModeProgress, 0 f)); // Get current set hour from some where
Intent intent = new Intent(context, SliceBroadcastReceiver.class).setAction(context.getString(R.string.intent_change_datetime));
ListBuilder.InputRangeBuilder inputRangeBuilder = new ListBuilder.InputRangeBuilder()
.setTitle(context.getString(R.string.text_time))
.setSubtitle("Custom at " + String.format("%02d:00", hour))
.setInputAction(PendingIntent.getBroadcast(
getContext(),
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)) // add a PendingIntent so a BroadcastReceiver can receive the event
.setValue(hour) // note that value is an int, meaning this slider is a *stepped* seekbar
.setMax(24)
.setMin(0);
builder.addInputRange(inputRangeBuilder);
// return builder.build() after this code snippet
然后在 BroadcastReceiver 里就可以获得 Slider 更新的值:
public class SliceBroadcastReceiver extends BroadcastReceiver {
private static Uri sliceUri = Uri.parse("content://com.justzht.lwp.diorama/settings");
@Override
public void onReceive(Context context, Intent intent) {
String intentAction = intent.getAction();
if (intentAction != null) {
if (intentAction.equals(context.getString(R.string.intent_change_datetime)) &&
intent.getExtras() != null &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
int hour = intent.getExtras().getInt(EXTRA_RANGE_VALUE, 0); // acquire the updated value using EXTRA_RANGE_VALUE
Utils.setValIfNotSame(SettingsManager.getInstance().getModel().dateTimeManualModeProgress, hour / 24 f, false); // save this value to somewhere
context.getContentResolver().notifyChange(sliceUri, null); // notify the app to refresh slice
}
}
}
}
最后记得调用 void notifyChange(Uri uri, ContentObserver observer)
刷新 Slice 即可。