美文网首页
关于Android桌面小组件相关的开发,涉及到的一些点

关于Android桌面小组件相关的开发,涉及到的一些点

作者: JeffreyWorld | 来源:发表于2023-03-16 14:33 被阅读0次

    你可能用过一些 Android APP 的小组件,比如:

    • 支付宝的小组件:之前疫情期间添加了对应小组件卡片在桌面,可点击小卡片上的查看健康码的按钮,可一键打开健康码。
    • 音乐类 APP的小组件:添加对应对应小组件后, 可在 APP 的主屏幕中轻松看到当前播放歌曲的相关信息:歌曲封面、歌曲名、歌手名称、所属专辑名称等。
    • 时钟类 APP 的小组件:可添加各种样式的时钟小组件在屏幕,装饰你的主屏幕,在你喜欢的小组件上来查看时间。
    • 天气类 APP 的小组件:可在主屏幕直接看到天气相关信息,不用再打开天气的 APP

    如果你所开发的项目的产品经理并没有相关开发需求,可能很多 Android 开发并没有接触过相关桌面主屏幕的相关小组件开发,这篇文章主要介绍下相关开发的知识点。

    AppWidgetProvider:
    帮助实现 AppWidget 提供程序的便利类。 你可以用 AppWidgetProvider 做的事情,你也可以用一个普通的 BroadcastReceiver 做。 AppWidgetProvider 只是从 onReceive(Context,Intent) 中接收到的 Intent 中解析出相关字段,并使用接收到的 extra 调用 hook 方法。
    扩展此类并覆盖 onUpdateonDeletedonEnabledonDisabled 方法中的一个或多个以实现您自己的 AppWidget 功能。

    public class WidgetDemoProvider extends AppWidgetProvider {
    
        /**
         * 当 Widget 第一次被添加时调用,例如用户添加了两个你的 Widget,那么只有在添加第一个 Widget 时该  
         * 方法会被调用。所以该方法比较适合执行你所有 Widgets 只需进行一次的操作。对用广播的 Action 为 
         * ACTION_APPWIDGET_ENABLE。
         */
        @Override
        public void onEnabled(Context context) {
            super.onEnabled(context);
        }
    
        /**
         * 与 onEnabled 恰好相反,当你的最后一个 Widget 被删除时调用该方法,所以这里用来清理之前在 onEnabled() 中进行的操作。
         * 当最后一个该类型的小部件从桌面移除时调用,对应的广播的 Action 为 ACTION_APPWIDGET_DISABLED。
         *
         */
        public void onDisabled(Context context) {
            super.onDisabled(context);
        }
    
        /**
         * 当 Widget 第一次被添加或者大小发生变化时调用该方法,可以在此控制 Widget 元素的显示和隐藏。
         * 当小部件布局发生更改的时候调用。对应广播的 Action 为 ACTION_APPWIDGET_OPTIONS_CHANGED。
         *
         * @param appWidgetManager 您可以调用 AppWidgetManager.updateAppWidget 的 AppWidgetManager 对象。
         * @param appWidgetId 大小改变的widget的appWidgetId。
         * @param newOptions
         */
        @Override
        public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
            super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
        }
    
        /**
         * 当小部件从备份中还原,或者恢复设置的时候,会调用,实际用的比较少。对应广播的 Action 为 ACTION_APPWIDGET_RESTORED。
         * @param oldWidgetIds
         * @param newWidgetIds
         */
        @Override
        public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
            super.onRestored(context, oldWidgetIds, newWidgetIds);
        }
    
        /**
         * AppWidget 更新事件
         *
         * 小部件被添加时或者每次小部件更新时都会调用一次该方法,配置文件中配置小部件的更新周期 updatePeriodMillis,每次更新都会调用。对应广播 Action 为:ACTION_APPWIDGET_UPDATE 和 ACTION_APPWIDGET_RESTORED
         *
         * @param appWidgetManager 更新 AppWidget 状态; 获取有关已安装 AppWidget 提供程序和其他 AppWidget 相关状态的信息。
         * @param appWidgetIds 需要更新的 appWidgetIds。 请注意,这可能是此提供程序的所有 AppWidget 实例,也可能只是其中的一个子集。
         */
        @Override
        public void onUpdate(Context context, AppWidgetManager appWidgetManager,
                int[] appWidgetIds) {
        }
    
        /**
         * 当 Widget 被删除时调用该方法。
         *
         * 每删除一个小部件就调用一次。对应的广播的 Action 为: ACTION_APPWIDGET_DELETED 。
         *
         * @param appWidgetIds 已从其组件集群中删除的 appWidgetIds。
         */
        @Override
        public void onDeleted(Context context, int[] appWidgetIds) {
            super.onDeleted(context, appWidgetIds);
        }
    
        /**
         *  接收广播的回调函数
         * onReceive() 中处理的是 Widget 相关的广播事件,然后分发到各个回调函数中onUpdate(), onDeleted(), onEnabled(), onDisabled, onAppWidgetOptionsChanged()。
         *
         * @param context
         * @param intent
         */
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
                this.onEnabled(context);
            }
            else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
                this.onDisabled(context);
            }
            ...省略
            super.onReceive(context, intent);
        }
    }
    

    根据自己的业务,写好 WidgetDemoProvider 的自定义代码后,需要在 AndroidManifest.xml 里注册一下:

          <receiver android:name=".widget.WidgetDemoProvide"
                android:exported="true">
                <intent-filter>
                    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                    <action android:name="xxx..."/>  <!-- 此处可以添加自己需要的 -->
                </intent-filter>
                <meta-data android:name="android.appwidget.provider"
                    android:resource="@xml/appwidget_demo" /> <!-- 此处可以添加自己需要的给用户提前预览的自定义小组件布局 -->
            </receiver>
    
    

    <receiver> 元素需要 android:name 属性,该属性指定小部件使用的 AppWidgetProviderAppWidgetProvider 的父类就是 BroadcastReceiver)。
    <intent-filter> 中的 <action> 元素指定小部件接受 ACTION_APPWIDGET_UPDATE 广播。这是必须明确声明的唯一一项广播,用以接收小部件的增删改等信息。
    <meta-data> 元素指定小部件的资源,并且需要以下属性:
    android:name - 指定元数据名称。必须使用 android.appwidget.provider 将数据标识为 AppWidgetProviderInfo 描述符。
    android:resource - 指定 AppWidgetProviderInfo 资源位置。

    appwidget_demo.xml 的示例代码:

    <?xml version="1.0" encoding="utf-8"?>
    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="180dp"
        android:minHeight="180dp"
        android:updatePeriodMillis="1800000"
        android:previewImage="@mipmap/app_widget_preview_3x3" 
        android:initialLayout="@layout/app_widget_preview_layout_3_3"
        android:resizeMode="horizontal|vertical"> 
    </appwidget-provider>
    

    minWidthminHeight :指定小部件默认情况下占用的最小空间。
    注意:为使小部件能够在设备间移植,小部件的最小大小不得超过 4 x 4 单元格。
    minResizeWidthminResizeHeight:指定小部件的绝对最小大小。
    updatePeriodMillis:定义小部件框架通过调用 onUpdate() 回调方法来从 AppWidgetProvider 请求更新的频率应该是多大。
    initialLayout:指向用于定义小部件布局的布局资源。
    configure:定义要在用户添加小部件时启动以便用户配置小部件属性的 Activity。
    previewImage:指定预览来描绘小部件经过配置后是什么样子的,用户在选择小部件时会看到该预览。
    autoAdvanceViewId :指定应由小部件的托管应用自动跳转的小部件子视图的视图 ID。
    resizeMode :指定可以按什么规则来调整微件的大小,可选值为“horizontal|vertical”,一般默认设置横竖都可以进行调整。
    minResizeHeight :指定可将微件大小调整到的最小高度。
    minResizeWidth:指定可将微件大小调整到的最小宽度。
    widgetCategory:声明小部件是否可以显示在主屏幕 (home_screen) 或锁定屏幕 (keyguard) 上。只有低于 5.0 的 Android 版本才支持锁定屏幕微件。对于 Android 5.0 及更高版本,只有 home_screen 有效,所以现在将这个值写为 home_screen 即可。

    如果在自己的相关业务代码里,比如 activity 里如何触发 WidgetDemoProvider 相关数据以及页面更新。可以看以下示例代码:

      private void sendNotify(){
        try {
          Class javaClass = Class.forName("xxx.WidgetDemoProvider");
          final Intent intent = new Intent(this, javaClass);
          intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
          int[] ids = AppWidgetManager.getInstance(GlobeContext.context).getAppWidgetIds(new ComponentName(GlobeContext.context, javaClass));
          intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
          sendBroadcast(intent);
    
        } catch (ClassNotFoundException e) {
          e.printStackTrace();
        }
      }
    

    一些开发注意事项:

    • 小组件的宽高是可以支持用户自行调整的,只需简单的设置最低宽高,但是可调整的最小粒度是根据手机的 icon 为标准。小组件数量无限制,用户对小组件的大小和具体功能喜好都不太一样,所以解决方案就简单粗暴一点,你能想到的适配尺寸,每种尺寸搞一个,用户自己选择合适的尺寸就好。大、中、小、大中、中小、微小、超大等尺寸,可以全部做一遍。
    android:resizeMode="horizontal|vertical"
    widget 可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸
    
    • 更新时间分为主动更新和定时更新。
      主动更新:即在 APP 中可以动态更新这个桌面小组件,这种情况更新没有时间限制。
      定时更新:小组件需要展示的数据可能已经发生了变化,但是 APP 已经被系统杀死了,无法主动更新数据,就会导致小组件展示的数据可能是已过期的或者是旧的,这时候就可以用到小组件的定时更新功能,但是这个定时更新有一个限制,基于省电逻辑,最快的更新周期为 30 分钟。(如果是在 onUpdate 方法中写个定时器定时更新,这样是不行的,会被系统给杀死,杀死之后小组件不会消失,而是一直显示最后一次更新时候的状态,直到下一次更新数据,类似于电子水墨屏的逻辑。)
    • 一般点击整个小组件,我们直接调起 APP。点击跳转页面需要用到 PendingIntent,这玩意的 Flag 有很多种模式,具体可以查看文章底部的参考文档,坑就坑在这个 Flag,31 之后的系统有改动,会报错,所以 31 的系统需要用 PendingIntent.FLAG_IMMUTABLE,具体看代码。
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
                this.onEnabled(context);
            }
            else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
                this.onDisabled(context);
            }
            if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
                Uri data = intent.getData();
                int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
                switch (buttonId) {
                    case R.id.widget_layout:
                       Intent intent = new Intent(context, RemotePlayerActivity.class);
                       intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                       context.startActivity(goIntent);
                       RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.app_widget_layout);
                       //将按钮与点击事件绑定
                       remoteView.setOnClickPendingIntent(R.id.widget_layout,getPendingIntent(context, R.id.widget_layout));
                       break;
                }
            }
            super.onReceive(context, intent);
        }
    
    
        private PendingIntent getPendingIntent(Context context, int buttonId) {
            Intent intent = new Intent();
            intent.setClass(context, WidgetDemoProvider.class);
            intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
            intent.setData(Uri.parse("harvic:" + buttonId));
            PendingIntent pendingIntent;
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
                pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
            } else {
                pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
            }
            return pendingIntent;
        }
    
    
    • 通过 PendingIntent 就可以直接调起 APP 的相关页面,不过这里也有坑,假设你 APP 的启动页面是 MainActivity 页面,点击小组件你就让它跳转到 MainActivity 页面走正常的 APP 启动流程,就等同于是点击小组件就能启动 APP,哪怕 APP 被杀死了。坑就坑在于,通过这种方式打开的 APP,它不会走 Application 类,也就是你如果是在 Application 中初始化了某些东西,但是 APP 已经被系统杀死了,这时候你再点击小组件启动 APP,这时就会发现好多组件用不了,因为没初始化。
      所以相对省事的做法就是把 Application 的所有需要初始化的东西都放 MainActivity 里面初始化了,目前为了工信部隐私相关合规,应该很多 APP 的初始化代码应该已经从 Application 放到用户点同意隐私协议弹框后再去初始化了。

    RemoteViews,从字面意思理解为它是一个远程视图。是一种远程的 View,它在其它进程中显示,却可以在另一个进程中更新。RemoteViewsAndroid 中的使用场景主要有:自定义通知栏和桌面小部件。

    RemoteViewsService,是管理 RemoteViews 的服务。一般,当 AppWidget 中包含 GridViewListViewStackView 等集合视图时,才需要使用 RemoteViewsService 来进行更新、管理。RemoteViewsService 更新集合视图的一般步骤是:通过 setRemoteAdapter() 方法来设置 RemoteViews对应 RemoteViewsService
    之后在 RemoteViewsService 中,实现 RemoteViewsFactory 接口。然后,在 RemoteViewsFactory 接口中对集合视图的各个子项进行设置,例如 ListView 中的每一 Item

    RemoteViewsFactory 通过 RemoteViewsService 中的介绍,我们知道 RemoteViewsService 是通过
    RemoteViewsFactory 来具体管理 layout 中集合视图的,RemoteViewsFactoryRemoteViewsService 中的一个内部接口。RemoteViewsFactory 提供了一系列的方法管理集合视图中的每一项。
    例如:
    public RemoteViews getViewAt(int position):
    通过getViewAt()来获取“集合视图”中的第position项的视图,视图是以RemoteViews的对象返回的。
    public int getCount() :
    通过getCount()来获取“集合视图”中所有子项的总数。

    • 用户可重新设置原有 widget。Android 12 之前,重新设置 widget 意味着用户必须删除现有 widget,然后使用新配置重新添加。Android 12 在多个方面改进了 widget 的配置方式,从而帮助用户采用更简单的方式对 widget 进行个性化配置。可重组的 widget 允许用户对 widget 进行自定义设置。在 Android 12 中,用户将无需通过删除和重新添加 widget 来调整这些原有设定。
      要使用这一功能,您需在 appwidget-provider 中把 widgetFeatures 属性设置为 reconfigurable
      当用户配置该 widget 时,新的配置会被记录在 ListWidgetConfigureActivity 中。
      如果您的 widget 依赖默认设置,在 Android 12 中您可跳过初始化操作,通过默认配置来设置 widget
    <appwidget-provider
       android:configure="com.example.android.appwidget.ListWidgetConfigureActivity"
       android:widgetFeatures="reconfigurable|configuration_optional"
       ... />
    
    • Android 12 中 Widget 的尺寸限制改进。除了现有的 minWidthminHeighminResizeWidth 以及 minResizeHeight 以外,Android 12 还添加了新的 appwidget-provider 属性。您可以使用新的 maxResizeWidthmaxResizeHeight 属性,来定义用户所能够调整的 widget 尺寸的最大高度和宽度。新的 targetCellWidthtargetCellHeight 属性能够定义设备主屏幕上的 widget 默认尺寸。如果之前有 targetCellWidthtargetCellHeight 属性的话,小部件也不至于像现在这么乱而导致用户不想使用。
    <appwidget-provider
       android:maxResizeWidth="240dp"
       android:maxResizeHeight="180dp"
       android:minWidth="180dp"
       android:minHeight="110dp"
       android:minResizeWidth="180dp"
       android:minResizeHeight="110dp"
       android:targetCellWidth="3"
       android:targetCellHeight="2"
       ... />
    <!-- maxResizeWidth:定义用户所能够调整的小部件尺寸的最大宽度
    maxResizeHeight:定义用户所能够调整的小部件尺寸的最大高度
    targetCellWidth:定义设备主屏幕上的小部件默认宽度所占格数(即使不同型号的手机中也会占定义好的格数,但手机系统版本必须在 Android 12 及以上)
    targetCellHeight:定义设备主屏幕上的小部件默认高度所占格数 -->
    
    
    • 新的小部件控件。Android 12 使用以下现有控件新增了对有状态行为的支持:CheckBoxSwitch
      RadioButton,上面这几个控件大家应该非常熟悉了,但在 Android 12 之前在小部件中想要使用的话也是不可能的。
    • Android 12 以上可以通过 system_app_widget_background_radiussystem_app_widget_inner_radius 系统参数来设置微件圆角的半径。

    相关文章

      网友评论

          本文标题:关于Android桌面小组件相关的开发,涉及到的一些点

          本文链接:https://www.haomeiwen.com/subject/byrlrdtx.html