假如要在Android体系中找一个一向存在,但一向被人忽略,并且有非常好用的功用,那么Widget,一定算一个。这个从Android 1.x就现已存在的功用,经历了近10年的迭代,在遭到很多无视和白眼之后,又从头回到了大家的视野之内,当然,也有或许是App内部现已没东西好卷了,所以大家又把目光放到了App之外,但不管怎样,Widget在Android 12之后,都开端焕发一新,官网镇楼,让咱们从头来了解下这个最了解的陌生人。

developer.android.com/develop/ui/…

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的运用仍是比较简略的,所以本文也不准备来解说这些基础知识,下面咱们针对开发中会遇到的一些实践需求来进行剖析。

appwidget-provider装备文件

这个xml文件尽管简略,但仍是有些有意思的东西的。

尺度

在这儿咱们能够为Widget装备尺度信息,经过maxResizeWidth、maxResizeHeight和minWidth、minHeight,咱们能够大致将Widget的尺度控制在MxN的格子内,这也是Widget在桌面上的展现办法,它并不是经过指定的宽高来展现的,而是桌面所占有的格子数。

官方规划文档中,对格子数和尺度的转换规范,有一个表格,如下所示。

Android-Widget重装上阵

咱们在规划的时分,也应该尽量遵循这个尺度束缚,防止在桌面上展现反常。在Android12之后,描绘文件中,还增加了targetCellWidth和targetCellHeight两个参数,他们能够直接指定Widget所占有的格子数,这样更加方便,但由于它仅支撑Android12+,所以,一般这些特点会一起设置。

有意思的是这个尺度规范并不适用于所有的设备,由于ROM的碎片化问题,各个厂商的桌面都不相同,所以。。。只能参阅参阅。

updatePeriodMillis

这个参数用于指定Widget的被迫改写频率,它由体系控制,所以具有很强的不定性,并且它也不能随意设置,官网上对这个特点的约束如下所示。

Android-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,例如2×2、4×2、4×1等,并创立多个xml面试文件,然后让用户能够挑选增加哪一个Widget。

不过在Android12之后,设置一个Widget,经过拉动来改动尺度,就能够动态改动Widget的不同展现效果了,但这仅限于Android12+,所以需求权衡运用利害。

configure

经过configure特点能够装备增加Widget时的Configure Activity,这个在创立默认的Widget项目时就现已能够挑选创立了,所以不多讲了,实践上便是一个简略的Activity,你能够装备一些参数,写入SP,然后在Widget中进行读取,然后完成自定义装备。

运用内引发Widget的增加页面

大部分时分,咱们都是经过在桌面上长按的办法来增加Widget,可是在Android API 26之后,体系供给了一向新的办法来在运用内引发——requestPinAppWidget。

文档如下。

developer.android.com/reference/a…

代码如下所示。

fun requestToPinWidget(context: Context) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val appWidgetManager: AppWidgetManager? = getSystemService(context, AppWidgetManager::class.java)
        appWidgetManager?.let {
            val myProvider = ComponentName(context, NewAppWidget::class.java)
            if (appWidgetManager.isRequestPinAppWidgetSupported) {
                val pinnedWidgetCallbackIntent = Intent(context, MainGroupActivity::class.java)
                val successCallback: PendingIntent = PendingIntent.getBroadcast(context, 0,
                    pinnedWidgetCallbackIntent, PendingIntent.FLAG_UPDATE_CURRENT)
                appWidgetManager.requestPinAppWidget(myProvider, null, successCallback)
            }
        }
    }
}

经过这种办法,就能够直接引发Widget的增加进口,然后防止用户手动在桌面中进行增加。

运用内主动更新Widget

前面咱们说到了,当App活着的时分,能够主动来更新Widget,并且有两种办法能够完成,一种是经过播送ACTION_APPWIDGET_UPDATE,触发Widget的update回调,然后进行更新,代码如下所示。

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

这种办法的实质便是发送更新的播送,除此之外,还能够运用AppWidgetManager来直接对Widget进行更新,代码如下。

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

这种办法便是经过AppWidgetManager来对指定的Widget进行修正,运用新的RemoteViews来更新当时Widget。

这两种办法一种是主动替换,一种是被迫改写,具体的运用场景能够依据事务的不同来运用不同的办法。

运用外被迫更新Widget

产品现在从头开端注重Widget的一个重要原因,实践上便是App内部卷不动了,Widget能够在不打开App的情况下,对App进行引流,所以,运用外的Widget更新,便是一个很重要的组成部分,Widget需求展现用户感兴趣的内容,才干触发用户的点击。

前面咱们说到了经过设置updatePeriodMillis来进行Widget的更新,可是这种办法存在一些运用约束,假如你需求彻底自主的控制Widget的改写,那么能够运用AlarmManager或许WorkManager,相似的代码如下所示。

private fun scheduleUpdates(context: Context) {
        val activeWidgetIds = getActiveWidgetIds(context)
        if (activeWidgetIds.isNotEmpty()) {
            val nextUpdate = ZonedDateTime.now() + WIDGET_UPDATE_INTERVAL
            val pendingIntent = getUpdatePendingIntent(context)
            context.alarmManager.set(
                AlarmManager.RTC_WAKEUP,
                nextUpdate.toInstant().toEpochMilli(), // alarm time in millis since 1970-01-01 UTC
                pendingIntent
            )
        }
    }

当然,这种办法也同样会遭到ROM的约束,所以说,不管是WorkManager仍是AlarmManager,或许是updatePeriodMillis,都不是安稳可靠的,随它去吧,强扭的瓜不甜。

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

多布局动态适配

由于在Android12之后,用户能够在单个Widget上进行修正,然后修正Widget当时的装备,所以,用户在拖动修正Widget的尺度时,就需求动态去调整Widget的布局,以主动适应不同的尺度。咱们能够经过下面的办法,来进行修正。

internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, widgetData: AppWidgetData) {
    val views41 = RemoteViews(context.packageName, R.layout.new_app_widget41).also { updateView(it, context, appWidgetId, widgetData) }
    val views42 = RemoteViews(context.packageName, R.layout.new_app_widget42).also { updateView(it, context, appWidgetId, widgetData) }
    val views21 = RemoteViews(context.packageName, R.layout.new_app_widget21).also { updateView(it, context, appWidgetId, widgetData) }
    val viewMapping: Map<SizeF, RemoteViews> = mapOf(
        SizeF(180f, 110f) to views21,
        SizeF(270f, 110f) to views41,
        SizeF(270f, 280f) to views42
    )
    appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))
}
private fun updateView(remoteViews: RemoteViews, context: Context, appWidgetId: Int, widgetData: AppWidgetData) {
    remoteViews.setTextViewText(R.id.xxx, widgetData.xxx)
}

它的中心便是RemoteViews(viewMapping),经过这个就能够动态适配当时用户挑选的尺度。

那么假如是Android12之前呢?

咱们需求重写onAppWidgetOptionsChanged回调来获取当时Widget的宽高,然后修正不同的布局,模板代码如下所示。

override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle) {
  super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
  val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
  val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
  val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
  val rows: Int = getWidgetCellsM(minHeight)
  val columns: Int = getWidgetCellsN(minWidth)
  updateAppWidget(context, appWidgetManager, appWidgetId, rows, columns)
}
fun getWidgetCellsN(size: Int): Int {
    var n = 2
    while (73 * n - 16 < size) {
        ++n
    }
    return n - 1
}
fun getWidgetCellsM(size: Int): Int {
    var m = 2
    while (118 * m - 16 < size) {
        ++m
    }
    return m - 1
}

其间的核算公式,n x m:(73n-16)x(118m-16)便是文档中说到的算法

可是这种计划有一个致命的问题,那便是不同的ROM的核算办法彻底不相同,有或许在Vivo上一个格子的高度只要80,可是在Pixel中,一个格子便是100,所以,在不同的设备上显现的n x m不相同,也是很正常的事。

也正是由于这样的问题,假如不是只在Android 12+的设备上运用,那么一般都是固定好Widget的巨细,防止运用动态布局,这也是没办法的权衡之举。

RemoteViews行为

RemoteViews不像一般的View,所以咱们不能像写一般布局的办法相同来操纵View,但RemoteViews供给了一些set办法来帮助咱们对RemoteViews中的View进行修正,例如下面的代码。

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

再比如点击后改写Widget,实践上便是创立一个PendingIntent。

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

原理

RemoteViews一般用在通知和Widget中,分别经过NotificationManager和AppWidgetManager来进行管理,它们则是经过Binder来和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通讯,所以,RemoteViews实践上是运行在SystemServer中的,咱们在修正RemoteViews时,就需求进行跨进程通讯了,而RemoteViews封装了一系列跨进程通讯的办法,简化了咱们的调用,这也是为什么RemoteViews不支撑全部的View办法的原因,RemoteViews抽象了一系列的set办法,并将它们抽象为一致的Action接口,这样就能够供给跨进程通讯的功率,同时精简中心的功用。

怎么进行后台恳求

Widget在后台进行更新时,一般会恳求网络,然后依据返回数据来修正Widget的数据展现。

AppWidgetProvider实质是播送,所以它具有和播送一致的生命周期,ROM一般会定制播送的生命周期时刻,例如设置为5s、7s,假如超越这个时刻,那么就会产生ANR或许其它反常。

所以,咱们一般不会把网络恳求直接写在AppWidgetProvider中,一个比较好的办法,便是经过Service来进行更新。

首先咱们创立一个Service,用来进行后台恳求。

class AppWidgetRequestService : Service() {
    override fun onBind(intent: Intent): IBinder? {
        return null
    }
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val appWidgetManager = AppWidgetManager.getInstance(this)
        val allWidgetIds = intent?.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)
        if (allWidgetIds != null) {
            for (appWidgetId in allWidgetIds) {
                BackgroundRequest.getWidgetData {
                    NewAppWidget.updateAppWidget(this, appWidgetManager, appWidgetId, AppWidgetData(book1Cover = it))
                }
            }
        }
        return super.onStartCommand(intent, flags, startId)
    }
}

在onStartCommand中,咱们创立一个协程,来进行真正的网络恳求。

object BackgroundRequest : CoroutineScope by MainScope() {
    fun getWidgetData(onSuccess: (result: String) -> Unit) {
        launch(Dispatchers.IO) {
            val response = RetrofitClient.getXXXApi().getXXXX()
            if (response.isSuccess) {
                onSuccess(response.data.toString())
            }
        }
    }
}

所以,在AppWidgetProvider的update里边,就需求进行下修正,将原有逻辑改为对Service的发动。

class NewAppWidget : AppWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        val intent = Intent(context.applicationContext, AppWidgetRequestService::class.java)
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            context.startForegroundService(intent)
        } else {
            context.startService(intent)
        }
    }
}

动画?

有必要这么卷吗,Widget里边还要加动画。由于RemoteViews里边不能完成正常的View动画,所以,Widget里边的动画根本都是经过相似「帧动画」的办法来完成的,即将动画抽成一帧一帧的图,然后经过Animator来进行切换,然后完成动画效果,群友给出了一篇比较好的实践,大家能够参阅参阅,我就不卷了。

/post/704862…

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