Widgets have been an important part of the Android system since 2008, and an important aspect of customizing the home screen. You can think of a Widget as an "at-a-glance" view of an app that lets users see app data and core functionality at a glance without having to open the app from the home screen. However, since the launch of Android, the API of AppWidget has basically not changed much. From 2012 to 2021, there is only one Android version that includes updates to the AppWidget API. And with the launch of Android 12, it also brought some much-needed improvements to the Widget API.
In this article, we will introduce what updates to the Widget API have been brought in Android 12, and what useful tools are available to make developing app Widgets even better. If you prefer to see this through video, check it out here:
https://www.bilibili.com/video/BV1Ra411z7dN/?aid=210483278&cid=479269668&page=1
△ Build a more modern application widget in Android 12
Widget
Widget runs in a remote process called AppWidgetHost, such as Home Screen Launcher, and because of this, its operation is limited. Let's take a look at how Widgets work.
On the front end, the application first registers AppWidgetProvider to define Widget behavior, and AppWidgetProviderInfo to define metadata. Then AndroidManifest refers to this information, let the operating system read metadata through AndroidManifest, such as the Widget's initial layout and default size, and provide a preview of the Widget, and then the provider will use the linked account to update the layout and update the Widget. It should be noted here that the number of builds applied to the Widget is limited, so the operating system updates the Widget through the receiver's broadcast event (including update information), which also means that the Widget receives information from the application regularly for updates. .
API
The launch of Android 12 brings a lot of updates to the AppWidget API. This article will not introduce all the APIs one by one, but will focus on a few APIs that are very useful for Widget building.
implements rounded corners
In Android 12, many key interface elements have begun to use rounded corners. In order to make AppWidget look consistent with other system component styles, Android 12 introduces two new ones: system_app_widget_background_radius and system_app_widget_inner_radius , the former parameter is used to set the Widget's corner radius, and the latter is used to set the Widget's inner view's corner radius. To use these parameters, you only need to define a drawable object with the system parameter corner set, as shown in the code:
// res/drawable/app_widget_background.xml
<shape android:shape="rectangle">
<corners android:radius="@android:dimen/system_app_widget_background_radius">
…
</shape>
// res/drawable/app_widget_inner_view_background.xml
<shape android:shape="rectangle">
<corners android:radius="@android:dimen/system_app_widget_inner_radius">
…
</shape>
The drawable is then applied to the widget's outer container, which applies the corner radius provided by the system parameter to the widget background. Likewise, apply the inner view's drawable to the layout representing the Widget's inner container, as shown in the code:
// res/layout/widget_layout.xml
<LinearLayout
android:background=”@drawable/app_widget_background”
…>
<LinearLayout
android:background=”@drawable/app_widget_inner_view_background”
…>
</LinearLayout>
</LinearLayout>
△ Left: Widget with rounded corners; right: inner view with rounded corners
From the effect, we can see that the corner radius of the current inner container of the Widget is smaller than that of the outer container, which is how the new parameters are used.
Dynamic Color
As we previously announced at Google I/O, starting with Android 12, widgets can use device theme colors, including light and dark themes, for buttons, backgrounds, and other components. This allows for smoother transitions and consistency across widgets.
We have added a dynamic color API, you can directly obtain and use the theme background, color and other parameters provided on the Pixel device system, so as to make the widget consistent with the style of the home screen:
// res/layout/widget_layout.xml
<LinearLayout
android:theme="@android:style/Theme.DeviceDefault.DayNight"
android:background="?android:attr/colorBackground">
<ImageView
android:tint="?android:attr/colorAccent" />
…
</LinearLayout>
You can see that when the theme property is set, the widget directly extracts the main color from the system wallpaper and applies it to the dark and light themes.
Responsive Layout
Android 12 introduces new APIs to implement responsive layouts, which can automatically switch to different layouts as the Widget resize. As shown in the figure below, the user can arbitrarily change the size of the Widget by dragging, and the Widget will also dynamically update the content to be displayed according to the different sizes.
So how to make the Widget dynamically update the display content as the size changes? Take the following code as an example, we define three different parameters, including the minimum supported width and height, and the corresponding RemoteView within this size range, The system will automatically adjust the Widget according to the actual size.
val viewMapping: Map<SizeF, RemoteViews> = mapof(
SizeF(180.0f, 110.0f) to RemoteViews(
context. packageName,
R.layout.widget_small
),
SizeF (270.0f, 110.0f) to RemoteViews(
context.packageName,
R.layout.widget_medium
),
SizeF(270.0f, 280.0f) to RemoteViews(
context.packageName,
R.layout.widget_large
)
)
appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))
Also available in Android 12 are new targetCellWidth
and targetCellHeight
properties that specify the default larger cell size when a widget is placed on the home screen. Prior to Android 12, the minWidget
and minHeight
properties were available, which specified the default widget size in dp, and we recommend specifying both properties for backward compatibility. If your Widget is resizable, you can also use the minResizeWidth/Height and maxResizeWidth/Height properties provided by Android 12 to limit the resizable size range of your Widget.
<appwidget-provider
android:targetCellWidth="3"
android: targetCellHeight="2"
android:minWidth="140dp"
android:minHeight="110dp"
android:maxResizeWidth="570dp"
android:maxResizeHeight="450dp"
android:minResizeWidth="140dp"
android:minResizeHeight="110dp"
…>
Widget selector
Android 12 also improves the experience of using the Widget selector and introduces two new attributes. The first attribute is description, which describes the function of the Widget selector, through which you can understand the function of the Widget; the other is previewLayout, which specifies the XML layout displayed in the Widget selector. In fact, before Android 12, you can use the previewImage attribute to specify static resources to achieve a similar effect, but previewLayout is more precise and convenient. Also, since these previews are built at runtime, they can also be dynamically adapted to the device's theme.
<appwidget-provider
android:description=
"@string/app_widget_weather_description"
android:previewLayout=
"@layout/widget_weather_forecast_small"
…
/>
△ description property
△ previewLayout property
At present, many new APIs introduced by Android 12 have been introduced. I believe that more and more applications will use the new APIs to build a more modern Widget experience in the near future.
Glance
To build a great widget, in addition to using the more modern API, we also need more modern and better tools to help us. Glance is such an excellent tool, and it has also joined the Jetpack family. Glance is an API supported by Compose Runtime, through which you can use Compose-style syntax to create AppWidgets, which also means that you can build interfaces composable through Glance, and convert them into remote views to display in Widgets. You can also use the new API of Android 12 mentioned above and make it as backward compatible as possible. In addition, Glance is also responsible for some Widget life cycles and other common operations, which sounds very convenient.
△ Glance structure diagram
Next, we introduce how to use Glance to build a Widget. First, you still need to declare the AppWidget as before and link it to the receiver in the AndroidManifest. Of course, we use the GlanceAppWidgetReceiver and GlanceAppWidget provided by Glance here, and Glance will handle the large size for you. For part of the work, you only need to override the Content method in MyAppWidget to provide the AppWidget content. When defining the content, no longer use XML syntax, but use Compose syntax, the content to be displayed will be converted into a remote view and displayed in the AppWidget.
class MyAppWidget: GlanceAppWidget() {
@Composable
override fun Content() {
// 在这里创建 AppWidget
Column(
modifier = Modifier.expandHeight().expandWidth(),
verticalAlignment = Alignment.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = “Where to”, modifier = Modifier.padding(12.dp))
userDestinations()
}
}
}
class MyAppWidgetReceiver: GlanceAppWidgetReceiver() {
// 告知 MyAppWidgetReceiver 该使用哪个 GlanceAppWidget
override val glanceAppWidget: GlanceAppWidget = MyAppWidget()
}
It's important to understand that while Glance uses the Compose Runtime and Compose's syntax, it's still a standalone framework, and you can't reuse components defined in the Jetpack Compose UI due to the constraints of building remotely. But if you're already familiar with Jetpack Compose, Glance is very easy to understand.
Also, because Glance handles interactions in the same way that Glance uses the User Events API, it will be easier for us to handle interactions with users. If you understand how Widgets work, you will know that Widgets work on different processes, which makes it difficult to handle simple user events, because not being in the same process means that you don't own the Widget, and you can only handle each event through process callbacks. kind of event.
Glance abstracts these complexities. You only need to define a clickable modifier to the required composable object to allow it to support the processing of user click events. Glance will abstract all the injection behaviors, and the user can call back when the user clicks on the composable object. the defined operation. We also define some common operations, for example, how to start Activity, just call launchActivity and pass the Activity target class.
Button(
text = “Home”,
modifier = Modifier.clickable(launchActivity<NavigationActivity>)
)
In addition, we can also provide custom actions to execute some custom code, for example, we may want to update the geolocation and refresh the Widget every time the user clicks this button, as shown in the code below, Glance will handle it for you behind the scenes Some work needs to be injected, and the click is handled by the broadcast receiver, which ultimately calls the action code you define. However, please note that if the operation is a time-consuming operation such as a network request or database access, please use the WorkManager API.
Button(
text = “My Location”,
modifier = Modifier.clickable(customAction<UpdateLocationAction>)
)
We also mentioned earlier that you can use resizable widgets, but dealing with different responsive layouts is not easy. Glance tries to make this kind of work a little easier by defining three different SizeMode options. .
SizeMode.Single is the default option, which specifies that the content of the Widget we define here will not change due to changes in the available size, which means that the minimum supported size we define on the Widget metadata will only be called once through the Content method , if the available size of the widget changes, eg if the user resizes the widget, the content will not be refreshed. As shown in the figure below, for a Widget using the SizeMode.Single option, no matter how its size changes, its output size will never change. This is because the Content method is called only once, and the content does not change when the size changes. get refreshed.
class MyAppWidget: GlanceAppWidget() {
override val sizeMode = SizeMode.Single
@Composable
override fun Content() {
val size = LocalSize.current
//…
}
}
△ SizeMode.Single option diagram
Use the SizeMode.Exact option to refresh the content every time the size changes. This option will recreate the widget interface and call the Content method again every time the user resizes the widget, and at the same time provide the maximum available size so that we can change the interface if there is enough space, such as adding extra buttons and so on. As shown in the figure below, when the widget size changes, its internal output will also change at any time, because the widget interface will be recreated each time.
class MyAppWidget: GlanceAppWidget() {
override val sizeMode = SizeMode.Exact
@Composable
override fun Content() {
val size = LocalSize.current
//…
}
}
△ SizeMode.Exact option diagram
Although the SizeMode.Exact option seems to be able to fully meet the needs, it needs to recreate the interface every time, which may cause the interface transition when the user adjusts the size to be a bit unsmooth due to some performance issues. At this time, we can pass the SizeMode.Responsive option . For example, here we map some sizes to some specific shapes, whenever an AppWidget is created or updated, Glance will call the Content method defined by each Size, and each time it will be mapped to a specific size and stored in memory, the system The ability to choose the most appropriate size based on the available sizes when the user resizes a widget without recreating the interface provides smoother transitions and better performance. As shown in the figure below, when the size of the widget changes, its internal output will only change if its size can match the predefined size range. It should be noted that there is no new Create an interface.
△ SizeMode.Responsive option diagram
Similarly, we can also define more diverse styles in the Content() method, allowing Widgets to display more unique content in different sizes.
class MyAppWidget: GlanceAppWidget() {
companion object {
private val SMALL_SQUARE = DpSize (100.dp, 160. dp)
private val HORIZONTAL_RECTANGLE = DpSize (250.dp, 100.dp)
private val BIG_SQUARE = DpSize (250.dp, 250.dp)
}
override val sizeMode = SizeMode.Responsive(
SMALL_SQUARE, HORIZONTAL_RECTANGLE, BIG_SQUARE
)
@Composable
override fun Content() {
val size = LocalSize.current
//…
}
}
In addition to the above-mentioned content, there are more content such as support for Widget state management, and out-of-the-box Material You theme background, waiting for you to explore.
To learn more, you are welcome to check out the Android Developers website: Widgets Overview We look forward to you trying out the new APIs we provide, and looking forward to seeing the widgets you build and your feedback.
You are welcome here to submit feedback to us, or share your favorite content and found problems. Your feedback is very important to us, thank you for your support!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。