Android Widget的使用和需要注意的问题

一.简单上手

1. 配置并显示widget

1.1 继承AppWidgetProvider

自定义MyWidgetProvider继承AppWidgetProvider,重写相关方法。

class MyWidgetProvider : AppWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        //当更新widget的时候会触发,添加的时候也会触发
    }
    override fun onDeleted(context: Context, appWidgetIds: IntArray) {
        super.onDeleted(context, appWidgetIds)
        //删除widget的时候会触发
    }
    override fun onDisabled(context: Context?) {
        super.onDisabled(context)
        //最后一个widget被删除的时候触发
    }
    override fun onEnabled(context: Context?) {
        super.onEnabled(context)
        //第一个widget被添加的时候触发
    }
    override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) {
        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
        //当widget被第一次添加或者widget大小改变的时候触发
    }
    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)
        //处理方式和普通广播一样
    }
}

AppWidgetProvider实质上就是一个广播,其中处理了相关的action并给出了回调方法。

1.2 配置appwidget-provider

找到res目录下的xml目录,若没有xml目录就新建一个,然后新建一个文件widget_info.xml

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialKeyguardLayout="@layout/widget_layout"
    android:initialLayout="@layout/widget_layout"
    android:minWidth="@dimen/dp_320"
    android:minHeight="@dimen/dp_110"
    android:previewImage="@mipmap/img_widget_preview"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="5"
    android:widgetCategory="home_screen">
</appwidget-provider>

这里介绍下常用属性

  • android:initialLayout添加到桌面的widget布局
  • android:initialKeyguardLayout添加到锁屏页面的widget布局
  • android:minWidth最小宽度,通用计算方式: (N * 70)-30=宽度
  • android:minHeight最小高度,通用计算方式: (N * 70)-30=高度,宽度和高度的格数按照google标准是这样设置的,但是有很多厂家对Launcher重新定义,所以比如你设置的是5 * 1,但是某些手机上就会变成4 * 1。
  • android:previewImage预览图
  • android:resizeMode允许横向纵向拉伸
  • android:updatePeriodMillis刷新间隔,最小刷新间隔是半小时,设置小于半小时也会按半小时算,且这里还有一点要注意,并不是每过半小时就一定会准时刷新,受设备影响这个时间可能略有提前或延迟。还有当手机息屏后可能会进入休眠状态,在休眠状态时不会自动更新,当设备解锁从休眠状态恢复时会立即刷新widget。

1.3 配置AndroidManifest.xml

        <receiver
            android:name=".MyWidgetProvider"
            android:label="@string/app_widget_string">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/widget_info" />
        </receiver>

在AndroidManifest.xml中需要配置一个广播接受者,其中固定的两个配置参数

  • <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>指定这个才能接收到widget更新。
  • android:name="android.appwidget.provider"告诉系统这个广播接受者是一个widget。
    还可以在intent-filter里面配置自定义的action,用法就和普通广播一样。

现在已经可以添加widget显示啦,显示的内容为widget_layout.xml里的布局,没错就是这么简单。

2. 更新widget

上面已经显示了widget,接下来就要给widget更新UI。
更新widget的UI是通过AppWidgetManager的updateAppWidget方法实例来更新的,我们可以通过AppWidgetManager.getInstance(context)来获取实例。updateAppWidget有三个重载方法。

  • updateAppWidget(ComponentName provider, RemoteViews views)
    指定要刷新widget的ComponentName和RemoteViews,通过AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteView)来刷新。举个例子,我在桌面第一页和第三页都添加了同一个widget,现在若点击其中一个的刷新按钮两个widget要同时都更新界面,这时就可以用这个方法。这个方法也是最常用来更新widget的方式,可以刷新添加到桌面的所有widget。一般来说,更新widget并不要求在AppWidgetProvider中进行,因为AppWidgetProvider本质上就是一个广播,只要通过指定remoteView和ComponentName,可在任何包含上下文的环境下更新widget。
  • updateAppWidget(int[] appWidgetIds, RemoteViews views)
    刷新部分指定的widget
  • updateAppWidget(int appWidgetId, RemoteViews views)
    刷新一个指定的widget
class MyWidgetProvider : AppWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        for (appWidgetId in appWidgetIds) {
            appWidgetManager.updateAppWidget(appWidgetId, remoteView)
        }
        //uploadWidget(context)
    }
    private fun uploadWidget(context: Context) { 
        val remoteView = RemoteViews(context.packageName, R.layout.widget_layout)
        val componentName = ComponentName(context, javaClass) 
        AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteView)
    }
}

上面代码两种方式都能刷新全部widget

3. widget的点击

package com.example.kotlintest.widget

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.widget.RemoteViews
import com.example.kotlintest.R

class MyWidgetProvider : AppWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        val intent = Intent(REFRESH_CLICK).apply {
            component = ComponentName(context, MyWidgetProvider::class.java)
        }
        val pendingIntent = PendingIntent.getBroadcast(
            context,
            R.id.tv_refresh,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT
        )
        val remoteView = RemoteViews(context.packageName, R.layout.widget_layout)
        remoteView.setOnClickPendingIntent(R.id.tv_refresh, pendingIntent)
        uploadWidget(context,remoteView)
    }
    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)
        when (intent.action) {
            REFRESH_CLICK -> {
                //点击事件
            }
        }
    }
    private fun uploadWidget(context: Context,remoteView: RemoteViews) {
        val componentName = ComponentName(context, javaClass)
        AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteView)
    }
    companion object {
        const val REFRESH_CLICK = "com.example.kotlintest.action.CLICK_REFRESH"
    }
}

二. 开发widget中需要注意处理的点

1. 初始化问题

当widget刷新时,如果应用没有处于开启状态下,这时会创建APP进程并初始化Application,之后回调widget的onUpdate方法。然而这里会有一个问题,由于部分app为了性能优化,将部分初始化操作移动到了引导页或Main页面里了,这样当widget想使用某些功能时,由于只创建了Application,在引导页或main页面里进行初始化的那部分功能没有进行初始化,便会抛出各种异常。所以这里开发的时候需要重点检查一遍。

2. UI设置

  • 当添加widget出现小组件添加错误、显示失败等,优先检查xml布局是否正确,尤其是不能包含自定义View等。
  • 通过RemoteViews更新widget,可能每次更新都创建了一个RemoteViews对象,但是RemoteViews只是一个action集合,只代表你对systemServer端widget的操作,一旦通过RemoteViews更新过widget,有些步骤就可以不用重复设置(列如点击事件)
  • widget不支持动画,如果一定要实现动画,可以开子线程循环刷新bitmap。

3. 网络请求

尽量不要直接在AppWidgetProvider中进行网络请求,和耗时操作。

  • 在AppWidgetProvider中进行网络请求,当未开启APP情况下,会请求失败抛出SocketTimeoutException异常。这一点很重要,很多系统都会限制在后台程序里静态广播的网络请求。如果有需要,请开启Service,在Service中进行网络请求。
  • 由于AppWidgetProvider优先级很低,代表当前进程容易被系统回收,所以尽量不要再AppWidgetProvider中进行耗时操作,否则可能会出现AppWidgetProvider中的任务未执行完进程就已经被系统回收。建议耗时操作开启Service执行。

4. 定时任务

很大一部分app都有定时刷新widget的需求,而系统的刷新间隔要求大于等于30分钟,这显然是满足不了需求。这里有两种方案。

  1. 单独进程的前台service
  2. 通过JobScheduler
    如果对实时性要求不是太高,可以考虑使用JobScheduler

5. 关于Service通知问题

我们知道在Android8.0后开启Service需要指定为前台通知,这样就会有一个通知栏效果。如果在widget中想开启Service进行网络请求,而又不想出通知,可以使用bindService方式。
bindService是Context的方法,网上大部分文章都拿Activity做例子,导致很多人不知道bindService其实在Application等Context的子类中都能使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容