老司机带你吃牛轧糖--适配Android 7.1 Nougat新特性
2017-02-05 16:30 阅读(251)

What's new in Android 7.1 Nougat?

Android 7.1 Nougat 已经推出有一段时间,相信大多数人和我一样,并没有用上最新的系统,但是,总有一群走在时代的前列线上的Geek们,勇于尝鲜,艰苦奋斗,为刷新版本号贡献自己的力量。好吧,实际上就是我还没有用上7.1,有些眼馋了。那么,和开发者息息相关的有哪些新特性呢?


Android 7.1 Nougat

本次主要介绍3个新特性:App ShortcutsRound Icon Resource 和 Image Keyboard Support。所有的新特性可以访问谷歌开发者中文博客的文章欢迎使用Android 7.1.1 Nougat

App Shortcuts

作为一个密切关注Android发展的伪Geek,在7.1正式版未发布之前,通过网上的一些爆料文章,我就了解到了这一新功能。实际上,这个功能刚开始出现时,我还以为Google Pixel要上压感屏了呢,事实证明,的确是我想多了。

App Shortcuts允许用户直接在启动器中显示一些操作,让用户立即执行应用的深层次的功能。触发这一功能的操作就是「长按」。这一功能类似于iOS中的「3D Touch」。

下面通过一张GIF,直观的感受一下App Shortcuts是怎样的。(由于我的一加3并没有升级到最新的7.1,还只是7.0,所以我安装了Nova Launcher来体验。)


App Shortcuts

长按图标,收到震动后松手,如果能够看到图标上弹出了支持的跳转操作,说明成功的呼出了Shortcuts功能,如果不支持这一功能,在Nova Launcher上弹出的就是卸载或者移除操作,在Pixel Launcher上不会出现弹出菜单,显示的是常见的长按操作。长按弹出的操作,可以将这个操作已快捷方式图标的形式直接放置在主屏上。如果长按主图标不松手,就可以调整位置了。

目前,一个应用最多可以支持 5 个Shortcut,可以通过getMaxShortcutCountPerActivity查看Launcher最多支持Shortcut的数量。每一个Shortcut都对应着一个或者多个intent,当用户选择某一个Shortcut时,应该做出特定的动作。下面是一些将一些特定的动作作为Shortcuts的例子:

App Shortcut可以分为两种不同的类型: Static Shortcuts(静态快捷方式) 和 Dynamic Shortcuts(动态快捷方式)。

Using Static Shortcuts

创建Static Shortcuts分为以下几步:

1.在工程的manifest文件 (AndroidManifest.xml)下,找到 intent filter设置为 android.intent.action.MAIN 和 android.intent.category.LAUNCHER 的Activity。

2.在次Activity下添加<meta-data>标签,引用定义shortcuts的资源文件。

<activity
        android:name=".homepage.MainActivity"
        android:configChanges="orientation|keyboardHidden|screenSize|screenLayout"
        android:label="@string/app_name"
        android:theme="@style/AppTheme.NoActionBar">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>

        <meta-data
            android:name="android.app.shortcuts"
            android:resource="@xml/shortcuts" />
    </activity>

3.创建新的资源文件res/xml/shortcuts.xml

<?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_search_circle"
        android:shortcutId="search_bookmarks"
        android:shortcutShortLabel="@string/search_bookmarks"
        android:shortcutLongLabel="@string/search_bookmarks">

        <intent
            android:action="android.intent.action.VIEW"
            android:targetPackage="com.marktony.zhihudaily"
            android:targetClass="com.marktony.zhihudaily.search.SearchActivity" />

        <!--如果你的一个shortcut关联着多个intent,你可以在这里继续添
            加。最后一个intent决定着用户在加载这个shortcut时会看到什么-->

        <categories android:name="android.shortcut.conversation" />

    </shortcut>

    <!--在这里添加更多的shortcut-->

</shortcuts>

shortcut下标签的含义:

到这里,最简单的shortcut就添加成功了。运行包含上面的文件的项目,点击shortcut就可以直接进入 SearchActivity,当按下back键时,直接就退出了应用。如果希望不退出应用,而是进入 MainActivity 时,应该怎么办呢?不用着急,在shortcut继续添加intent就可以了。

<?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_search_circle"
        android:shortcutId="search_bookmarks"
        android:shortcutShortLabel="@string/search_bookmarks"
        android:shortcutLongLabel="@string/search_bookmarks">

        <intent
                android:action="android.intent.action.MAIN"
                android:targetClass="com.marktony.zhihudaily.homepage.MainActivity"
            android:targetPackage="com.marktony.zhihudaily" />

        <intent
            android:action="android.intent.action.VIEW"
            android:targetPackage="com.marktony.zhihudaily"
            android:targetClass="com.marktony.zhihudaily.search.SearchActivity" />

        <categories android:name="android.shortcut.conversation" />

    </shortcut>

      <!--在这里添加更多的shortcut-->

 </shortcuts>

Using Dynamic Shortcuts

动态快捷方式应该和应用内的特定的、上下文敏感的action链接。这些action应该可以在用户的几次使用之间、甚至是在应用运行过程中被改变。好的候选action包括打电话给特定的人、导航至特定的地方、或者展示当前游戏的分数。

ShortcutManager API允许我们在动态快捷方式上完成下面的操作:

下面是在MainActivity的onCreate()中创建动态快捷方式的例子:

@Override
protected void onCreate(Bundle savedInstanceState) {

    ...

    ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);

    ShortcutInfo webShortcut = new ShortcutInfo.Builder(this, "shortcut_web")
            .setShortLabel("github")
            .setLongLabel("Open Tonny's github web site")
            .setIcon(Icon.createWithResource(this, R.drawable.ic_dynamic_shortcut))
            .setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("https://marktony.github.io")))
            .build();

    shortcutManager.setDynamicShortcuts(Collections.singletonList(webShortcut));
}

也可以为动态快捷方式创建返回栈。

@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));
}

创建一个新的空的Activity,名字叫做DynamicShortcutActivity,在manifest文件中注册。

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

通过清除array中的排序过的intents,当我们通过创建好的shortcut进入DynamicShortcutActivity之后,按下back键,MainActivity就会被加载。

需要注意的是,在动态创建快捷方式之前,最好是检查一下是否超过了所允许的最大值。否则会抛出相应的异常。

Extra Bits

在 ShortcutInfo.Builder 中有一个专门的方法 setRank(int) ,通过设置不同的等级,我们就可以控制动态快捷方式的出现顺序,等级越高,出现在快捷方式列表中的位置就越高。

ForegroundColorSpan colorSpan = new ForegroundColorSpan(getResources().getColor(android.R.color.holo_red_dark, getTheme()));
  String label = "github";
  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 Shortcuts Best Practices

当设计和创建应用的shortcuts时,应该遵守下面的指导建议:

Round Icon Resources

在Android 7.1上,Google推出了一个部分用户可能不太喜欢的特性--圆形图标。圆形图标长什么样,可以看看下面的图。


round icon

同时,圆形图标规范也作为一部分内容加入到了更新说明和开发文档中。
应用程序现在可以定义圆形启动器图标以用于特定的移动设备之上。当启动器请求应用程序图标时,程序框架应返回 android:icon 或 android:roundIcon,视设备具体要求而定。因此,应用程序在开发时应该确保同时定义 android:icon和 android:roundIcon 两个变量。您可以使用 Image Asset Studio 来设计圆形图标。

您应该确保在支持新的圆形图标的设备上测试您的应用程序,以确保应用程序图标的外观无虞和实际效果。测试您的资源的一种方法是在 Google Pixel 设备上安装您的应用。您还可以通过运行 Android 模拟器并使用 Google API 模拟器系统(目标 API 等级为 25)测试您的图标。

我们可以通过 Android Studio 自带的 Image Asset Studio设计图标。在项目的 res 目录下点击鼠标右键,选择 new --> Image Asset 即可设计图标。


Image Asset Studio

更多关于设计应用图标的信息,可以参考Material Design guidelines

Image Keyboard Support

在较早版本的Android系统中,软键盘(例如我们所熟知的Input Method Editors,或者说IME),只能够给应用发送unicode编码的emoji,对于rich content,应用只能通过使用自建的私有的API实现发送图片的功能。而在Android 7.1中,SDK包含了一个全新的Commit Content API,输入法应用不仅可以调用此 API 实现发送图片和其他rich content,一些通讯应用(比如 Google Messenger)也可以通过此 API 来更好地处理这些来自输入法的图片、网页信息和 GIF 内容。


image keyboard sample

How it works

 1. 当用户点击EditText时, editor会发送一个它所能接受的 EditorInfo.contentMimeTypes MIME 内容类型的列表。

  2. IME读取这个在软键盘中支持类型和展示内容的列表。

  3. 当用户选择一张图片后,IME调用 commitContent() 并向editor发送一个InputContentInfo。 commitContent() 方法是一个类似于 commitText() 的方法,但是是rich content的。 InputContentInfo 包含着一个表示content provider中内容的URI。然后我们的应用就可以请求相应的权限并读取URI中的内容。


image keyboard diagram

Adding Image Support to Apps

为了接收来自IME的rich content,应用必须告诉IME它所能接收的内容类型并之指定当接收到内容后的回调方法。下面是一个怎样创建一个能够接收PNG图片的 EditText 的演示代码。

EditText editText = new EditText(this) {
    @Override
    public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
        final InputConnection ic = super.onCreateInputConnection(editorInfo);
        EditorInfoCompat.setContentMimeTypes(editorInfo,
                new String [] {"image/png"});

        final InputConnectionCompat.OnCommitContentListener callback =
            new InputConnectionCompat.OnCommitContentListener() {
                @Override
                public boolean onCommitContent(InputContentInfoCompat inputContentInfo,
                        int flags, Bundle opts) {
                    // read and display inputContentInfo asynchronously
                    if (BuildCompat.isAtLeastNMR1() && (flags &
                        InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
                        try {
                            inputContentInfo.requestPermission();
                        }
                        catch (Exception e) {
                            return false; // return false if failed
                        }
                    }

                    // read and display inputContentInfo asynchronously.
                    // call inputContentInfo.releasePermission() as needed.

                    return true;  // return true if succeeded
                }
            };
        return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
    }
};

代码还是蛮多的,解释一下。

下面是一些实践小技巧。

为了测试APP,需要确保你的设备或者虚拟机的键盘能够发送rich content。你可以在Android 7.1或者更高的系统中使用Google Keyboard,或者是安装CommitContent IME sample.

你可以在CommitContent App sample获取到完整的示例代码。

Adding Image Support to IMEs

想要IME支持发送rich content,需要引入下面所展示的Commit Content API。

@Override
  public void onStartInputView(EditorInfo info, boolean restarting) {
      String[] mimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo);

      boolean gifSupported = false;
      for (String mimeType : mimeTypes) {
          if (ClipDescription.compareMimeTypes(mimeType, "image/gif")) {
              gifSupported = true;
          }
      }

      if (gifSupported) {
          // the target editor supports GIFs. enable corresponding content
      } else {
          // the target editor does not support GIFs. disable corresponding content
      }
  }

当用户选择了一张图片时,将内容提交给APP。当IME有正在编辑的文本时,应该避免调用 commitContent() ,因为这样可能导致editor失去焦点。下面的代码片段展示了怎样提交一张GIF图片。

/**
   * Commits a GIF image
   *
   * @param contentUri Content URI of the GIF image to be sent
   * @param imageDescription Description of the GIF image to be sent
   */
  public static void commitGifImage(Uri contentUri, String imageDescription) {
      InputContentInfoCompat inputContentInfo = new InputContentInfoCompat(
              contentUri,
              new ClipDescription(imageDescription, new String[]{"image/gif"}));
      InputConnection inputConnection = getCurrentInputConnection();
      EditorInfo editorInfo = getCurrentInputEditorInfo();
      Int flags = 0;
      If (android.os.Build.VERSION.SDK_INT >= 25) {
          flags |= InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION;
      }
      InputConnectionCompat.commitContent(
              inputConnection, editorInfo, inputContentInfo, flags, opts);
  }

权限授予的例子,我们可以在CommitContent IME sample中的doCommitContent()方法。

为了测试IME,确保我们的设备或者模拟器拥有接收rich content的的应用。我们可以在Android 7.1或者更高的系统中使用Google Messenger应用或者安装CommitContent App Sample

获取完整的示例代码,可以访问CommitContent IME Sample

Summary

Google在刷新版本号的路上简直是在策马奔腾了,嘚儿驾。我们也能够看到Google的努力,Android也在变的越来越好,加油吧,小机器人。

本次Shortcuts部分的代码可以在我的GitHub仓库ZhiHuDaily中看到。欢迎star哟。


作者:TonnyL