Android Jetpack架构组件(十)之Slices

2021-01-18 10:23:55 浏览数 (1)

一、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>,如下所示。

代码语言:txt复制
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 可以包含标头行、带有单张图片的网格以及带有两个文字单元格的网格。

在这里插入图片描述在这里插入图片描述

0 人点赞