探索Android Nougat 7.1的app快捷方式

原文:Exploring Android Nougat 7.1 App Shortcuts 

谷歌为我们带来了牛轧糖的第二个版本7.1(API 25),但这并只是一个次要版本,实际上它绑定了一些有趣的功能。其中一个额外的功能就是应用快捷方式(App Shortcuts)。

本文概要

  • 应用快捷方式对于用户发现应用的操作是非常有用的,提高用户粘性。

  • 它们可以是静态或者动态的

  • 静态的一旦定义好就写死了(你只能在app部署时更新它们)

  • 动态的可以即时改变

  • 你可以为从快捷方式打开的界面创建一个回退栈

  • 快捷方式可以重新排序,但是只在同一类型下,静态快捷方式总是在最下面

  • 快捷方式的描述是CharSequence的,所以可以用span去调整它们

你可以在这里查看这篇文章的示例app。

如果你想知道我是如何一步一步做的,请继续阅读。

是什么

应用快捷方式是把app常用操作和任务暴露给启动器的一种手段。用户可以通过长按app的图标打开快捷方式。

它们有两种类型:

  • 静态的:在资源文件中静态的定义;除非修改了这个文件并重新部署app不然不能改变

  • 动态的:运行时;无需重新部署就能更新快捷方式

注意:一个app最多只能有5个快捷方式。

通过把常用操作暴露出来可以让用户在不需要额外导航的情况下更易返回他们想到的页面。

如何做?

为app添加快捷方式非常简单。让我们从创建一个简单的静态快捷方式开始。

注意:你必须使用其launcher支持快捷方式的Android Nougat 7.1的设备。

静态快捷方式

我假设你已经在Android Studio中创建了一个新项目。找到AndroidManifest.xml并添加如下meta-data标签到你的主activity:

<?xml version="1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android"  
  package="com.catinean.appshortcutsdemo">
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter>
      <meta-data
        android:name="android.app.shortcuts"
        android:resource="@xml/shortcuts" />
    </activity>
  </application>
</manifest>

在meta-data中android:resource对应于定义在res/xml/shortcuts.xml中的资源。这里你需要定义所有的静态快捷方式。让我们在这里添加一个打开特定activity的快捷方式吧(就我而言,我定义了一个StaticShortcutActivity):

<?xml version="1.0" encoding="utf-8"?>  
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">  
  <shortcut
    android:enabled="true"
    android:icon="@drawable/ic_static_shortcut"
    android:shortcutDisabledMessage="@string/static_shortcut_disabled_message"
    android:shortcutId="static"
    android:shortcutLongLabel="@string/static_shortcut_long_label"
    android:shortcutShortLabel="@string/static_shortcut_short_label">
    <intent
      android:action="android.intent.action.VIEW"
      android:targetClass="com.catinean.appshortcutsdemo.StaticShortcutActivity"
      android:targetPackage="com.catinean.appshortcutsdemo" />
  </shortcut>
</shortcuts>

你可以看到这个文件的根是,它可以包含多个块。每个块代表一个静态的快捷方式。一个shortcut可以设置如下属性:

  • **enabled:**shortcut是否启用。如果你决定禁用某个静态shortcut,你可以把这个属性设置为false或者直接把它移除。

  • **icon:**显示在快捷方式左边的图标。

  • **shortcutDisabledMessage:**当你禁用了shortcut之后,它将不会显示在用户长按应用图标后打开的快捷方式里,但是用户可以把一个快捷方式拖拽到launcher的某个页面,被禁用之后这个快捷方式就会显示为灰色,点击则会显示一个内容为shortcutDisabledMessage的Toast。

  • **shortcutLongLabel:**当launcher的空间足够时将会显示shortcut的长文本描述。

  • **shortcutShortLabel:**shortcut的简要说明,这项是必须的。

  • **intent:**这里定义快捷方式被点击之后将会打开的intent。

让我们看看我们的静态快捷方式是什么样的:

app_shortcut_static-1.gif

不错,但是你会发现点击返回之后,用户回到了launcher。那如何让用户回到的是app呢?为此,我们可以在前一个intent之前添加多个intent标签:

<?xml version="1.0" encoding="utf-8"?>  
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">  
  <shortcut
  ...>
    <intent
      android:action="android.intent.action.MAIN"
      android:targetClass="com.catinean.appshortcutsdemo.MainActivity"
      android:targetPackage="com.catinean.appshortcutsdemo" />
    <intent
      android:action="android.intent.action.VIEW"
      android:targetClass="com.catinean.appshortcutsdemo.StaticShortcutActivity"
      android:targetPackage="com.catinean.appshortcutsdemo" />
  </shortcut>
</shortcuts>

注意我们是如何在已有的之前添加一个指向MainActivity的的。这将创建一个intent的后退栈,最后一个才是被快捷方式打开的那个。对于我们的情况而言,后退栈就是MainActivity->StaticShortcutActivity,所以当点击后退用户将回到MainActivity:

app_shortcut_static_back_stack.gif

添加静态快捷方式非常简单,现在让我们转到定义动态的。

动态快捷方式

就如其名字那样,动态快捷方式可以在运行时动态改变,而不需要重新部署app。你可能已经猜到了,它不像静态的那样通过静态资源(shortcuts.xml)来定义,而是在代码中创建。

让我们来添加第一个动态快捷方式!为此,你需要使用ShortcutManagerShortcutInfo.Builder。我将在MainActivity的onCreate()方法中构建动态快捷方式:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
    ShortcutInfo webShortcut = new ShortcutInfo.Builder(this, "shortcut_web")
            .setShortLabel("catinean.com")
            .setLongLabel("Open catinean.com web site")
            .setIcon(Icon.createWithResource(this, R.drawable.ic_dynamic_shortcut))
            .setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("https://catinean.com")))
            .build();
    shortcutManager.setDynamicShortcuts(Collections.singletonList(webShortcut));
}

这里我们得到了一个shortcutManager并创建了一个ShortcutInfo。通过ShortcutInfo.Builder我们可以为shortcut设置各种属性。以上所有builder方法对应在静态快捷方式中使用的相应属性,所以就不再解释了。但是有一个属性隐藏得比较深,那就是shortcut的id,它作为StaticInfo.Builder构造函数的第二个参数被传递进来 - shortcut_web。我定义了一个打开我网站的Intent。最后我把这个动态的快捷方式设置到ShortcutManager。让我们看看此时我们的快捷方式是怎样的:

app_shortcut_dynamic_website

不错!现在我们的app有了两个快捷方式了-一个静态的一个动态的。

让我们继续添加一个指向app内部activity的动态快捷方式,看看如何为它创建一个后退栈:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    ShortcutInfo dynamicShortcut = new ShortcutInfo.Builder(this, "shortcut_dynamic")
            .setShortLabel("Dynamic")
            .setLongLabel("Open dynamic shortcut")
            .setIcon(Icon.createWithResource(this, R.drawable.ic_dynamic_shortcut_2))
            .setIntents(
                    new Intent\[\]{
                            new Intent(Intent.ACTION_MAIN, Uri.EMPTY, this, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK),
                            new Intent(DynamicShortcutActivity.ACTION)
                    })
            .build();
    shortcutManager.setDynamicShortcuts(Arrays.asList(webShortcut, dynamicShortcut));
}

你可以看到为了建立一个后退栈我们在builder上调用了 setIntents()(前面是 setIntent()):

  • 第一个intent对应MainActivity。为了清除app已存在的任务让MainActivity作为根activity,我们设置了一个 FLAG_ACTIVITY_CLEAR_TASK标志。

  • 第二个对应DynamicShortcutActivity(只是我创建的一个空activity)。我们需要为这个Intent指定一个action,这个action定义在DynamicShortcutActivity的一个静态变量中和AndroidManifest.xml中为DynamicShortcutActivity定义的intent-filter是一样的。

<activity  
      android:name=".DynamicShortcutActivity"
      android:label="Dynamic shortcut activity">
      <intent-filter>
        <action android:name="com.catinean.appshortcutsdemo.OPEN_DYNAMIC_SHORTCUT" />
        <category android:name="android.intent.category.DEFAULT" />
      </intent-filter></activity>

通过这种顺序定义intent,我们可以确保用户通过快捷方式进入DynamicShortcutActivity之后,按下back打开的是MainActivity。

让我们看看效果:

app_shortcut_dynamic_activity.gif

快捷方式的顺序

现在我们有了一个静态快捷方式和2个动态快捷方式,如何为它们指定自己想要的顺序呢。仔细看看ShortcutInfo.Builder方法,其中一个方法吸引了我们的注意: setRank(int)。通过为动态快捷方式设置一个排序可以控制它们的显示顺序:排序越高,快捷方式排在越上面。

假设我们想让第二个快捷方式(catinean.com)放在最上面。我们可以动态改变已经添加了的动态快捷方式的顺序。让我们在MainActivity里点击一个按钮之后做这件事情:

findViewById(R.id.main_rank_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
          ShortcutInfo webShortcut = new ShortcutInfo.Builder(MainActivity.this, "shortcut_web")
                  .setRank(1)
                  .build();
          ShortcutInfo dynamicShortcut = new ShortcutInfo.Builder(MainActivity.this, "shortcut_dynamic")
                  .setRank(0)
                  .build();
          shortcutManager.updateShortcuts(Arrays.asList(webShortcut, dynamicShortcut));
      }});

在按钮的listener中我们为每个已经添加了的快捷方式创建了新的ShortcutInfo,使用相同的ID,但是为shortcut_web设置了较高的序号而shortcut_dynamic设置了较低的序号。最后我们使用ShortcutManager的updateShortcuts(List) 方法更新快捷方式:

app_shortcut_ranks.gif

你可以从上面的gif图中看到静态的快捷方式排在了列表的底部。你不能改变一个静态快捷方式的顺序,因为它们是按照shortcuts.xml文件中定义的顺序来显示的。我们只有一个静态快捷方式,它具有默认的顺序0,无法改变。

Extra bits

如果我们仔细看 ShortcutInfo.Builder的setShortLabel(CharSequence) 方法,可以发现它接受一个CharSequence参数。这意味着什么?意味着我们可以设置自定义的span。

假设我们想把按钮按下时的文字颜色变成红色。我们可以创建一个SpannableStringBuilder,并为它设置一个带有目标颜色的ForegroundColorSpan,然后把这个spannableStringBuilder作为一个shortLabel传递给spannableStringBuilder(因为SpannableStringBuilder就是一个CharSequence):

findViewById(R.id.main_rank_button).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        ForegroundColorSpan colorSpan = new ForegroundColorSpan(getResources().getColor(android.R.color.holo_red_dark, getTheme()));
        String label = "catinean.com";
        SpannableStringBuilder colouredLabel = new SpannableStringBuilder(label);
        colouredLabel.setSpan(colorSpan, 0, label.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        ShortcutInfo webShortcut = new ShortcutInfo.Builder(MainActivity.this, "shortcut_web")
                .setShortLabel(colouredLabel)
                .setRank(1)
                .build();
        ...
    }});

app_shortcut_colour.gif

本文demo的github地址: https://github.com/Electryc/AppShortcutsDemo

译者注

关于动态快捷方式,本文并没有明确的说明其使用场景,本文动态快捷方式的举例也完全可以用静态快捷方式替代。其实动态快捷方式主要应用于这种场景。假设一个阅读类app,用户要把某篇指定的文章页面或者某个栏目页面放入快捷方式,显然静态的就不能做到了,因为这个时候构造intent的参数是由用户自己去指定的。

最后附上官网地址:https://developer.android.com/preview/shortcuts.html