简介
在Android 7.1 中引入了Launcher Shortcuts API。
该API允许应用定义一组Intent,当用户长按应用启动器图标时,自定义的Intent就会显示。
特点:
-
每个快捷方式都引用一个Intent,每个Intent都会启动一个特定的动作或任务;
-
可以通过将应用程序快捷方式添加到APK中的资源文件中,来为其添加静态快捷方式,也可以动态添加。
静态快捷方式一般是常见的理想操作,动态快捷方式可以是用户的偏好,行为等要突出显示的操作;
-
每个应用最多5个快捷键;
源码
主要用的有三个类MyReceiver、ShortcutHelper和MainActivity。
ShortcutHelper类
该类主要是用于对快捷方式执行创建、注册、查找等操作。
源码:
public class ShortcutHelper {
private static final String TAG = "ShortcutHelper";
/**
* 直接刷新
*/
private static final String EXTRA_LAST_REFRESH = "com.zcd.myappshortcuts.EXTRA_LAST_REFRESH";
/**
* 刷新间隔时间
*/
private static final long REFRESH_INTERVAL_MS = 60 * 60 * 1000;
private final Context mContext;
private final ShortcutManager mShortcutManager;
public ShortcutHelper(Context mContext) {
this.mContext = mContext;
this.mShortcutManager = mContext.getSystemService(ShortcutManager.class);
}
/**
* 动态发布快捷方法时需要调用此方法
*/
public void maybeRestoreAllDynamicShortcuts(){
if (mShortcutManager.getDynamicShortcuts().size() == 0){
//TODO 动态快捷方式在重装app后并不会恢复
}
}
/**
* 发布快捷方式的应用程序应在用户选择包含给定ID的快捷方式时,
* 或当用户在应用程序中完成相当于选择快捷方式的操作时调用此方法。
* @param id 给定ID
*/
public void reportShortcutUsed(String id){
mShortcutManager.reportShortcutUsed(id);
}
/**
* 获取调用ShortcutManager是否成功
* @param supplier 结果
*/
private void callShortcutManager(BooleanSupplier supplier){
try {
if (!supplier.getAsBoolean()){
Utils.showToast(mContext, "调用ShortcutManager已到达限制");
}
}catch (Exception e){
Log.e(TAG, "callShortcutManager: " + e);
Utils.showToast(mContext, "在调用ShortcutManager时发生错误:" + e.getMessage());
}
}
/**
* 从此应用程序自身返回所有可变快捷方式
* @return 所有可变快捷方式
*/
public List<ShortcutInfo> getShortcuts(){
//快捷方式信息
final List<ShortcutInfo> ret = new ArrayList<>();
//快捷方式id
final HashSet<String> seenKeys = new HashSet<>();
//获取动态快捷方式
for (ShortcutInfo shortcut :
mShortcutManager.getDynamicShortcuts()) {
if (!shortcut.isImmutable()) {
ret.add(shortcut);
seenKeys.add(shortcut.getId());
}
}
//获取静态快捷方式
for (ShortcutInfo shortcut :
mShortcutManager.getPinnedShortcuts()) {
if (!shortcut.isImmutable() && !seenKeys.contains(shortcut.getId())){
ret.add(shortcut);
seenKeys.add(shortcut.getId());
}
}
return ret;
}
/**
* 当Activity开始运行时调用
* 获取所有已推送快捷方式并刷新
* @param force 是否强制
*/
public void refreshShortcuts(final boolean force) {
new AsyncTask<Void, Void, Void>(
) {
@Override
protected Void doInBackground(Void... voids) {
Log.i(TAG, "doInBackground: 正在刷新快捷方式");
final long now = System.currentTimeMillis();
//检查时间是否到达更新的阈值
final long staleThreshold = force ? now : now - REFRESH_INTERVAL_MS;
final List<ShortcutInfo> updateList= new ArrayList<>();
for (ShortcutInfo shortcut :
getShortcuts()) {
//如果是不变的
if (shortcut.isImmutable()){
continue;
}
Log.i(TAG, "doInBackground: 刷新快捷方式->" + shortcut.getId());
final ShortcutInfo.Builder builder =
new ShortcutInfo.Builder(mContext, shortcut.getId());
setSiteInformation(builder, shortcut.getIntent().getData());
setExtras(builder);
updateList.add(builder.build());
}
if (updateList.size() > 0){
callShortcutManager(()->mShortcutManager.updateShortcuts(updateList));
}
return null;
}
}.execute();
}
/**
* 为指定的url创建快捷方式
* @param urlAsString 目标url
* @return 快捷方式
*/
private ShortcutInfo createShortcutForUrl(String urlAsString){
Log.i(TAG, "createShortcutForUrl: 根据URL创建快捷方式->" + urlAsString);
final ShortcutInfo.Builder builder = new ShortcutInfo.Builder(mContext, urlAsString);
final Uri uri = Uri.parse(urlAsString);
builder.setIntent(new Intent(Intent.ACTION_VIEW, uri));
setSiteInformation(builder, uri);
setExtras(builder);
return builder.build();
}
private ShortcutInfo.Builder setExtras(ShortcutInfo.Builder builder){
final PersistableBundle extras = new PersistableBundle();
extras.putLong(EXTRA_LAST_REFRESH, System.currentTimeMillis());
builder.setExtras(extras);
return builder;
}
/**
* 设置快捷方式的图标及其他信息
* @param builder 对应快捷方式构造器
* @param uri 图标地址
* @return 完整快捷方式
*/
private ShortcutInfo.Builder setSiteInformation(ShortcutInfo.Builder builder, Uri uri){
//TODO 可在此处进行本地化处理
builder.setShortLabel(uri.getHost());
builder.setLongLabel(uri.toString());
Bitmap bmp = fetchFavicon(uri);
if (bmp != null) {
builder.setIcon(Icon.createWithBitmap(bmp));
} else {
builder.setIcon(Icon.createWithResource(mContext, R.drawable.link));
}
return builder;
}
/**
* 过滤非法url
* @param urlAsString 目标url
* @return 合法url
*/
private String normalizeUrl(String urlAsString){
if (urlAsString.startsWith("http://")
|| urlAsString.startsWith("https://")){
return urlAsString;
}else {
return "http://" + urlAsString;
}
}
/**
* 添加快捷方式
* @param urlAsString 快捷方式指定的url
*/
public void addWebSiteShortcut(String urlAsString){
final String uriFinal = urlAsString;
callShortcutManager(()->{
final ShortcutInfo shortcut = createShortcutForUrl(normalizeUrl(uriFinal));
return mShortcutManager.addDynamicShortcuts(Arrays.asList(shortcut));
});
}
/**
* 移除目标快捷方式
* @param shortcutInfo 目标快捷方式
*/
public void removeShortcut(ShortcutInfo shortcutInfo){
mShortcutManager.removeDynamicShortcuts(Arrays.asList(shortcutInfo.getId()));
}
/**
* 停用快捷方式
* @param shortcutInfo 目标快捷方式
*/
public void disableShortcut(ShortcutInfo shortcutInfo){
mShortcutManager.disableShortcuts(Arrays.asList(shortcutInfo.getId()));
}
/**
* 启用快捷方式
* @param shortcutInfo 目标快捷方式
*/
public void enableShortcut(ShortcutInfo shortcutInfo){
mShortcutManager.enableShortcuts(Arrays.asList(shortcutInfo.getId()));
}
/**
* 获取icon
* @param uri icon地址
* @return 可用icon
*/
private Bitmap fetchFavicon(Uri uri){
final Uri iconUri = uri.buildUpon().path("favicon.icon").build();
Log.i(TAG, "fetchFavicon: 已从指定uri获取icon->" + iconUri);
InputStream is = null;
BufferedInputStream bis = null;
try{
URLConnection conn = new URL(iconUri.toString()).openConnection();
conn.connect();
is = conn.getInputStream();
bis = new BufferedInputStream(is, 8192);
return BitmapFactory.decodeStream(bis);
}catch (Exception e){
Log.i(TAG, "fetchFavicon: 加载icon失败");
return null;
}
}
}
ShortcutManager#reportShortcutUsed()方法是做什么的?
发布快捷方式的应用程序应在用户选择包含给定ID的快捷方式时, 或当用户在应用程序中完成相当于选择快捷方式的操作时调用此方法。(注册快捷方式)
在该项目中创建一个快捷方式的流程是什么?
步骤如下:
- 通过Context.getSystemService()方法获取ShortcutManager对象,用于更新快捷方式;
- 创建一个ShortcutInfo.Builder类对象;
- 为ShortcutInfo.Builder类对象添加意图;
- 为ShortcutInfo.Builder类对象设置本地化;
- 为ShortcutInfo.Builder类对象设置图标及标签;
- 为ShortcutInfo.Builder类对象设置最后一次的更新时间,用于定时更新;
- 执行ShortcutInfo.Builder类对象的build()方法,生成一个ShortcutInfo对象;
- 通过ShortcutManager.addDynamicShortcuts()方法将生成的快捷方式添加,此种方式添加的快捷方式在重装之后就会消失;
PersistableBundle类是什么?
从字符串键到各种类型值的映射。该类支持的类型集有目的地限制为简单对象,这些对象可以安全地持久化到磁盘并从磁盘恢复。
MyReceiver类
主要是用于接收更新快捷方式的。
源码:
public class MyReceiver extends BroadcastReceiver {
private static final String TAG = "MyReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive: " + intent);
if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())){
new ShortcutHelper(context).refreshShortcuts(true);
}
}
}
MainActivity类
主要是用于显示已有的网站、添加删除网站以及启用停用网站。
源码:
/**
* 主界面
* 使用ListActivity时,与其绑定的View中必须要有一个id为“@android:id/list”的ListView
* setListAdapter()方法也是ListActivity中的方法
*/
public class MainActivity extends ListActivity implements View.OnClickListener {
private static final String TAG = "MainActivity";
private static final String ID_ADD_WEBSITE = "add_website";
/**
* 用于接收静态快捷方式发出的意图
*/
private static final String ACTION_ADD_WEBSITE
= "com.zcd.myappshortcuts.ADD_WEBSITE";
private MyAdapter mAdapter;
private ShortcutHelper mHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHelper = new ShortcutHelper(this);
mHelper.maybeRestoreAllDynamicShortcuts();
mHelper.refreshShortcuts(false);
if (ACTION_ADD_WEBSITE.equals(getIntent().getAction())){
addWebSite();
}
mAdapter = new MyAdapter(this.getApplicationContext());
setListAdapter(mAdapter);
}
@Override
protected void onResume() {
super.onResume();
refreshList();
}
/**
* 添加新的网站
* 点击时会有弹窗
*/
private void addWebSite(){
Log.i(TAG, "addWebSite: 正在添加新的网址");
//让启动器建立一个预测模型(prediction model)
mHelper.reportShortcutUsed(ID_ADD_WEBSITE);
final EditText editUri = new EditText(this);
editUri.setHint("https://www.baidu.com");
editUri.setInputType(EditorInfo.TYPE_TEXT_VARIATION_URI);
new AlertDialog.Builder(this)
.setTitle("添加新的网址")
.setMessage("网站的URL")
.setView(editUri)
.setPositiveButton("添加", ((dialog, which) -> {
final String url = editUri.getText().toString().trim();
if (url.length() > 0){
addUriAsync(url);
}
}))
.show();
}
/**
* 添加网址
*/
public void onAddPressed(View view) {
addWebSite();
}
/**
* 异步添加uri
* @param uri 目标uri
*/
private void addUriAsync(String uri){
new AsyncTask<Void, Void, Void>(){
@Override
protected Void doInBackground(Void... voids) {
mHelper.addWebSiteShortcut(uri);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
refreshList();
}
}.execute();
}
private void refreshList(){
mAdapter.setShortcuts(mHelper.getShortcuts());
}
@Override
public void onClick(View v) {
final ShortcutInfo shortcutInfo
= (ShortcutInfo)((View) v.getParent()).getTag();
switch (v.getId()){
case R.id.disable:
if (shortcutInfo.isEnabled()){
mHelper.disableShortcut(shortcutInfo);
}else {
mHelper.enableShortcut(shortcutInfo);
}
refreshList();
break;
case R.id.remove:
mHelper.removeShortcut(shortcutInfo);
refreshList();
break;
}
}
private static final List<ShortcutInfo> EMPTY_LIST = new ArrayList<>();
/**
* 获取目标快捷方式的类型信息
* @param shortcutInfo 目标快捷方式
* @return 相关信息
*/
private String getType(ShortcutInfo shortcutInfo){
final StringBuilder sb = new StringBuilder();
String sep = "";
if (shortcutInfo.isDynamic()){
sb.append(sep)
.append("Dynamic");
sep = ",";
}
if (shortcutInfo.isPinned()){
sb.append(sep)
.append("Pinned");
sep = ",";
}
if (!shortcutInfo.isEnabled()){
sb.append(sep)
.append("Disabled");
sep = ",";
}
return sb.toString();
}
/**
* 用于显示所有Web网站
*/
private class MyAdapter extends BaseAdapter{
private final Context mContext;
private final LayoutInflater mInflater;
private List<ShortcutInfo> mList = EMPTY_LIST;
private MyAdapter(Context context) {
mContext = context;
mInflater = mContext.getSystemService(LayoutInflater.class);
}
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public boolean areAllItemsEnabled() {
return true;
}
@Override
public boolean isEnabled(int position) {
return true;
}
public void setShortcuts(List<ShortcutInfo> list){
mList = list;
notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final View view;
if (convertView != null){
view = convertView;
}else {
view = mInflater.inflate(R.layout.list_item, null);
}
bindView(view, position, mList.get(position));
return view;
}
public void bindView(View view, int position, ShortcutInfo shortcutInfo){
view.setTag(shortcutInfo);
final TextView line1 = view.findViewById(R.id.line1);
final TextView line2 = view.findViewById(R.id.line2);
line1.setText(shortcutInfo.getLongLabel());
line2.setText(getType(shortcutInfo));
final Button remove = view.findViewById(R.id.remove);
final Button disable = view.findViewById(R.id.disable);
disable.setText(
shortcutInfo.isEnabled() ? "停用" : "启用"
);
remove.setOnClickListener(MainActivity.this);
disable.setOnClickListener(MainActivity.this);
}
}
}
如何添加静态快捷方式?
即在<shortcuts>标签中声明一个<shortcut>子标签即可,如果想给该静态快捷方式添加相关功能可在该<shortcut>子标签中添加相关<intent>子标签,并为该意图添加action、目标包以及目标类等信息。
源码:
<?xml version="1.0" encoding="utf-8"?>
<shortcuts
xmlns:android="http://schemas.android.com/apk/res/android">
<!--静态快捷方式-->
<shortcut
android:shortcutId = "add_website"
android:icon="@drawable/add"
android:shortcutShortLabel="@string/add_new_website_short"
android:shortcutLongLabel="@string/add_new_website">
<!--android:targetPackage指明Intent的目标package-->
<!--android:targetClass指明Intent的目标类-->
<intent
android:action="com.zcd.myappshortcuts.ADD_WEBSITE"
android:targetPackage="com.zcd.myappshortcuts"
android:targetClass="com.zcd.myappshortcuts.MainActivity"/>
</shortcut>
</shortcuts>
EditText.setInputType()方法的取值是什么?
该方法用于定义的常量设置内容的类型。
LayoutInflater类是做什么的?
将布局XML文件实例化为其相应的视图对象。它从不直接使用。相反,使用Activity.GetLayoutFlater()或Context#getSystemService以检索已连接到当前上下文并为正在运行的设备正确配置的标准LayoutFlater实例。
要为自己的视图创建一个新的layoutFlater,可以使用cloneInContext(Context)克隆现有的ViewFactory,然后调用setFactory(工厂布局)包括你的工厂。
由于预编译的原因,XML文件的处理在很大程度上依赖于性能。因此,目前不可能在运行时将layoutFlater与XmlPullParser一起使用,而不是在纯XML文件上使用;它只适用于从编译资源(R.something文件)返回的XmlPullParser
注意:这个类不是线程安全的,一个给定的实例只能由一个线程访问。
关于BaseAdapter类的相关方法
-
hasStableIds()做什么的
当一个id始终引用同一个对象时,返回
true
;否则,返回
false
; -
areAllItemsEnabled()做什么的
如果启用了所有项,则返回
true
;否则,返回
false
; -
isEnabled()做什么的
目标项是否可用;
-
notifyDataSetChanged()如何实现的
在BaseAdapter类内部维护了一个DataSetObservable集合,集合中为所有订阅者,在调用该方法时会遍历该集合。
遍历方式为最后订阅的最先获取更新信息。
该方法是线程安全的。
相关布局文件
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:id="@+id/btn_add"
android:text="@string/add_new_website"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onAddPressed"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#444444"
android:text="@string/existing_shortcuts"/>
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
list_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<LinearLayout
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="16sp"
/>
<TextView
android:id="@+id/line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#444444"
/>
</LinearLayout>
<Button
android:id="@+id/remove"
android:text="@string/remove_shortcut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:visibility="visible"
style="@android:style/Widget.Material.Button.Borderless"/>
<Button
android:id="@+id/disable"
android:text="@string/disable_shortcut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:visibility="visible"
style="@android:style/Widget.Material.Button.Borderless"/>
</LinearLayout>
shortscuts.xml:
<?xml version="1.0" encoding="utf-8"?>
<shortcuts
xmlns:android="http://schemas.android.com/apk/res/android">
<!--静态快捷方式-->
<shortcut
android:shortcutId = "add_website"
android:icon="@drawable/add"
android:shortcutShortLabel="@string/add_new_website_short"
android:shortcutLongLabel="@string/add_new_website">
<!--android:targetPackage指明Intent的目标package-->
<!--android:targetClass指明Intent的目标类-->
<intent
android:action="com.zcd.myappshortcuts.ADD_WEBSITE"
android:targetPackage="com.zcd.myappshortcuts"
android:targetClass="com.zcd.myappshortcuts.MainActivity"/>
</shortcut>
</shortcuts>