文章目录

  • 1. 基础
  • 2. 实现窗口小部件(App Widget)
  • 第一步:创建窗口小部件布局文件
  • 第二步:添加`AppWidgetProviderInfo`元数据配置
  • 第三步:创建窗口小部件提供程序类
  • 第四步:在`AndroidManifest.xml`中声明App Widget
  • 3. Demo项目源码



    App Widget是微型应用程序视图,可以嵌入其他应用程序(例如主屏幕)中并定期更新内容。这些视图在用户界面中称为“窗口小部件”,可以通过App窗口小部件提供程序发布窗口小部件。能够容纳其他App Widget的应用程序组件称为App Widget宿主(例如主屏幕)。

Google官方文档:Build an App Widget

1. 基础

创建一个App Widget,需要以下基础内容:

  • AppWidgetProviderInfo对象
    描述App Widget的元数据,例如App Widget的布局,大小、更新频率等等,这个对象必须在res/xml目录下定义XML文件。
  • AppWidgetProvider类继承实现
    AppWidgetProvider继承自BroadcastReceiver,内部定义了基于广播事件来实现App Widget的接口方法,通过这个类,在App Widget updatedenableddisableddeleted的时候都会接收到相应的广播。
  • App Widget视图布局
    App Widget的视图布局文件,在res/layout下面定义的XML布局文件。

2. 实现窗口小部件(App Widget)

第一步:创建窗口小部件布局文件

在项目中添加窗口小部件的布局文件,布局文件没什么可讲的,跟普通的布局文件一样,但是要注意,App Widget对布局和控件的支持是有限的。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextClock
        android:id="@+id/timeText"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="3"
        android:format12Hour="hh:mm"
        android:format24Hour="HH:mm"
        android:gravity="center"
        android:textColor="@android:color/black"
        android:textSize="40sp"
        android:textStyle="bold"/>
    <TextClock
        android:id="@+id/dateText"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:format12Hour="yyyy/MM/dd E"
        android:format24Hour="yyyy/MM/dd E"
        android:gravity="center"
        android:textColor="@android:color/black"
        android:textSize="16sp"/>
</LinearLayout>

TextClockTextView的子类,所以也是被AppWidget所支持的,更多关于TextClock的使用,请参考:Android数字时钟神一般的实现——TextClock

  • App Widget布局文件支持的layout
  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout
  • App Widget布局文件支持的widget
  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper
  • 为App Widget添加边距
        从Android 4.0(Api level 14)开始,会自动为App Widget添加边距,如果你的targetSdkVersion设置成14及以上,可无需配置此项。为App Widget添加边距,只需要在布局的根layout中添加android:padding属性只可。

第二步:添加AppWidgetProviderInfo元数据配置

    AppWidgetProviderInfo元数据配置是放在res/xml中的一个XML文件,他描述一个App Widget的基本性质,比如最小布局尺寸、初始布局尺寸、更新时间间隔以及配置Activity(可选)等等。配置文件的根节点是<appwidget-provider>的单个元素。

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="180dp"
    android:minHeight="110dp"
    android:updatePeriodMillis="0"
    android:previewImage="@mipmap/ic_launcher"
    android:initialLayout="@layout/clock_widget"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>
  • <appwidget-provider>属性简介
  • minWidthminHeight 属性值指定默认情况下 App Widget占用的最小空间量 。在默认主屏幕中,定义了具有特定高度和宽度的单元格网格,用来放置App Widget。如果定义t的最小宽度或高度的值不匹配的单元格网格的尺寸,则该App Widget尺寸会向上取最接近的大小。

尺寸计算规则如下:

单元网格数量

(行/列)

可用尺寸(dp)

(minWidth/minHeight)

1

40dp

2

110dp

3

180dp

4

250dp



n

70 × n − 30

有关调整应用程序小部件大小的更多信息,请参阅应用程序小部件设计指南

注意:为使您的应用程序小部件可跨设备移植,应用程序小部件的最小尺寸不得大于4 x 4单元。

  • minResizeWidthminResizeHeight属性值指定应用Widget的绝对最小尺寸。这些值指定App Widget尺寸小于该值将难以辨认或无法使用。使用这些属性可以使用户将App Widget的大小调整为小于minWidthminHeight属性定义的默认大小 。这些属性在Android 3.1中引入,计算规则如上所述。
    有关调整应用程序小部件大小的更多信息,请参阅应用程序小部件设计指南
  • updatePeriodMillis属性定义App Widget更新频率,这个时间设定了AppWidgetProvider调用onUpdate() 回调方法请求更新的频率。该值不能保证实际更新操作会准时发生,建议在设定是不要太频繁地进行更新(为了节省电池,每小时不要超过一次),配置中的更新频率可以允许用户自行调整。

注意:如果App Widget在更新(如定义updatePeriodMillis)时设备处于睡眠状态,则设备将唤醒以执行更新。如果更新频率非常低,则不会对电池寿命造成重大问题。但是,如果您需要频繁地更新,可以配置在设备处于睡眠状态时不需要更新,这个可以根据唤醒设备的警报来执行更新,为此,在AppWidgetProvider收到AlarmManager的Intent警报 ,将警报类型设置为ELAPSED_REALTIMERTC,这样在设备醒着时才会发出警报。然后设置 updatePeriodMillis为零(“0”)。

  • initialLayout属性定义App Widget布局的布局资源
  • configure属性定义了当用户添加App Widget时启动的Activity,这个Activity是用来配置App Widget属性的一个页面(可选,详情参阅创建应用程序小部件配置活动)。
  • previewImage属性指定App Widget的预览图,在小部件选择列表中展示。如果未提供,则用应用程序的启动器图标。该字段对应 于文件中元素的 android:previewImage属性。有关使用的更多讨论,请参见设置预览图像。(在Android 3.1中引入)
  • resizeMode属性指定App Widget调整大小的规则。该resizeMode属性的值 包括“水平(horizontal)”,“垂直(vertical)”和“无(none)”,支持多种模式填写多个值,中间用|隔开。(在Android 3.1中引入)
  • minResizeHeight属性指定App Widget可以调整到的最小高度(以dps为单位)。如果该字段值大于minHeight或未启用垂直调整大小,则此字段无效。(在Android 4.0中引入)
  • minResizeWidth 属性指定App Widget可以调整到的最小宽度(以dps为单位)。如果该字段大于minWidth或未启用水平调整大小,则此字段无效。(在Android 4.0中引入)

  • widgetCategory属性声明您的App Widget支持显示的位置,可以是主屏幕(home_screen),锁定屏幕(keyguard),或者同时显示在两者上。在低于Android 5.0的版本支持锁定屏幕小部件,对于Android 5.0及更高版本,仅home_screen有效。

有关<appwidget-provider>元素接受的属性的更多信息,请参考:AppWidgetProviderInfo类介绍

第三步:创建窗口小部件提供程序类

    AppWidgetProvider 类继承自 BroadcastReceiver类,内部实现了相关逻辑,为处理App Widget的广播提供便利。AppWidgetProvider只会接收与App Widget相关的广播,例如:App Widget的更新(updated)、删除(deleted)、启用(enabled)、禁用(disabled) ,当接收到这些广播AppWidgetProvider将会调用对应的方法。

  • 创建一个继承自AppWidgetProvider,并实现相应的方法
package com.owen.clockwidget

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import android.app.PendingIntent
import android.app.Service
import android.content.ComponentName
import android.content.IntentFilter
import android.os.Bundle
import android.os.Handler
import android.os.SystemClock
import android.util.Log
import java.util.*

/**
 *
 * <br/>Author:yunying.zhang
 * <br/>Email: yunyingzhang@rastar.com
 * <br/>Date: 2019/12/5
 */
class ClockWidgetProvider : AppWidgetProvider() {
    val TAG = "ClockWidgetProvider"
    private val mHandler = Handler()

    override fun onReceive(context: Context?, intent: Intent?) {
        Log.i(TAG, "小部件提供程序接收到广播")
        super.onReceive(context, intent)

        if(Intent.ACTION_TIME_CHANGED == intent?.action) {
            updateWidget(context)
        }
    }

    override fun onUpdate(context: Context?, appWidgetManager: AppWidgetManager?, appWidgetIds: IntArray?) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        Log.i(TAG, "小部件更新")
        // 一个App Widget提供程序为一个App Widget提供支持,但是一个App Widget却可以在多个地方添加,
        // 每一个显示的App Widget视图有一个id,如果多个地方添加将会有多个id,这里循环对每一个视图进行更新
        appWidgetIds?.forEach { appWidgetId ->
            // 创建App Widget点击事件意图(说明:如果不需要实现点击事件,可以不定义此项)
            // 通知AppWidgetManager更新当前的App Widget
            appWidgetManager?.updateAppWidget(appWidgetId, buildView(context))

        }
    }

    override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager,
        appWidgetId: Int, newOptions: Bundle) {
        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
        Log.i(TAG, "$appWidgetId 小部件选项更新")
    }

    override fun onEnabled(context: Context) {
        super.onEnabled(context)
        Log.i(TAG, "小部件启用")
    }

    override fun onDisabled(context: Context) {
        super.onDisabled(context)
        Log.i(TAG, "小部件禁用")
    }

    override fun onDeleted(context: Context, appWidgetIds: IntArray) {
        super.onDeleted(context, appWidgetIds)
        appWidgetIds?.forEach {
            Log.i(TAG, "$it 小部件被删除")
        }
    }

    companion object {
        fun buildView(context: Context?): RemoteViews {
            val c = Calendar.getInstance()
            val timePendingIntent: PendingIntent = Intent(context, MainActivity::class.java)
                .let { intent ->
                    PendingIntent.getActivity(context, 0, intent, 0)
                }
            // 如果有多个控件可点击,可以创建多个点击事件意图
            val datePendingIntent:PendingIntent = Intent(Intent.ACTION_QUICK_CLOCK).let { intent->
                PendingIntent.getActivity(context, 1, intent, 0)
            }

            // 获取App Widget的视图布局
            return RemoteViews(context?.packageName, R.layout.clock_widget).apply {
                // 设置点击事件意图(说明:如果App Widget内部有多个控件点击事件,可以在此添加多个控件的点击事件)
                setOnClickPendingIntent(R.id.timeText, timePendingIntent)
                setOnClickPendingIntent(R.id.dateText, datePendingIntent)
            }
        }

        fun updateWidget(context: Context?) {
            val appWidgetManager = AppWidgetManager.getInstance(context)

            val componentName = ComponentName(context!!, ClockWidgetProvider::class.java)

            val appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
            appWidgetIds?.forEach {
                appWidgetManager.updateAppWidget(componentName, buildView(it, context))
            }
        }
    }
}
  • AppWidgetProvider回调方法简介
  • onUpdate() 这个方法在App Widget定期更新的时候调用(在AppWidgetProviderInfo中由updatePeriodMillis定义更新平路),另外,当App Widget第一次添加时也会调用该方法,因此该方法中应该包含必要的启动代码(例如:控件事件监听处理、启动临时服务等)。但是如果你定义了配置Activity(通过AppWidgetProviderInfo中的configure节点配置),当用户添加App Widget时将不会调用改方法(但是在后续的更新调用该方法),此时,App Widget的第一次更新由配置Activity在配置完成时实现。
  • onAppWidgetOptionsChanged() 这个方法在App Widget第一次防止或者在任何时候改变尺寸是调用。在这个回调方法中,可以根据App Widget的尺寸范围,显示或者隐藏一些内容。可以通过getAppWidgetOptions()获取App Widget的尺寸范围信息,返回一个Bundle类型对象。

AppWidgetOptions信息字段说明:

  • OPTION_APPWIDGET_MIN_WIDTH(key值:appWidgetMinWidth)包含当前App Widget宽度下限(单位:dp)
  • OPTION_APPWIDGET_MIN_HEIGHT(key值:appWidgetMinHeight)包含当前App Widget高度下限(单位:dp)
  • OPTION_APPWIDGET_MAX_WIDTH(key值:appWidgetMaxWidth)包含当前App Widget宽度上限(单位:dp)
  • OPTION_APPWIDGET_MAX_HEIGHT(key值:appWidgetMaxHeight)包含当前App Widget高度上限(单位:dp)
  • onDeleted(Context, int[]) 这个方法在App Widget从App Widget host中删除的时候调用。
  • onEnabled(Context) 这个方法在App Widget第一次创建对象的时候调用。如果同一个App Widget多次添加,这个方法也只会调用一次。
  • onDisabled(Context) 这个方法在App Widget的最后一个实例被删除的时候调用,如果同一个App Widget多次添加了,删除一个不会调用该方法,直到最后一个被删除才会调用。
  • onReceive(Context, Intent) 这个方法在接收到任何支持的广播的时候都会调用,AppWidgetProvider实现了过滤接收所有以上所有App Widget的广播,通常不需要实现这个方法,但是如果需要接收其他广播,需要在AndroidManifest.xml中定义提供器的时候添加<intent-filter>,并在该方法内实现相应的代码逻辑。

    AppWidgetProvider类最重要的回调方法就是onUpdate(),因为他在每个App Widget添加到host的时候都会调用(除非你定义了配置Activity),如果你的App Widget接受任何的用户事件交互,那么需要在这个回调方法内实现点击事件的注册。

第四步:在AndroidManifest.xml中声明App Widget

    在AndroidManifest.xml中,声明App Widget,请注意以下示例代码中的android:name属性。

<application>
    .......
    <receiver android:name=".ClockWidgetProvider">
        <!-- 定义广播过滤器,以下项目是必须,如果有其他广播需要接收,可增加广播接收过滤器 -->
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>

        <!-- AppWidgetProviderInfo元数据配置 -->
        <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/clock_widget_info" />
    </receiver>
</application>

完成了以上步骤,接下来就可以编译安装应用,然后在设备的桌面就可以添加App Widget了。

3. Demo项目源码

demo项目源码可在Github上下载:ClockWidget

说明:因demo源码会不断更新,当前文章的代码可能会跟Github上的存在差异