Skip to content

Commit 7efe17b

Browse files
authored
Add sample layouts for toolbar style widgets (#216)
* Toolbar specific shared components and resources Used by Toolbar w/ header and Search Toolbar layouts * A search toolbar layout that displays search button or a search bar with additional actions * A toolbar layout that displays app branding, a primary entrypoint and 4 secondary entry points. It is a variant of search toolbar but with a title bar or its components. * Add search and toolbar layouts to the canonical layout showcase activity.
1 parent 20c7a4e commit 7efe17b

28 files changed

+1696
-0
lines changed

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

+32
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,38 @@
3838
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
3939
</intent-filter>
4040
</activity>
41+
42+
<!-- Toolbar variants -->
43+
44+
<!-- Toolbar with header -->
45+
<receiver
46+
android:name=".glance.layout.toolbars.ToolBarAppWidgetReceiver"
47+
android:enabled="@bool/glance_appwidget_available"
48+
android:exported="false"
49+
android:label="@string/sample_toolbar_app_widget_name">
50+
<intent-filter>
51+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
52+
<action android:name="android.intent.action.LOCALE_CHANGED" />
53+
</intent-filter>
54+
<meta-data
55+
android:name="android.appwidget.provider"
56+
android:resource="@xml/sample_toolbar_widget_info" />
57+
</receiver>
58+
<!-- A search toolbar -->
59+
<receiver
60+
android:name=".glance.layout.toolbars.SearchToolBarAppWidgetReceiver"
61+
android:enabled="@bool/glance_appwidget_available"
62+
android:exported="false"
63+
android:label="@string/sample_search_toolbar_app_widget_name">
64+
<intent-filter>
65+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
66+
<action android:name="android.intent.action.LOCALE_CHANGED" />
67+
</intent-filter>
68+
<meta-data
69+
android:name="android.appwidget.provider"
70+
android:resource="@xml/sample_search_toolbar_widget_info" />
71+
</receiver>
72+
4173
<!-- Long text variants -->
4274
<receiver
4375
android:name=".glance.layout.text.LongTextAppWidgetReceiver"

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

+14
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ import com.example.platform.ui.appwidgets.glance.layout.collections.ImageGridApp
5353
import com.example.platform.ui.appwidgets.glance.layout.collections.ImageTextListAppWidgetReceiver
5454
import com.example.platform.ui.appwidgets.glance.layout.text.LongTextAppWidgetReceiver
5555
import com.example.platform.ui.appwidgets.glance.layout.text.TextWithImageAppWidgetReceiver
56+
import com.example.platform.ui.appwidgets.glance.layout.toolbars.SearchToolBarAppWidgetReceiver
57+
import com.example.platform.ui.appwidgets.glance.layout.toolbars.ToolBarAppWidgetReceiver
5658
import kotlinx.coroutines.CoroutineScope
5759
import kotlinx.coroutines.launch
5860

@@ -290,4 +292,16 @@ private val canonicalLayoutWidgets = listOf(
290292
imageRes = R.drawable.cl_activity_row_image_grid,
291293
receiver = ImageGridAppWidgetReceiver::class.java,
292294
),
295+
CanonicalLayoutRowData(
296+
rowTitle = R.string.cl_title_toolbar,
297+
rowDescription = R.string.cl_description_toolbar,
298+
imageRes = R.drawable.cl_activity_row_toolbar,
299+
receiver = ToolBarAppWidgetReceiver::class.java,
300+
),
301+
CanonicalLayoutRowData(
302+
rowTitle = R.string.cl_title_search_toolbar,
303+
rowDescription = R.string.cl_description_search_toolbar,
304+
imageRes = R.drawable.cl_activity_row_search_toolbar,
305+
receiver = SearchToolBarAppWidgetReceiver::class.java,
306+
),
293307
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2024 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.toolbars
17+
18+
import android.content.Context
19+
import androidx.compose.runtime.Composable
20+
import androidx.glance.GlanceId
21+
import androidx.glance.GlanceTheme
22+
import androidx.glance.appwidget.GlanceAppWidget
23+
import androidx.glance.appwidget.GlanceAppWidgetReceiver
24+
import androidx.glance.appwidget.SizeMode
25+
import androidx.glance.appwidget.provideContent
26+
import com.example.platform.ui.appwidgets.R
27+
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarButton
28+
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayout
29+
import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity
30+
31+
/**
32+
* A widget to demonstrate the [SearchToolBarLayout].
33+
*/
34+
class SearchToolBarAppWidget : GlanceAppWidget() {
35+
// Unlike the "Single" size mode, using "Exact" allows us to have better control over rendering in
36+
// different sizes. And, unlike the "Responsive" mode, it doesn't cause several views for each
37+
// supported size to be held in the widget host's memory.
38+
override val sizeMode: SizeMode = SizeMode.Exact
39+
40+
override suspend fun provideGlance(context: Context, id: GlanceId) {
41+
provideContent {
42+
GlanceTheme {
43+
WidgetContent()
44+
}
45+
}
46+
}
47+
48+
@Composable
49+
fun WidgetContent() {
50+
SearchToolBarLayout(
51+
searchButton = SearchToolBarButton(
52+
iconRes = R.drawable.sample_search_icon,
53+
contentDescription = "Search notes",
54+
text = "Search",
55+
onClick = actionStartDemoActivity("search notes button")
56+
),
57+
trailingButtons = listOf(
58+
SearchToolBarButton(
59+
iconRes = R.drawable.sample_mic_icon,
60+
contentDescription = "audio",
61+
onClick = actionStartDemoActivity("audio button")
62+
),
63+
SearchToolBarButton(
64+
iconRes = R.drawable.sample_videocam_icon,
65+
contentDescription = "video note",
66+
onClick = actionStartDemoActivity("video note button")
67+
),
68+
SearchToolBarButton(
69+
iconRes = R.drawable.sample_camera_icon,
70+
contentDescription = "camera",
71+
onClick = actionStartDemoActivity("camera button")
72+
),
73+
SearchToolBarButton(
74+
iconRes = R.drawable.sample_share_icon,
75+
contentDescription = "share",
76+
onClick = actionStartDemoActivity("share button")
77+
),
78+
)
79+
)
80+
}
81+
}
82+
83+
/**
84+
* Receiver registered in the manifest for the [SearchToolBarAppWidget].
85+
*/
86+
class SearchToolBarAppWidgetReceiver : GlanceAppWidgetReceiver() {
87+
override val glanceAppWidget: GlanceAppWidget = SearchToolBarAppWidget()
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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.toolbars
18+
19+
import android.content.Context
20+
import androidx.compose.runtime.Composable
21+
import androidx.glance.GlanceId
22+
import androidx.glance.GlanceTheme
23+
import androidx.glance.appwidget.GlanceAppWidget
24+
import androidx.glance.appwidget.GlanceAppWidgetReceiver
25+
import androidx.glance.appwidget.SizeMode
26+
import androidx.glance.appwidget.provideContent
27+
import com.example.platform.ui.appwidgets.R
28+
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarButton
29+
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayout
30+
import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity
31+
32+
/**
33+
* A widget to demonstrate the [ToolBarLayout].
34+
*/
35+
class ToolBarAppWidget : GlanceAppWidget() {
36+
// Unlike the "Single" size mode, using "Exact" allows us to have better control over rendering in
37+
// different sizes. And, unlike the "Responsive" mode, it doesn't cause several views for each
38+
// supported size to be held in the widget host's memory.
39+
override val sizeMode: SizeMode = SizeMode.Exact
40+
41+
override suspend fun provideGlance(context: Context, id: GlanceId) {
42+
provideContent {
43+
GlanceTheme {
44+
WidgetContent()
45+
}
46+
}
47+
}
48+
49+
@Composable
50+
fun WidgetContent() {
51+
ToolBarLayout(
52+
appName = "App name",
53+
appIconRes = R.drawable.sample_app_logo,
54+
headerButton = ToolBarButton(
55+
iconRes = R.drawable.sample_add_icon,
56+
contentDescription = "add",
57+
onClick = actionStartDemoActivity("add button")
58+
),
59+
buttons = listOf(
60+
ToolBarButton(
61+
iconRes = R.drawable.sample_mic_icon,
62+
contentDescription = "mic",
63+
onClick = actionStartDemoActivity("mic button")
64+
),
65+
ToolBarButton(
66+
iconRes = R.drawable.sample_share_icon,
67+
contentDescription = "share",
68+
onClick = actionStartDemoActivity("share button")
69+
),
70+
ToolBarButton(
71+
iconRes = R.drawable.sample_videocam_icon,
72+
contentDescription = "video",
73+
onClick = actionStartDemoActivity("video button")
74+
),
75+
ToolBarButton(
76+
iconRes = R.drawable.sample_camera_icon,
77+
contentDescription = "camera",
78+
onClick = actionStartDemoActivity("camera button")
79+
)
80+
)
81+
)
82+
}
83+
}
84+
85+
/**
86+
* Receiver registered in the manifest for the [ToolBarAppWidget].
87+
*/
88+
class ToolBarAppWidgetReceiver : GlanceAppWidgetReceiver() {
89+
override val glanceAppWidget: GlanceAppWidget = ToolBarAppWidget()
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2024 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.toolbars.layout
17+
18+
import androidx.compose.runtime.Composable
19+
import androidx.compose.ui.unit.Dp
20+
import androidx.compose.ui.unit.dp
21+
import androidx.glance.ColorFilter
22+
import androidx.glance.GlanceModifier
23+
import androidx.glance.GlanceTheme
24+
import androidx.glance.Image
25+
import androidx.glance.ImageProvider
26+
import androidx.glance.action.Action
27+
import androidx.glance.action.clickable
28+
import androidx.glance.appwidget.cornerRadius
29+
import androidx.glance.background
30+
import androidx.glance.layout.Alignment
31+
import androidx.glance.layout.Box
32+
import androidx.glance.layout.height
33+
import androidx.glance.layout.size
34+
import androidx.glance.layout.width
35+
import androidx.glance.semantics.contentDescription
36+
import androidx.glance.semantics.semantics
37+
import androidx.glance.unit.ColorProvider
38+
39+
/**
40+
* A rectangular button displaying the provided icon on a background of
41+
* provided corner radius and colors.
42+
*
43+
* @param imageProvider icon to be displayed at center of the button
44+
* @param onClick [Action] to be performed on click of button
45+
* @param roundedCornerShape type of rounding to be applied to the button
46+
* @param contentDescription description about the button that can be used by the accessibility
47+
* services
48+
* @param iconSize size of the icon displayed at center of the button
49+
* @param modifier the modifier to be applied to this button.
50+
* @param backgroundColor background color for the button
51+
* @param contentColor color of the icon displayed at center of the button
52+
*/
53+
@Composable
54+
fun RectangularIconButton(
55+
imageProvider: ImageProvider,
56+
onClick: Action,
57+
roundedCornerShape: RoundedCornerShape,
58+
contentDescription: String,
59+
iconSize: Dp,
60+
modifier: GlanceModifier,
61+
backgroundColor: ColorProvider = GlanceTheme.colors.primary,
62+
contentColor: ColorProvider = GlanceTheme.colors.onPrimary,
63+
) {
64+
Box(
65+
contentAlignment = Alignment.Center,
66+
modifier = modifier
67+
.background(backgroundColor)
68+
.cornerRadius(roundedCornerShape.cornerRadius)
69+
.semantics { this.contentDescription = contentDescription }
70+
.clickable(onClick)
71+
) {
72+
Image(
73+
provider = imageProvider,
74+
contentDescription = null,
75+
colorFilter = ColorFilter.tint(contentColor),
76+
modifier = GlanceModifier.size(iconSize)
77+
)
78+
}
79+
}
80+
81+
/**
82+
* A fixed height pill-shaped button meant to be displayed in a title bar.
83+
*
84+
* @param iconImageProvider icon to be displayed in the button
85+
* @param iconSize size of the icon displayed at center of the button
86+
* @param backgroundColor background color for the button
87+
* @param contentColor color of the icon displayed in the button
88+
* @param contentDescription description about the button that can be used by the accessibility
89+
* services
90+
* @param onClick [Action] to be performed on click of button
91+
* @param modifier the modifier to be applied to this button.
92+
*/
93+
@Composable
94+
fun PillShapedButton(
95+
iconImageProvider: ImageProvider,
96+
iconSize: Dp,
97+
backgroundColor: ColorProvider,
98+
contentColor: ColorProvider,
99+
contentDescription: String,
100+
onClick: Action,
101+
modifier: GlanceModifier,
102+
) {
103+
Box( // A clickable transparent outer container
104+
contentAlignment = Alignment.Center,
105+
modifier = modifier
106+
.semantics { this.contentDescription = contentDescription }
107+
.height(48.dp)
108+
.clickable(onClick),
109+
) {
110+
Box( // A filled background with smaller height
111+
contentAlignment = Alignment.Center,
112+
modifier = GlanceModifier
113+
.width(52.dp)
114+
.height(32.dp)
115+
.background(backgroundColor)
116+
.cornerRadius(RoundedCornerShape.FULL.cornerRadius)
117+
) { // The icon.
118+
Image(
119+
provider = iconImageProvider,
120+
contentDescription = null,
121+
colorFilter = ColorFilter.tint(contentColor),
122+
modifier = GlanceModifier.size(iconSize)
123+
)
124+
}
125+
}
126+
}
127+
128+
/**
129+
* Defines the roundness of a shape inline with the tokens used in M3
130+
* https://m3.material.io/styles/shape/shape-scale-tokens
131+
*/
132+
enum class RoundedCornerShape(val cornerRadius: Dp) {
133+
FULL(100.dp),
134+
MEDIUM(16.dp),
135+
}

0 commit comments

Comments
 (0)