UI我爱编程Widget

Android 开发之 App Widget 详解

2018-04-15  本文已影响98人  zztigyk

简介

App Widget:应用程序窗口小部件,微型的应用程序视图,它可以被嵌入到其它应用程序中,比如桌面,并接收周期性的更新。你可以通过一个 App Widget Provider 来发布一个 Widget,比如时钟、天气、音乐播放器等等。可以容纳 Widget 的应用叫做 App Widget Host,详细参考App Widgets| Android Developers

App Widget Provider是Android中提供的用于实现桌面小工具的类,其本质是一个广播,即BroadcastReceiver。

创建一个 App Widget 的主要步骤

  1. 在 AndroidManifest 中声明 App Widget
  2. 在 xml 目录定义 App Widget 的初始化 xml 文件
  3. 实现 Widget 具体布局的 Layout xml。
  4. 继承 AppWidgetProvider 类,实现具体的 Widget 业务逻辑。

在 AndroidManifest 中声明 App Widget

 <!-- 声明widget对应的AppWidgetProvider -->
        <!--android:name属性声明的就是 Widget 所用的 AppWidgetProvider 类-->
        <receiver android:name=".common.ExampleAppWidgetProvider">
            <intent-filter>
                <!--所有的窗口小部件都接收android.appwidget.action.APPWIDGET_UPDATE 动作的广播,
                该广播根据android:updatePeriodMillis设定的间隔时间发出广播,用于定时更新桌面上的所有窗口小部件。-->
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
                <!--定义一个自定义的动作广播,可以通过在该广播接收器中注册自定义的动作以使窗口小部件接收自定义的广播。-->
                <action android:name="com.skywang.widget.UPDATE_ALL"/>
            </intent-filter>
            <!--声明了 Widget 的 AppWidgetProviderInfo 对应的资源 xml 的位置,用的是 xml 目录下的 example_appwidget_info.xml。-->
            <meta-data
                    android:name="android.appwidget.provider"
                    android:resource="@xml/example_appwidget_info"/>
        </receiver>

在 xml 目录定义 App Widget 的初始化 xml 文件

<appwidget-provider
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="300dp"
        android:minHeight="72dp"
        android:updatePeriodMillis="86400000"
        android:previewImage="@drawable/ic_launcher"
        android:initialLayout="@layout/example_appwidget"
        android:resizeMode="horizontal|vertical"
        android:widgetCategory="home_screen|keyguard">

    <!--
    android:minWidth : 最小宽度
    android:minHeight : 最小高度
    android:updatePeriodMillis : 更新widget的时间间隔(ms),"86400000"为1个小时
    android:previewImage : 预览图片
    android:initialLayout : 加载到桌面时对应的布局文件
    android:resizeMode : widget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸
    android:widgetCategory : widget可以被显示的位置。home_screen表示可以将widget添加到桌面,keyguard表示widget可以被添加到锁屏界面。
    android:initialKeyguardLayout : 加载到锁屏界面时对应的布局文件
     -->
</appwidget-provider>

minWidth & minHeight:定义了 Widget 的最小宽高,当 minWidth 和 minHeight 不是桌面 cell 的整数倍时,Widget 的宽高会被阔至与其最接近的 cells 大小。Google 官方给出了一个大致估算 minWidth & minHeight 的公式,根据 Widget 所占的 cell 数量来计算宽高:70 × n − 30,n 是所占的 cell 数量。
updatePeriodMillis:定义了 Widget 的刷新频率,也就是 App Widget Framework 多久请求一次 AppWidgetProvider 的 onUpdate() 回调函数。该时间间隔并不保证精确,出于节约用户电量的考虑,Android 系统默认最小更新周期是 30 分钟,也就是说:如果您的程序需要实时更新数据,设置这个更新周期是 2 秒,那么您的程序是不会每隔 2 秒就收到更新通知的,而是要等到 30 分钟以上才可以,要想实时的更新 Widget,一般可以采用 Service 和 AlarmManager 对 Widget 进行更新。
previewImage:当用户选择添加 Widget 时的预览图片。如果该属性没有定义,则展示 application 的 launcher icon。该属性是在 3.0 以后引入的。
initialLayout:Widget 的布局 Layout 文件。
configure:定义了用户在添加 Widget 时弹出的配置页面的 Activity,用户可以在此进行 Widget 的一些配置,该 Activity 是可选的,如果不需要可以不进行声明。
resizeMode:Widget 在水平和垂直方向是否可以调整大小,值可以为:horizontal(水平方向可以调整大小),vertical(垂直方向可以调整大小),none(不可以调整大小),也可以 horizontal|vertical 组合表示水平和垂直方向均可以调整大小。
widgetCategory:表示 Widget 可以显示的位置,包括 home_screen(桌面),keyguard(锁屏),keyguard 属性需要 5.0 或以上 Android 版本才可以。
其它更多详细属性可以参考 AppWidgetProviderInfo

继承 AppWidgetProvider 类

AppWidgetProvider 继承自 BroadcastReceiver,内部逻辑非常简单,就是在 onReceive() 中处理 Widget 相关的广播事件(ACTION_APPWIDGET_UPDATE, ACTION_APPWIDGET_DELETED, ACTION_APPWIDGET_ENABLED, ACTION_APPWIDGET_DISABLED, ACTION_APPWIDGET_OPTIONS_CHANGED)分发到各个回调函数中(onUpdate(), onDeleted(), onEnabled(), onDisabled, onAppWidgetOptionsChanged())。

onUpdate():是最重要的回调函数,根据 updatePeriodMillis 定义的定期刷新操作会调用该函数,此外当用户添加 Widget 时 也会调用该函数,可以在这里进行必要的初始化操作。但如果在<appwidget-provider>中声明了 android:configure 的 Activity,在用户添加 Widget 时,不会调用 onUpdate(),需要由 configure Activity 去负责去调用 AppWidgetManager.updateAppWidget() 完成 Widget 更新,后续的定时更新还是会继续调用 onUpdate() 的。
onDeleted():当 Widget 被删除时调用该方法。
onEnabled():当 Widget 第一次被添加时调用,例如用户添加了两个你的 Widget,那么只有在添加第一个 Widget 时该方法会被调用。所以该方法比较适合执行你所有 Widgets 只需进行一次的操作。
onDisabled():与 onEnabled 恰好相反,当你的最后一个 Widget 被删除时调用该方法,所以这里用来清理之前在 onEnabled() 中进行的操作。
onAppWidgetOptionsChanged():当 Widget 第一次被添加或者大小发生变化时调用该方法,可以在此控制 Widget 元素的显示和隐藏。

示例代码:

public class ExampleAppWidgetProvider extends AppWidgetProvider {
    private static final String TAG = "ExampleAppWidget";

    // 启动ExampleAppWidgetService服务对应的action
    private final Intent EXAMPLE_SERVICE_INTENT =
            new Intent("android.appwidget.action.EXAMPLE_APP_WIDGET_SERVICE");
    // 更新 widget 的广播对应的action
    private final String ACTION_UPDATE_ALL = "com.skywang.widget.UPDATE_ALL";
    // 保存 widget 的id的HashSet,每新建一个 widget 都会为该 widget 分配一个 id。
    private static Set idsSet = new HashSet();

    // onUpdate() 在更新 widget 时,被执行,
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        Log.d(TAG, "onUpdate(): appWidgetIds.length="+appWidgetIds.length);

        // 每次 widget 被创建时,对应的将widget的id添加到set中
        for (int appWidgetId : appWidgetIds) {
            idsSet.add(Integer.valueOf(appWidgetId));
        }
        PreferencesUtils.putString(context, Keys.IDSSET, CharFunction.toJSONString(idsSet));
        updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
    }

    // 当 widget 被初次添加 或者 当 widget 的大小被改变时,被调用
    @Override
    public void onAppWidgetOptionsChanged(Context context,
                                          AppWidgetManager appWidgetManager, int appWidgetId,
                                          Bundle newOptions) {
        updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);

    }

    // widget被删除时调用
    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        Log.d(TAG, "onDeleted(): appWidgetIds.length="+appWidgetIds.length);

        // 当 widget 被删除时,对应的删除set中保存的widget的id
        for (int appWidgetId : appWidgetIds) {
            idsSet.remove(Integer.valueOf(appWidgetId));
        }
        PreferencesUtils.putString(context,Keys.IDSSET, CharFunction.toJSONString(idsSet));
        super.onDeleted(context, appWidgetIds);
    }

    // 第一个widget被创建时调用
    @Override
    public void onEnabled(Context context) {
        Log.d(TAG, "onEnabled");
        updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
        super.onEnabled(context);
    }

    // 最后一个widget被删除时调用
    @Override
    public void onDisabled(Context context) {
        Log.d(TAG, "onDisabled");
        // 在最后一个 widget 被删除时,终止服务
        context.stopService(EXAMPLE_SERVICE_INTENT);
        super.onDisabled(context);
    }


    // 接收广播的回调函数
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        Log.d(TAG, "OnReceive:Action: " + action);
        if (ACTION_UPDATE_ALL.equals(action)) {
            // “更新”广播
            updateAllAppWidgets(context, AppWidgetManager.getInstance(context), idsSet);
        }

        super.onReceive(context, intent);
    }

    // 更新所有的 widget
    private void updateAllAppWidgets(Context context, AppWidgetManager appWidgetManager, Set set) {
        Log.d(TAG, "updateAllAppWidgets(): size="+set.size());
        // widget 的id
        int appID;
        // 迭代器,用于遍历所有保存的widget的id
        Iterator it = set.iterator();
        while (it.hasNext()) {
            appID = ((Integer)it.next()).intValue();
            // 获取 example_appwidget.xml 对应的RemoteViews
            RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.example_appwidget);
            remoteView.setTextViewText(R.id.tv_gongli, CalendarUtil.getAllInfoHtml(Calendar.getInstance()));
            remoteView.setOnClickPendingIntent(R.id.tv_gongli, getPendingIntent(context, R.id.tv_gongli));
            // 更新 widget
            appWidgetManager.updateAppWidget(appID, remoteView);
        }
    }

    private PendingIntent getPendingIntent(Context context, int buttonId) {
        Intent intent = new Intent();
        intent.setClass(context, ExampleAppWidgetProvider.class);
        intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
        intent.setData(Uri.parse("custom:" + buttonId));
        PendingIntent pi = PendingIntent.getBroadcast(context, 0, EXAMPLE_SERVICE_INTENT, 0 );
        return pi;
    }


}


创建 App Widget Configuration Activity

如果你的 Widget 需要用户配置一些选项,你可以为你的 Widget 创建 Configuration Activity,当用户添加 Widget 时会自动弹出该 Activity。Configuration Activity 和普通 Activity 一样需要在 Manifest 中声明,但是需要额外声明一个 intent-filter: APPWIDGET_CONFIGURE,例如:

<activity android:name=".ExampleAppWidgetConfigure">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
</activity>

同时还需要在上述的 appwidget-provider 中声明:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:configure="com.example.android.ExampleAppWidgetConfigure"
    ... >
</appwidget-provider>

有两点需要注意的是:

  1. Activity 必须返回带 EXTRA_APPWIDGET_ID 的 result。
  2. 声明Configuration Activity 后 onUpdate() 在 Widget 添加时不会被调用,Activity 负责调用 AppWidgetManager.updateAppWidget() 完成 Widget 更新。
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
    mAppWidgetId = extras.getInt(
        AppWidgetManager.EXTRA_APPWIDGET_ID,
        AppWidgetManager.INVALID_APPWIDGET_ID);
}

AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
RemoteViews views = new RemoteViews(context.getPackageName(),R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);

Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();

参考:
https://www.jianshu.com/p/985547afd22f
http://glgjing.github.io/blog/2015/11/05/android-kai-fa-zhi-app-widget-xiang-jie/
https://blog.csdn.net/yangwen123/article/details/8042499

上一篇 下一篇

猜你喜欢

热点阅读