Android 应用微件的使用

1,705 阅读10分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

概述

应用微件可以在用户的Launcher上显示我们应用的一些重要信息,比如当前播放的音乐以及对音乐的一些操作,又或者是当前的天气信息,日历信息等。如果我们的应用会对用户提供一些很重要的信息,那么我们选择创建应用微件来给用户展示这些重要的信息。

而且个人感觉应用微件也是代替状态栏用户通知的一种好的办法,之前倾向于使用WorkManager配合Notification来给用户创建提醒,但是国内的OEM对WorkManager的限制非常多,这就导致用户处于后台一段时间之后会被杀死,而这个应用创建的WorkManager在后期也不会执行。在这种情况下,创建一个桌面微件,当用户看到这个微件并想做一些操作的时候就会主动去点击这个微件,从而避免了应用被动去提醒用户。当然这里所说的也只是和用户相关的操作,如果是和用户无关的后台操作那么这种方式也是不合适的。

应用微件的布局

应用微件的布局和普通的布局是一样的,唯一不同的是我们不了解微件的宽高,其实在桌面上我们可以将桌面想象成一个一个的格子,我们的应用微件大概会占据几个格子。这样可能仍然不好想象,那我们可以大概想象我们的微件的宽高比,然后再思考如何布局。

在这里首先需要考虑好我们需要提供一个什么样的功能,这个功能都需要显示什么样的元素,然后再想象我们在一个正方形(也可能是长方形,这个是看我们怎么定义)的格子中如何排列这些元素,我们的应用期望给用户提供一个显示今日账单的微件,所以我们使用了下面的布局:

<!--应用微件的示例布局-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/root"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/shape_radius_10_solid_white">

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

        <!--今日已用-->
        <TextView
                android:id="@+id/tv_today_used"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@string/today_used"
                android:paddingHorizontal="@dimen/dp_10"
                android:paddingVertical="@dimen/dp_10"
                />

        <!--使用的金额-->
        <TextView
                android:id="@+id/tv_amount"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:text="¥100"
                android:textColor="@color/color_f54949"
                android:textSize="@dimen/sp_28"
                android:maxLines="1"
                android:maxLength="10"
                android:gravity="center"
                android:textStyle="bold"
                />
        <!--点击添加账单-->
        <TextView
                android:id="@+id/tv_click_add_bill"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingVertical="@dimen/dp_10"
                android:paddingHorizontal="@dimen/dp_10"
                android:text="@string/click_add_bill"
                android:textSize="@dimen/sp_14"
                android:gravity="end"
                />

    </LinearLayout>

</FrameLayout>

另外这里需要注意的是:自定义的View是没有办法使用的,因为应用微件是基于RemoteViews,目前只能使用下面的View

  1. 可以使用的布局类包括: FrameLayout,LinearLayout,RelativeLayout,GridLayout
  2. 可以使用的View包括: AnalogClock,Button,Chronometer,ImageButton,ImageView,ProgressBar,TextView,ViewFilpper,ListView,GridView,StackView,AdapterViewFlipper

创建AppWidgetProvider的子类

当我们创建了一个布局文件之后,我们就需要去创建一个AppWidgetProvider的子类。追踪一下代码就可以发现,这其实是一个BroadcastReceiver,只不过在AppWidgetProvider中帮我们执行了onReceiver()方法,然后根据不同的action调用不同的方法。我们可以通过重写这些方法来定义我们的实现。

下面的代码仅仅是创建了一个AppWidgetProvider的子类,如下所示:

class ExampleAppWidgetProvider : AppWidgetProvider() {
        override fun onUpdate(
        context: Context?,
        appWidgetManager: AppWidgetManager?,
        appWidgetIds: IntArray?
    ) {
    }
}

需要注意的是,我们必须重写其中的onUpdate()方法,否则在添加应用微件的时候应用微件显示的不对(看起来是没有显示任何元素但是其实已经占据了相应的位置).

声明微件

既然AppWidgetProvider的本质就是广播,那我们自然也需要像普通的广播那样进行声明,之后才可以正常使用,上面创建了一个名为ExampleAppWidgetProvider的微件,下面我们需要在Manifest文件中声明这个微件,如下所示:

        <receiver android:name=".widget.ExampleAppWidgetProvider">
            <intent-filter>
                <!-- 接收下面的action用于更新数据 -->
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
            <!-- 配置当前应用微件的元数据 -->
            <meta-data
                    android:name="android.appwidget.provider"
                    android:resource="@xml/example_appwidget_info" />
        </receiver>

上面的这些元素都是必要的,我们必须声明这些元素才可以正常使用应用微件:

  1. 我们必须要声明receiver标签,就和声明普通的广播是一样的,其中的name属性指定了我们要使用的应用微件属于哪个类;
  2. 必须在intent-filter标签中指定名为android.appwidget.action.APPWIDGET_UPDATEaction,这是唯一必须声明的action,我们也可以声明其它action,但那些不是必须的;
  3. meta-data标签中的name属性是固定的,resource则是表示关于我们的微件的配置信息存储的文件路径,我们的配置信息存储在@xml/example_appwidget_info这个文件中.

配置微件

上面已经声明了需要使用的微件,针对微件信息,还需要单独进行配置,在@xml/example_appwidget_info这个文件中的信息为:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        android:minWidth="@dimen/dp_80"
        android:minHeight="@dimen/dp_60"
        android:updatePeriodMillis="0"
        android:initialLayout="@layout/appwidget_example"
        android:resizeMode="none"
        android:widgetCategory="home_screen"
        >

</appwidget-provider>

这个文件中的元素解释如下:

  • minWidthminHeight属性的值定义了应用微件默认情况下占用的最小尺寸空间,当时如上所述,虽然我们定义了这两个属性的值,但是在创建应用微件的时候并不一定会严格使用这两个属性的值,而是根据网格的大小去适配的,如果网格的大小和指定的尺寸不一致,那么微件的尺寸会向上舍入到最接近网格的大小;
  • updatePeriodMillis: 这个属性定义应用微件框架通过调用onUpdate()回调方法来从AppWidgetProvider请求更新的频率应该是多大,但是并不能保证会按照这个频率去触发更新。而且还有一个问题是每当需要更新应用微件的时候需要唤醒设备,如果设置的时间比较短,可能会一直唤醒设备,这里文档中建议我们使用AlarmManager设置一个具有AppWidgetProvider会接收的Intent闹钟,并将闹钟的类型设置为ELAPSED_REALTIME或者RTC,这样只有设备处于唤醒状态时,闹钟才会响起.之后需要将这个属性的值设置为0;
  • initialLayout: 这个属性用来指定显示的应用微件的资源文件,这里设置的就是我们上面定义的那个资源文件;
  • resizeMode: 这个属性用来指定用户可以根据什么样的规则来调整应用文件的大小,这里设置的none表示不接受任何方向对大小的调整.还可以接受horizontalvertical表示在横向和纵向调整大小,如果想要设置在横向和纵向均可以调整大小,则可以设置horizontal|vertical;
  • widgetCategory: 这个属性声明微件是否可以显示在主屏幕(home_screen)或者锁定屏幕(keyguard)上,但是只有在低于5.0的版本上才支持锁定屏幕微件,后续的版本只支持home_screen.

除了上面已经设置的属性,还可以设置下面的属性:

  • configure: 这个属性用来配置当应用微件被用户添加到屏幕上的时候打开的配置信息的Activity,如果我们希望在用户添加屏幕微件的时候对屏幕微件进行配置,则可以在这里设置Activity的全路径;
  • previewImage: 这个属性用于配置用户可以预览的应用微件的图片,在用户希望添加此屏幕微件之前,用户可以通过这个图片查看到添加屏幕微件之后的效果;
  • minResizeHeightminResizeWidth这两个属性指定可以将微件调整到的最小高度和最小宽度。

预览

通过上面的几步操作,我们就可以在桌面上创建我们的应用微件了,下面是在模拟器上添加应用微件的预览效果:

添加应用微件

AppWidgetProvider

上面我们已经了解到,AppWidgetProvider其实本质是一个广播,它仅接收和应用微件相关的广播,例如当更新,删除,启用或者停用应用微件时均会发出不同的广播,当发生这些广播事件的时候,AppWidgetProvider中不同的方法会被调用。

onUpdate()

调用此方法可以按照上面配置的updatePeriodMillis属性的时间间隔来更新微件。当用户添加微件的时候也会调用此方法,这个时候它需要进行基本设置。但是需要注意的是,如果我们配置了添加应用微件时需要启动的配置Activity,那么这种情况下不会调用onUpdate()方法,此时我们需要在Activity页面自行发出广播来通知应用微件已被添加。

比如在我们的应用中,我们会在应用微件一开始被添加的时候获取用户的账单,然后再把这个账单信息更新到应用微件上,同时我们还需要设置针对某一个按钮的点击操作,所以我们在onUpdate()中执行了下面的操作:

    override fun onUpdate(
        context: Context?,
        appWidgetManager: AppWidgetManager?,
        appWidgetIds: IntArray?
    ) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)

        appWidgetIds?.forEach {
            Logs.e("appWidgetId is:$it")
        }
        val intent = Intent(context, YourActivity::class.java)
        val pendingIntent = PendingIntent.getActivity(context,0,intent,0)
        
        //设置点击事件
        val workManagerIntent = Intent(context, YourActivity::class.java)
        val workManagerPending = PendingIntent.getActivity(context,0,workManagerIntent,0)
        
        //创建更新后的RemoteViews
        val remoteViews = RemoteViews(context?.packageName,R.layout.appwidget_example).apply {
            this.setOnClickPendingIntent(R.id.tv_click_add_bill, pendingIntent)
            this.setOnClickPendingIntent(R.id.root, workManagerPending)
            this.setTextViewText(R.id.tv_amount, "¥150")
        }
        //更新应用微件
        appWidgetManager?.updateAppWidget(
            ComponentName(context!!.packageName, this.javaClass.name),
            remoteViews
        )
    }

onAppWidgetOptionsChanged()

这个方法会在首次放置应用微件或者用户调整应用微件大小的时候回调,通过这个方法我们能够了解当前应用微件的大概尺寸,然后我们可以根据不同的尺寸去更新不同的RemoteViews,如下所示:

    override fun onAppWidgetOptionsChanged(
        context: Context?,
        appWidgetManager: AppWidgetManager?,
        appWidgetId: Int,
        newOptions: Bundle?
    ) {
        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
        newOptions?.let {
            val minWidth = it.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH,0)
            val maxWidth = it.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH,0)
            val minHeight = it.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT,0)
            val maxHeight = it.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT,0)
            Logs.e("app widget size  min:[$minWidth,$minHeight],max:[$maxWidth,$maxHeight]")
        }
    }

运行程序并调整应用微件的大小,可以看到如下的日志信息:

app widget size  min:[137,208],max:[209,273]
app widget size  min:[214,208],max:[322,273]
app widget size  min:[214,133],max:[322,176]
app widget size  min:[137,133],max:[209,176]

使用这个方法需要注意三点:

  1. 和文档中介绍的不一样,在首次放置应用微件的时候并没有回调这个方法
  2. 这个方法在Android4.1中引入,使用的时候需要注意版本
  3. 这个方法中返回的尺寸单位为dp

onDeleted()

每次从应用微件托管应用中删除应用微件时,均会调用此方法。

    override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
        super.onDeleted(context, appWidgetIds)
        Logs.e("delete widget:${Arrays.toString(appWidgetIds)}")
    }

当删除应用微件时,会得到如下的日志信息:

delete widget:[18]
delete widget:[19]
delete widget:[20]

onEnabled()

当应用微件被启用的时候会回调这个方法,需要注意的是:如果我们多次添加同一个应用微件,仍然只有第一个应用微件被添加的时候会回调这个方法。在这个方法中可以做一些针对当前应用微件的公共操作,比如打开临时数据库等。

    override fun onEnabled(context: Context?) {
        super.onEnabled(context)
        Logs.e("app widget enable")
    }

运行程序会得到如下的日志:

app widget enable
update appWidgetId is:21
update appWidgetId is:22
update appWidgetId is:23

从上面的信息可以看出,只有添加第一个应用微件的时候打印了app widget enable,后续只回调了onUpdate()方法。

onDisabled()

应用微件被完全移除的时候会回调此方法,如果当前桌面上有多个应用微件,只有当最后一个应用微件被删除的时候会回调此方法。在此方法中可以回收一些针对应用微件的资源,比如数据库连接等。

    override fun onDisabled(context: Context?) {
        super.onDisabled(context)
        Logs.e("app widget disable")
    }

运行上面的程序可以得到如下的日志:

delete widget:[24]
delete widget:[25]
delete widget:[26]
app widget disable

onReceive()

这个是继承自父类的方法,虽然在AppWidgetProvider中已经帮我们封装了一些常用的操作,但是有时候可能仍然不满足我们的需求,这个时候我们就可以通过执行一些自定义的action来执行我们想要的操作。

固定应用微件

在上面的操作中,我们都是通过在应用微件托管程序(也就是Launcher)中长按空白区域来添加应用微件的,对于我们的应用来说,这样做实在是太被动了,我们期望能够主动通知用户,让用户主动去添加我们提供的这个应用微件。

下面的代码演示了主动向用户发起添加应用微件的请求:

        if (SdkVersionUtils.checkAndroidO()) {
            val service = this.getSystemService(AppWidgetManager::class.java)
            val provider = ComponentName(this, ExampleAppWidgetProvider::class.java)
            val successCallback =
                if (service.isRequestPinAppWidgetSupported) {
                    //如果支持申请应用微件,则可以在此处创建一个广播,用于应用微件申请成功后的通知,如果不需要则直接使用null即可
                }else{
                    null
                }
            service.requestPinAppWidget(provider,null,successCallback)
        } else {
            Snackbar.make(
                mBinding.root,
                getString(R.string.current_version_not_support_request_fixed_app_widget),
                Snackbar.LENGTH_SHORT
            ).show()
        }

运行上面的程序,申请添加应用微件之后就可以在应用微件托管程序中出现了:

申请添加应用微件

更新应用微件中的数据

虽然我们设置了updatePeriodMillis来在间隔一段时间之后主动更新应用微件中的数据,但是一方面系统并不会向我们保证一定执行此操作,另一方面我们的应用希望在用户更新了数据之后立即反映到应用微件上,所以这个时候我们就需要主动去更新应用微件的数据。

其实查看过onUpdate()方法的实现的话,这里也就没必要继续看了,因为使用的是同样的方法,创建一个新的RemoteViews对象,并将最新的数据设置到RemoteViews中相应的View上面,然后调用AppWidgetManagerupdateXXX()方法即可更新了,如下所示:

        val remoteViews = RemoteViews(this.packageName,R.layout.appwidget_example).apply {
            this.setTextViewText(R.id.tv_amount,"100.9")
        }
        val manager = AppWidgetManager.getInstance(this)
        val componentName = ComponentName(this,ExampleAppWidgetProvider::class.java)
        manager.updateAppWidget(componentName,remoteViews)

运行上面的代码就可以更新应用微件中的数据了:

更新应用微件的数据

从上面的图片可以看出,应用微件中的数据成功被更新了。