From : http://blog.csdn.net/bat1992/article/details/54862956
What's new in Android 7.1 Nougat?
Android 7.1 Nougat 已经推出有一段时间,相信大多数人和我一样,并没有用上最新的系统,但是,总有一群走在时代的前列线上的Geek们,勇于尝鲜,艰苦奋斗,为刷新版本号贡献自己的力量。好吧,实际上就是我还没有用上7.1,有些眼馋了。那么,和开发者息息相关的有哪些新特性呢?
本次主要介绍3个新特性:App Shortcuts, Round 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来体验。)
长按图标,收到震动后松手,如果能够看到图标上弹出了支持的跳转操作,说明成功的呼出了Shortcuts功能,如果不支持这一功能,在Nova Launcher上弹出的就是卸载或者移除操作,在Pixel Launcher上不会出现弹出菜单,显示的是常见的长按操作。长按弹出的操作,可以将这个操作已快捷方式图标的形式直接放置在主屏上。如果长按主图标不松手,就可以调整位置了。
目前,一个应用最多可以支持 5 个Shortcut,可以通过getMaxShortcutCountPerActivity查看Launcher最多支持Shortcut的数量。每一个Shortcut都对应着一个或者多个intent,当用户选择某一个Shortcut时,应该做出特定的动作。下面是一些将一些特定的动作作为Shortcuts的例子:
-
在地图APP中,指引用户至最常用的位置
-
在聊天APP中,发送信息至某个好友
-
在多媒体APP中,播放下一个电视节目
-
在游戏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.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下标签的含义:
-
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推出了一个部分用户可能不太喜欢的特性--圆形图标。圆形图标长什么样,可以看看下面的图。
同时,圆形图标规范也作为一部分内容加入到了更新说明和开发文档中。
应用程序现在可以定义圆形启动器图标以用于特定的移动设备之上。当启动器请求应用程序图标时,程序框架应返回 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 即可设计图标。
更多关于设计应用图标的信息,可以参考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 内容。
How it works
-
当用户点击EditText时, editor会发送一个它所能接受的 EditorInfo.contentMimeTypes MIME 内容类型的列表。
-
IME读取这个在软键盘中支持类型和展示内容的列表。
-
当用户选择一张图片后,IME调用 commitContent() 并向editor发送一个InputContentInfo。 commitContent() 方法是一个类似于 commitText() 的方法,但是是rich content的。 InputContentInfo 包含着一个表示content provider中内容的URI。然后我们的应用就可以请求相应的权限并读取URI中的内容。
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 并复写了它改变 InputConnection 的 onCreateInputConnection(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/guide%20/topics/providers/content-providers.html)文档, 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。