Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f2879a9

Browse files
author
Chris Banes
committedAug 14, 2020
[Jetcaster] Rework dominant color palette to use MaterialTheme
Currently the generated color is only used within a vertical gradient composable. This CL expands the result and pushes it into the MaterialTheme. Change-Id: I672f5cd9b51ce5ece677da4543655bd363688c01
1 parent eff57d0 commit f2879a9

File tree

10 files changed

+345
-200
lines changed

10 files changed

+345
-200
lines changed
 

‎Jetcaster/app/src/main/java/com/example/jetcaster/data/Feeds.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ package com.example.jetcaster.data
2121
* in this sample app.
2222
*/
2323
val SampleFeeds = listOf(
24+
"https://www.omnycontent.com/d/playlist/aaea4e69-af51-495e-afc9-a9760146922b/dc5b55ca-5f00-4063-b47f-ab870163d2b7/ca63aa52-ef7b-43ee-8ba5-ab8701645231/podcast.rss",
25+
"https://audioboom.com/channels/2399216.rss",
26+
"http://nowinandroid.googledevelopers.libsynpro.com/rss",
27+
"https://fragmentedpodcast.com/feed/",
28+
"https://feeds.megaphone.fm/replyall",
2429
"http://feeds.feedburner.com/blogspot/AndroidDevelopersBackstage",
2530
"https://feeds.thisamericanlife.org/talpodcast",
2631
"https://feeds.npr.org/510289/podcast.xml",

‎Jetcaster/app/src/main/java/com/example/jetcaster/ui/home/Home.kt

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.example.jetcaster.ui.home
1818

19+
import androidx.compose.foundation.Box
1920
import androidx.compose.foundation.Icon
2021
import androidx.compose.foundation.Text
2122
import androidx.compose.foundation.background
@@ -47,6 +48,7 @@ import androidx.compose.material.icons.filled.Search
4748
import androidx.compose.runtime.Composable
4849
import androidx.compose.runtime.collectAsState
4950
import androidx.compose.runtime.getValue
51+
import androidx.compose.runtime.launchInComposition
5052
import androidx.compose.runtime.remember
5153
import androidx.compose.ui.Alignment
5254
import androidx.compose.ui.Modifier
@@ -65,12 +67,15 @@ import com.example.jetcaster.data.PodcastWithExtraInfo
6567
import com.example.jetcaster.ui.home.discover.Discover
6668
import com.example.jetcaster.ui.theme.JetcasterTheme
6769
import com.example.jetcaster.ui.theme.Keyline1
68-
import com.example.jetcaster.util.DominantColorVerticalGradient
70+
import com.example.jetcaster.util.DynamicThemePrimaryColorsFromImage
6971
import com.example.jetcaster.util.Pager
7072
import com.example.jetcaster.util.PagerState
7173
import com.example.jetcaster.util.ToggleFollowPodcastIconButton
74+
import com.example.jetcaster.util.constrastAgainst
7275
import com.example.jetcaster.util.quantityStringResource
76+
import com.example.jetcaster.util.rememberDominantColorState
7377
import com.example.jetcaster.util.statusBarPadding
78+
import com.example.jetcaster.util.verticalGradientScrim
7479
import dev.chrisbanes.accompanist.coil.CoilImage
7580
import java.time.Duration
7681
import java.time.LocalDateTime
@@ -129,6 +134,13 @@ fun HomeAppBar(
129134
}
130135
}
131136

137+
/**
138+
* This is the minimum amount of calculated constrast for a color to be used on top of the
139+
* surface color. These values are defined within the WCAG AA guidelines, and we use a value of
140+
* 3:1 which is the minimum for user-interface components.
141+
*/
142+
private const val MinConstastOfPrimaryVsSurface = 3f
143+
132144
@Composable
133145
fun HomeContent(
134146
featuredPodcasts: List<PodcastWithExtraInfo>,
@@ -140,37 +152,63 @@ fun HomeContent(
140152
onCategorySelected: (HomeCategory) -> Unit
141153
) {
142154
Column(modifier = modifier) {
143-
Stack(Modifier.fillMaxWidth()) {
155+
// We dynamically theme this sub-section of the layout to match the selected
156+
// 'top podcast'
157+
158+
val surfaceColor = MaterialTheme.colors.surface
159+
val dominantColorState = rememberDominantColorState { color ->
160+
// We want a color which has sufficient contrast against the surface color
161+
color.constrastAgainst(surfaceColor) >= MinConstastOfPrimaryVsSurface
162+
}
163+
164+
DynamicThemePrimaryColorsFromImage(dominantColorState) {
144165
val clock = AnimationClockAmbient.current
145166
val pagerState = remember(clock) { PagerState(clock) }
146167

147-
DominantColorVerticalGradient(
148-
imageSourceUrl = featuredPodcasts.getOrNull(pagerState.currentPage)
149-
?.podcast?.imageUrl,
150-
modifier = Modifier.matchParentSize()
151-
)
168+
val selectedImageUrl = featuredPodcasts.getOrNull(pagerState.currentPage)
169+
?.podcast?.imageUrl
152170

153-
Column(Modifier.fillMaxWidth()) {
154-
HomeAppBar(
155-
modifier = Modifier.fillMaxWidth()
156-
.statusBarPadding()
157-
.preferredHeight(56.dp) /* TODO: change height to 48.dp in landscape */
171+
// When the selected image url changes, call updateColorsFromImageUrl() or reset()
172+
if (selectedImageUrl != null) {
173+
launchInComposition(selectedImageUrl) {
174+
dominantColorState.updateColorsFromImageUrl(selectedImageUrl)
175+
}
176+
} else {
177+
dominantColorState.reset()
178+
}
179+
180+
Stack(Modifier.fillMaxWidth()) {
181+
Box(
182+
Modifier.matchParentSize()
183+
.verticalGradientScrim(
184+
color = MaterialTheme.colors.primary.copy(alpha = 0.38f),
185+
startYPercentage = 1f,
186+
endYPercentage = 0f
187+
)
158188
)
159189

160-
if (featuredPodcasts.isNotEmpty()) {
161-
Spacer(Modifier.height(16.dp))
162-
163-
FollowedPodcasts(
164-
items = featuredPodcasts,
165-
pagerState = pagerState,
166-
onPodcastUnfollowed = onPodcastUnfollowed,
167-
modifier = Modifier
168-
.padding(start = Keyline1, top = 16.dp, end = Keyline1)
169-
.fillMaxWidth()
170-
.preferredHeight(200.dp)
190+
Column(Modifier.fillMaxWidth()) {
191+
HomeAppBar(
192+
Modifier.fillMaxWidth()
193+
.statusBarPadding()
194+
.preferredHeight(56.dp) /* TODO: change height to 48.dp in landscape */
171195
)
172196

173-
Spacer(Modifier.height(16.dp))
197+
if (featuredPodcasts.isNotEmpty()) {
198+
Spacer(Modifier.height(16.dp))
199+
200+
FollowedPodcasts(
201+
items = featuredPodcasts,
202+
pagerState = pagerState,
203+
onPodcastUnfollowed = onPodcastUnfollowed,
204+
modifier = Modifier
205+
.padding(start = Keyline1, top = 16.dp, end = Keyline1)
206+
.fillMaxWidth()
207+
.preferredHeight(200.dp)
208+
)
209+
210+
Spacer(Modifier.height(16.dp))
211+
}
174212
}
175213
}
176214
}

‎Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Color.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fun Colors.compositedOnSurface(alpha: Float): Color {
3434
val Yellow800 = Color(0xFFF29F05)
3535
val Red300 = Color(0xFFEA6D7E)
3636

37-
val Colors = darkColors(
37+
val JetcasterColors = darkColors(
3838
primary = Yellow800,
3939
onPrimary = Color.Black,
4040
primaryVariant = Yellow800,

‎Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Shape.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
2020
import androidx.compose.material.Shapes
2121
import androidx.compose.ui.unit.dp
2222

23-
val Shapes = Shapes(
23+
val JetcasterShapes = Shapes(
2424
small = RoundedCornerShape(percent = 50),
2525
medium = RoundedCornerShape(size = 8.dp),
2626
large = RoundedCornerShape(size = 0.dp)

‎Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Theme.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ fun JetcasterTheme(
2424
content: @Composable () -> Unit
2525
) {
2626
MaterialTheme(
27-
colors = Colors,
28-
typography = Typography,
29-
shapes = Shapes,
27+
colors = JetcasterColors,
28+
typography = JetcasterTypography,
29+
shapes = JetcasterShapes,
3030
content = content
3131
)
3232
}

‎Jetcaster/app/src/main/java/com/example/jetcaster/ui/theme/Type.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ private val Montserrat = fontFamily(
3131
font(R.font.montserrat_semibold, FontWeight.SemiBold)
3232
)
3333

34-
val Typography = Typography(
34+
val JetcasterTypography = Typography(
3535
h1 = TextStyle(
3636
fontFamily = Montserrat,
3737
fontSize = 96.sp,
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2020 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.jetcaster.util
18+
19+
import androidx.compose.foundation.isSystemInDarkTheme
20+
import androidx.compose.material.Colors
21+
import androidx.compose.material.darkColors
22+
import androidx.compose.material.lightColors
23+
import androidx.compose.runtime.Composable
24+
import androidx.compose.ui.graphics.Color
25+
import androidx.compose.ui.graphics.compositeOver
26+
import androidx.compose.ui.graphics.luminance
27+
import kotlin.math.max
28+
import kotlin.math.min
29+
30+
@Composable
31+
fun Colors.copy(
32+
primary: Color = this.primary,
33+
primaryVariant: Color = this.primaryVariant,
34+
secondary: Color = this.secondary,
35+
secondaryVariant: Color = this.secondaryVariant,
36+
background: Color = this.background,
37+
surface: Color = this.surface,
38+
error: Color = this.error,
39+
onPrimary: Color = this.onPrimary,
40+
onSecondary: Color = this.onSecondary,
41+
onBackground: Color = this.onBackground,
42+
onSurface: Color = this.onSurface,
43+
onError: Color = this.onError,
44+
darkColors: Boolean = isSystemInDarkTheme()
45+
): Colors = if (darkColors) {
46+
darkColors(
47+
primary = primary,
48+
primaryVariant = primaryVariant,
49+
secondary = secondary,
50+
background = background,
51+
surface = surface,
52+
error = error,
53+
onPrimary = onPrimary,
54+
onSecondary = onSecondary,
55+
onBackground = onBackground,
56+
onSurface = onSurface,
57+
onError = onError
58+
)
59+
} else {
60+
lightColors(
61+
primary = primary,
62+
primaryVariant = primaryVariant,
63+
secondary = secondary,
64+
secondaryVariant = secondaryVariant,
65+
background = background,
66+
surface = surface,
67+
error = error,
68+
onPrimary = onPrimary,
69+
onSecondary = onSecondary,
70+
onBackground = onBackground,
71+
onSurface = onSurface,
72+
onError = onError
73+
)
74+
}
75+
76+
fun Color.constrastAgainst(background: Color): Float {
77+
val fg = if (alpha < 1f) compositeOver(background) else this
78+
79+
val fgLuminance = fg.luminance() + 0.05f
80+
val bgLuminance = background.luminance() + 0.05f
81+
82+
return max(fgLuminance, bgLuminance) / min(fgLuminance, bgLuminance)
83+
}

0 commit comments

Comments
 (0)
Please sign in to comment.