context

The rise of widgets

Apple’s iOS 14 will be released in September 2020, and one of the big updates is that Apple also supports widgets! It’s not easy. Apple is finally using features that Android has had for years. Let’s take a look at the widgets in Apple.

Weather maps, etc. Clock reminders, etc.

Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose Compose With today’s article, let’s take a look at the final result of today’s implementation:

The style implemented today You can swipe up and down to see the weather for a week

Isn’t that cool? Ha ha ha! This is based on the weather app I wrote earlier (Github address also at the end).

Although Android has had widgets for many years, widgets are rarely used in Android phones. At most, widgets come with the phone when it comes out of the factory… Actually a lot of our common application has a lot of small parts, due to the use of really is not much, so there are very low (helping to ridicule, commonly used are too rogue software, each application has a lot of functions as the widget, such as: trill has several, headlines also has several, iQIYI, youku would say…)

Why are so few android widgets used? The main style is too ugly, there is as above said that too rogue do not want to use. Google had almost forgotten about widgets, but last year apple woke up to the fact that there were widgets in Android, and made some big updates and upgrades to them.

Android widget pain

It’s not just users who don’t like android widgets. Developers don’t want to make widgets either. Why? Since the widget is attached to the desktop, it does not belong to the original application process, and if you want to change the layout across processes, you need to use RemoteViews, but RemoteViews is not hard to use, it is quite hard to use, not only can you not use custom views, Even our common RecyclerView and other controls can not be used, can only use several official fixed controls,

The following layout classes are supported:

FrameLayout, LinearLayout, RelativeLayout, GridLayout

And the following controls: AnalogClock, Button, Chronometer, ImageButton, ImageView, ProgressBar, TextView, ViewFlipper, ListView, GridView , StackView, AdapterViewFlipper

Note: The controls in this section refer to those before Android 12. Android 12 has added some new controls, which will be described in the following section.

So much for the bickering. Let’s get to work!

Updates to small Android 12 widgets

As mentioned earlier, Google is making big changes to widgets in Android 12, so let’s talk about this one.

Users can reset the original widget

Previously, users could only remove and add widgets if they wanted to reset them, but in Android 12, users will not need to remove and add widgets to adjust these Settings.

Setting it up is as simple as adding a line of configuration:

<? The XML version = "1.0" encoding = "utf-8"? > <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:configure="com.zj.weather.common.widget.WeatherWidgetConfigureActivity" android:widgetFeatures="reconfigurable" . />Copy the code

WidgetFeatures is the new reconfigurable widget configuration item in Android 12, and the other is the Activity configuration widget. To make widgetFeatures work, you must configure the Activity. You can’t reset a widget if you don’t know where to configure it!

Size limits for widgets

Before Android 12, the size of Android widgets was extremely confusing. Each app had the wrong size in the widget bar. For example, the size of the app was 4 x 1, and when you changed the page layout, the app size would change and it would no longer be 4 x 1.

Google is probably aware of this and has added a size limit for the widget in Android 12, in addition to the existing minWidth, minHeigh, minResizeWidth, and minResizeHeight. New maxResizeWidth, maxResizeHeight, targetCellWidth and targetCellHeight attributes are also added. The meanings of these attributes are explained in detail below.

  • MaxResizeWidth: Defines the maximum width of the widget size that the user can adjust
  • MaxResizeHeight: Defines the maximum height that the user can adjust the size of the widget
  • TargetCellWidth: Defines the default width of the widget on the device’s home screen (this can be used on different phones, but only Android 12 or later)
  • TargetCellHeight: Defines the default height of the widget on the device’s home screen

If there had been targetCellWidth and targetCellHeight, the widget would not have been so cluttered that users would not want to use it.

New widget control

Android 12 adds support for stateful behavior using the following existing controls:

  • CheckBox
  • Switch
  • RadioButton

These controls should be familiar, but they were impossible to use in a widget before Android 12.

Widget UI update

I think you’ve already seen this, but I’m just going to pass it by, which is a rounded corner for the widget by default, The radius of the widget fillet can be set using the system_app_widget_background_radius and system_app_widget_inner_radius system parameters.

Here’s a picture from the official document.

Work, work, work

After all this talk about the widget’s past and present, and the Android 12 update, it’s time to get to work.

Writing configuration files

Declare the widget in the manifest

If you want to add a widget to Android, you should first declare it in androidmanifest.xml, because the widget is actually a BroadcastReceiver. You know that all four components need to be declared in androidmanifest.xml to be used, so let’s declare the widget in the manifest first.

<receiver
    android:name=".common.widget.WeatherWidget"
    android:exported="true" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
​
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/weather_widget_info" />
</receiver>
Copy the code

The < Receiver > element requires the Android: Name attribute, which specifies the AppWidgetProvider used by the widget (the parent of AppWidgetProvider is BroadcastReceiver).

The

element in

specifies that the widget accepts the ACTION_APPWIDGET_UPDATE broadcast. This is the only broadcast that must be explicitly stated to receive widget additions, deletions, changes, etc.

The

element specifies the widget’s resources and requires the following attributes:

  • android:name– Specifies the metadata name. You must use theandroid.appwidget.providerIdentify the data asAppWidgetProviderInfoDescriptor.
  • android:resource– specifiedAppWidgetProviderInfoResource location.

Write the configuration file for the widget

The widget is declared in the manifest file, so let’s write the configuration file for the widget. The configuration file is placed in the XML file as follows: res -> XML. If you don’t have this folder locally, create one.

<? The XML version = "1.0" encoding = "utf-8"? > <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:configure="com.zj.weather.common.widget.WeatherWidgetConfigureActivity" android:initialKeyguardLayout="@layout/weather_widget" android:initialLayout="@layout/weather_widget" android:minWidth="170dp" android:minHeight="90dp" android:previewImage="@mipmap/weather_widget" android:resizeMode="horizontal|vertical" android:targetCellWidth="3" android:targetCellHeight="2" android:updatePeriodMillis="86400000" android:widgetCategory="home_screen" android:widgetFeatures="reconfigurable" />Copy the code

As you can see, the new Android 12 configuration has been used, with minimum width and height, preview images, etc. Let’s take a closer look at what each configuration does.

  • MinWidth and minHeight: Specifies the minimum space that the widget takes up by default.

    Note: In order for widgets to be portable between devices, the minimum size of widgets must be no larger than 4 x 4 cells.

  • MinResizeWidth and minResizeHeight: Specifies the absolute minimum size of the widget.

  • UpdatePeriodMillis: Defines how often the widget framework should request updates from the AppWidgetProvider by calling the onUpdate() callback method.

  • InitialLayout: Points to the layout resource used to define the widget layout.

  • Configure: defines the Activity to be started when the user adds the widget so that the user can configure the widget properties.

  • PreviewImage: Specifies a preview of what the widget will look like when configured, which the user will see when selecting the widget.

  • AutoAdvanceViewId: Specifies the view ID of the widget child view that should be automatically jumped to by the widget’s hosting application.

  • ResizeMode: what can be specified according to the rules to adjust the size of the micro parts, optional value of “horizontal | vertical”, somehow the general default Settings can be adjusted.

  • MinResizeHeight: Specifies the minimum height to which a widget can be resized.

  • MinResizeWidth: Specifies the minimum width to which the widget can be resized.

  • WidgetCategory: Declares whether the widget can be displayed on the home screen (home_screen) or the lock screen (keyguard). Only versions of Android below 5.0 support locking screen widgets. For Android 5.0 and later, only home_screen is valid, so write this value as home_screen for now.

Write the layout

The root layout

Now that the configuration file is ready, let’s write the layout. Let’s think about how the layout should be written. As you can see from the diagram at the beginning of this article, let’s write the root layout first.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/background"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#00000000"
    android:theme="@style/Theme.Design.NoActionBar">
​
    <StackView
        android:id="@+id/stack_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:loopViews="true" />
​
</FrameLayout>
Copy the code

The child layout

As you can see, the layout is very simple, just a StackView, which inherits from the AdapterViewAnimator. StackView, like ListView and GridView, needs child layouts, so here we go.

<? The XML version = "1.0" encoding = "utf-8"? > <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"  android:id="@+id/widget_ll_item"> <ImageView android:id="@+id/widget_iv_bg"/> <LinearLayout> <TextView android:id="@+id/widget_tv_city" /> <TextView android:id="@+id/widget_tv_date"/> <ImageView android:id="@+id/widget_iv_icon" /> <ImageView android:id="@+id/widget_iv_small_icon" /> <TextView android:id="@+id/widget_tv_temp" /> </LinearLayout> </FrameLayout>Copy the code

Due to the length of the layout to simplify the next, detailed layout can see the end of the project to provide source code.

Contains a list of collection widgets

Since we have stackViews in our layout, in addition to the requirements listed above, widgets that contain collections must be able to bind to RemoteViewsService using the BIND_REMOTEVIEWS permission in the manifest file. This prevents other applications from freely accessing the widget’s data.

<service
    android:name=".common.widget.WeatherWidgetService"
    android:exported="false"
    android:permission="android.permission.BIND_REMOTEVIEWS" />
Copy the code

The AppWidgetProvider class that contains the collection widget

As with regular widgets, most of the code in the AppWidgetProvider subclass is usually in onUpdate(). When you create the widget that contains the collection, you must call setRemoteAdapter() to set up the adapter, which tells the collection view where to get its data. RemoteViewsService can then return the RemoteViewsFactory implementation, and the widget can provide the appropriate data. When this method is called, you must pass in the Intent that points to the RemoteViewsService implementation, along with the widget ID that specifies the widget to update, to see how it works.

override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { appWidgetIds.forEach { appWidgetId-> updateAppWidget(context, appWidgetManager, AppWidgetId) val cityInfo = loadTitlePref(context, appWidgetId) val views = RemoteViews(context.packagename, R.layout.weather_widget) val intent = Intent(context, WeatherWidgetService::class.java).apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, Parse (toUri(intent.uri_intent_scheme))} views.apply {// Set StackView adapter setRemoteAdapter(R.id.stack_view, intent) setEmptyView(R.id.stack_view, R.id.empty_view) } val toastPendingIntent: PendingIntent = Intent( context, WeatherWidget::class.java ).run { action = CLICK_ITEM_ACTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) PendingIntent.getBroadcast( context, 0, this, Pendingintent.flag_update_current or pendingIntent.flag_immutable)} // Set the template for the click event views.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent) appWidgetManager.updateAppWidget(appWidgetId, views) } }Copy the code

RemoteViewsService implementation

As mentioned above, you must set up an adapter to create widgets that contain collections.

class WeatherWidgetService : RemoteViewsService() {
    override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
        return WeatherRemoteViewsFactory(this.applicationContext, intent)
    }
}
Copy the code

Can see WeatherWidgetService inherited from RemoteViewsService, and realized the WeatherRemoteViewsFactory.

class WeatherRemoteViewsFactory(private val context: Context, intent: Intent) : RemoteViewsService.RemoteViewsFactory, CoroutineScope by MainScope() { private var cityInfo: CityInfo? = null init { intent.getStringExtra(CITY_INFO)? .apply { cityInfo = Gson().fromJson(this, CityInfo::class.java) } } override fun getViewAt(position: Int): RemoteViews { if (widgetItems.size ! = WEEK_COUNT) { return RemoteViews(context.packageName, R.layout.weather_widget_loading) } return RemoteViews(context.packageName, R.layout.widget_item).apply { val weather = widgetItems[position] setTextViewText(R.id.widget_tv_temp, "${weather. Min} - ${weather. Max} ℃") setTextViewText (R.i d.w idget_tv_city, "${cityInfo? .city ? : ""} ${cityInfo? .name ?: }") setImageViewBitmap(r.idget_iv_bg, fillet(context = context, bitmap = zoomImg(context, weather.icon), roundDp = 10) ) layoutAdapter(weather.icon) setTextViewText(R.id.widget_tv_date, weather.time) setImageViewResource( R.id.widget_iv_icon, Iconutils.getweathericon (weather.icon)) // Set the click event val fillInIntent = Intent(). Apply {EXTRA_ITEM, weather.time) } setOnClickFillInIntent(R.id.widget_ll_item, fillInIntent) } } override fun getLoadingView(): Return RemoteViews(context.packagename, r.layout. weather_widget_loading)}}Copy the code

RemoteViewsFactory (RemoteViewsFactory) ¶ RemoteViewsFactory (RemoteViewsFactory) ¶

Setting up the Activity

Configuring an Activity we talked about how to add it to the widget’s configuration file above, and the rest is just like normal activities.

Because the widget does not support Compose, we wrote the Layout above, but we can use Compose in the Activity!

@AndroidEntryPoint class WeatherWidgetConfigureActivity : BaseActivity() { private val viewModel by viewModels<CityListViewModel>() public override fun onCreate(savedInstanceState: Bundle?) {super. OnCreate (savedInstanceState) / / refresh city data viewModel. RefreshCityList setContent () {PlayWeatherTheme {Surface (color  = MaterialTheme.colors.background) { ConfigureWidget( viewModel, onCancelListener = { setResult(RESULT_CANCELED) finish() }) { cityInfo -> onConfirm(cityInfo) } } } } }Copy the code

We don’t need to write the Layout. Let’s look at the implementation of ConfigureWidget.

@OptIn(ExperimentalPagerApi::class) @Composable private fun ConfigureWidget( viewModel: CityListViewModel, onCancelListener: () -> Unit, onConfirmListener: (CityInfo) -> Unit ) { val cityList by viewModel.cityInfoList.observeAsState(arrayListOf()) val buttonHeight = 45.dp val  pagerState = rememberPagerState() Column(modifier = Modifier.fillMaxSize()) { Spacer(modifier = Modifier.height(80.dp)) FillMaxWidth (), textAlign = textalign.center, fontSize = 26.sp, color = Color(red = 53, green = 128, blue = 186) ) Box(modifier = Modifier.weight(1f)) { HorizontalPager( state = pagerState, count = cityList.size, modifier = Modifier.fillMaxSize() ) { page -> Card( shape = RoundedCornerShape(10.dp), backgroundColor = MaterialTheme.colors.onSecondary, modifier = Modifier.size(300.dp) ) { val cityInfo = cityList[page] Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { Text(text = cityInfo.name, fontSize = 30.sp) } } } DrawIndicator(pagerState = pagerState) } Spacer(modifier = Modifier.height(50.dp)) Divider( modifier = Modifier .fillMaxWidth() .height(1.dp) ) Row { TextButton( modifier = Modifier .weight(1f) .height(buttonHeight), onClick = { onCancelListener() } ) { Text( text = stringResource(id = R.string.city_dialog_cancel), fontSize = 16.sp, color = Color(red = 53, green = 128, blue = 186) ) } Divider( modifier = Modifier .width(1.dp) .height(buttonHeight) ) TextButton( modifier = Modifier .weight(1f) .height(buttonHeight), onClick = { onConfirmListener(cityList[pagerState.currentPage]) } ) { Text( text = stringResource(id = R.string.city_dialog_confirm), fontSize = 16.sp, color = Color(red = 53, green = 128, blue = 186) ) } } } }Copy the code

The layout is simple, a linear layout that wraps the title, city ViewPager, ok and cancel buttons, and then calls back the ok button click event in a higher-order function.

In the pit of

OK, this article is basically over, the above can be found in other blogs, but the point is, there are many things that can not be found on the Internet, including in the official document is very general, there is no actual application case, let’s talk about it in detail.

Layout fit problem

At apple, the layout of the widget when adding a fixed well, later you can’t modify, want to change can only be deleted and add again, but the size of small and medium-sized parts in the android can stretch, long press can be wide high adjustment, so the hard to avoid is layout of adaptation problems.

The pre-Android 12 solution

Before the Android 12 if you want to fit different wide compete display layout needs to be rewritten under onAppWidgetOptionsChanged () method, and then get to the minimum width is high, the current widget according to the different can be layout of wide high adaptation.

override fun onAppWidgetOptionsChanged( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle ) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, NewOptions) / / See the dimensions and val options = appWidgetManager. GetAppWidgetOptions (appWidgetId) / / widget minimal wide high val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) val minHeight = OPTION_APPWIDGET_MIN_HEIGHT) // Calculate the size of the widget val rows: Int = getCellsForSize(minHeight) val columns: Int = getCellsForSize(minWidth) XLog.e("rows:$rows columns:$columns") updateAppWidget(context, appWidgetManager, appWidgetId, rows, columns) }Copy the code

The code above mentions a getCellsForSize() method, which is defined according to the official documentation for counting the number of cells in a widget. Take a look:

/** * returns the number of cells required for a given size widget. * * @param size Size of the widget in dp. * @return The size of the number of cells. */ fun getCellsForSize(size: Int): Int { var n = 2 while (70 * n - 30 < size) { ++n } return n - 1 }Copy the code

Attention!! The number of cells calculated here may not be correct, it may be fine on some phones, but it may be wrong on some phones, we must pay attention to this, there is no way to do it, there are too many phone manufacturers, each desktop implementation is slightly different, this is normal.

A post-Android 12 solution

After Android 12, you can adapt with responsive layouts by first creating a set of layouts of different sizes, then calling the updateAppWidget() function and passing in a set of layouts that will automatically change when the widget size changes.

Val viewMapping = mapOf(SizeF(150f, 110f) to RemoteViews(context.packagename, layout), val viewMapping = mapOf(SizeF(150f, 110f) to RemoteViews(context.packagename, layout), 110 f) to RemoteViews (context. PackageName, layout),) / / counter widget manager to update the widget appWidgetManager. UpdateAppWidget (appWidgetId, RemoteViews(viewMapping))Copy the code

So do some simple, quite so RemoteViews internal for us to do the treatment, without having to rewrite onAppWidgetOptionsChanged () method, but it can only be done in later versions of Android 12 and use, according to the demand to use.

StackView data refresh problem

This question is really quite sick, may be I level is limited, the official refresh is notifyAppWidgetViewDataChanged () method, the figure was almost crazy to me…

Is also my own problem, people have told the refresh process also write a problem.

I used to put the weather data request in onCreate method, and then use runBlocking() method to convert asynchrony to synchronization, get the data and do the next step, but this would be anR.

Then I wrote a higher-order function:

/** * Get the weather for the next week ** @param context /* @param cityInfo The city to get the weather for * @param onSuccessListener Get the successful callback */ fun getWeather7Day( context: Context, cityInfo: CityInfo? , onSuccessListener: (MutableList<WeekWeather>) -> kotlin.Unit ) { QWeather.getWeather7D(context, getLocation(cityInfo = cityInfo), getDefaultLocale(context), Unit.METRIC, object : QWeather.OnResultWeatherDailyListener { override fun onError(e: Throwable) { XLog.e("getWeather7Day1 onError: $e") showToast(context, e.message) } override fun onSuccess(weatherDailyBean: WeatherDailyBean?) { onSuccessListener(weatherDailyBean.daily) } }) }Copy the code

We call back when we get the data, and then we assign the data, but the data doesn’t refresh…

Is also too silly, data assignment complete refresh is not good…

private fun notifyWeatherWidget( context: Context, appWidgetId: Int ) { WeatherWidgetUtils.getWeather7Day(context = context, CityInfo = cityInfo) {items - > / / assignment widgetItems = items val MGR = AppWidgetManager. GetInstance (context) / / refresh mgr.notifyAppWidgetViewDataChanged( appWidgetId, R.id.stack_view ) XLog.e(TAG, "init: $widgetItems") } }Copy the code

That’s it. Let’s put down the official flow chart.

The desktop image shows rounded corners

This is to show the problem, the weather background and widget does not support the custom View, so only through the picture itself, you need to images with rounded corners, it is very simple, online search a lot of, but after I set up is not what I want effect, I want to look at the width is high, it is simple, just add a line configuration:

android:scaleType="centerCrop"
Copy the code

Run again and find that the rounded corners are not set… Ok, it’s cut, so you have to cut it to the desired size first, and then add the rounded corners…

*/ zoomImg(bm: Bitmap): Bitmap {val h = bm.height val retX: Int val retY: Int val wh = w.toDouble() / h.toDouble() val nwh = w.toDouble() / w.toDouble() if (wh > nwh) { retX = h * w / w retY = h } else {retX = w retY = w * w/w} val startX = if (w > retX) (w-retx) / 2 else 0 Val startY = if (h > retY) (h-rety) / 2 else 0 Val bit = bitmap.createBitmap (bm, startX, startY, retX, retY, null, false) bm.recycle() return bit }Copy the code

Once you’ve done that, you can cut the corners, and then you can put the image in the ImageView.

setImageViewBitmap(
    R.id.widget_iv_bg,
    fillet(context = context, bitmap = zoomImg(context, weather.icon), roundDp = 10)
)
Copy the code

Finished work

You can check out my new book, Jetpack Compose: New UI Programming for Android, which includes the Compose framework in its entirety.

Purchase address of JINGdong

Dangdang Purchase address

Pooh pooh, too shameless, recommending his new book again…

Github address: github.com/zhujiang521…

If you’re learning about Compose or want to learn about Android widgets, this project should be of some help to you. If it helps, don’t forget to hit the Star. Thank you very much.

In fact, there are some details that I haven’t covered, so if you have any questions, please feel free to ask in the comments section.

Let me finish here, goodbye!