老司机带你吃牛轧糖 - 适配 Android 7.1 Nougat 新特性

What's new in Android 7.1 Nougat?

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

Android 7.1 Nougat

本次主要介绍3个新特性:App Shortcuts, Round Icon ResourceImage Keyboard Support。全部的新特性能够访问谷歌开发者中文博客的文章欢迎使用Android 7.1.1 Nougatjava

App Shortcuts

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

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

下面经过一张GIF,直观的感觉一下App Shortcuts是怎样的。(因为个人一加3并无升级到最新的7.1,还只是7.0,因此我安装了Nova Launcher来体验。)github

App Shortcuts

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

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

  • 在地图APP中,指引用户至最经常使用的位置框架

  • 在聊天APP中,发送信息至某个好友异步

  • 在多媒体APP中,播放下一个电视节目async

  • 在游戏APP中,加载至上次保存的地方

App Shortcut能够分为两种不一样的类型: Static Shortcuts(静态快捷方式) 和 Dynamic Shortcuts(动态快捷方式)。

  • Static Shortcuts:在打包到apk的资源文件中定义,因此,直到下一次更新版本时才能改变静态快捷方式的详细说明。

  • Dynamic Shortcuts:经过ShortcutManager API在运行时发布,在运行时,应用能够发布,升级和移除快捷方式。

Using Static Shortcuts

建立Static Shortcuts分为如下几步:

1.在工程的manifest文件 (AndroidManifest.xml)下,找到 intent filter设置为 android.intent.action.MAINandroid.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下标签的含义:

  • enabled:见名知意,shortcut是否可用。若是你决定让这个static shortcut不在可用的话,可直接将其设置为 false ,或者直接从 shortcuts 标签中移除。

  • icon:显示在左边的图标,可用使用Vector drawable

  • shortcutDisabledMessage:当禁用此shortcut后,它仍然会出如今用户长按应用图标后的快捷方式列表里,也能够被拖动并固定到桌面上,可是它会呈现灰色而且用户点击时会弹出Toast这个标签所定义的内容。

  • shortcutLongLabel:当启动器有足够多的空间时,会显示这个标签所定义的内容。

  • shortcutShortLabel:shortcut的简要说明,是必需字段。当shortcut被添加到桌面上时,显示的也是这个字段。

  • intent:shortcut关联的一个或者多个intent,当用户点击shortcut时被打开。

  • shortcutId:shortcut的惟一标示id,若存在具备相同shortcutId的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容许咱们在动态快捷方式上完成下面的操做:

  • 发布:使用setDynamicShortcuts()从新定义整个动态快捷方式列表,或者是使用addDynamicShortcuts()向已存在的动态快捷方式列表中添加快捷方式。

  • 更新:使用updateShortcuts()方法。

  • 移除:使用removeDynamicShortcuts()方法移除特定动态快捷方式或者使用removeAllDynamicShortcuts()移除全部动态快捷方式。

下面是在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

  • 当static shortcut 和 dynamic shortcut一块儿展现时,其出现的顺序是怎样定制呢?

    在 **ShortcutInfo.Builder** 中有一个专门的方法 **setRank(int)** ,经过设置不一样的等级,咱们就能够控制动态快捷方式的出现顺序,等级越高,出如今快捷方式列表中的位置就越高。
  • 咱们还能够设置动态快捷方式的shortLabel的字体颜色。

    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时,应该遵照下面的指导建议:

  • 遵循设计规范:为了保持咱们的应用和系统应用的快捷方式在视觉上一致性,应该遵照App Shortcuts Design Guidelines

  • 发布4个不一样的快捷方式:尽管如今的API支持静态和动态总共5个快捷方式,可是为了提升shortcut的视觉效果,建议只添加4个不一样的快捷方式。

  • 限制快捷方式描述的文本长度:在Launcher中,显示快捷方式时,空间长度受到了限制。若是可能的话,应该将「short description」的文字长度控制在10个字母之内,将「long discription」的长度限制在25个字母之内。

  • 保存shortcut和action的历史记录:建立的每个shortcut,应该考虑到用户可以经过不一样的方式完成相同的任务。在这种状况下,记得调用 reportShortcutUsed() 方法,这样,launcher就能够提升shortcut对应的actions的反应速度。

  • 只有在shortcuts的意义存在时更新:当改变更态快捷方式时,只有在shortcut仍然保持着它的含义时,调用 updateShortcuts() 方法改变它的信息。不然,应该使用addDynamicShortcuts() 或者 setDynamicShortcuts() 建立一个具备新含义的ID的快捷方式。

    举个例子,若是咱们已经建立了导航到一个超市的快捷方式,若是超市的名称改变了可是位置并无变化时,只更新信息是合适的。可是若是用户开始在一个不一样位置的超市购物时,最好是建立一个全新的快捷方式(而不只仅是更新信息了)。
  • 在备份和恢复时,动态shortcuts不该该被保存:正是由于这个缘由,推荐咱们在须要APP启动和从新发布动态快捷方式时,检查 getDynamicShortcuts() 的对象的数量。能够参考Backup and Restore部分的代码片断。

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

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

  • 例子使用了support library,而且引用的是 android.support.v13.view.inputmethod 而不是 android.view.inputmethod

  • 例子建立了一个 EditText 并复写了它改变 InputConnectiononCreateInputConnection(EditorInfo) 方法. InputConnection 是IME和正在接收输入的沟通管道。

  • 调用 super.onCreateInputConnection() 保留了内建的行为(包括发送和接收文本),并提供给咱们一个 InputConnection 的引用。

  • setContentMimeTypes()EditorInfo 添加了一个所支持的MIME类型的列表。 须要保证在 setContentMimeTypes() 以前调用 super.onCreateInputConnection()

  • 回调在IME提交内容是被执行。 onCommitContent() 方法有一个对包含了内容URI的 InputContentInfoCompat 的引用。

    • 当咱们的应用运行在API Level 25或者更高而且IME设置了 INPUT_CONTENT_GRANT_READ_URI_PERMISSION flag时,咱们应该请求而且释放权限。不然,咱们应该在此以前就拥有content URI的访问权限,一是由于权限是由IME受权的,二是content provider不对访问进行约束。更多的信息能够访问Adding Image Support to IMEs

  • createWrapper() 包装了inputConnection和已修改的editorInfo,新的InputConnection的回调而且返回。

下面是一些实践小技巧。

  • 不支持rich content的Editor不该该调用 setContentTypes() 并把 EditorInfo.contentMimeTypes 设置为null。

  • Editor应该忽略掉在 InputConnectionInfo 中指定的MIME类型和所接收类型不通的内容。

  • rich content不影响也不被文本指针的位置所影响。editor在进行内容处理是能够直接忽略掉光标的位置。

  • 在editor的 OnCommitContentListener.onCommitContent() 方法中,咱们能够异步的返回true,甚至是在加载内容以前。

  • 不一样于文本内容在被提交以前能够在IME中被编辑,rich content会被当即提交。须要注意特性,若是想要提供编辑或者删除内容的能力,咱们须要本身提供处理逻辑。

为了测试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。

  • 复写 onStartInput() 或者 onStartInputView() ,并读取来自目标editor的支持内容类型列表。

    @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);
    }
  • 做为一个IME开发者,有很大可能你须要引入你本身的content provider来响应content URI请求。若是你的IME支持来自像 MediaStore 这样已经存在的content provider却是能够例外。关于建立content provider的更多信息,能够参见 CommitContent IME sample, [Content Provider] (https://developer.android.com...文档, File Provider文档。

  • 若是正在建立本身的content provider,建议不要export(将 android:export 设置为false)。经过设置 android:grandUriPermission 为true容许在provider内部进行权限授予替代。而后,你的IME在内容提交时能够授予访问content URI的权限。有两种实现的方法:

    • 在Android 7.1(API Level 25)或更高的系统中,当调用 commitContent 方法时,将flag参数设置为 INPUT_CONTENT_GRANT_READ_URI_PERMISSION 。而后,APP收到的 InputContentInfo 对象能够经过调用 requestPermission() 方法和 releasePermission() 请求和释放临时访问权限。

    • 在Android 7.0(API Level 24)或者更低的系统中, INPUT_CONTENT_GRANT_READ_URI_PERMISSION 直接被忽略,因此咱们须要手动的授予内容访问权限。方法就是 grantUriPermission() ,可是咱们也能够引入知足本身要求的机制。

权限授予的例子,咱们能够在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哟。

相关文章
相关标签/搜索