Android-Widget reloaded

Original link: https://xuyisheng.top/android-widget-back/

Android-Widget reloaded

If you want to find one that has always existed in the Android system, but has been ignored by people, and has very useful functions, then Widget must be counted as one. This function, which has existed since Android 1.x, has gone through nearly 10 years of iterations. After being ignored and looked down upon, it has returned to everyone’s sight. Of course, it is also possible that there is nothing good inside the app. Rolled up, so everyone put their attention outside the App again, but no matter what, Widgets have begun to take on a new look after Android 12. Let’s get to know this most familiar stranger again.

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

Widget uses RemoteView, which is the same as Notification. RemoteView is a component inherited from Parcelable and can be used across processes. In the Widget, the behavior of the Widget is managed by AppWidgetProvider, the Widget is laid out through RemoteView, and the Widget is refreshed through AppWidgetManager. The basic usage, we can achieve through a set of template code, in Android Studio, you can directly New Widget. In this way, Android Studio can automatically generate a Widget template code for you. We will not post the detailed code. Let’s analyze the composition of the code.

First, each Widget contains an AppWidgetProvider. This is the logical management class of Widget, which inherits from BroadcastReceiver. Then, we need to register this Receiver in the manifest and specify its configuration file in meta-data. Its configuration file is an xml, which describes adding Widget some information displayed.

From these perspectives, the use of Widgets is relatively simple, so this article is not going to explain these basics. Let’s analyze some of the actual needs that will be encountered during development.

appwidget-provider configuration file

Although this xml file is simple, there are still some interesting things.

size

Here we can configure the size information for the Widget. Through maxResizeWidth, maxResizeHeight and minWidth, minHeight, we can roughly control the size of the Widget within the MxN grid, which is also the way the Widget is displayed on the desktop, it is not through the specified width. High to show, but the number of squares occupied by the desktop.

In the official design document, there is a table for the conversion standard of grid number and size, as shown below.

Android-Widget reloaded

When designing, we should also try to follow this size constraint to avoid displaying anomalies on the desktop. After Android12, two parameters, targetCellWidth and targetCellHeight, are also added to the description file. They can directly specify the number of grids occupied by the Widget, which is more convenient, but since it only supports Android12+, these properties are usually set together.

The interesting thing is that this size standard does not apply to all devices, because of the fragmentation of the ROM, the desktops of various manufacturers are different, so. . . For reference only.

updatePeriodMillis

This parameter is used to specify the passive refresh frequency of the Widget. It is controlled by the system, so it has strong uncertainty, and it cannot be set arbitrarily. The restrictions on this property on the official website are as follows.

Android-Widget reloaded

updatePeriodMillis only supports setting an interval of more than 30 minutes, that is, 1800000 milliseconds. This is also to ensure background energy consumption. Even if you set updatePeriodMillis less than 30 minutes, it will not take effect.

For Widgets, updatePeriodMillis controls the frequency at which the system passively refreshes the Widget. If the current App is alive, the Widget can be modified by broadcasting at any time.

And this value is likely to be different for different ROMs, so this is a less stable refresh mechanism.

other

In addition to some of the properties we mentioned above, there are a few more things to watch out for.

  • resizeMode: The direction of stretching, which can be set to horizontal|vertical, which means that both sides can be stretched.
  • widgetCategory: For the current App, it can only be set as home_screen. Before 5.0, it could be set as the lock screen, and now it is basically no longer needed.
  • widgetFeatures: This is a newly added property after Android 12. After setting it to reconfigurable, you can directly adjust the size of the widget without deleting the old widget and adding a new widget as before.

configuration table

The main function of this configuration file is to display a brief description when adding a Widget. Therefore, there can be multiple description xml files in an App, and there are several description files. When adding a widget, several description files will be displayed. Thumbnails of widgets, usually we will create several widgets of different sizes, such as 2×2, 4×2, 4×1, etc., and create multiple xml interview files, so that users can choose which widget to add.

However, after Android 12, setting a Widget and changing the size by pulling can dynamically change the different display effects of the Widget, but this is limited to Android 12+, so you need to weigh the pros and cons.

configure

Through the configure attribute, you can configure the Configure Activity when adding a Widget. This can already be created when the default Widget project is created, so I won’t talk about it, but it is actually a simple Activity. You can configure some parameters and write SP , and then read it in the Widget to achieve custom configuration.

Add page to evoke Widget within the application

Most of the time, we add widgets by long-pressing on the desktop, but after Android API 26, the system provides a new way to evoke in the application – requestPinAppWidget.

Documentation is below.

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

The code is shown below.

 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) } } } }

In this way, the Widget’s addition entry can be directly invoked, thereby avoiding the user’s manual addition to the desktop.

Actively update widgets within the app

We mentioned earlier that when the App is alive, it can actively update the Widget, and there are two ways to achieve it. One is to trigger the update callback of the Widget by broadcasting ACTION_APPWIDGET_UPDATE, so as to update the code. The code is as follows.

 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)

The essence of this method is to send an updated broadcast. In addition, AppWidgetManager can also be used to update the Widget directly. The code is as follows.

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

This method is to modify the specified Widget through the AppWidgetManager, and use the new RemoteViews to update the current Widget.

One of these two methods is active replacement and the other is passive refresh. The specific usage scenarios can use different methods according to different businesses.

Passively update widgets outside the app

An important reason why products are starting to pay attention to Widgets now is that the internal scrolling of the App does not move, and the Widget can drain the App without opening the App. Therefore, the update of the Widget outside the App is a very important component. In some parts, the Widget needs to display the content that the user is interested in in order to trigger the user’s click.

Earlier we mentioned updating the Widget by setting updatePeriodMillis, but there are some limitations in this method. If you need to control the refresh of the Widget completely independently, you can use AlarmManager or WorkManager. Similar codes are shown below.

 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 ) } }

Of course, this method will also be limited by ROM, so whether it is WorkManager, AlarmManager, or updatePeriodMillis, it is not stable and reliable. Let it go, the twisted melon is not sweet.

Generally speaking, it is enough to use updatePeriodMillis. The purpose of the Widget is to drain traffic, and the real-time performance of the content is not so strict. UpdatePeriodMillis is sufficient in most scenarios.

Dynamic adaptation of multiple layouts

After Android 12, users can modify a single Widget to modify the current configuration of the Widget. Therefore, when the user drags and modifies the size of the Widget, it is necessary to dynamically adjust the layout of the Widget to automatically adapt to different sizes. We can modify it in the following way.

 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) }

Its core is RemoteViews (viewMapping), through which you can dynamically adapt to the size selected by the current user.

So what if it was before Android 12?

We need to override the onAppWidgetOptionsChanged callback to get the width and height of the current Widget to modify different layouts. The template code is as follows.

 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 }

The calculation formula, nxm: (73n-16)x(118m-16), is the algorithm mentioned in the document.

But this solution has a fatal problem, that is, the calculation methods of different ROMs are completely different. It is possible that the height of a grid on Vivo is only 80, but in Pixel, a grid is 100, so, on different devices It is normal that the nxm shown above is different.

It is precisely because of this problem that if it is not only used on Android 12+ devices, the size of the Widget is usually fixed and the use of dynamic layout is avoided. This is also a trade-off that cannot be done.

RemoteViews behavior

RemoteViews are not like normal Views, so we cannot manipulate Views in the same way as writing normal layouts, but RemoteViews provides some set methods to help us modify Views in RemoteViews, such as the following code.

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

Another example is to refresh the Widget after clicking, which actually creates a 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)

principle

RemoteViews are usually used in notifications and Widgets, and are managed by NotificationManager and AppWidgetManager respectively. They communicate with NotificationManagerService and AppWidgetService in the SystemServer process through Binder. Therefore, RemoteViews actually run in SystemServer. We are modifying RemoteViews requires cross-process communication, and RemoteViews encapsulates a series of cross-process communication methods, which simplifies our calls. This is why RemoteViews does not support all View methods. RemoteViews abstracts a series of set methods , and abstract them into a unified Action interface, which can provide the efficiency of cross-process communication while simplifying the core functions.

How to make background requests

When a widget is updated in the background, it usually requests the network, and then modifies the data display of the widget according to the returned data.

AppWidgetProvider is essentially broadcasting, so it has the same life cycle as broadcasting. ROM usually customizes the life cycle time of broadcasting, for example, it is set to 5s, 7s, if it exceeds this time, ANR or other exceptions will be generated.

Therefore, we generally do not write network requests directly in AppWidgetProvider. A better way is to update them through Service.

First we create a Service to make background requests.

 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) } }

In onStartCommand, we create a coroutine to make the real network request.

 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()) } } } }

Therefore, in the update of AppWidgetProvider, it is necessary to modify the original logic to start the 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) context.startService(intent) } }

animation?

Is it necessary to roll like this, and animation should be added to the Widget. Since the normal View animation cannot be realized in RemoteViews, the animation in the Widget is basically realized by a method similar to “frame animation”, that is, the animation is drawn into a frame-by-frame picture, and then switched through the Animator to achieve The animation effect, the group of friends gave a good practice, you can refer to it, I will not roll it.

https://juejin.cn/post/7048623673892143140

The usage scenarios of Widgets are mainly based on practical functions. Only when users find them useful can they bring more activity to the App, otherwise they will only be tasteless.

This article is reprinted from: https://xuyisheng.top/android-widget-back/
This site is for inclusion only, and the copyright belongs to the original author.

Leave a Comment