一、Slices简介
Slice 是一种界面模板,可以在 Google 搜索应用中以及 Google 助理中等其他位置显示您应用中的丰富而动态的互动内容。同时,Slice 支持全屏应用体验之外的互动,可以帮助用户更快地执行任务。
目前,Android Jetpack 内置了对 Slice 的支持,并且可以向后一直扩展到 Android 4.4,覆盖约 95% 的 Android 用户。借助Slice,开发者可以根据应用的设计自定义 Slice 的颜色、文字、图像、视频等。
同时,我们还可以在使用Slice 包含切换开关和滑块之类的互动控件。
二、使用入门
2.1 下载并安装Slice查看器
为了在不实现 SliceView API 的情况下测试 Slice,我们需要下载一个对应版本的 Slice 查看器 。当然,我们也可以下载它的源码,然后自己编译,下载的地址为: Slice 查看器源代码。
下载slice-viewer.apk之后,我们在所在的目录中运行以下命令将 Slice 查看器安装到您的设备上。
代码语言:txt复制adb install -r -t slice-viewer.apk
2.2 运行Slice查看器
我们可以使用 Android Studio 或者使用命令行启动 Slice 查看器。
2.2.1 使用Android Studio 启动Slice
打开Android项目,然后依次选择 【Run】->【Edit Configurations...】,然后点击左上角的绿色加号并选中【Android App】选型,如下图所示。
然后,在名称字段中输入“slice”,从 Module 下拉列表中选择应用模块,从 Launch Options 下的 Launch 下拉列表中,选择 URL并在 URL 字段中输入 slice-<your slice URI>
,如下所示。
slice-content://com.example.your.sliceuri
2.2.2 通过 ADB命令行启动 Slice
首先,在Android Studio 的命令行面板中运行您的应用,命令如下。
代码语言:txt复制adb install -t -r <yourapp>.apk
然后,通过运行以下命令来查看您的 Slice。
代码语言:txt复制adb shell am start -a android.intent.action.VIEW -d slice-<your slice URI>
//例子
adb shell am start -a android.intent.action.VIEW -d slice-content://com.example.android.slice.demos/wifi
然后,就可以在设备上看到Wifi的连接情况,如下图所示。
2.2.3 在一个位置集中查看所有 Slice
除了启动单个 Slice 外,我们还可以查看 Slice 的持久性列表。例如,使用搜索栏通过 URI(例如,content://com.example.android.app/hello)手动搜索Slice,每次搜索时相应的 Slice 都会添加到列表中。
我们可以滑动 Slice 以将其从列表中移除,也可以点按 Slice 的 URI 可查看仅包含该 Slice 的网页。
2.2.4 修改Slice模式
我们可以在呈现 Slice 应用时修改 SliceView#mode
,因此我们需要确保 Slice 在每种模式下均按预期显示,选择页面右上方的菜单图标即可更改模式。
2.3 构建Slice
首先,在新建的Android项目的在build.gradle添加如下依赖。
代码语言:txt复制def slice_version = "1.1.0-alpha01"
implementation "androidx.slice:slice-core:$slice_version"
implementation "androidx.slice:slice-builders:$slice_version"
需要说明的是,Slice需要最低版本为19,所以需要修改Android项目的minSdkVersion版本。然后,打开Android Studio 项目,右键点击 src 软件包,然后依次选择 【New】--> 【Other】 --> 【Slice Provider】,如下图所示。
此时回创建一个扩展 SliceProvider 的类,然后在AndroidManifest.xml 添加所需的提供程序条目,并修改您的 build.gradle 以添加所需的 Slice 依赖项,然后打开AndroidManifest.xml 的修改如下所示。
代码语言:txt复制<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.app">
...
<application>
...
<provider android:name="MySliceProvider"
android:authorities="com.example.android.app"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.app.slice.category.SLICE" />
</intent-filter>
</provider>
...
</application>
</manifest>
需要说明的是,默认情况下,SliceProvider 模板指向的是 AndroidX 库,如果您的Android项目使用旧版支持库,请修改 build.gradle 文件以指向 com.android.support:slices-builders:(latest version),而不是等效的 AndroidX 库。
事实上,每个 Slice 都有一个关联的 URI。当界面想要显示 Slice 时,它会通过该 URI 向您的应用发送绑定请求,然后应用会通过 onBindSlice 方法处理该请求,并动态构建 Slice,界面随后会根据情况显示 Slice。
MySliceProvider继承于ContentProvider,其APP间数据的传递通过
ContentProvider的方式,应用APP向搜索APP对外提供其对应Slice的Uri,封装成Slice对象通过Parcelable序列化的方式实现APP之间的数据传递。新建类继承SliceProvider,并重写onBindSlice()方法,在该方法里可以编写Slice展示模块中的相关逻辑代码。然后,我们打开onBindSlice 方法,代码如下。
代码语言:txt复制@Override
public Slice onBindSlice(Uri sliceUri) {
if (getContext() == null) {
return null;
}
SliceAction activityAction = createActivityAction();
ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY);
// Create parent ListBuilder.
if ("/hello".equals(sliceUri.getPath())) {
listBuilder.addRow(new ListBuilder.RowBuilder()
.setTitle("Hello World")
.setPrimaryAction(activityAction)
);
} else {
listBuilder.addRow(new ListBuilder.RowBuilder()
.setTitle("URI not recognized")
.setPrimaryAction(activityAction)
);
}
return listBuilder.build();
}
然后,使用您在上述“Slice 查看器”部分中创建的 slice 运行配置,传入 Hello World Slice 的 Slice URI(例如,slice-content://com.android.example.slicesample/hello),以在 Slice 查看器中查看该 Slice,命令如下。
代码语言:txt复制adb shell am start -a android.intent.action.VIEW -d slice-content://com.xzh.slice/hello
运行效果如下图所示。
2.4 互动 Slice
与通知类似,如需处理 Slice 中的点按操作,我们可以附加在用户互动时触发的 PendingIntent 对象,比如点击Slice模块打开宿主App,我们打开MySliceProvider,然后在onBindSlice()方法中调用如下代码。
代码语言:txt复制public Slice createSlice(Uri sliceUri) {
if (getContext() == null) {
return null;
}
SliceAction activityAction = createActivityAction();
return new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
.addRow(new ListBuilder.RowBuilder()
.setTitle("Perform action in app.")
.setPrimaryAction(activityAction)
).build();
}
public SliceAction createActivityAction() {
if (getContext() == null) {
return null;
}
return SliceAction.create(
PendingIntent.getActivity(
getContext(),
0,
new Intent(getContext(), MainActivity.class),
0
),
IconCompat.createWithResource(getContext(), R.drawable.ic_home),
ListBuilder.ICON_IMAGE,
"Enter app"
);
}
完整的代码地址为: 互动 Slice。重新运行Android项目,效果如下图所示。
当然,Slice 还支持在发送到应用的 intent 中包含状态的其他输入类型,如切换开关,代码如下。
代码语言:txt复制public Slice createBrightnessSlice(Uri sliceUri) {
if (getContext() == null) {
return null;
}
SliceAction toggleAction = SliceAction.createToggle(
createToggleIntent(),
"Toggle adaptive brightness",
true
);
ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
.addRow(new ListBuilder.RowBuilder()
.setTitle("Adaptive brightness")
.setSubtitle("Optimizes brightness for available light.")
.setPrimaryAction(toggleAction)
).addInputRange(new ListBuilder.InputRangeBuilder()
.setInputAction(brightnessPendingIntent)
.setMax(100)
.setValue(45)
);
return listBuilder.build();
}
public PendingIntent createToggleIntent() {
Intent intent = new Intent(getContext(), MyBroadcastReceiver.class);
return PendingIntent.getBroadcast(getContext(), 0, intent, 0);
}
然后,我们在自定义一个BroadcastReceiver广播通知接收器,代码如下。
代码语言:txt复制public class MyBroadcastReceiver extends BroadcastReceiver {
public static String EXTRA_MESSAGE = "message";
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra(EXTRA_TOGGLE_STATE)) {
Toast.makeText(context, "Toggled: " intent.getBooleanExtra(
EXTRA_TOGGLE_STATE, false),
Toast.LENGTH_LONG).show();
}
}
}
然后,我们还需要在AndroidManifest.xml中注册MyBroadcastReceiver。最后,重新运行Android项目,
2.5 动态 Slice
在使用Slice时,还可以包含动态内容。在以下示例中,Slice 的内容中包括接收的广播数量。
代码语言:txt复制public Slice createDynamicSlice(Uri sliceUri) {
if (getContext() == null || sliceUri.getPath() == null) {
return null;
}
ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY);
switch (sliceUri.getPath()) {
case "/count":
SliceAction toastAndIncrementAction = SliceAction.create(
createToastAndIncrementIntent("Item clicked."),
actionIcon,
ListBuilder.ICON_IMAGE,
"Increment."
);
listBuilder.addRow(
new ListBuilder.RowBuilder()
.setPrimaryAction(toastAndIncrementAction)
.setTitle("Count: " MyBroadcastReceiver.sReceivedCount)
.setSubtitle("Click me")
);
break;
default:
listBuilder.addRow(
new ListBuilder.RowBuilder()
.setPrimaryAction(createActivityAction())
.setTitle("URI not found.")
);
break;
}
return listBuilder.build();
}
public PendingIntent createToastAndIncrementIntent(String s) {
Intent intent = new Intent(getContext(), MyBroadcastReceiver.class)
.putExtra(MyBroadcastReceiver.EXTRA_MESSAGE, s);
return PendingIntent.getBroadcast(getContext(), 0, intent, 0);
}
在此示例中,虽然显示了计数,但它不会自行更新。我们可以修改广播接收器,以使用 ContentResolver#notifyChange 来通知系统发生了更改,代码如下。
代码语言:txt复制public class MyBroadcastReceiver extends BroadcastReceiver {
public static int sReceivedCount = 0;
public static String EXTRA_MESSAGE = "message";
private static Uri sliceUri = Uri.parse("content://com.xzh.slice/count");
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra(EXTRA_TOGGLE_STATE)) {
Toast.makeText(context, "Toggled: " intent.getBooleanExtra(
EXTRA_TOGGLE_STATE, false),
Toast.LENGTH_LONG).show();
sReceivedCount ;
context.getContentResolver().notifyChange(sliceUri, null);
}
}
}
然后,重新运行Android项目,并在命令行运行如下命令。
代码语言:txt复制 adb shell am start -a android.intent.action.VIEW -d slice-content://com.xzh.slice/count
本小节涉及的代码Slice源码
三、 Slice模板
3.1 定义 Slice 模板
Slice是通过ListBuilder类来创建的,在ListBuilder中,我们可以添加不同类型的行模块在应用中进行展示。
3.1.1 SliceAction
Slice 模板的最基本元素是 SliceAction,SliceAction 包含一个标签以及一个 PendingIntent,SliceAction可以是以下某一项。
- 图标按钮
- 默认切换开关
- 自定义切换开关
SliceAction 由模板构建器调用,我们可以为 SliceAction 定义一种图片模式,该模式决定了如何为操作呈现图片,图片模式的常量如下。
- ICON_IMAGE:超小尺寸,可着色
- SMALL_IMAGE:小尺寸,不可着色
- LARGE_IMAGE:最大尺寸,不可着色
3.1.2 HeaderBuilder
HeaderBuilder 主要为模板设置标头,标头可以支持以下几项:
- 标题
- 副标题
- 摘要副标题
- 主要操作
效果如下图。
本小节涉及的完整代码为Slice模版
例如,下面是一个包含标头的简单列表 Slice,代码如下。
代码语言:txt复制public Slice createSliceWithHeader(Uri sliceUri) {
if (getContext() == null) {
return null;
}
// Construct the parent.
ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
.setAccentColor(0xff0F9D58) // Specify color for tinting icons.
.setHeader( // Create the header and add to slice.
new HeaderBuilder()
.setTitle("Get a ride")
.setSubtitle("Ride in 4 min.")
.setSummary("Work in 1 hour 45 min | Home in 12 min.")
).addRow(new RowBuilder() // Add a row.
.setPrimaryAction(
createActivityAction()) // A slice always needs a SliceAction.
.setTitle("Home")
.setSubtitle("12 miles | 12 min | $9.00")
.addEndItem(IconCompat.createWithResource(getContext(), R.drawable.ic_home),
SliceHints.ICON_IMAGE)
); // Add more rows if needed...
return listBuilder.build();
}
然后,在onBindSlice()方法中调用上面的createSliceWithHeader()方法即可,重新运行Android应用,并执行如下的命令。
代码语言:txt复制 adb shell am start -a android.intent.action.VIEW -d slice-content://com.xzh.slice/hello
运行效果如下图所示。
当然,我们也可以给Slice标头显示 SliceAction,如下所示。
代码语言:txt复制public Slice createSliceWithActionInHeader(Uri sliceUri) {
if (getContext() == null) {
return null;
}
// Construct our slice actions.
SliceAction noteAction = SliceAction.create(takeNoteIntent,
IconCompat.createWithResource(getContext(), R.drawable.ic_pencil),
ListBuilder.ICON_IMAGE, "Take note");
SliceAction voiceNoteAction = SliceAction.create(voiceNoteIntent,
IconCompat.createWithResource(getContext(), R.drawable.ic_mic),
ListBuilder.ICON_IMAGE,
"Take voice note");
SliceAction cameraNoteAction = SliceAction.create(cameraNoteIntent,
IconCompat.createWithResource(getContext(), R.drawable.ic_camera),
ListBuilder.ICON_IMAGE,
"Create photo note");
// Construct the list.
ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
.setAccentColor(0xfff4b400) // Specify color for tinting icons
.setHeader(new HeaderBuilder() // Construct the header.
.setTitle("Create new note")
.setSubtitle("Easily done with this note taking app")
)
.addRow(new RowBuilder()
.setTitle("Enter app")
.setPrimaryAction(createActivityAction())
)
// Add the actions to the ListBuilder.
.addAction(noteAction)
.addAction(voiceNoteAction)
.addAction(cameraNoteAction);
return listBuilder.build();
}
3.1.3 RowBuilder
除此之外,我们可以使用 RowBuilder 构造一行内容,行可以支持以下任意一项。
- 标题
- 副标题
- 起始项:SliceAction、图标或时间戳
- 结束项:SliceAction、图标或时间戳
- 主要操作
并且,RowBuilder还支持多种方式组合行内容,但须遵守以下限制。
- 起始项不能显示在 Slice 的第一行中
- 结束项不能同时包含 SliceAction 对象和 Icon 对象
- 一行只能包含一个时间戳
例如,下面是一个行包含一项主要操作和一个默认切换开关的例子,代码如下。
代码语言:txt复制public Slice createActionWithActionInRow(Uri sliceUri) {
if (getContext() == null) {
return null;
}
// Primary action - open wifi settings.
SliceAction primaryAction = SliceAction.create(wifiSettingsPendingIntent,
IconCompat.createWithResource(getContext(), R.drawable.ic_wifi),
ListBuilder.ICON_IMAGE,
"Wi-Fi Settings"
);
// Toggle action - toggle wifi.
SliceAction toggleAction = SliceAction.createToggle(wifiTogglePendingIntent,
"Toggle Wi-Fi", isConnected /* isChecked */);
// Create the parent builder.
ListBuilder listBuilder = new ListBuilder(getContext(), wifiUri, ListBuilder.INFINITY)
// Specify color for tinting icons / controls.
.setAccentColor(0xff4285f4)
// Create and add a row.
.addRow(new RowBuilder()
.setTitle("Wi-Fi")
.setPrimaryAction(primaryAction)
.addEndItem(toggleAction));
// Build the slice.
return listBuilder.build();
}
运行效果如下。
3.1.4 GridBuilder
当然,我们也可以使用 GridBuilder 构造内容网格,网格单元格是使用 CellBuilder 构造的。一个单元格最多可以支持两行文字和一张图片。
代码语言:txt复制public Slice createSliceWithGridRow(Uri sliceUri) {
if (getContext() == null) {
return null;
}
// Create the parent builder.
ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
.setHeader(
// Create the header.
new HeaderBuilder()
.setTitle("Famous restaurants")
.setPrimaryAction(SliceAction
.create(pendingIntent, icon, ListBuilder.ICON_IMAGE,
"Famous restaurants"))
)
// Add a grid row to the list.
.addGridRow(new GridRowBuilder()
// Add cells to the grid row.
.addCell(new CellBuilder()
.addImage(image1, ListBuilder.LARGE_IMAGE)
.addTitleText("Top Restaurant")
.addText("0.3 mil")
.setContentIntent(intent1)
).addCell(new CellBuilder()
.addImage(image2, ListBuilder.LARGE_IMAGE)
.addTitleText("Fast and Casual")
.addText("0.5 mil")
.setContentIntent(intent2)
)
.addCell(new CellBuilder()
.addImage(image3, ListBuilder.LARGE_IMAGE)
.addTitleText("Casual Diner")
.addText("0.9 mi")
.setContentIntent(intent3))
.addCell(new CellBuilder()
.addImage(image4, ListBuilder.LARGE_IMAGE)
.addTitleText("Ramen Spot")
.addText("1.2 mi")
.setContentIntent(intent4))
// Every slice needs a primary action.
.setPrimaryAction(createActivityAction())
);
return listBuilder.build();
}
3.1.5 RangeBuilder
RangeBuilder是一个用来创建包含进度条或输入范围(如滑块)的行。例如,以下示例演示了如何使用 InputRangeBuilder 构建包含音量滑块的 Slice。
代码语言:txt复制public Slice createSliceWithRange(Uri sliceUri) {
if (getContext() == null) {
return null;
}
// Construct the parent.
ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
.addRow(new RowBuilder() // Every slice needs a row.
.setTitle("Enter app")
// Every slice needs a primary action.
.setPrimaryAction(createActivityAction())
)
.addInputRange(new InputRangeBuilder() // Create the input row.
.setTitle("Ring Volume")
.setInputAction(volumeChangedPendingIntent)
.setMax(100)
.setValue(30)
);
return listBuilder.build();
}
3.2 延迟内容
当我们使用 SliceProvider.onBindSlice() 返回 Slice时可能回出现耗时调用问题,出现的现象是闪烁。如果您的 Slice 内容无法快速加载,我们可以使用占位符内容构造 Slice,同时在构建器中注明内容正在加载。一旦内容可供显示,请使用 Slice URI 调用 getContentResolver().notifyChange(sliceUri, null),如果再次调用 SliceProvider.onBindSlice(),那么可以使用新内容重新构造 Slice。
例如,下面是“骑车上班”的例子,上班距离是动态确定的,可能不会立即显示,那么在内容加载时就显示 null ,代码如下。
代码语言:txt复制public Slice createSliceShowingLoading(Uri sliceUri) {
if (getContext() == null) {
return null;
}
// Construct the parent.
ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
// Construct the row.
.addRow(new RowBuilder()
.setPrimaryAction(createActivityAction())
.setTitle("Ride to work")
// We’re waiting to load the time to work so indicate that on the slice by
// setting the subtitle with the overloaded method and indicate true.
.setSubtitle(null, true)
.addEndItem(IconCompat.createWithResource(getContext(), R.drawable.ic_work),
ListBuilder.ICON_IMAGE)
);
return listBuilder.build();
}
private SliceAction createActivityAction() {
return SliceAction.create(
PendingIntent.getActivity(
getContext(),
0,
new Intent(getContext(), MainActivity.class),
0
),
IconCompat.createWithResource(getContext(), R.drawable.ic_home),
ListBuilder.ICON_IMAGE,
"Enter app"
);
}
3.3 处理 Slice 中停用滚动的情况
Slice 模板的呈现界面可能不支持在模板内滚动。在这种情况下,某些内容可能不会显示,举个例子,假设一个 Slice 中显示了一个 Wi-Fi 网络列表,效果如下。
如果这个 Wi-Fi 列表较长,且停用了滚动操作,那么我们可以添加查看更多按钮,以确保用户可以看到列表中的所有项目。
代码语言:txt复制public Slice seeMoreActionSlice(Uri sliceUri) {
if (getContext() == null) {
return null;
}
ListBuilder listBuilder = new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY);
// ...
listBuilder.setSeeMoreAction(seeAllNetworksPendingIntent);
// ...
return listBuilder.build();
}
运行效果如下图所示。
3.4 组合模板
除此之外,Slice可以将多种行类型组合在一起,创建内容丰富的动态 Slice。例如,Slice 可以包含标头行、带有单张图片的网格以及带有两个文字单元格的网格。