Skip to content

Commit 5b56e2a

Browse files
authored
Merge pull request #418 from android/feature/full-bleed-snap-widget
Add Full Bleed Snap Scrolling Glance Canonical Layout
2 parents f148dee + 0f404de commit 5b56e2a

16 files changed

Lines changed: 554 additions & 14 deletions

File tree

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ material3Android = "1.3.2"
5757
media3 = "1.5.0"
5858
constraintlayout = "2.1.4"
5959
glide-compose = "1.0.0-beta01"
60-
glance = "1.1.0"
60+
glance = "1.3.0-alpha01"
6161
tensorflowLite = "2.9.0"
6262
tensorflowLiteGpuDelegatePlugin = "0.4.4"
6363
tensorflowLiteSupport = "0.4.2"

samples/user-interface/appwidgets/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ plugins {
2323

2424
android {
2525
namespace = "com.example.platform.ui.appwidgets"
26-
compileSdk = 36
26+
compileSdk = 37
2727

2828
defaultConfig {
29-
minSdk = 21
29+
minSdk = 23
3030
}
3131

3232
compileOptions {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@
104104
android:name="android.appwidget.provider"
105105
android:resource="@xml/sample_text_with_image_widget_info" />
106106
</receiver>
107+
<receiver
108+
android:name=".glance.layout.text.FullBleedImageAppWidgetReceiver"
109+
android:enabled="@bool/glance_appwidget_available"
110+
android:exported="false"
111+
android:label="@string/sample_full_bleed_image_app_widget_name">
112+
<intent-filter>
113+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
114+
<action android:name="android.intent.action.LOCALE_CHANGED" />
115+
</intent-filter>
116+
<meta-data
117+
android:name="android.appwidget.provider"
118+
android:resource="@xml/sample_full_bleed_image_widget_info" />
119+
</receiver>
107120

108121
<!-- List of image + text -->
109122
<receiver

samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/CanonicalLayoutActivity.kt

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
1+
/*
2+
* Copyright 2026 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+
117
package com.example.platform.ui.appwidgets.glance.layout
218

19+
import android.appwidget.AppWidgetManager
320
import android.content.Context
421
import android.content.Intent
522
import android.net.Uri
23+
import android.os.Build
624
import android.os.Bundle
725
import android.util.Log
826
import androidx.activity.ComponentActivity
927
import androidx.activity.compose.setContent
1028
import androidx.activity.enableEdgeToEdge
1129
import androidx.annotation.DrawableRes
30+
import androidx.annotation.RequiresApi
1231
import androidx.annotation.StringRes
1332
import androidx.compose.foundation.Image
1433
import androidx.compose.foundation.background
@@ -53,12 +72,15 @@ import com.example.platform.ui.appwidgets.glance.layout.collections.ImageGridApp
5372
import com.example.platform.ui.appwidgets.glance.layout.collections.ImageTextListAppWidgetReceiver
5473
import com.example.platform.ui.appwidgets.glance.layout.text.LongTextAppWidgetReceiver
5574
import com.example.platform.ui.appwidgets.glance.layout.text.TextWithImageAppWidgetReceiver
75+
import com.example.platform.ui.appwidgets.glance.layout.text.FullBleedImageAppWidgetReceiver
5676
import com.example.platform.ui.appwidgets.glance.layout.toolbars.ExpressiveToolbarAppWidgetReceiver
5777
import com.example.platform.ui.appwidgets.glance.layout.toolbars.SearchToolBarAppWidgetReceiver
5878
import com.example.platform.ui.appwidgets.glance.layout.toolbars.ToolBarAppWidgetReceiver
5979
import kotlinx.coroutines.CoroutineScope
6080
import kotlinx.coroutines.launch
6181

82+
import androidx.lifecycle.lifecycleScope
83+
6284
class CanonicalLayoutActivity : ComponentActivity() {
6385

6486
companion object {
@@ -72,6 +94,35 @@ class CanonicalLayoutActivity : ComponentActivity() {
7294
enableEdgeToEdge()
7395

7496
super.onCreate(savedInstanceState)
97+
98+
// Publish Generated Widget Previews
99+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
100+
lifecycleScope.launch {
101+
try {
102+
val context = this@CanonicalLayoutActivity
103+
val receiver = FullBleedImageAppWidgetReceiver::class.java
104+
val glanceAppWidgetManager = GlanceAppWidgetManager(context)
105+
val appWidgetManager = context.getSystemService(AppWidgetManager::class.java)
106+
107+
val providerInfo = appWidgetManager?.installedProviders?.firstOrNull {
108+
it.provider.className == receiver.name
109+
}
110+
111+
if (providerInfo?.generatedPreviewCategories == 0) {
112+
val result = glanceAppWidgetManager.setWidgetPreviews(FullBleedImageAppWidgetReceiver::class)
113+
val status = when (result) {
114+
GlanceAppWidgetManager.SET_WIDGET_PREVIEWS_RESULT_SUCCESS -> "Success"
115+
GlanceAppWidgetManager.SET_WIDGET_PREVIEWS_RESULT_RATE_LIMITED -> "Rate-Limited"
116+
else -> "Error ($result)"
117+
}
118+
Log.i("CanonicalLayoutActivity", "Published previews for ${receiver.simpleName}: $status")
119+
}
120+
} catch (e: Exception) {
121+
Log.e("CanonicalLayoutActivity", "Failed to set widget previews", e)
122+
}
123+
}
124+
}
125+
75126
setContent {
76127
MaterialTheme {
77128
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
@@ -195,7 +246,7 @@ private fun CanonicalLayoutRow(widget: CanonicalLayoutRowData, modifier: Modifie
195246
painter = painterResource(id = widget.imageRes),
196247
contentDescription = "Screenshot of ${widget.rowTitle}",
197248
contentScale = ContentScale.FillWidth,
198-
modifier = modifier,
249+
modifier = modifier.fillMaxWidth(),
199250
)
200251
}
201252

@@ -287,6 +338,12 @@ private val canonicalLayoutWidgets = listOf(
287338
imageRes = R.drawable.cl_activity_row_text_image,
288339
receiver = TextWithImageAppWidgetReceiver::class.java,
289340
),
341+
CanonicalLayoutRowData(
342+
rowTitle = R.string.cl_title_full_bleed_image,
343+
rowDescription = R.string.cl_description_full_bleed_image,
344+
imageRes = R.drawable.cl_activity_row_full_bleed_image,
345+
receiver = FullBleedImageAppWidgetReceiver::class.java,
346+
),
290347
CanonicalLayoutRowData(
291348
rowTitle = R.string.cl_title_grid,
292349
rowDescription = R.string.cl_description_grid,

samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/collections/data/FakeImageGridDataRepository.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 2026 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+
117
package com.example.platform.ui.appwidgets.glance.layout.collections.data
218

319
import android.content.Context
@@ -16,11 +32,11 @@ import com.example.platform.ui.appwidgets.glance.layout.collections.layout.Image
1632
import com.example.platform.ui.appwidgets.glance.layout.utils.ImageUtils.getMaxPossibleImageSize
1733
import com.example.platform.ui.appwidgets.glance.layout.utils.ImageUtils.getMaxWidgetMemoryAllowedSizeInBytes
1834
import kotlinx.coroutines.Dispatchers
35+
import kotlinx.coroutines.coroutineScope
1936
import kotlinx.coroutines.async
2037
import kotlinx.coroutines.awaitAll
2138
import kotlinx.coroutines.flow.Flow
2239
import kotlinx.coroutines.flow.MutableStateFlow
23-
import kotlinx.coroutines.runBlocking
2440
import com.example.platform.ui.appwidgets.glance.layout.computeIfAbsent as computeIfAbsentExt
2541
/**
2642
* A fake in-memory implementation of repository that produces a list of
@@ -74,7 +90,7 @@ class FakeImageGridDataRepository {
7490
val width = IMAGE_SIZE.coerceAtMost(imageSizeLimit.width)
7591
val height = width * 9 / 16
7692

77-
val mappedItems = runBlocking {
93+
val mappedItems = coroutineScope {
7894
items.map { item ->
7995
async(Dispatchers.IO) {
8096
var bitmap: Bitmap? = null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2026 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 details; and limitations under the License.
14+
*/
15+
16+
package com.example.platform.ui.appwidgets.glance.layout.text
17+
18+
import android.content.Context
19+
import android.util.Log
20+
import android.os.Build
21+
import androidx.annotation.RequiresApi
22+
import androidx.compose.runtime.collectAsState
23+
import androidx.compose.runtime.getValue
24+
import androidx.glance.GlanceId
25+
import androidx.glance.GlanceTheme
26+
import androidx.glance.appwidget.GlanceAppWidget
27+
import androidx.glance.appwidget.GlanceAppWidgetManager
28+
import androidx.glance.appwidget.GlanceAppWidgetReceiver
29+
import androidx.glance.appwidget.SizeMode
30+
import androidx.glance.appwidget.provideContent
31+
import androidx.compose.ui.unit.DpSize
32+
import androidx.compose.ui.unit.dp
33+
import com.example.platform.ui.appwidgets.glance.layout.collections.data.FakeImageGridDataRepository
34+
import com.example.platform.ui.appwidgets.glance.layout.collections.data.FakeImageGridDataRepository.Companion.getImageGridDataRepo
35+
import com.example.platform.ui.appwidgets.glance.layout.text.layout.FullBleedImageLayout
36+
import kotlinx.coroutines.Dispatchers
37+
import kotlinx.coroutines.withContext
38+
39+
/**
40+
* Glance widget showcasing Full Bleed Snap Scrolling, powered by [FakeImageGridDataRepository].
41+
*/
42+
class FullBleedImageAppWidget : GlanceAppWidget() {
43+
override val sizeMode: SizeMode = SizeMode.Exact
44+
45+
override val previewSizeMode = SizeMode.Responsive(
46+
setOf(
47+
DpSize(109.dp, 115.dp)
48+
)
49+
)
50+
51+
override suspend fun provideGlance(context: Context, id: GlanceId) {
52+
val repo = getImageGridDataRepo(id)
53+
54+
val initialData = withContext(Dispatchers.IO) {
55+
repo.load(context)
56+
}
57+
58+
provideContent {
59+
val data by repo.data().collectAsState(initial = initialData)
60+
61+
GlanceTheme {
62+
FullBleedImageLayout(
63+
data = data
64+
)
65+
}
66+
}
67+
}
68+
69+
override suspend fun providePreview(context: Context, widgetCategory: Int) {
70+
val repo = FakeImageGridDataRepository()
71+
72+
val initialData = withContext(Dispatchers.IO) {
73+
repo.load(context)
74+
}
75+
76+
provideContent {
77+
GlanceTheme {
78+
FullBleedImageLayout(
79+
data = initialData.take(1)
80+
)
81+
}
82+
}
83+
}
84+
}
85+
86+
/**
87+
* Receiver for the Full Bleed Snap Scrolling widget.
88+
*/
89+
class FullBleedImageAppWidgetReceiver : GlanceAppWidgetReceiver() {
90+
override val glanceAppWidget: GlanceAppWidget = FullBleedImageAppWidget()
91+
92+
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
93+
val glanceAppWidgetManager = GlanceAppWidgetManager(context)
94+
appWidgetIds.forEach { id ->
95+
try {
96+
val glanceId = glanceAppWidgetManager.getGlanceIdBy(id)
97+
FakeImageGridDataRepository.cleanUp(glanceId)
98+
} catch (e: IllegalArgumentException) {
99+
Log.w("FullBleedImageReceiver", "Skipping cleanup for invalid AppWidget ID: $id", e)
100+
}
101+
}
102+
super.onDeleted(context, appWidgetIds)
103+
}
104+
}

0 commit comments

Comments
 (0)