Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Jetcaster/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ limitations under the License.
[epstore]: mobile/src/main/java/com/example/jetcaster/data/EpisodeStore.kt
[catstore]: mobile/src/main/java/com/example/jetcaster/data/CategoryStore.kt
[db]: mobile/src/main/java/com/example/jetcaster/data/room/JetcasterDatabase.kt
[glance]: https://developer.android.com/develop/ui/compose/glance
[homevm]: mobile/src/main/java/com/example/jetcaster/ui/home/HomeViewModel.kt
[homeui]: mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt
[compose]: https://developer.android.com/jetpack/compose
Expand Down
1 change: 1 addition & 0 deletions Jetcaster/glancewidget/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
implementation(libs.coil.kt.compose)

implementation(libs.androidx.core.ktx)
implementation(libs.android.material3)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.util.Log
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
Expand All @@ -32,6 +30,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Expand Down Expand Up @@ -59,7 +58,6 @@ import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.layout.size
import androidx.glance.material3.ColorProviders
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
Expand All @@ -70,7 +68,7 @@ import coil.request.ImageRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

internal val TAG = "JetcasterAppWidegt"
internal val TAG = "JetcasterAppWidget"

/**
* Implementation of App Widget functionality.
Expand All @@ -85,28 +83,32 @@ data class JetcasterAppWidgetViewState(
val podcastTitle: String,
val isPlaying: Boolean,
val albumArtUri: String,
val useDynamicColor: Boolean
)

private object Sizes {
val short = 72.dp
val minWidth = 140.dp
val smallBucketCutoffWidth = 250.dp // anything from minWidth to this will have no title

val imageNormal = 80.dp
val imageCondensed = 60.dp
val normal = 80.dp
val medium = 56.dp
val condensed = 48.dp
}

private enum class SizeBucket { Invalid, Narrow, Normal }
private enum class SizeBucket { Invalid, Narrow, Normal, NarrowShort, NormalShort }

@Composable
private fun calculateSizeBucket(): SizeBucket {
val size: DpSize = LocalSize.current
val width = size.width
val height = size.height

return when {
width < Sizes.minWidth -> SizeBucket.Invalid
width <= Sizes.smallBucketCutoffWidth -> SizeBucket.Narrow
else -> SizeBucket.Normal
width <= Sizes.smallBucketCutoffWidth ->
if (height >= Sizes.short) SizeBucket.Narrow else SizeBucket.NarrowShort
else ->
if (height >= Sizes.short) SizeBucket.Normal else SizeBucket.NormalShort
}
}

Expand All @@ -122,29 +124,39 @@ class JetcasterAppWidget : GlanceAppWidget() {
podcastTitle = "Now in Android",
isPlaying = false,
albumArtUri = "https://static.libsyn.com/p/assets/9/f/f/3/" +
"9ff3cb5dc6cfb3e2e5bbc093207a2619/NIA000_PodcastThumbnail.png",
useDynamicColor = false
"9ff3cb5dc6cfb3e2e5bbc093207a2619/NIA000_PodcastThumbnail.png"
)

provideContent {
val sizeBucket = calculateSizeBucket()
val playPauseIcon = if (testState.isPlaying) PlayPauseIcon.Pause else PlayPauseIcon.Play
val artUri = Uri.parse(testState.albumArtUri)

GlanceTheme(
colors = ColorProviders(
light = lightColorScheme(),
dark = darkColorScheme()
)
) {
GlanceTheme {
when (sizeBucket) {
SizeBucket.Invalid -> WidgetUiInvalidSize()
SizeBucket.Narrow -> WidgetUiNarrow(
SizeBucket.Narrow -> Widget(
iconSize = Sizes.medium,
imageUri = artUri,
playPauseIcon = playPauseIcon
)

SizeBucket.Normal -> WidgetUiNormal(
iconSize = Sizes.normal,
title = testState.episodeTitle,
subtitle = testState.podcastTitle,
imageUri = artUri,
playPauseIcon = playPauseIcon
)

SizeBucket.NarrowShort -> Widget(
iconSize = Sizes.condensed,
imageUri = artUri,
playPauseIcon = playPauseIcon
)

SizeBucket.NormalShort -> WidgetUiNormal(
iconSize = Sizes.condensed,
title = testState.episodeTitle,
subtitle = testState.podcastTitle,
imageUri = artUri,
Expand All @@ -162,20 +174,23 @@ private fun WidgetUiNormal(
subtitle: String,
imageUri: Uri,
playPauseIcon: PlayPauseIcon,
iconSize: Dp,
) {
Scaffold(titleBar = {} /* title bar will be optional starting in glance 1.1.0-beta3*/) {

Scaffold {
Row(
GlanceModifier.fillMaxSize(), verticalAlignment = Alignment.Vertical.CenterVertically
) {
AlbumArt(imageUri, GlanceModifier.size(Sizes.imageNormal))
AlbumArt(imageUri, GlanceModifier.size(iconSize))
PodcastText(title, subtitle, modifier = GlanceModifier.padding(16.dp).defaultWeight())
PlayPauseButton(playPauseIcon, {})
PlayPauseButton(GlanceModifier.size(iconSize), playPauseIcon, {})
}
}
}

@Composable
private fun WidgetUiNarrow(
private fun Widget(
iconSize: Dp,
imageUri: Uri,
playPauseIcon: PlayPauseIcon,
) {
Expand All @@ -184,9 +199,9 @@ private fun WidgetUiNarrow(
modifier = GlanceModifier.fillMaxSize(),
verticalAlignment = Alignment.Vertical.CenterVertically
) {
AlbumArt(imageUri, GlanceModifier.size(Sizes.imageCondensed))
AlbumArt(imageUri, GlanceModifier.size(iconSize))
Spacer(GlanceModifier.defaultWeight())
PlayPauseButton(playPauseIcon, {})
PlayPauseButton(GlanceModifier.size(iconSize), playPauseIcon, {})
}
}
}
Expand All @@ -209,22 +224,44 @@ private fun AlbumArt(
@Composable
fun PodcastText(title: String, subtitle: String, modifier: GlanceModifier = GlanceModifier) {
val fgColor = GlanceTheme.colors.onPrimaryContainer
Column(modifier) {
Text(
text = title,
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Medium, color = fgColor),
maxLines = 2,
)
Text(
text = subtitle,
style = TextStyle(fontSize = 14.sp, color = fgColor),
maxLines = 2,
)
val size = LocalSize.current
when {
size.height >= Sizes.short -> Column(modifier) {
Text(
text = title,
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = fgColor
),
maxLines = 2,
)
Text(
text = subtitle,
style = TextStyle(fontSize = 14.sp, color = fgColor),
maxLines = 2,
)
}
else -> Column(modifier) {
Text(
text = title,
style = TextStyle(
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
color = fgColor
),
Comment on lines +248 to +252
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be getting the style from the theme itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, fgColor is set to GlanceTheme.colors.onPrimaryContainer

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole TextStyle though I mean, fontSize, weight etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, but I feel like that refactor is outside of the scope of this PR. The goal was to add widget previews.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking more, yes it could, no I don't think we should.

We'd need to build a bunch of XML resources, and that's not the pattern we're tyring to demonstrate with Glance widgets. See : https://github.com/android/platform-samples/blob/main/samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/collections/layout/ImageTextListLayout.kt#L383 for an example

maxLines = 1,
)
}
}
}

@Composable
private fun PlayPauseButton(state: PlayPauseIcon, onClick: () -> Unit) {
private fun PlayPauseButton(
modifier: GlanceModifier = GlanceModifier.size(Sizes.normal),
state: PlayPauseIcon,
onClick: () -> Unit
) {
val (iconRes: Int, description: Int) = when (state) {
PlayPauseIcon.Play -> R.drawable.outline_play_arrow_24 to R.string.content_description_play
PlayPauseIcon.Pause -> R.drawable.outline_pause_24 to R.string.content_description_pause
Expand All @@ -234,7 +271,8 @@ private fun PlayPauseButton(state: PlayPauseIcon, onClick: () -> Unit) {
val contentDescription = LocalContext.current.getString(description)

SquareIconButton(
provider,
modifier = modifier,
imageProvider = provider,
contentDescription = contentDescription,
onClick = onClick
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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
*
* 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.jetcaster.glancewidget

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.components.Scaffold
import androidx.glance.appwidget.components.SquareIconButton
import androidx.glance.appwidget.compose
import androidx.glance.appwidget.provideContent
import androidx.glance.layout.Alignment
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.size
import androidx.glance.layout.wrapContentSize

private object SizesPreview {
val medium = 56.dp
}

suspend fun updateWidgetPreview(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
try {
val appwidgetManager = AppWidgetManager.getInstance(context)

appwidgetManager.setWidgetPreview(
ComponentName(context, JetcasterAppWidgetReceiver::class.java),
AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN,
JetcasterAppWidgetPreview().compose(
context,
size = DpSize(160.dp, 64.dp)
),
)
} catch (e: Exception) {
Log.e(TAG, e.message, e)
}
}
}

class JetcasterAppWidgetPreview : GlanceAppWidget() {
override val sizeMode: SizeMode
get() = SizeMode.Exact

override suspend fun provideGlance(context: Context, id: GlanceId) {

provideContent {
GlanceTheme {
Widget()
}
}
}
}

@Composable
private fun Widget() {

Scaffold {
Row(
modifier = GlanceModifier.fillMaxSize(),
verticalAlignment = Alignment.Vertical.CenterVertically
) {
Image(
modifier = GlanceModifier.wrapContentSize().size(SizesPreview.medium),
provider = ImageProvider(R.drawable.widget_preview_thumbnail),
contentDescription = ""
)
Spacer(GlanceModifier.defaultWeight())
SquareIconButton(
modifier = GlanceModifier.size(SizesPreview.medium),
imageProvider = ImageProvider(R.drawable.outline_play_arrow_24),
contentDescription = "",
onClick = { }
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M320,760L320,200L760,480L320,760ZM400,480L400,480L400,480L400,480ZM400,614L610,480L400,346L400,614Z"/>

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this in xml?

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 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.
-->

<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="16dp"/>
</shape>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading