Skip to content

Commit ce4c345

Browse files
authored
Merge pull request #164 from secondsun/canonical-layouts
Canonical layouts
2 parents 3be03ee + 72955c3 commit ce4c345

File tree

103 files changed

+6091
-4
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+6091
-4
lines changed

gradle/libs.versions.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ appcompat = "1.6.1"
4343
material = "1.12.0-beta01"
4444
constraintlayout = "2.1.4"
4545
glide-compose = "1.0.0-beta01"
46+
glance = "1.1.0-SNAPSHOT"
4647

4748
[libraries]
4849

@@ -124,8 +125,8 @@ play-services-location = { module = "com.google.android.gms:play-services-locati
124125

125126
androidx-work-runtime-ktx = "androidx.work:work-runtime-ktx:2.9.0"
126127
androidx-core-remoteviews = "androidx.core:core-remoteviews:1.0.0"
127-
androidx-glance-appwidget = "androidx.glance:glance-appwidget:1.0.0"
128-
androidx-glance-material3 = "androidx.glance:glance-material3:1.0.0"
128+
androidx-glance-appwidget = {group = "androidx.glance", name = "glance-appwidget", version.ref = "glance"}
129+
androidx-glance-material3 = {group = "androidx.glance", name = "glance-material3", version.ref = "glance"}
129130
androidx-graphics-core = { group = "androidx.graphics", name = "graphics-core", version = "1.0.0-beta01" }
130131
androidx-startup = 'androidx.startup:startup-runtime:1.1.1'
131132
androidx-window = { module = "androidx.window:window", version.ref = "androidx-window" }

samples/user-interface/appwidgets/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ sample together with the
2121

2222
# Getting Started
2323

24+
## Widget Samples
25+
2426
The sample contains 3 type of widgets:
2527

2628
- ToDo list: showcasing how to create a list of items
@@ -32,6 +34,8 @@ Those are implemented in two packages:
3234
- [rv](src/main/java/com/example/platform/ui/appwidgets/rv) for RemoteViews implementation
3335
- [glance](src/main/java/com/example/platform/ui/appwidgets/glance) for Glance implementation
3436

37+
## Widget Pinning
38+
3539
In addition, the [AppWidgets.kt](src/main/java/com/example/platform/ui/appwidgets/AppWidgets.kt)
3640
showcases how to request the launcher to "pin" an appwidget.
3741

@@ -47,6 +51,11 @@ title="Showcase of the Images widget implementation" />
4751

4852
> More showcasing resources in the [screenshots folder](screenshots)
4953
54+
## Canonical Layouts
55+
56+
These [layouts](./src/main/java/com/example/platform/ui/appwidgets/glance/layout) demonstrate how to write responsive, high-quality layouts for use with your
57+
Glance widgets.
58+
5059
## Run a new configuration
5160

5261
When creating a new run configuration, it's important to ensure that the widget is recreated without

samples/user-interface/appwidgets/src/main/AndroidManifest.xml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,95 @@
3838
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
3939
</intent-filter>
4040
</activity>
41+
<!-- Long text variants -->
42+
<receiver
43+
android:name=".glance.layout.text.LongTextAppWidgetReceiver"
44+
android:enabled="@bool/glance_appwidget_available"
45+
android:exported="false"
46+
android:label="@string/sample_long_text_app_widget_name">
47+
<intent-filter>
48+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
49+
<action android:name="android.intent.action.LOCALE_CHANGED" />
50+
</intent-filter>
51+
<meta-data
52+
android:name="android.appwidget.provider"
53+
android:resource="@xml/sample_long_text_widget_info" />
54+
</receiver>
55+
<receiver
56+
android:name=".glance.layout.text.TextWithImageAppWidgetReceiver"
57+
android:enabled="@bool/glance_appwidget_available"
58+
android:exported="false"
59+
android:label="@string/sample_text_and_image_app_widget_name">
60+
<intent-filter>
61+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
62+
<action android:name="android.intent.action.LOCALE_CHANGED" />
63+
</intent-filter>
64+
<meta-data
65+
android:name="android.appwidget.provider"
66+
android:resource="@xml/sample_text_with_image_widget_info" />
67+
</receiver>
68+
69+
<!-- List of image + text -->
70+
<receiver
71+
android:name=".glance.layout.collections.ImageTextListAppWidgetReceiver"
72+
android:enabled="@bool/glance_appwidget_available"
73+
android:exported="false"
74+
android:label="@string/sample_text_and_image_list_app_widget_name">
75+
<intent-filter>
76+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
77+
<action android:name="android.intent.action.LOCALE_CHANGED" />
78+
</intent-filter>
79+
<meta-data
80+
android:name="android.appwidget.provider"
81+
android:resource="@xml/sample_image_text_list_widget_info" />
82+
</receiver>
83+
84+
<!-- List of Icon button + text -->
85+
<receiver
86+
android:name=".glance.layout.collections.CheckListAppWidgetReceiver"
87+
android:enabled="@bool/glance_appwidget_available"
88+
android:exported="false"
89+
android:label="@string/sample_check_list_app_widget_name">
90+
<intent-filter>
91+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
92+
<action android:name="android.intent.action.LOCALE_CHANGED" />
93+
</intent-filter>
94+
<meta-data
95+
android:name="android.appwidget.provider"
96+
android:resource="@xml/sample_check_list_widget_info" />
97+
</receiver>
98+
99+
<!-- Action List -->
100+
<activity android:name=".glance.layout.ActionDemonstrationActivity" />
101+
102+
<receiver
103+
android:name=".glance.layout.collections.ActionListAppWidgetAppWidgetReceiver"
104+
android:enabled="@bool/glance_appwidget_available"
105+
android:exported="false"
106+
android:label="@string/sample_action_list_app_widget_name">
107+
<intent-filter>
108+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
109+
<action android:name="android.intent.action.LOCALE_CHANGED" />
110+
</intent-filter>
111+
<meta-data
112+
android:name="android.appwidget.provider"
113+
android:resource="@xml/sample_action_list_widget_info" />
114+
</receiver>
115+
116+
<!-- Image Grid -->
117+
<receiver
118+
android:name=".glance.layout.collections.ImageGridAppWidgetReceiver"
119+
android:enabled="@bool/glance_appwidget_available"
120+
android:exported="false"
121+
android:label="@string/sample_image_grid_app_widget_name">
122+
<intent-filter>
123+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
124+
<action android:name="android.intent.action.LOCALE_CHANGED" />
125+
</intent-filter>
126+
<meta-data
127+
android:name="android.appwidget.provider"
128+
android:resource="@xml/sample_image_grid_widget_info" />
129+
</receiver>
41130

42131
<receiver
43132
android:name=".rv.list.ListAppWidget"

samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/AppWidgets.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,19 @@ import com.google.android.catalog.framework.annotations.Sample
5656
fun AppWidgets() {
5757
val context = LocalContext.current
5858
val widgetManager = AppWidgetManager.getInstance(context)
59+
5960
// Get a list of our app widget providers to retrieve their info
6061
val widgetProviders = widgetManager.getInstalledProvidersForPackage(context.packageName, null)
62+
.showCanonicalLayoutsFirst()
63+
64+
AppWidgetsList(widgetProviders)
65+
}
6166

67+
@RequiresApi(Build.VERSION_CODES.O)
68+
@Composable
69+
fun AppWidgetsList(widgetProviders: List<AppWidgetProviderInfo>) {
70+
val context = LocalContext.current
71+
val widgetManager = AppWidgetManager.getInstance(context)
6272
LazyColumn(contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
6373
item {
6474
AppInfoText()
@@ -77,6 +87,16 @@ fun AppWidgets() {
7787
}
7888
}
7989

90+
private fun MutableList<AppWidgetProviderInfo>.showCanonicalLayoutsFirst(): List<AppWidgetProviderInfo> {
91+
return this.toMutableList().apply {
92+
sortWith (
93+
compareBy {
94+
if (it.provider.className.startsWith("com.example.platform.ui.appwidgets.glance.layout")) -1 else 1
95+
}
96+
)
97+
}
98+
}
99+
80100

81101
/**
82102
* Extension method to request the launcher to pin the given AppWidgetProviderInfo
@@ -130,7 +150,7 @@ private fun WidgetInfoCard(providerInfo: AppWidgetProviderInfo) {
130150
val context = LocalContext.current
131151
val label = providerInfo.loadLabel(context.packageManager)
132152
val description = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
133-
providerInfo.loadDescription(context).toString()
153+
(providerInfo.loadDescription(context)?:"").toString();
134154
} else {
135155
"Description not available"
136156
}
@@ -143,7 +163,7 @@ private fun WidgetInfoCard(providerInfo: AppWidgetProviderInfo) {
143163
}
144164
) {
145165
Row(modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween) {
146-
Column {
166+
Column(modifier = Modifier.padding(end = 8.dp)) {
147167
Text(
148168
text = label,
149169
style = MaterialTheme.typography.titleSmall
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.platform.ui.appwidgets.glance.layout
17+
18+
import androidx.activity.ComponentActivity
19+
import androidx.activity.compose.setContent
20+
import androidx.compose.foundation.layout.Box
21+
import androidx.compose.foundation.layout.fillMaxSize
22+
import androidx.compose.material3.Text
23+
import androidx.compose.ui.Alignment
24+
import androidx.compose.ui.Modifier
25+
import androidx.glance.action.ActionParameters
26+
import com.google.android.catalog.framework.ui.CatalogActivity
27+
28+
internal val ActionSourceMessageKey = ActionParameters.Key<String>("actionSourceMessageKey")
29+
30+
/**
31+
* Activity that is launched on clicks from different parts of sample widgets. Displays string
32+
* describing source of the click.
33+
*/
34+
class ActionDemonstrationActivity : CatalogActivity() {
35+
36+
override fun onResume() {
37+
super.onResume()
38+
setContent {
39+
Box(
40+
modifier = Modifier.fillMaxSize(),
41+
contentAlignment = Alignment.Center
42+
) {
43+
val source = intent.getStringExtra(ActionSourceMessageKey.name) ?: "Unknown"
44+
Text("Launched from $source")
45+
}
46+
}
47+
}
48+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.platform.ui.appwidgets.glance.layout
18+
19+
/**
20+
* This function should be removed when API level is raised to 24. It exists to provide removeIf
21+
* from java.util.Collection
22+
*/
23+
fun <T> MutableList<T>.removeIf(filter: (T) -> Boolean): Boolean {
24+
25+
var removed = false
26+
var toRemove = mutableListOf<T>()
27+
28+
forEach { item ->
29+
if (filter(item)) {
30+
removed = true
31+
toRemove.add(item)
32+
}
33+
}
34+
35+
removeAll(toRemove)
36+
return removed
37+
38+
}
39+
40+
fun <K, V> MutableMap<K,V>.computeIfAbsent(key : K, calulation :(K)->V):V? {
41+
var value = get(key)
42+
43+
if (value == null) {
44+
value = calulation(key)
45+
put(key, value)
46+
}
47+
return value
48+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.example.platform.ui.appwidgets.glance.layout.collections
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.collectAsState
7+
import androidx.compose.runtime.getValue
8+
import androidx.glance.GlanceId
9+
import androidx.glance.GlanceTheme
10+
import androidx.glance.LocalContext
11+
import androidx.glance.appwidget.AppWidgetId
12+
import androidx.glance.appwidget.GlanceAppWidget
13+
import androidx.glance.appwidget.GlanceAppWidgetReceiver
14+
import androidx.glance.appwidget.SizeMode
15+
import androidx.glance.appwidget.provideContent
16+
import com.example.platform.ui.appwidgets.glance.layout.collections.data.FakeActionListDataRepository
17+
import com.example.platform.ui.appwidgets.glance.layout.collections.data.FakeActionListDataRepository.Companion.getActionListDataRepo
18+
import com.example.platform.ui.appwidgets.glance.layout.collections.layout.ActionListItem
19+
import com.example.platform.ui.appwidgets.glance.layout.collections.layout.ActionListLayout
20+
import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity
21+
import com.example.platform.ui.appwidgets.R
22+
import kotlinx.coroutines.Dispatchers
23+
import kotlinx.coroutines.withContext
24+
25+
/** A sample [GlanceAppWidget] demonstrating the [ActionListLayout]. */
26+
class ActionListAppWidget : GlanceAppWidget() {
27+
// Unlike the "Single" size mode, using "Exact" allows us to have better control over rendering in
28+
// different sizes. And, unlike the "Responsive" mode, it doesn't cause several views for each
29+
// supported size to be held in the widget host's memory.
30+
override val sizeMode: SizeMode = SizeMode.Exact
31+
32+
override suspend fun provideGlance(context: Context, id: GlanceId) {
33+
val repo = getActionListDataRepo(id)
34+
35+
val initialItems = withContext(Dispatchers.Default) {
36+
repo.load()
37+
}
38+
39+
provideContent {
40+
val items by repo.items().collectAsState(initial = initialItems)
41+
val checkedItems by repo.checkedItems().collectAsState(initial = emptyList())
42+
43+
GlanceTheme {
44+
WidgetContent(
45+
items = items,
46+
checkedItems = checkedItems,
47+
checkItemAction = { key -> repo.checkItem(key) }
48+
)
49+
}
50+
}
51+
}
52+
53+
@Composable
54+
fun WidgetContent(
55+
items: List<ActionListItem>,
56+
checkedItems: List<String>,
57+
checkItemAction: (String) -> Unit,
58+
) {
59+
val context = LocalContext.current
60+
61+
ActionListLayout(
62+
title = context.getString(R.string.sample_action_list_app_widget_name),
63+
titleIconRes = R.drawable.sample_home_icon,
64+
titleBarActionIconRes = R.drawable.sample_power_settings_icon,
65+
titleBarActionIconContentDescription = context.getString(
66+
R.string.sample_action_list_settings_label
67+
),
68+
titleBarAction = actionStartDemoActivity("Power settings title bar action"),
69+
items = items,
70+
checkedItems = checkedItems,
71+
actionButtonClick = checkItemAction,
72+
)
73+
}
74+
}
75+
76+
class ActionListAppWidgetAppWidgetReceiver : GlanceAppWidgetReceiver() {
77+
override val glanceAppWidget = ActionListAppWidget()
78+
79+
@SuppressLint("RestrictedApi")
80+
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
81+
appWidgetIds.forEach {
82+
FakeActionListDataRepository.cleanUp(AppWidgetId(appWidgetId = it))
83+
}
84+
super.onDeleted(context, appWidgetIds)
85+
}
86+
}

0 commit comments

Comments
 (0)