使用 Android Studio 开发 widget 安卓桌面插件
•What
AppWidget 即桌面小部件,也叫桌面控件,就是能直接显示在Android系统桌面上的小程序;
这么说可能有点抽象,看图:
像这种,桌面上的天气、时钟、搜索框等等,都属于 APP Widget;
一些用户使用比较频繁的程序,可以做成AppWidget,这样能方便地使用。
AppWidget 是Android 系统应用开发层面的一部分,有着特殊用途,使用得当的化,的确会为app 增色不少;
它的工作原理是把一个进程的控件嵌入到另外一个进程的窗口里的一种方法。
长按桌面空白处,会出现一个 AppWidget 的文件夹;
在里面找到相应的 AppWidget ,长按拖出,即可将 AppWidget 添加到桌面;
•How
首先,新建一个项目,我命名为 TestAppWidget:
将项目结构模式改为 Project 模式:
然后,右击 app, New->Widget->App Widget :
来到如下界面:
- Class Name : 小控件的名字,这里我选择默认的
- resizeMode : 小控件可以被拉伸的方向
- horizontal : 水平拉伸
- vertical : 数值拉伸
- Both : 两者都
- none : 无
- Minimum Width : 小控件占用的宽度单元格
- Minimun Height : 小控件占用的高度单元格
Width,Height 暂且都选为默认值,后期觉得不合适可以更改;
一切准备就绪,点击 FINISH 就创建了一个 Android Studio 默认的 App Widget;
让我们来看看这个默认的 Widget 长啥样,首先将这个 Widget 放置到桌面:
上图红框框中的便是默认的 Widget 的样式;
•通过代码深入了解Widget
AppWidget 是通过 BroadcastReceiver 的形式进行控制的;
开发 AppWidget 的主要类为 AppWidgetProvider,该类继承自 BroadcastReceiver。
为了实现桌面小部件,开发者只要开发一个继承自 AppWidgetProvider 的子类,并重写它的 onUpdate() 方法即可。
重写该方法,一般来说可按如下几个步骤进行:
1、创建一个 RemoteViews 对象,这个对象加载时指定了桌面小部件的界面布局文件。
2、设置 RemoteViews 创建时加载的布局文件中各个元素的属性。
3、创建一个 ComponentName 对象
4、调用 AppWidgetManager 更新桌面小部件。
在 App/src/main/java 目录下,有一个自动生成的 NewAppWidget.java 文件:
NewAppWidget.java
/** * Implementation of App Widget functionality. */ public class NewAppWidget 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.new_app_widget); 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 ,Android Studio 默认帮我们重写 onUpdate() 方法遍历 appWidgetId,并调用了 updateAppWidget() 方法。
再看 updateAppWidget() 方法,很简单,只有四行:
- 第一行:CharSequence widgetText = context.getString(R.string.appwidget_text);
- 声明了一个字符串
- 第二行: RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
- 创建了一个 RemoteViews 对象
- 第一个参数传应用程序包名
- 第二个参数指定了 RemoteViews 加载的布局文件
- 这一行对应上面步骤中说的第一点
可以看到在 app/src/main/res/layout/ 目录下面 Android Studio 自动生成了一个 new_app_widget.xml 文件:
new_app_widget.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="?attr/appWidgetBackgroundColor" android:padding="@dimen/widget_margin" android:theme="@style/ThemeOverlay.TestAppWidget.AppWidgetContainer"> <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="?attr/appWidgetBackgroundColor" android:contentDescription="@string/appwidget_text" android:text="@string/appwidget_text" android:textColor="?attr/appWidgetTextColor" android:textSize="24sp" 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 对象,明明就不需要。
的确,这个例子中也没有用到。
如果我们手敲第四步代码,Android Studio 的智能提示会告诉你 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 对象,我们也可以根据需要选择性地去更新。
到这里,所有步骤都结束了,就完了?还没。
前面说了,自定义的 NewAppWidget 继承自 AppWidgetProvider,而 AppWidgetProvider 又是继承自 BroadCastReceiver;
所以说 NewAppWidget 本质上是一个广播接收者,属于四大组件之一,需要我们的清单文件中注册。
打开 AndroidManifest.xml 文件可以看到,的确是注册了小部件的,内容如下:
AndroidManifest.xml
<receiver android:name=".NewAppWidget"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/new_app_widget_info" /> </receiver>上面代码中有一个 Action,这个 Action 必须要加,且不能更改,属于系统规范,是作为小部件的标识而存在的。
如果不加,这个 Receiver 就不会出现在小部件列表里面。
然后看到小部件指定了 android:resource="@xml/new_app_widget_info" 作为 meta-data;
细心的你发现了,在 res 目录下面建立了一个 xml 文件夹,xml 文件夹下有一个 new_app_widget_info.xml 文件:
new_app_widget_info.xml
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialKeyguardLayout="@layout/new_app_widget" android:initialLayout="@layout/new_app_widget" android:minWidth="40dp" android:minHeight="40dp" android:previewImage="@drawable/example_appwidget_preview" android:resizeMode="horizontal|vertical" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen"> </appwidget-provider>这里配置了一些小部件的基本信息:
- minWidth 和 minHeight 属性值指定默认情况下 App Widget 占用的最小空间量 。
- 在默认主屏幕中,定义了具有特定高度和宽度的单元格网格,用来放置 App Widget
- 如果定义的最小宽度或高度的值不匹配的单元格网格的尺寸,则该App Widget尺寸会向上取最接近的大小
尺寸规则计算如下:
有关调整应用程序小部件大小的更多信息,请参阅 应用程序小部件设计指南。
PS : 为使应用程序小部件可跨设备移植,应用程序小部件的最小尺寸不得大于4 x 4单元。
resizeMode 属性指定 App Widget 调整大小的规则
- 该 resizeMode 属性的值包括 horizontal(水平) , vertical(垂直) 和 none(无)
- 支持多种模式填写多个值,中间用 | 隔开
- minResizeWidth 和 minResizeHeight 属性值指定应用 Widget 的绝对最小尺寸。
- 这些值指定 App Widget 尺寸小于该值将难以辨认或无法使用
- 使用这些属性可以使用户将 App Widget 的大小调整为小于 minWidth 和 minHeight 属性定义的默认大小
- 这些属性在Android 3.1中引入,计算规则如上所述。
minResizeHeight 属性指定App Widget 可以调整到的最小高度(以dps为单位)
如果该字段值大于 minHeight 或未启用垂直调整大小,则此字段无效
minResizeWidth 属性指定App Widget可以调整到的最小宽度(以dps为单位)
如果该字段大于 minWidth 或未启用水平调整大小,则此字段无效
有关调整应用程序小部件大小的更多信息,请参阅 应用程序小部件设计指南。
updatePeriodMillis 属性定义 App Widget 更新频率,单位为毫秒。
这个时间设定了 AppWidgetProvider 调用 onUpdate() 回调方法请求更新的频率
该值不能保证实际更新操作会准时发生,建议在设定是不要太频繁地进行更新(为了节省电池,每小时不要超过一次)
配置中的更新频率可以允许用户自行调整
需要注意的是:
- 如果App Widget在更新(如定义updatePeriodMillis)时设备处于睡眠状态,则设备将唤醒以执行更新
- 如果更新频率非常低,则不会对电池寿命造成重大问题
- 如果您需要频繁地更新,可以配置在设备处于睡眠状态时不需要更新,这个可以根据唤醒设备的警报来执行更新
- 为此,在AppWidgetProvider收到AlarmManager的Intent警报 ,将警报类型设置为ELAPSED_REALTIME或RTC
- 这样在设备醒着时才会发出警报。然后设置updatePeriodMillis为零(“0”)
initialLayout 属性定义 App Widget 布局的布局资源
configure 属性定义了当用户添加 App Widget 时启动的
这个 Activity 是用来配置 App Widget 属性的一个页面(可选,详情参阅创建应用程序小部件配置活动)
previewImage 属性指定 App Widget 的预览图,在小部件选择列表中展示
- 如果未提供,则用应用程序的启动器图标
- 该字段对应于文件中元素的 android:previewImage 属性
- 有关使用的更多讨论,请参见设置预览图像
widgetCategory 属性声明您的 App Widget 支持显示的位置
可以是主屏幕(home_screen),锁定屏幕(keyguard),或者同时显示在两者上
在低于 Android 5.0 的版本支持锁定屏幕小部件,对于Android 5.0及更高版本,仅 home_screen 有效
关 <appwidget-provider> 元素接受的属性的更多信息,请参考:AppWidgetProviderInfo类介绍
为了开发出更强大一点小部件,我们还需要进一步了解 RemoteViews 和 AppWidgetProvider。
•AppWidget的妆容——RemoteViews
下面简单说说 RemoteViews 相关的几个类。
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 提供了一系列 setXXX() 方法来为小部件的子视图设置属性。具体可以参考 API 文档。
RemoteViewsService
RemoteViewsService,是管理RemoteViews的服务。
一般,当AppWidget 中包含 GridView、ListView、StackView 等集合视图时;
才需要使用RemoteViewsService来进行更新、管理。
RemoteViewsService 更新集合视图的一般步骤是:
- 通过 setRemoteAdapter() 方法来设置 RemoteViews 对应 RemoteViewsService 。
- 之后在 RemoteViewsService 中,实现 RemoteViewsFactory 接口。
- 然后,在 RemoteViewsFactory 接口中对集合视图的各个子项进行设置,例如 ListView 中的每一Item。
RemoteViewsFactory
通过 RemoteViewsService 中的介绍,我们知道 RemoteViewsService 是通过 RemoteViewsFactory 来具体管理 layout 中集合视图的;
RemoteViewsFactory 是 RemoteViewsService 中的一个内部接口。
RemoteViewsFactory 提供了一系列的方法管理集合视图中的每一项,例如:
- RemoteViews getViewAt(int position)
- 通过getViewAt()来获取“集合视图”中的第position项的视图,视图是以RemoteViews的对象返回的。
- int getCount()
- 通过getCount()来获取“集合视图”中所有子项的总数。
•AppWidget的美貌——AppWidgetProvider
我们说一位女孩漂亮,除了因为她穿的衣服、化的妆漂亮以外,我想最主要的原因还是她本人长的漂亮吧。
同样,小部件之所以有附着在桌面,跨进程更新 View 的能力,主要是因为 AppWidgetProvider 是一个广播接收者。
我们发现,上面的例子中,Android Studio 帮我们自动生成的代码中;
除了 onUpdate() 方法被我们重写了,还有重写 onEnable() 和 onDisable() 两个方法;
但都是空实现,这两个方法什么时候会被调用?
还有,我们说自定义的 NewAppWidget 继承自 AppWidgetProvider,而 NewAppWidget 又是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 重写了该方法,用于分发具体的时间给上述的方法。
•声明
参考资料: