Skip to content

Commit

Permalink
Add sample layouts for toolbar style widgets (#216)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
shamalip authored Dec 5, 2024
1 parent 20c7a4e commit 7efe17b
Show file tree
Hide file tree
Showing 28 changed files with 1,696 additions and 0 deletions.
32 changes: 32 additions & 0 deletions samples/user-interface/appwidgets/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,38 @@
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>

<!-- Toolbar variants -->

<!-- Toolbar with header -->
<receiver
android:name=".glance.layout.toolbars.ToolBarAppWidgetReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/sample_toolbar_app_widget_name">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/sample_toolbar_widget_info" />
</receiver>
<!-- A search toolbar -->
<receiver
android:name=".glance.layout.toolbars.SearchToolBarAppWidgetReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/sample_search_toolbar_app_widget_name">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/sample_search_toolbar_widget_info" />
</receiver>

<!-- Long text variants -->
<receiver
android:name=".glance.layout.text.LongTextAppWidgetReceiver"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import com.example.platform.ui.appwidgets.glance.layout.collections.ImageGridApp
import com.example.platform.ui.appwidgets.glance.layout.collections.ImageTextListAppWidgetReceiver
import com.example.platform.ui.appwidgets.glance.layout.text.LongTextAppWidgetReceiver
import com.example.platform.ui.appwidgets.glance.layout.text.TextWithImageAppWidgetReceiver
import com.example.platform.ui.appwidgets.glance.layout.toolbars.SearchToolBarAppWidgetReceiver
import com.example.platform.ui.appwidgets.glance.layout.toolbars.ToolBarAppWidgetReceiver
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

Expand Down Expand Up @@ -290,4 +292,16 @@ private val canonicalLayoutWidgets = listOf(
imageRes = R.drawable.cl_activity_row_image_grid,
receiver = ImageGridAppWidgetReceiver::class.java,
),
CanonicalLayoutRowData(
rowTitle = R.string.cl_title_toolbar,
rowDescription = R.string.cl_description_toolbar,
imageRes = R.drawable.cl_activity_row_toolbar,
receiver = ToolBarAppWidgetReceiver::class.java,
),
CanonicalLayoutRowData(
rowTitle = R.string.cl_title_search_toolbar,
rowDescription = R.string.cl_description_search_toolbar,
imageRes = R.drawable.cl_activity_row_search_toolbar,
receiver = SearchToolBarAppWidgetReceiver::class.java,
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.platform.ui.appwidgets.glance.layout.toolbars

import android.content.Context
import androidx.compose.runtime.Composable
import androidx.glance.GlanceId
import androidx.glance.GlanceTheme
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.provideContent
import com.example.platform.ui.appwidgets.R
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarButton
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayout
import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity

/**
* A widget to demonstrate the [SearchToolBarLayout].
*/
class SearchToolBarAppWidget : GlanceAppWidget() {
// Unlike the "Single" size mode, using "Exact" allows us to have better control over rendering in
// different sizes. And, unlike the "Responsive" mode, it doesn't cause several views for each
// supported size to be held in the widget host's memory.
override val sizeMode: SizeMode = SizeMode.Exact

override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
WidgetContent()
}
}
}

@Composable
fun WidgetContent() {
SearchToolBarLayout(
searchButton = SearchToolBarButton(
iconRes = R.drawable.sample_search_icon,
contentDescription = "Search notes",
text = "Search",
onClick = actionStartDemoActivity("search notes button")
),
trailingButtons = listOf(
SearchToolBarButton(
iconRes = R.drawable.sample_mic_icon,
contentDescription = "audio",
onClick = actionStartDemoActivity("audio button")
),
SearchToolBarButton(
iconRes = R.drawable.sample_videocam_icon,
contentDescription = "video note",
onClick = actionStartDemoActivity("video note button")
),
SearchToolBarButton(
iconRes = R.drawable.sample_camera_icon,
contentDescription = "camera",
onClick = actionStartDemoActivity("camera button")
),
SearchToolBarButton(
iconRes = R.drawable.sample_share_icon,
contentDescription = "share",
onClick = actionStartDemoActivity("share button")
),
)
)
}
}

/**
* Receiver registered in the manifest for the [SearchToolBarAppWidget].
*/
class SearchToolBarAppWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = SearchToolBarAppWidget()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.platform.ui.appwidgets.glance.layout.toolbars

import android.content.Context
import androidx.compose.runtime.Composable
import androidx.glance.GlanceId
import androidx.glance.GlanceTheme
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.provideContent
import com.example.platform.ui.appwidgets.R
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarButton
import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayout
import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity

/**
* A widget to demonstrate the [ToolBarLayout].
*/
class ToolBarAppWidget : GlanceAppWidget() {
// Unlike the "Single" size mode, using "Exact" allows us to have better control over rendering in
// different sizes. And, unlike the "Responsive" mode, it doesn't cause several views for each
// supported size to be held in the widget host's memory.
override val sizeMode: SizeMode = SizeMode.Exact

override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
WidgetContent()
}
}
}

@Composable
fun WidgetContent() {
ToolBarLayout(
appName = "App name",
appIconRes = R.drawable.sample_app_logo,
headerButton = ToolBarButton(
iconRes = R.drawable.sample_add_icon,
contentDescription = "add",
onClick = actionStartDemoActivity("add button")
),
buttons = listOf(
ToolBarButton(
iconRes = R.drawable.sample_mic_icon,
contentDescription = "mic",
onClick = actionStartDemoActivity("mic button")
),
ToolBarButton(
iconRes = R.drawable.sample_share_icon,
contentDescription = "share",
onClick = actionStartDemoActivity("share button")
),
ToolBarButton(
iconRes = R.drawable.sample_videocam_icon,
contentDescription = "video",
onClick = actionStartDemoActivity("video button")
),
ToolBarButton(
iconRes = R.drawable.sample_camera_icon,
contentDescription = "camera",
onClick = actionStartDemoActivity("camera button")
)
)
)
}
}

/**
* Receiver registered in the manifest for the [ToolBarAppWidget].
*/
class ToolBarAppWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = ToolBarAppWidget()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.platform.ui.appwidgets.glance.layout.toolbars.layout

import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.glance.ColorFilter
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.action.Action
import androidx.glance.action.clickable
import androidx.glance.appwidget.cornerRadius
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.height
import androidx.glance.layout.size
import androidx.glance.layout.width
import androidx.glance.semantics.contentDescription
import androidx.glance.semantics.semantics
import androidx.glance.unit.ColorProvider

/**
* A rectangular button displaying the provided icon on a background of
* provided corner radius and colors.
*
* @param imageProvider icon to be displayed at center of the button
* @param onClick [Action] to be performed on click of button
* @param roundedCornerShape type of rounding to be applied to the button
* @param contentDescription description about the button that can be used by the accessibility
* services
* @param iconSize size of the icon displayed at center of the button
* @param modifier the modifier to be applied to this button.
* @param backgroundColor background color for the button
* @param contentColor color of the icon displayed at center of the button
*/
@Composable
fun RectangularIconButton(
imageProvider: ImageProvider,
onClick: Action,
roundedCornerShape: RoundedCornerShape,
contentDescription: String,
iconSize: Dp,
modifier: GlanceModifier,
backgroundColor: ColorProvider = GlanceTheme.colors.primary,
contentColor: ColorProvider = GlanceTheme.colors.onPrimary,
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.background(backgroundColor)
.cornerRadius(roundedCornerShape.cornerRadius)
.semantics { this.contentDescription = contentDescription }
.clickable(onClick)
) {
Image(
provider = imageProvider,
contentDescription = null,
colorFilter = ColorFilter.tint(contentColor),
modifier = GlanceModifier.size(iconSize)
)
}
}

/**
* A fixed height pill-shaped button meant to be displayed in a title bar.
*
* @param iconImageProvider icon to be displayed in the button
* @param iconSize size of the icon displayed at center of the button
* @param backgroundColor background color for the button
* @param contentColor color of the icon displayed in the button
* @param contentDescription description about the button that can be used by the accessibility
* services
* @param onClick [Action] to be performed on click of button
* @param modifier the modifier to be applied to this button.
*/
@Composable
fun PillShapedButton(
iconImageProvider: ImageProvider,
iconSize: Dp,
backgroundColor: ColorProvider,
contentColor: ColorProvider,
contentDescription: String,
onClick: Action,
modifier: GlanceModifier,
) {
Box( // A clickable transparent outer container
contentAlignment = Alignment.Center,
modifier = modifier
.semantics { this.contentDescription = contentDescription }
.height(48.dp)
.clickable(onClick),
) {
Box( // A filled background with smaller height
contentAlignment = Alignment.Center,
modifier = GlanceModifier
.width(52.dp)
.height(32.dp)
.background(backgroundColor)
.cornerRadius(RoundedCornerShape.FULL.cornerRadius)
) { // The icon.
Image(
provider = iconImageProvider,
contentDescription = null,
colorFilter = ColorFilter.tint(contentColor),
modifier = GlanceModifier.size(iconSize)
)
}
}
}

/**
* Defines the roundness of a shape inline with the tokens used in M3
* https://m3.material.io/styles/shape/shape-scale-tokens
*/
enum class RoundedCornerShape(val cornerRadius: Dp) {
FULL(100.dp),
MEDIUM(16.dp),
}
Loading

0 comments on commit 7efe17b

Please sign in to comment.