Android之AppWidget(桌面小部件)开发浅析
什么是appwidget
appwidget 即桌面小部件,也叫桌面控件,就是能直接显示在android系统桌面上的小程序,先看图:
图中我用黄色箭头指示的即为appwidget,一些用户使用比较频繁的程序,可以做成appwidget,这样能方便地使用。典型的程序有时钟、天气、音乐播放器等。appwidget 是android 系统应用开发层面的一部分,有着特殊用途,使用得当的化,的确会为app 增色不少,它的工作原理是把一个进程的控件嵌入到别外一个进程的窗口里的一种方法。长按桌面空白处,会出现一个 appwidget 的文件夹,在里面找到相应的 appwidget ,长按拖出,即可将 appwidget 添加到桌面,
如何开发appwidget
appwidget 是通过 broadcastreceiver 的形式进行控制的,开发 appwidget 的主要类为 appwidgetprovider, 该类继承自 broadcastreceiver。为了实现桌面小部件,开发者只要开发一个继承自 appwidgetprovider 的子类,并重写它的 onupdate() 方法即可。重写该方法,一般来说可按如下几个步骤进行:
1、创建一个 remoteviews 对象,这个对象加载时指定了桌面小部件的界面布局文件。
2、设置 remoteviews 创建时加载的布局文件中各个元素的属性。
3、创建一个 componentname 对象
4、调用 appwidgetmanager 更新桌面小部件。
下面来看一个实际的例子,用 android studio 自动生成的例子来说。(注:我用的是最新版的 as 2.2.3,下面简称 as。)
新建了一个 helloworld 项目,然后新建一个 appwidget ,命名为 myappwidgetprovider,按默认下一步,就完成了一个最简单的appwidget的开发。运行程序之后,将小部件添加到桌面。操作步骤和默认效果如下:
我们看看 as 为我们自动生成了哪些代码呢?对照着上面说的的步骤我们来看看。
首先,有一个 myappwidgetprovider 的类。
package com.example.joy.remoteviewstest; import android.appwidget.appwidgetmanager; import android.appwidget.appwidgetprovider; import android.content.context; import android.widget.remoteviews; /** * implementation of app widget functionality. */ public class myappwidgetprovider extends appwidgetprovider { static void updateappwidget(context context, appwidgetmanager appwidgetmanager, int appwidgetid) { charsequence widgettext = context.getstring(r.string.appwidget_text); // construct the remoteviews object remoteviews views = new remoteviews(context.getpackagename(), r.layout.my_app_widget_provider); views.settextviewtext(r.id.appwidget_text, widgettext); // instruct the widget manager to update the widget appwidgetmanager.updateappwidget(appwidgetid, views); } @override public void onupdate(context context, appwidgetmanager appwidgetmanager, int[] appwidgetids) { // there may be multiple widgets active, so update all of them for (int appwidgetid : appwidgetids) { updateappwidget(context, appwidgetmanager, appwidgetid); } } @override public void onenabled(context context) { // enter relevant functionality for when the first widget is created } @override public void ondisabled(context context) { // enter relevant functionality for when the last widget is disabled } }
该类继承自 appwidgetprovider ,as默认帮我们重写 onupdate() 方法,遍历 appwidgetids, 调用了 updateappwidget() 方法。再看 updateappwidget() 方法,很简单,只有四行:
第一行,charsequence widgettext = context.getstring(r.string.appwidget_text);声明了一个字符串;
第二行,remoteviews views = new remoteviews(context.getpackagename(), r.layout.my_app_widget_provider);
创建了一个 remoteviews 对象,第一个参数传应用程序包名,第二个参数指定了,remoteviews 加载的布局文件。这一行对应上面步骤中说的第一点。可以看到在 res/layout/ 目录下面 as 自动生成了一个 my_app_widget_provider.xml 文件,内容如下:
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#09c" android:padding="@dimen/widget_margin"> <textview android:id="@+id/appwidget_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerhorizontal="true" android:layout_centervertical="true" android:layout_margin="8dp" android:background="#09c" android:contentdescription="@string/appwidget_text" android:text="@string/appwidget_text" android:textcolor="#ffffff" android:textstyle="bold|italic" /> </relativelayout>
这个文件就是我们最后看到的桌面小部件的样子,布局文件中只有一个textview。这是你可能会问,想要加图片可以吗?可以,就像正常的activity布局一样添加 imageview 就行了,聪明的你可能开始想自定义小部件的样式了,添加功能强大外观漂亮逼格高的自定义控件了,很遗憾,不可以。小部件布局文件可以添加的组件是有限制的,详细内容在下文介绍remoteviews 时再说。
第三行,views.settextviewtext(r.id.appwidget_text, widgettext);
将第一行声明的字符串赋值给上面布局文件中的 textview,注意这里赋值时,指定textview的 id,要对应起来。这一行对于了上面步骤中的第二点。
第四行,appwidgetmanager.updateappwidget(appwidgetid, views);
这里调用了 appwidgetmanager.updateappwidget() 方法,更新小部件。这一行对应了上面步骤中的第四点。
这时,你可能有疑问了,上面明明说了四个步骤,其中第三步,创建一个 componentname 对象,明明就不需要。的确,这个例子中也没有用到。如果我们手敲第四步代码,as的智能提示会告诉你,appwidgetmanager.updateappwidget() 有三个重载的方法。源码中三个方法没有写在一起,为了方便,这里我复制贴出官方 api 中的介绍
void |
updateappwidget(componentname provider, remoteviews views) set the remoteviews to use for all appwidget instances for the supplied appwidget provider. |
|||||||||||||
void |
updateappwidget(int[] appwidgetids, remoteviews views) set the remoteviews to use for the specified appwidgetids. |
|||||||||||||
void |
updateappwidget(int appwidgetid, remoteviews views) set the remoteviews to use for the specified appwidgetid. |
这个三个方法都接收两个参数,第二个参数都是 remoteviews 对象。其中第一个方法的第一个参数就是 componentname 对象,更新所有的 appwidgetprovider 提供的所有的 appwidget 实例,第二个方法时更新明确指定 id 的 appwidget 的对象集,第三个方法,更新明确指定 id 的某个 appwidget 对象。所以一般我们使用第一个方法,针对所有的 appwidget 对象,我们也可以根据需要选择性地去更新。
到这里,所有步骤都结束了,就完了?还没。前面说了,自定义的 myappwidgetprovider 继承自 appwidgetprovider,而 appwidgetprovider 又是继承自 broadcastreceiver,
所以说 myappwidgetprovider 本质上是一个广播接受者,属于四大组件之一,需要我们的清单文件中注册。打开androidmanifest.xml文件可以看到,的确是注册了小部件的,内容如下:
<receiver android:name=".myappwidgetprovider"> <intent-filter> <action android:name="android.appwidget.action.appwidget_update" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/my_app_widget_provider_info" /> </receiver>
上面代码中有一个 action,这个 action 必须要加,且不能更改,属于系统规范,是作为小部件的标识而存在的。如果不加,这个 receiver 就不会出现在小部件列表里面。然后看到小部件指定了 @xml/my_app_widget_provider_info 作为meta-data,细心的你发现了,在 res/ 目录下面建立了一个 xml 文件夹,下面新建了一个 my_app_widget_provider_info.xml 文件,内容如下:
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialkeyguardlayout="@layout/my_app_widget_provider" android:initiallayout="@layout/my_app_widget_provider" android:minheight="40dp" android:minwidth="40dp" android:previewimage="@drawable/example_appwidget_preview" android:resizemode="horizontal|vertical" android:updateperiodmillis="86400000" android:widgetcategory="home_screen"> </appwidget-provider>
这里配置了一些小部件的基本信息,常用的属性有 initiallayout 就是小部件的初始化布局, minheight 定义了小部件的最小高度,previewimage 指定了小部件在小部件列表里的预览图,updateperiodmillis 指定了小部件更新周期,单位为毫秒。更多属性,可以查看api文档。
到这里,上面这个极简单的小部件开发过程就真的结束了。为了开发出更强大一点小部件,我们还需要进一步了解 remoteviews 和 appwidgetprovider。
appwidget的妆容——remoteviews
下面简单说说 remoteviews 相关的几个类。
1.1 remoteviews
remoteviews,从字面意思理解为它是一个远程视图。是一种远程的 view,它在其它进程中显示,却可以在另一个进程中更新。remoteviews 在android中的使用场景主要有:自定义通知栏和桌面小部件。
在remoteviews 的构造函数中,第二个参数接收一个 layout 文件来确定 remoteviews 的视图;然后,我们调用remoteviews 中的 set 方法对 layout 中的各个组件进行设置,例如,可以调用 settextviewtext() 来设置 textview 组件的文本。
前面提到,小部件布局文件可以添加的组件是有限制的,它可以支持的 view 类型包括四种布局:framelayout、linearlayout、relativelayout、gridlayout 和 13 种view: analogclock、button、chronometer、imagebutton、imageview、progressbar、textview、viewflipper、listview、gridview、stackview、adapterviewflipper、viewsub。注意:remoteviews 也并不支持上述 view 的子类。
remoteviews 提供了一系列 setxxx() 方法来为小部件的子视图设置属性。具体可以参考 api 文档。
1.2 remoteviewsservice
remoteviewsservice,是管理remoteviews的服务。一般,当appwidget 中包含 gridview、listview、stackview 等集合视图时,才需要使用remoteviewsservice来进行更新、管理。remoteviewsservice 更新集合视图的一般步骤是:
(01) 通过 setremoteadapter() 方法来设置 remoteviews 对应 remoteviewsservice 。
(02) 之后在 remoteviewsservice 中,实现 remoteviewsfactory 接口。然后,在 remoteviewsfactory 接口中对集合视图的各个子项进行设置,例如 listview 中的每一item。
1.3 remoteviewsfactory
通过remoteviewsservice中的介绍,我们知道remoteviewsservice是通过 remoteviewsfactory来具体管理layout中集合视图的,remoteviewsfactory是remoteviewsservice中的一个内部接口。remoteviewsfactory提供了一系列的方法管理集合视图中的每一项。例如:
remoteviews getviewat(int position)
通过getviewat()来获取“集合视图”中的第position项的视图,视图是以remoteviews的对象返回的。
int getcount()
通过getcount()来获取“集合视图”中所有子项的总数。
appwidget的美貌——appwidgetprovider
我们说一位女同事漂亮,除了因为她穿的衣服、化的妆漂亮以外,我想最主要的原因还是她本人长的漂亮吧。同样,小部件之所以有附着在桌面,跨进程更新 view 的能力,主要是因为appwidgetprovider 是一个广播接收者。
我们发现,上面的例子中,as 帮我们自动生成的代码中,除了 onupdate() 方法被我们重写了,还有重写 onenable() 和 ondisable() 两个方法,但都是空实现,这两个方法什么时候会被调用?还有,我们说自定义的 myappwidgetprovider,继承自 appwidgetprovider,而 myappwidgetprovider 又是broadcastreceiver 的子类,而我们却没有向写常规广播接收者一样重写 onreceiver() 方法?下面跟进去 appwidgetprovider 源码,一探究竟。
这个类代码并不多,其实,appwidgetprovider 出去构造方法外,总共只有下面这些方法:
onenable() :当小部件第一次被添加到桌面时回调该方法,可添加多次,但只在第一次调用。对用广播的 action 为 action_appwidget_enable。
onupdate(): 当小部件被添加时或者每次小部件更新时都会调用一次该方法,配置文件中配置小部件的更新周期 updateperiodmillis,每次更新都会调用。对应广播 action 为:action_appwidget_update 和 action_appwidget_restored 。
ondisabled(): 当最后一个该类型的小部件从桌面移除时调用,对应的广播的 action 为 action_appwidget_disabled。
ondeleted(): 每删除一个小部件就调用一次。对应的广播的 action 为: action_appwidget_deleted 。
onrestored(): 当小部件从备份中还原,或者恢复设置的时候,会调用,实际用的比较少。对应广播的 action 为 action_appwidget_restored。
onappwidgetoptionschanged(): 当小部件布局发生更改的时候调用。对应广播的 action 为 action_appwidget_options_changed。
最后就是 onreceive() 方法了,appwidgetprovider 重写了该方法,用于分发具体的时间给上述的方法。看看源码:
public void onreceive(context context, intent intent) { // protect against rogue update broadcasts (not really a security issue, // just filter bad broacasts out so subclasses are less likely to crash). string action = intent.getaction(); if (appwidgetmanager.action_appwidget_update.equals(action)) { bundle extras = intent.getextras(); if (extras != null) { int[] appwidgetids = extras.getintarray(appwidgetmanager.extra_appwidget_ids); if (appwidgetids != null && appwidgetids.length > 0) { this.onupdate(context, appwidgetmanager.getinstance(context), appwidgetids); } } } else if (appwidgetmanager.action_appwidget_deleted.equals(action)) { bundle extras = intent.getextras(); if (extras != null && extras.containskey(appwidgetmanager.extra_appwidget_id)) { final int appwidgetid = extras.getint(appwidgetmanager.extra_appwidget_id); this.ondeleted(context, new int[] { appwidgetid }); } } else if (appwidgetmanager.action_appwidget_options_changed.equals(action)) { bundle extras = intent.getextras(); if (extras != null && extras.containskey(appwidgetmanager.extra_appwidget_id) && extras.containskey(appwidgetmanager.extra_appwidget_options)) { int appwidgetid = extras.getint(appwidgetmanager.extra_appwidget_id); bundle widgetextras = extras.getbundle(appwidgetmanager.extra_appwidget_options); this.onappwidgetoptionschanged(context, appwidgetmanager.getinstance(context), appwidgetid, widgetextras); } } else if (appwidgetmanager.action_appwidget_enabled.equals(action)) { this.onenabled(context); } else if (appwidgetmanager.action_appwidget_disabled.equals(action)) { this.ondisabled(context); } else if (appwidgetmanager.action_appwidget_restored.equals(action)) { bundle extras = intent.getextras(); if (extras != null) { int[] oldids = extras.getintarray(appwidgetmanager.extra_appwidget_old_ids); int[] newids = extras.getintarray(appwidgetmanager.extra_appwidget_ids); if (oldids != null && oldids.length > 0) { this.onrestored(context, oldids, newids); this.onupdate(context, appwidgetmanager.getinstance(context), newids); } } } }
appwidget 练习
下面再自己写个例子,学习 remoteviews 中的其它知识点,这个例子中小部件布局中用到 button 和 listview。上代码:
小部件的布局文件 mul_app_widget_provider.xml 如下:
<?xml version="1.0" encoding="utf-8"?> <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <linearlayout android:layout_width="100dp" android:layout_height="200dp" android:orientation="vertical"> <imageview android:id="@+id/iv_test" android:layout_width="match_parent" android:layout_height="100dp" android:src="@mipmap/ic_launcher"/> <button android:id="@+id/btn_test" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="点击跳转"/> </linearlayout> <textview android:layout_width="1dp" android:layout_height="200dp" android:layout_marginleft="5dp" android:layout_marginright="5dp" android:background="#f00"/> <listview android:id="@+id/lv_test" android:layout_width="100dp" android:layout_height="200dp"> </listview> </linearlayout>
小部件的配置信息 mul_app_widget_provider_info.xml 如下:
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initiallayout="@layout/mul_app_widget_provider" android:minheight="200dp" android:minwidth="200dp" android:previewimage="@mipmap/a1" android:updateperiodmillis="86400000"> </appwidget-provider>
mulappwidgetprovider.java:
package com.example.joy.remoteviewstest; import android.app.pendingintent; import android.appwidget.appwidgetmanager; import android.appwidget.appwidgetprovider; import android.content.componentname; import android.content.context; import android.content.intent; import android.os.bundle; import android.text.textutils; import android.widget.remoteviews; import android.widget.toast; public class mulappwidgetprovider extends appwidgetprovider { public static final string change_image = "com.example.joy.action.change_image"; private remoteviews mremoteviews; private componentname mcomponentname; private int[] imgs = new int[]{ r.mipmap.a1, r.mipmap.b2, r.mipmap.c3, r.mipmap.d4, r.mipmap.e5, r.mipmap.f6 }; @override public void onupdate(context context, appwidgetmanager appwidgetmanager, int[] appwidgetids) { mremoteviews = new remoteviews(context.getpackagename(), r.layout.mul_app_widget_provider); mremoteviews.setimageviewresource(r.id.iv_test, r.mipmap.ic_launcher); mremoteviews.settextviewtext(r.id.btn_test, "点击跳转到activity"); intent skipintent = new intent(context, mainactivity.class); pendingintent pi = pendingintent.getactivity(context, 200, skipintent, pendingintent.flag_cancel_current); mremoteviews.setonclickpendingintent(r.id.btn_test, pi); // 设置 listview 的adapter。 // (01) intent: 对应启动 listviewservice(remoteviewsservice) 的intent // (02) setremoteadapter: 设置 listview 的适配器 // 通过setremoteadapter将 listview 和listviewservice关联起来, // 以达到通过 gridwidgetservice 更新 gridview 的目的 intent lvintent = new intent(context, listviewservice.class); mremoteviews.setremoteadapter(r.id.lv_test, lvintent); mremoteviews.setemptyview(r.id.lv_test,android.r.id.empty); // 设置响应 listview 的intent模板 // 说明:“集合控件(如gridview、listview、stackview等)”中包含很多子元素,如gridview包含很多格子。 // 它们不能像普通的按钮一样通过 setonclickpendingintent 设置点击事件,必须先通过两步。 // (01) 通过 setpendingintenttemplate 设置 “intent模板”,这是比不可少的! // (02) 然后在处理该“集合控件”的remoteviewsfactory类的getviewat()接口中 通过 setonclickfillinintent 设置“集合控件的某一项的数据” /* * setpendingintenttemplate 设置pendingintent 模板 * setonclickfillinintent 可以将fillinintent 添加到pendingintent中 */ intent tointent = new intent(change_image); pendingintent pendingintent = pendingintent.getbroadcast(context, 200, tointent, pendingintent.flag_update_current); mremoteviews.setpendingintenttemplate(r.id.lv_test, pendingintent); mcomponentname = new componentname(context, mulappwidgetprovider.class); appwidgetmanager.updateappwidget(mcomponentname, mremoteviews); } @override public void onreceive(context context, intent intent) { super.onreceive(context, intent); if(textutils.equals(change_image,intent.getaction())){ bundle extras = intent.getextras(); int position = extras.getint(listviewservice.initent_data); mremoteviews = new remoteviews(context.getpackagename(), r.layout.mul_app_widget_provider); mremoteviews.setimageviewresource(r.id.iv_test, imgs[position]); mcomponentname = new componentname(context, mulappwidgetprovider.class); appwidgetmanager.getinstance(context).updateappwidget(mcomponentname, mremoteviews); } } }
mainactivity.java:
package com.example.joy.remoteviewstest; import android.support.v7.app.appcompatactivity; import android.os.bundle; public class mainactivity extends appcompatactivity { @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); } }
下面重点是 listview 在小部件中的用法:
package com.example.joy.remoteviewstest; import android.content.context; import android.content.intent; import android.os.bundle; import android.widget.remoteviews; import android.widget.remoteviewsservice; import java.util.arraylist; import java.util.list; public class listviewservice extends remoteviewsservice { public static final string initent_data = "extra_data"; @override public remoteviewsfactory ongetviewfactory(intent intent) { return new listremoteviewsfactory(this.getapplicationcontext(), intent); } private class listremoteviewsfactory implements remoteviewsservice.remoteviewsfactory { private context mcontext; private list<string> mlist = new arraylist<>(); public listremoteviewsfactory(context context, intent intent) { mcontext = context; } @override public void oncreate() { mlist.add("一"); mlist.add("二"); mlist.add("三"); mlist.add("四"); mlist.add("五"); mlist.add("六"); } @override public void ondatasetchanged() { } @override public void ondestroy() { mlist.clear(); } @override public int getcount() { return mlist.size(); } @override public remoteviews getviewat(int position) { remoteviews views = new remoteviews(mcontext.getpackagename(), android.r.layout.simple_list_item_1); views.settextviewtext(android.r.id.text1, "item:" + mlist.get(position)); bundle extras = new bundle(); extras.putint(listviewservice.initent_data, position); intent changeintent = new intent(); changeintent.setaction(mulappwidgetprovider.change_image); changeintent.putextras(extras); /* android.r.layout.simple_list_item_1 --- id --- text1 * listview的item click:将 changeintent 发送, * changeintent 它默认的就有action 是provider中使用 setpendingintenttemplate 设置的action*/ views.setonclickfillinintent(android.r.id.text1, changeintent); return views; } /* 在更新界面的时候如果耗时就会显示 正在加载... 的默认字样,但是你可以更改这个界面 * 如果返回null 显示默认界面 * 否则 加载自定义的,返回remoteviews */ @override public remoteviews getloadingview() { return null; } @override public int getviewtypecount() { return 1; } @override public long getitemid(int position) { return position; } @override public boolean hasstableids() { return false; } } }
最后看看清单文件:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.joy.remoteviewstest"> <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" /> </intent-filter> </activity> <receiver android:name=".mulappwidgetprovider" android:label="@string/app_name"> <intent-filter> <action android:name="com.example.joy.action.change_image"/> <action android:name="android.appwidget.action.appwidget_update"/> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/mul_app_widget_provider_info"> </meta-data> </receiver> <service android:name=".listviewservice" android:permission="android.permission.bind_remoteviews" android:exported="false" android:enabled="true"/> </application> </manifest>
这个小部件添加到桌面后有一个 imageview 显示小机器人,下面有一个 button ,右边有一个listview。
这里主要看看,button 和 listview 在 remoteviews中如何使用。、
button 设置 text 和 textview 一样,因为 button 本身继承自 textview,button 设置点击事件如下:
intent skipintent = new intent(context, mainactivity.class); pendingintent pi = pendingintent.getactivity(context, 200, skipintent, pendingintent.flag_cancel_current); mremoteviews.setonclickpendingintent(r.id.btn_test, pi);
用到方法 setonclickpendingintent,pendingintent 表示延迟的 intent , 与通知中的用法一样。这里点击之后跳转到了 mainactivity。
关于 listview 的用法就复杂一些了。首先需要自定义一个类继承自 remoteviewsservices ,并重写 ongetviewfactory 方法,返回 remoteviewsservice.remoteviewsfactory 接口的对象。这里定义了一个内部类实现该接口,需要重写多个方法,与 listview 的多布局适配很类似。重点方法是
public remoteviews getviewat(int position){}
这个方法中指定了 listview 的每一个 item 的布局以及内容,同时通过 setonclickfillinintent() 或者 setonclickpendingintent() 给 item 设置点击事件。这里我实现的点击 item,替换左边的 imageview 的图片。重写了 mulappwidgetprovider 类的 onreceiver 方法,处理替换图片的逻辑。
程序运行效果如下图:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。