From df5f7dd2c550cab8f732cd0a04aa570765e57fd9 Mon Sep 17 00:00:00 2001 From: shabinder Date: Fri, 1 Jan 2021 17:20:33 +0530 Subject: [PATCH] Dynamic Gradient. --- .../com/shabinder/spotiflyer/MainActivity.kt | 65 ++++++----- .../shabinder/spotiflyer/SharedViewModel.kt | 22 +++- .../com/shabinder/spotiflyer/ui/home/Home.kt | 79 ++++++++++--- .../spotiflyer/ui/home/HomeViewModel.kt | 24 +++- .../spotiflyer/ui/tracklist/TrackList.kt | 104 ++++++++++++++---- .../shabinder/spotiflyer/ui/utils/Colors.kt | 16 +++ .../spotiflyer/ui/utils/DynamicTheming.kt | 100 +++++++++++++++++ .../spotiflyer/ui/utils/GradientScrim.kt | 81 ++++++++++++++ .../main/res/drawable/ic_musicplaceholder.xml | 18 +-- app/src/main/res/drawable/ic_share_open.xml | 4 +- .../main/res/drawable/ic_song_placeholder.xml | 4 +- 11 files changed, 440 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/com/shabinder/spotiflyer/ui/utils/Colors.kt create mode 100644 app/src/main/java/com/shabinder/spotiflyer/ui/utils/DynamicTheming.kt create mode 100644 app/src/main/java/com/shabinder/spotiflyer/ui/utils/GradientScrim.kt diff --git a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 603925f0..f320c70f 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -15,19 +15,19 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Providers +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.setContent import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController +import com.example.jetcaster.util.verticalGradientScrim import com.shabinder.spotiflyer.navigation.ComposeNavigation import com.shabinder.spotiflyer.navigation.navigateToPlatform import com.shabinder.spotiflyer.networking.SpotifyService @@ -55,8 +55,7 @@ import com.shabinder.spotiflyer.utils.showDialog as showDialog1 @AndroidEntryPoint class MainActivity : AppCompatActivity() { - private var spotifyService : SpotifyService? = null - lateinit var navController: NavHostController + private lateinit var navController: NavHostController @Inject lateinit var moshi: Moshi @Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest @@ -70,20 +69,28 @@ class MainActivity : AppCompatActivity() { ComposeLearnTheme { Providers(AmbientContentColor provides colorOffWhite) { ProvideWindowInsets { - Column { - val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.87f) + val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.6f) + navController = rememberNavController() + val gradientColor by sharedViewModel.gradientColor.collectAsState() + + Column( + modifier = Modifier.fillMaxSize().verticalGradientScrim( + color = gradientColor.copy(alpha = 0.38f), + startYPercentage = 1f, + endYPercentage = 0f, + fixedHeight = 700f, + ) + ) { // Draw a scrim over the status bar which matches the app bar Spacer( - Modifier.background(appBarColor).fillMaxWidth().statusBarsHeight() + Modifier.background(appBarColor).fillMaxWidth() + .statusBarsHeight() ) - AppBar( backgroundColor = appBarColor, modifier = Modifier.fillMaxWidth() ) - navController = rememberNavController() - ComposeNavigation(navController) } } @@ -141,7 +148,7 @@ class MainActivity : AppCompatActivity() { } /** - * Adding my own new Spotify Web Api Requests! + * Adding my own Spotify Web Api Requests! * */ private fun implementSpotifyService(token: String) { val httpClient: OkHttpClient.Builder = OkHttpClient.Builder() @@ -154,25 +161,26 @@ class MainActivity : AppCompatActivity() { chain.proceed(request) }).addInterceptor(NetworkInterceptor()) - val retrofit = Retrofit.Builder() - .baseUrl("https://api.spotify.com/v1/") - .client(httpClient.build()) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() - - spotifyService = retrofit.create(SpotifyService::class.java) - sharedViewModel.spotifyService.value = spotifyService + val retrofit = Retrofit.Builder().run{ + baseUrl("https://api.spotify.com/v1/") + client(httpClient.build()) + addConverterFactory(MoshiConverterFactory.create(moshi)) + build() + } + sharedViewModel.spotifyService.value = retrofit.create(SpotifyService::class.java) } fun authenticateSpotify() { - sharedViewModel.viewModelScope.launch { - log("Spotify Authentication","Started") - val token = spotifyServiceTokenRequest.getToken() - token.value?.let { - showDialog1("Success: Spotify Token Acquired") - implementSpotifyService(it.access_token) + if(sharedViewModel.spotifyService.value == null){ + sharedViewModel.viewModelScope.launch { + log("Spotify Authentication","Started") + val token = spotifyServiceTokenRequest.getToken() + token.value?.let { + showDialog1("Success: Spotify Token Acquired") + implementSpotifyService(it.access_token) + } + log("Spotify Token", token.value.toString()) } - log("Spotify Token", token.value.toString()) } } @@ -227,7 +235,8 @@ fun AppBar( } } }, - modifier = modifier + modifier = modifier, + elevation = 0.dp ) } diff --git a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt index 0e8d440c..ca0b89b9 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt @@ -17,6 +17,8 @@ package com.shabinder.spotiflyer +import androidx.compose.material.MaterialTheme +import androidx.compose.ui.graphics.Color import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -25,8 +27,13 @@ import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.models.PlatformQueryResult import com.shabinder.spotiflyer.networking.GaanaInterface import com.shabinder.spotiflyer.networking.SpotifyService +import com.shabinder.spotiflyer.ui.colorPrimary +import com.shabinder.spotiflyer.ui.colorPrimaryDark +import com.shabinder.spotiflyer.ui.home.HomeCategory import dagger.hilt.android.scopes.ActivityRetainedScoped import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow @ActivityRetainedScoped class SharedViewModel @ViewModelInject constructor( @@ -34,6 +41,17 @@ class SharedViewModel @ViewModelInject constructor( val gaanaInterface : GaanaInterface, val ytDownloader: YoutubeDownloader ) : ViewModel() { - var intentString = MutableLiveData() - var spotifyService = MutableLiveData() + var spotifyService = MutableStateFlow(null) + + private val _gradientColor = MutableStateFlow(colorPrimaryDark) + val gradientColor : StateFlow + get() = _gradientColor + + fun updateGradientColor(color: Color) { + _gradientColor.value = color + } + + fun resetGradient() { + _gradientColor.value = colorPrimaryDark + } } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/home/Home.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/home/Home.kt index 3345b6c4..d1bc72c6 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/home/Home.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/home/Home.kt @@ -2,6 +2,7 @@ package com.shabinder.spotiflyer.ui.home import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* @@ -21,24 +22,26 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.net.toUri import androidx.navigation.NavController -import androidx.navigation.compose.navigate -import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.R +import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.navigation.navigateToPlatform import com.shabinder.spotiflyer.ui.SpotiFlyerTypography import com.shabinder.spotiflyer.ui.colorAccent import com.shabinder.spotiflyer.ui.colorPrimary -import com.shabinder.spotiflyer.utils.mainActivity import com.shabinder.spotiflyer.utils.openPlatform import com.shabinder.spotiflyer.utils.sharedViewModel -import com.shabinder.spotiflyer.utils.showDialog +import dev.chrisbanes.accompanist.glide.GlideImage +import kotlinx.coroutines.flow.StateFlow @Composable fun Home(navController: NavController, modifier: Modifier = Modifier) { @@ -47,7 +50,6 @@ fun Home(navController: NavController, modifier: Modifier = Modifier) { Column(modifier = modifier) { val link by viewModel.link.collectAsState() - val selectedCategory by viewModel.selectedCategory.collectAsState() AuthenticationBanner(viewModel,modifier) @@ -57,7 +59,7 @@ fun Home(navController: NavController, modifier: Modifier = Modifier) { navController, modifier ) - + val selectedCategory by viewModel.selectedCategory.collectAsState() HomeTabBar( selectedCategory, @@ -68,10 +70,13 @@ fun Home(navController: NavController, modifier: Modifier = Modifier) { when(selectedCategory){ HomeCategory.About -> AboutColumn() - HomeCategory.History -> HistoryColumn() + HomeCategory.History -> HistoryColumn(viewModel.downloadRecordList,navController) } - } + //Update Download List + viewModel.getDownloadRecordList() + //reset Gradient + sharedViewModel.resetGradient() } @@ -186,7 +191,7 @@ fun AboutColumn(modifier: Modifier = Modifier) { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, - modifier = modifier.fillMaxWidth().padding(8.dp) + modifier = modifier.fillMaxSize().padding(8.dp).weight(1f) ) { Text( text = stringResource(id = R.string.made_with_love), @@ -206,8 +211,52 @@ fun AboutColumn(modifier: Modifier = Modifier) { } @Composable -fun HistoryColumn() { - //TODO("Not yet implemented") +fun HistoryColumn( + downloadRecordList: StateFlow>, + navController: NavController +) { + val list by downloadRecordList.collectAsState() + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + content = { + items(list) { + DownloadRecordItem(item = it,navController = navController) + } + }, + modifier = Modifier.padding(top = 8.dp).fillMaxSize() + ) +} + +@Composable +fun DownloadRecordItem(item: DownloadRecord,navController: NavController) { + Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) { + val imgUri = item.coverUrl.toUri().buildUpon().scheme("https").build() + GlideImage( + data = imgUri, + //Loading Placeholder Makes Scrolling very stuttery +// loading = { Image(vectorResource(id = R.drawable.ic_song_placeholder)) }, + error = { Image(vectorResource(id = R.drawable.ic_musicplaceholder)) }, + contentScale = ContentScale.Inside, +// fadeIn = true, + modifier = Modifier.preferredHeight(75.dp).preferredWidth(90.dp) + ) + Column(modifier = Modifier.padding(horizontal = 8.dp).preferredHeight(60.dp).weight(1f),verticalArrangement = Arrangement.SpaceEvenly) { + Text(item.name,maxLines = 1,overflow = TextOverflow.Ellipsis,style = SpotiFlyerTypography.h6,color = colorAccent) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize() + ){ + Text(item.type,fontSize = 13.sp) + Text("Tracks: ${item.totalFiles}",fontSize = 13.sp) + } + } + Image( + imageVector = vectorResource(id = R.drawable.ic_share_open), + modifier = Modifier.clickable(onClick = { navController.navigateToPlatform(item.link) }) + ) + } } @Composable @@ -236,7 +285,7 @@ fun HomeTabBar( TabRow( selectedTabIndex = selectedIndex, indicator = indicator, - modifier = modifier + modifier = modifier, ) { categories.forEachIndexed { index, category -> Tab( @@ -289,14 +338,16 @@ fun SearchPanel( RoundedCornerShape(30.dp) ), backgroundColor = Color.Black, - textStyle = AmbientTextStyle.current.merge(TextStyle(fontSize = 20.sp,color = Color.White)), + textStyle = AmbientTextStyle.current.merge(TextStyle(fontSize = 18.sp,color = Color.White)), shape = RoundedCornerShape(size = 30.dp), activeColor = Color.Transparent, inactiveColor = Color.Transparent ) OutlinedButton( modifier = Modifier.padding(12.dp).wrapContentWidth(), - onClick = {navController.navigateToPlatform(link)}, + onClick = { + navController.navigateToPlatform(link) + }, border = BorderStroke(1.dp, Brush.horizontalGradient(listOf(colorPrimary, colorAccent))) ){ Text(text = "Search",style = SpotiFlyerTypography.h6,modifier = Modifier.padding(4.dp)) diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/home/HomeViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/home/HomeViewModel.kt index ea3a12da..f4ebec17 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/home/HomeViewModel.kt @@ -1,10 +1,17 @@ package com.shabinder.spotiflyer.ui.home import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.shabinder.spotiflyer.database.DatabaseDAO +import com.shabinder.spotiflyer.database.DownloadRecord +import com.shabinder.spotiflyer.utils.sharedViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -class HomeViewModel: ViewModel() { +class HomeViewModel : ViewModel() { private val _link = MutableStateFlow("") val link:StateFlow @@ -30,6 +37,21 @@ class HomeViewModel: ViewModel() { _selectedCategory.value = s } + private val _downloadRecordList = MutableStateFlow>(listOf()) + val downloadRecordList: StateFlow> + get() = _downloadRecordList + + fun getDownloadRecordList() { + viewModelScope.launch { + withContext(Dispatchers.IO){ + _downloadRecordList.value = sharedViewModel.databaseDAO.getRecord().toMutableList() + } + } + } + + init { + getDownloadRecordList() + } } enum class HomeCategory { diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/tracklist/TrackList.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/tracklist/TrackList.kt index 5312099d..c0dedc35 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/tracklist/TrackList.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/tracklist/TrackList.kt @@ -3,62 +3,123 @@ package com.shabinder.spotiflyer.ui.tracklist import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ExtendedFloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.viewModel -import androidx.navigation.NavController -import com.bumptech.glide.RequestManager +import androidx.core.net.toUri import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.models.PlatformQueryResult import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.ui.SpotiFlyerTypography import com.shabinder.spotiflyer.ui.colorAccent -import dev.chrisbanes.accompanist.glide.GlideImage +import com.shabinder.spotiflyer.ui.utils.calculateDominantColor +import com.shabinder.spotiflyer.utils.sharedViewModel +import dev.chrisbanes.accompanist.coil.CoilImage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /* * UI for List of Tracks to be universally used. -* */ +**/ @Composable fun TrackList( result: PlatformQueryResult, source: Source, modifier: Modifier = Modifier ){ - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp), - content = { - items(result.trackList) { - TrackCard(track = it) - } - }, - modifier = Modifier.fillMaxSize() - ) + val coroutineScope = rememberCoroutineScope() + Box(modifier = modifier.fillMaxSize()){ + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + content = { + item { + CoverImage(result.title,result.coverUrl,coroutineScope) + } + items(result.trackList) { + TrackCard(track = it) + } + }, + modifier = Modifier.fillMaxSize(), + ) + DownloadAllButton( + onClick = {}, + modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter) + ) + } } +@Composable +fun CoverImage( + title: String, + coverURL: String, + scope: CoroutineScope, + modifier: Modifier = Modifier, +) { + Column( + modifier.padding(vertical = 8.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CoilImage( + data = coverURL, + contentScale = ContentScale.Crop, + loading = { Image(vectorResource(id = R.drawable.ic_musicplaceholder)) }, + modifier = Modifier + .preferredWidth(210.dp) + .preferredHeight(230.dp) + .clip(MaterialTheme.shapes.medium) + ) + Text( + text = title, + style = SpotiFlyerTypography.h5, + //color = colorAccent, + ) + } + scope.launch { updateGradient(coverURL) } +} + +@Composable +fun DownloadAllButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + ExtendedFloatingActionButton( + text = { Text("Download All") }, + onClick = onClick, + icon = { Icon(imageVector = vectorResource(R.drawable.ic_download_arrow),tint = Color.Black) }, + backgroundColor = colorAccent, + modifier = modifier + ) +} @Composable fun TrackCard(track:TrackDetails) { Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) { - GlideImage( - data = track.albumArtURL, - loading = { Image(vectorResource(id = R.drawable.ic_song_placeholder)) }, + val imgUri = track.albumArtURL.toUri().buildUpon().scheme("https").build() + CoilImage( + data = imgUri, + //Loading Placeholder Makes Scrolling very stuttery +// loading = { Image(vectorResource(id = R.drawable.ic_song_placeholder)) }, error = { Image(vectorResource(id = R.drawable.ic_musicplaceholder)) }, contentScale = ContentScale.Inside, +// fadeIn = true, modifier = Modifier.preferredHeight(75.dp).preferredWidth(90.dp) ) - Column(modifier = Modifier.padding(horizontal = 8.dp).fillMaxHeight().weight(1f),verticalArrangement = Arrangement.SpaceBetween) { + Column(modifier = Modifier.padding(horizontal = 8.dp).preferredHeight(60.dp).weight(1f),verticalArrangement = Arrangement.SpaceEvenly) { Text(track.title,maxLines = 1,overflow = TextOverflow.Ellipsis,style = SpotiFlyerTypography.h6,color = colorAccent) Row( horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.padding(horizontal = 12.dp).fillMaxSize() + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize() ){ Text("${track.artists.firstOrNull()}...",fontSize = 13.sp) Text("${track.durationSec/60} minutes, ${track.durationSec%60} sec",fontSize = 13.sp) @@ -66,4 +127,9 @@ fun TrackCard(track:TrackDetails) { } Image(vectorResource(id = R.drawable.ic_arrow)) } +} + +suspend fun updateGradient(imageURL:String){ + calculateDominantColor(imageURL)?.color + ?.let { sharedViewModel.updateGradientColor(it) } } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/utils/Colors.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/utils/Colors.kt new file mode 100644 index 00000000..cb6f029a --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/utils/Colors.kt @@ -0,0 +1,16 @@ +package com.shabinder.spotiflyer.ui.utils + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.luminance +import kotlin.math.max +import kotlin.math.min + +fun Color.contrastAgainst(background: Color): Float { + val fg = if (alpha < 1f) compositeOver(background) else this + + val fgLuminance = fg.luminance() + 0.05f + val bgLuminance = background.luminance() + 0.05f + + return max(fgLuminance, bgLuminance) / min(fgLuminance, bgLuminance) +} diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/utils/DynamicTheming.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/utils/DynamicTheming.kt new file mode 100644 index 00000000..87a0236f --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/utils/DynamicTheming.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2020 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.shabinder.spotiflyer.ui.utils + +import android.content.Context +import androidx.collection.LruCache +import androidx.compose.animation.animate +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.AmbientContext +import androidx.core.graphics.drawable.toBitmap +import androidx.palette.graphics.Palette +import coil.Coil +import coil.request.ImageRequest +import coil.request.SuccessResult +import coil.size.Scale +import com.shabinder.spotiflyer.utils.mainActivity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Immutable +data class DominantColors(val color: Color, val onColor: Color) + + +suspend fun calculateDominantColor(url: String): DominantColors? { + // we calculate the swatches in the image, and return the first valid color + return calculateSwatchesInImage(mainActivity, url) + // First we want to sort the list by the color's population + .sortedByDescending { swatch -> swatch.population } + // Then we want to find the first valid color + .firstOrNull { swatch -> Color(swatch.rgb).contrastAgainst(Color.Black) >= 3f } + // If we found a valid swatch, wrap it in a [DominantColors] + ?.let { swatch -> + DominantColors( + color = Color(swatch.rgb), + onColor = Color(swatch.bodyTextColor).copy(alpha = 1f) + ) + } +} + + +/** + * Fetches the given [imageUrl] with [Coil], then uses [Palette] to calculate the dominant color. + */ +suspend fun calculateSwatchesInImage( + context: Context, + imageUrl: String +): List { + val r = ImageRequest.Builder(context) + .data(imageUrl) + // We scale the image to cover 128px x 128px (i.e. min dimension == 128px) + .size(128).scale(Scale.FILL) + // Disable hardware bitmaps, since Palette uses Bitmap.getPixels() + .allowHardware(false) + .build() + + val bitmap = when (val result = Coil.execute(r)) { + is SuccessResult -> result.drawable.toBitmap() + else -> null + } + + return bitmap?.let { + withContext(Dispatchers.Default) { + val palette = Palette.Builder(bitmap) + // Disable any bitmap resizing in Palette. We've already loaded an appropriately + // sized bitmap through Coil + .resizeBitmapArea(0) + // Clear any built-in filters. We want the unfiltered dominant color + .clearFilters() + // We reduce the maximum color count down to 8 + .maximumColorCount(8) + .generate() + + palette.swatches + } + } ?: emptyList() +} diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/utils/GradientScrim.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/utils/GradientScrim.kt new file mode 100644 index 00000000..2b78a28f --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/utils/GradientScrim.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2020 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.util + +import androidx.annotation.FloatRange +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import com.shabinder.spotiflyer.utils.log +import kotlin.math.pow + +/** + * Draws a vertical gradient scrim in the foreground. + * + * @param color The color of the gradient scrim. + * @param startYPercentage The start y value, in percentage of the layout's height (0f to 1f) + * @param endYPercentage The end y value, in percentage of the layout's height (0f to 1f) + * @param decay The exponential decay to apply to the gradient. Defaults to `1.0f` which is + * a linear gradient. + * @param numStops The number of color stops to draw in the gradient. Higher numbers result in + * the higher visual quality at the cost of draw performance. Defaults to `16`. + */ +fun Modifier.verticalGradientScrim( + color: Color, + @FloatRange(from = 0.0, to = 1.0) startYPercentage: Float = 0f, + @FloatRange(from = 0.0, to = 1.0) endYPercentage: Float = 1f, + decay: Float = 1.0f, + numStops: Int = 16, + fixedHeight: Float? = null +): Modifier = composed { + val colors = remember(color, numStops) { + if (decay != 1f) { + // If we have a non-linear decay, we need to create the color gradient steps + // manually + val baseAlpha = color.alpha + List(numStops) { i -> + val x = i * 1f / (numStops - 1) + val opacity = x.pow(decay) + color.copy(alpha = baseAlpha * opacity) + } + } else { + // If we have a linear decay, we just create a simple list of start + end colors + listOf(color.copy(alpha = 0f), color) + } + } + + var height by remember { mutableStateOf(fixedHeight ?: 0f) } + val brush = remember(color, numStops, startYPercentage, endYPercentage, height) { + Brush.verticalGradient( + colors = colors, + startY = height * startYPercentage, + endY = height * endYPercentage + ) + } + + drawBehind { + height = fixedHeight ?: size.height +// log("Height",size.height.toString()) + drawRect(brush = brush) + } +} diff --git a/app/src/main/res/drawable/ic_musicplaceholder.xml b/app/src/main/res/drawable/ic_musicplaceholder.xml index 2df6af15..10bc5979 100644 --- a/app/src/main/res/drawable/ic_musicplaceholder.xml +++ b/app/src/main/res/drawable/ic_musicplaceholder.xml @@ -17,13 +17,13 @@ - - - - - - - - - + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_share_open.xml b/app/src/main/res/drawable/ic_share_open.xml index 3f029571..d8d8fbd1 100644 --- a/app/src/main/res/drawable/ic_share_open.xml +++ b/app/src/main/res/drawable/ic_share_open.xml @@ -15,8 +15,8 @@ ~ along with this program. If not, see . --> - + diff --git a/app/src/main/res/drawable/ic_song_placeholder.xml b/app/src/main/res/drawable/ic_song_placeholder.xml index b27653de..4a5a5d32 100644 --- a/app/src/main/res/drawable/ic_song_placeholder.xml +++ b/app/src/main/res/drawable/ic_song_placeholder.xml @@ -17,6 +17,6 @@ - - + +