当前位置:   article > 正文

一直存在、一直被忽略的功能Widget_widgets有什么用

widgets有什么用

如果要在Android系统中找一个一直存在,但一直被人忽略,而且有十分好用的功能,那么Widget,一定算一个。这个从Android 1.x就已经存在的功能,经历了近10年的迭代,在遭到无数无视和白眼之后,又重新回到了大家的视线之内,当然,也有可能是App内部已经没东西好卷了,所以大家又把目光放到了App之外,但不管怎样,Widget在Android 12之后,都开始焕发一新,官网镇楼,让我们重新来了解下这个最熟悉的陌生人。

https://developer.android.com/develop/ui/views/appwidgets/overview

Widget使用的是RemoteView,这与Notification的使用如出一辙,RemoteView是继承自Parcelable的组件,可以跨进程使用。在Widget中,通过AppWidgetProvider来管理Widget的行为,通过RemoteView来对Widget进行布局,通过AppWidgetManager来对Widget进行刷新。基本的使用方式,我们可以通过一套模板代码来实现,在Android Studio中,直接New Widget即可。这样Android Studio就可以自动为你生成一个Widget的模板代码,详细代码我们就不贴了,我们来分析下代码的组成。

首先,每个Widget都包含一个AppWidgetProvider。这是Widget的逻辑管理类,它继承自BroadcastReceiver,然后,我们需要在清单中注册这个Receiver,并在meta-data中指定它的配置文件,它的配置文件是一个xml,这里描述的是添加Widget时展示的一些信息。

从这些地方来看,其实Widget的使用还是比较简单的,所以本文也不准备来讲解这些基础知识,下面我们针对开发中会遇到的一些实际需求来进行分析。

1

appwidget-provider配置文件

这个xml文件虽然简单,但还是有些有意思的东西的。

尺寸

在这里我们可以为Widget配置尺寸信息,通过maxResizeWidth、maxResizeHeight和minWidth、minHeight,我们可以大致将Widget的尺寸控制在MxN的格子内,这也是Widget在桌面上的展示方式,它并不是通过指定的宽高来展示的,而是桌面所占据的格子数。

官方设计文档中,对格子数和尺寸的转换标准,有一个表格,如下所示。

我们在设计的时候,也应该尽量遵循这个尺寸约束,避免在桌面上展示异常。在Android12之后,描述文件中,还增加了targetCellWidth和targetCellHeight两个参数,他们可以直接指定Widget所占据的格子数,这样更加方便,但由于它仅支持Android12+,所以,通常这些属性会一起设置。

有意思的是这个尺寸标准并不适用于所有的设备,因为ROM的碎片化问题,各个厂商的桌面都不一样,所以。。。只能参考参考。

updatePeriodMillis

这个参数用于指定Widget的被动刷新频率,它由系统控制,所以具有很强的不定性,而且它也不能随意设置,官网上对这个属性的限制如下所示。

 

updatePeriodMillis只支持设置30分钟以上的间隔,即1800000milliseconds,这也是为了保证后台能耗,即使你设置了小于30分钟的updatePeriodMillis,它也不会生效。

对于Widget来说,updatePeriodMillis控制的是系统被动刷新Widget的频率,如果当前App是活着的,那么随时可以通过广播来修改Widget。

而且这个值很有可能因为不同ROM而不同,所以,这是一个不怎么稳定的刷新机制。

其它

除了上面我们提到的一些属性,还有一些需要留意的。

  • resizeMode:拉伸的方向,可以设置为horizontal|vertical,表示两边都可以拉伸。

  • widgetCategory:对于现在的App来说,只能设置为home_screen了,5.0之前可以设置为锁屏,现在基本已经不用了。

  • widgetFeatures:这是Android12之后新加的属性,设置为reconfigurable之后,就可以直接调整Widget的尺寸,而不用像之前那样先删除旧的Widget再添加新的Widget了。

配置表

这个配置文件的主要作用,就是在添加Widget时,展示一个简要的描述信息,所以,一个App中是可以存在多个描述xml文件的,而且有几个描述文件,添加时,就会展示几个Widget的缩略图,通常我们会创建几个不同尺寸的Widget,例如2x2、4x2、4x1等,并创建多个xml面试文件,从而让用户可以选择添加哪一个Widget。

不过在Android12之后,设置一个Widget,通过拉动来改变尺寸,就可以动态改变Widget的不同展示效果了,但这仅限于Android12+,所以需要权衡使用利弊。

configure

通过configure属性可以配置添加Widget时的Configure Activity,这个在创建默认的Widget项目时就已经可以选择创建了,所以不多讲了,实际上就是一个简单的Activity,你可以配置一些参数,写入SP,然后在Widget中进行读取,从而实现自定义配置。

2

应用内唤起Widget的添加页面

大部分时候,我们都是通过在桌面上长按的方式来添加Widget,但是在Android API 26之后,系统提供了一个新的方式来在应用内唤起——requestPinAppWidget。

文档如下:

https://developer.android.com/reference/android/appwidget/AppWidgetManager#requestPinAppWidget(android.content.ComponentName,%20android.os.Bundle,%20android.app.PendingIntent)

代码如下所示。

 
  1. fun requestToPinWidget(context: Context) {
  2.     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  3.         val appWidgetManager: AppWidgetManager? = getSystemService(context, AppWidgetManager::class.java)
  4.         appWidgetManager?.let {
  5.             val myProvider = ComponentName(context, NewAppWidget::class.java)
  6.             if (appWidgetManager.isRequestPinAppWidgetSupported) {
  7.                 val pinnedWidgetCallbackIntent = Intent(context, MainGroupActivity::class.java)
  8.                 val successCallback: PendingIntent = PendingIntent.getBroadcast(context, 0,
  9.                     pinnedWidgetCallbackIntent, PendingIntent.FLAG_UPDATE_CURRENT)
  10.                 appWidgetManager.requestPinAppWidget(myProvider, null, successCallback)
  11.             }
  12.         }
  13.     }
  14. }

通过这种方式,就可以直接唤起Widget的添加入口,从而避免用户手动在桌面中进行添加。

3

应用内主动更新Widget

前面我们提到了,当App活着的时候,可以主动来更新Widget,而且有两种方式可以实现,一种是通过广播ACTION_APPWIDGET_UPDATE,触发Widget的update回调,从而进行更新,代码如下所示。

 
  1. val manager = AppWidgetManager.getInstance(this)
  2. val ids = manager.getAppWidgetIds(ComponentName(this, XXXWidget::class.java))
  3. val updateIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
  4. updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
  5. sendBroadcast(updateIntent)

这种方式的本质就是发送更新的广播,除此之外,还可以使用AppWidgetManager来直接对Widget进行更新,代码如下。

  1. val remoteViews = RemoteViews(context.packageName, R.layout.xxx)
  2. val appWidgetManager = AppWidgetManager.getInstance(context)
  3. val componentName = ComponentName(context, XXXWidgetProvider::class.java)
  4. appWidgetManager.updateAppWidget(componentName, remoteViews)

这种方式就是通过AppWidgetManager来对指定的Widget进行修改,使用新的RemoteViews来更新当前Widget。

这两种方式一种是主动替换,一种是被动刷新,具体的使用场景可以根据业务的不同来使用不同的方式。

4

应用外被动更新Widget

产品现在重新开始重视Widget的一个重要原因,实际上就是App内部卷不动了,Widget可以在不打开App的情况下,对App进行引流,所以,应用外的Widget更新,就是一个很重要的组成部分,Widget需要展示用户感兴趣的内容,才能触发用户的点击。

前面我们提到了通过设置updatePeriodMillis来进行Widget的更新,但是这种方式存在一些使用限制,如果你需要完全自主的控制Widget的刷新,那么可以使用AlarmManager或者WorkManager,类似的代码如下所示。

 
  1. private fun scheduleUpdates(context: Context) {
  2.     val activeWidgetIds = getActiveWidgetIds(context)
  3.     if (activeWidgetIds.isNotEmpty()) {
  4.         val nextUpdate = ZonedDateTime.now() + WIDGET_UPDATE_INTERVAL
  5.         val pendingIntent = getUpdatePendingIntent(context)
  6.         context.alarmManager.set(
  7.             AlarmManager.RTC_WAKEUP,
  8.             nextUpdate.toInstant().toEpochMilli(), // alarm time in millis since 1970-01-01 UTC
  9.             pendingIntent
  10.         )
  11.     }
  12. }

当然,这种方式也同样会受到ROM的限制,所以说,不管是WorkManager还是AlarmManager,或者是updatePeriodMillis,都不是稳定可靠的,随它去吧,强扭的瓜不甜。

一般来说,使用updatePeriodMillis就够了,Widget的目的是为了引流,对内容的实时性其实并不是要求的那么严格,updatePeriodMillis在大部分场景下,都是够用的。

5

多布局动态适配

由于在Android12之后,用户可以在单个Widget上进行修改,从而修改Widget当前的配置,所以,用户在拖动修改Widget的尺寸时,就需要动态去调整Widget的布局,以自动适应不同的尺寸。我们可以通过下面的方式,来进行修改。

 
  1. internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, widgetData: AppWidgetData) {
  2.     val views41 = RemoteViews(context.packageName, R.layout.new_app_widget41).also { updateView(it, context, appWidgetId, widgetData) }
  3.     val views42 = RemoteViews(context.packageName, R.layout.new_app_widget42).also { updateView(it, context, appWidgetId, widgetData) }
  4.     val views21 = RemoteViews(context.packageName, R.layout.new_app_widget21).also { updateView(it, context, appWidgetId, widgetData) }
  5.     val viewMapping: Map<SizeF, RemoteViews> = mapOf(
  6.         SizeF(180f, 110f) to views21,
  7.         SizeF(270f, 110f) to views41,
  8.         SizeF(270f, 280f) to views42
  9.     )
  10.     appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))
  11. }
  12. private fun updateView(remoteViews: RemoteViews, context: Context, appWidgetId: Int, widgetData: AppWidgetData) {
  13.     remoteViews.setTextViewText(R.id.xxx, widgetData.xxx)
  14. }

它的核心就是RemoteViews(viewMapping),通过这个就可以动态适配当前用户选择的尺寸。

那么如果是Android12之前呢?

我们需要重写onAppWidgetOptionsChanged回调来获取当前Widget的宽高,从而修改不同的布局,模板代码如下所示。

  1. override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle) {
  2.     super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
  3.     val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
  4.     val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
  5.     val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
  6.     val rows: Int = getWidgetCellsM(minHeight)
  7.     val columns: Int = getWidgetCellsN(minWidth)
  8.     updateAppWidget(context, appWidgetManager, appWidgetId, rows, columns)
  9. }
  10. fun getWidgetCellsN(size: Int)Int {
  11.     var n = 2
  12.     while (73 * n - 16 < size) {
  13.         ++n
  14.     }
  15.     return n - 1
  16. }
  17. fun getWidgetCellsM(size: Int)Int {
  18.     var m = 2
  19.     while (118 * m - 16 < size) {
  20.         ++m
  21.     }
  22.     return m - 1
  23. }

其中的计算公式,n x m:(73n-16)x(118m-16)就是文档中提到的算法。

但是这种方案有一个致命的问题,那就是不同的ROM的计算方式完全不一样,有可能在Vivo上一个格子的高度只有80,但是在Pixel中,一个格子就是100,所以,在不同的设备上显示的n x m不一样,也是很正常的事。

也正是因为这样的问题,如果不是只在Android 12+的设备上使用,那么通常都是固定好Widget的大小,避免使用动态布局,这也是没办法的权衡之举。

6

RemoteViews行为

RemoteViews不像普通的View,所以我们不能像写普通布局的方式一样来操纵View,但RemoteViews提供了一些set方法来帮助我们对RemoteViews中的View进行修改,例如下面的代码。

remoteViews.setTextViewText(R.id.title, widgetData.xxx)

再比如点击后刷新Widget,实际上就是创建一个PendingIntent。

 
  1. val intentUpdate = Intent(context, XXXAppWidget::class.java).also {
  2.     it.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
  3.     it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
  4. }
  5. val pendingUpdate = PendingIntent.getBroadcast(
  6.     context, appWidgetId, intentUpdate,
  7.     PendingIntent.FLAG_UPDATE_CURRENT)
  8.     views.setOnClickPendingIntent(R.id.btn, pendingUpdate)

原理

RemoteViews通常用在通知和Widget中,分别通过NotificationManager和AppWidgetManager来进行管理,它们则是通过Binder来和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通信,所以,RemoteViews实际上是运行在SystemServer中的,我们在修改RemoteViews时,就需要进行跨进程通信了,而RemoteViews封装了一系列跨进程通信的方法,简化了我们的调用,这也是为什么RemoteViews不支持全部的View方法的原因,RemoteViews抽象了一系列的set方法,并将它们抽象为统一的Action接口,这样就可以提供跨进程通信的效率,同时精简核心的功能。

7

如何进行后台请求

Widget在后台进行更新时,通常会请求网络,然后根据返回数据来修改Widget的数据展示。

AppWidgetProvider本质是广播,所以它拥有和广播一致的生命周期,ROM通常会定制广播的生命周期时间,例如设置为5s、7s,如果超过这个时间,那么就会产生ANR或者其它异常。

所以,我们一般不会把网络请求直接写在AppWidgetProvider中,一个比较好的方式,就是通过Service来进行更新。

首先我们创建一个Service,用来进行后台请求。

 
  1. class AppWidgetRequestService : Service() {
  2.     override fun onBind(intent: Intent): IBinder? {
  3.         return null
  4.     }
  5.     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int)Int {
  6.         val appWidgetManager = AppWidgetManager.getInstance(this)
  7.         val allWidgetIds = intent?.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)
  8.         if (allWidgetIds != null) {
  9.             for (appWidgetId in allWidgetIds) {
  10.                 BackgroundRequest.getWidgetData {
  11.                     NewAppWidget.updateAppWidget(this, appWidgetManager, appWidgetId, AppWidgetData(book1Cover = it))
  12.                 }
  13.             }
  14.         }
  15.         return super.onStartCommand(intent, flags, startId)
  16.     }
  17. }

在onStartCommand中,我们创建一个协程,来进行真正的网络请求。

 
  1. object BackgroundRequest : CoroutineScope by MainScope() {
  2.     fun getWidgetData(onSuccess: (resultString) -> Unit) {
  3.         launch(Dispatchers.IO) {
  4.             val response = RetrofitClient.getXXXApi().getXXXX()
  5.             if (response.isSuccess) {
  6.                 onSuccess(response.data.toString())
  7.             }
  8.         }
  9.     }
  10. }

所以,在AppWidgetProvider的update里面,就需要进行下修改,将原有逻辑改为对Service的启动。

 
  1. class NewAppWidget : AppWidgetProvider() {
  2.     override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
  3.         val intent = Intent(context.applicationContext, AppWidgetRequestService::class.java)
  4.         intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
  5.         context.startForegroundService(intent)
  6.     }
  7. }

8

动画

有必要这么卷吗,Widget里面还要加动画。由于RemoteViews里面不能实现正常的View动画,所以,Widget里面的动画基本都是通过类似「帧动画」的方式来实现的,即将动画抽成一帧一帧的图,然后通过Animator来进行切换,从而实现动画效果,群友给出了一篇比较好的实践,大家可以参考参考,我就不卷了。

https://juejin.cn/post/7048623673892143140

Widget的使用场景主要还是以实用功能为主,只有让用户觉得有用,才能锦上添花给App带来更多的活跃,否则只能是鸡肋。

 

转自:Android中一直存在、一直被忽略的功能

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/313041
推荐阅读
相关标签
  

闽ICP备14008679号