Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(main): cache color #58

Open
wants to merge 2 commits into
base: try_mvi
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private fun Scope.getChartPresenter(): ChartPresenter {
private fun Scope.getMainPresenter(): MainPresenter {
val colorHolderSource = get<ColorHolderSource>()
debug("Create MainPresenter with $colorHolderSource", tag = "[presenter_module]")
return MainPresenter(get(), colorHolderSource, androidApplication())
return MainPresenter(get(), colorHolderSource)
}

@ExperimentalStdlibApi
Expand Down
29 changes: 24 additions & 5 deletions app/src/main/java/com/hoc/weatherapp/ui/main/ColorHolderSource.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
package com.hoc.weatherapp.ui.main

import android.app.Application
import android.os.Parcelable
import androidx.annotation.ColorInt
import androidx.annotation.MainThread
import com.hoc.weatherapp.R
import com.hoc.weatherapp.utils.asObservable
import com.hoc.weatherapp.utils.themeColor
import io.reactivex.Observable
import io.reactivex.subjects.BehaviorSubject
import kotlinx.parcelize.Parcelize

class ColorHolderSource(androidApplication: Application) {
@Parcelize
data class Colors(
@ColorInt
val statusBarColor: Int,
@ColorInt
val backgroundColor: Int // TODO: rename
) : Parcelable

@ColorInt
val defaultStatusBarColor = androidApplication.themeColor(R.attr.colorPrimaryVariant)

@ColorInt
val defaultBackgroundColor = androidApplication.themeColor(R.attr.colorSecondary)

private val subject = BehaviorSubject.createDefault(
androidApplication.themeColor(R.attr.colorPrimaryVariant) to
androidApplication.themeColor(R.attr.colorSecondary)
Colors(
defaultStatusBarColor,
defaultBackgroundColor
)
)

val colorObservable = subject.asObservable()
val colorObservable: Observable<Colors> = subject.hide()

@MainThread
fun change(colors: Pair<Int, Int>) = subject.onNext(colors)
fun change(colors: Colors) = subject.onNext(colors)
}
66 changes: 8 additions & 58 deletions app/src/main/java/com/hoc/weatherapp/ui/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,9 @@ import android.view.View
import android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
import android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.annotation.WorkerThread
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.palette.graphics.Palette
import androidx.palette.graphics.Target
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.BitmapTransitionOptions
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
Expand All @@ -40,13 +36,10 @@ import com.hoc.weatherapp.utils.Some
import com.hoc.weatherapp.utils.blur.GlideBlurTransformation
import com.hoc.weatherapp.utils.debug
import com.hoc.weatherapp.utils.startActivity
import com.hoc.weatherapp.utils.themeColor
import com.hoc.weatherapp.utils.ui.ZoomOutPageTransformer
import com.hoc.weatherapp.utils.ui.getBackgroundDrawableFromWeather
import com.hoc.weatherapp.utils.ui.getSoundUriFromCurrentWeather
import com.hoc081098.viewbindingdelegate.viewBinding
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import org.koin.android.ext.android.get
import org.koin.android.scope.AndroidScopeComponent
Expand All @@ -63,7 +56,8 @@ class MainActivity :
private val binding by viewBinding<ActivityMainBinding>()

private var mediaPlayer: MediaPlayer? = null
private val changeBackground = PublishSubject.create<Optional<Bitmap>>()
private val changeBackground =
PublishSubject.create<Optional<MainContract.BitmapAndBackgroundId>>()

private var target1: CustomViewTarget<*, *>? = null
private var target2: CustomViewTarget<*, *>? = null
Expand Down Expand Up @@ -163,6 +157,8 @@ class MainActivity :
weather: CurrentWeather,
city: City
) {
val backgroundId = getBackgroundDrawableFromWeather(weather, city)

Glide
.with(this)
.apply {
Expand All @@ -171,7 +167,7 @@ class MainActivity :
changeBackground.onNext(None)
}
.asBitmap()
.load(getBackgroundDrawableFromWeather(weather, city))
.load(backgroundId)
.apply(
RequestOptions
.bitmapTransform(GlideBlurTransformation(this, 20f))
Expand All @@ -186,7 +182,7 @@ class MainActivity :

override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
view.setImageBitmap(resource)
changeBackground.onNext(Some(resource))
changeBackground.onNext(Some(MainContract.BitmapAndBackgroundId(resource, backgroundId)))
}
})
.also { target1 = it }
Expand All @@ -211,32 +207,14 @@ class MainActivity :
}

override fun render(state: MainContract.ViewState) {
window.statusBarColor = state.vibrantColor
window.statusBarColor = state.statusBarColor
when (state) {
is MainContract.ViewState.NoSelectedCity -> renderNoSelectedCity()
is MainContract.ViewState.CityAndWeather -> renderCityAndWeather(state)
}
}

override fun changeColorIntent(): Observable<Pair<Int, Int>> {
return changeBackground
.switchMap { optional ->
when (optional) {
is Some -> {
Observable
.fromCallable {
getVibrantColor(
resource = optional.value,
colorPrimaryVariant = themeColor(R.attr.colorPrimaryVariant),
colorSecondary = themeColor(R.attr.colorSecondary),
)
}
.subscribeOn(Schedulers.computation())
}
None -> Observable.empty()
}
}
}
override fun changeColorIntent() = changeBackground

private fun renderCityAndWeather(state: MainContract.ViewState.CityAndWeather) {
updateBackground(state.weather, state.city)
Expand Down Expand Up @@ -277,31 +255,3 @@ class MainActivity :

override fun createPresenter() = get<MainPresenter>()
}

@WorkerThread
private fun getVibrantColor(
resource: Bitmap,
@ColorInt colorPrimaryVariant: Int,
@ColorInt colorSecondary: Int,
): Pair<Int, Int> {
return Palette
.from(resource)
.generate()
.let { palette ->
@ColorInt val darkColor = listOf(
palette.getSwatchForTarget(Target.DARK_VIBRANT)?.rgb,
palette.getSwatchForTarget(Target.VIBRANT)?.rgb,
palette.getSwatchForTarget(Target.LIGHT_VIBRANT)?.rgb,
palette.getSwatchForTarget(Target.DARK_MUTED)?.rgb,
palette.getSwatchForTarget(Target.MUTED)?.rgb,
palette.getSwatchForTarget(Target.DARK_MUTED)?.rgb
).find { it !== null } ?: colorPrimaryVariant

@ColorInt val lightColor = listOf(
palette.getSwatchForTarget(Target.LIGHT_VIBRANT)?.rgb,
palette.getSwatchForTarget(Target.LIGHT_MUTED)?.rgb
).find { it !== null } ?: colorSecondary

darkColor to lightColor
}
}
17 changes: 13 additions & 4 deletions app/src/main/java/com/hoc/weatherapp/ui/main/MainContract.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
package com.hoc.weatherapp.ui.main

import android.graphics.Bitmap
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.hannesdorfmann.mosby3.mvp.MvpView
import com.hoc.weatherapp.data.models.entity.City
import com.hoc.weatherapp.data.models.entity.CurrentWeather
import com.hoc.weatherapp.utils.Optional
import io.reactivex.Observable

interface MainContract {
sealed class ViewState {
abstract val vibrantColor: Int
@get:ColorInt
abstract val statusBarColor: Int

data class CityAndWeather(
val city: City,
val weather: CurrentWeather,
@ColorInt override val vibrantColor: Int
@ColorInt override val statusBarColor: Int
) : ViewState()

data class NoSelectedCity(@ColorInt override val vibrantColor: Int) : ViewState()
data class NoSelectedCity(@ColorInt override val statusBarColor: Int) : ViewState()
}

data class BitmapAndBackgroundId(
val bitmap: Bitmap,
@DrawableRes val backgroundId: Int
)

interface View : MvpView {
fun changeColorIntent(): Observable<Pair<Int, Int>>
fun changeColorIntent(): Observable<Optional<BitmapAndBackgroundId>>

fun render(state: ViewState)
}
Expand Down
84 changes: 72 additions & 12 deletions app/src/main/java/com/hoc/weatherapp/ui/main/MainPresenter.kt
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
package com.hoc.weatherapp.ui.main

import android.app.Application
import android.graphics.Bitmap
import androidx.annotation.ColorInt
import androidx.annotation.WorkerThread
import androidx.palette.graphics.Palette
import androidx.palette.graphics.Target
import com.hannesdorfmann.mosby3.mvi.MviBasePresenter
import com.hoc.weatherapp.R
import com.hoc.weatherapp.data.CurrentWeatherRepository
import com.hoc.weatherapp.ui.main.MainContract.ViewState.CityAndWeather
import com.hoc.weatherapp.ui.main.MainContract.ViewState.NoSelectedCity
import com.hoc.weatherapp.utils.None
import com.hoc.weatherapp.utils.Some
import com.hoc.weatherapp.utils.debug
import com.hoc.weatherapp.utils.themeColor
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.rxkotlin.Observables
import io.reactivex.schedulers.Schedulers
import java.util.concurrent.ConcurrentHashMap

class MainPresenter(
currentWeatherRepository: CurrentWeatherRepository,
private val colorHolderSource: ColorHolderSource,
private val androidApplication: Application
private val colorHolderSource: ColorHolderSource
) : MviBasePresenter<MainContract.View, MainContract.ViewState>() {
private var disposable: Disposable? = null
private val colorCache = ConcurrentHashMap<Int, ColorHolderSource.Colors>()

private val state = Observables.combineLatest(
source1 = currentWeatherRepository.getSelectedCityAndCurrentWeatherOfSelectedCity(),
source2 = colorHolderSource.colorObservable
).map {
when (val optional = it.first) {
None -> NoSelectedCity(androidApplication.themeColor(R.attr.colorPrimaryVariant))
) { weatherOptional, colorHolder ->
when (weatherOptional) {
None -> NoSelectedCity(colorHolderSource.defaultStatusBarColor)
is Some -> CityAndWeather(
city = optional.value.city,
weather = optional.value.currentWeather,
vibrantColor = it.second.first
city = weatherOptional.value.city,
weather = weatherOptional.value.currentWeather,
statusBarColor = colorHolder.statusBarColor
)
}
}
Expand All @@ -40,6 +45,32 @@ class MainPresenter(

override fun bindIntents() {
disposable = intent(MainContract.View::changeColorIntent)
.switchMap { optional ->
when (optional) {
is Some -> Observable.defer {
val (bitmap, backgroundId) = optional.value
colorCache[backgroundId]?.let {
debug("Hit cache: backgroundId=$backgroundId, pair=$it", TAG)
return@defer Observable.just(it)
}

Observable
.fromCallable {
getColors(
resource = bitmap,
defaultStatusBarColor = colorHolderSource.defaultStatusBarColor,
defaultBackgroundColor = colorHolderSource.defaultBackgroundColor,
)
}
.doOnNext {
colorCache[backgroundId] = it
debug("Update cache: backgroundId=$backgroundId, pair=$it", TAG)
}
.subscribeOn(Schedulers.computation())
}
None -> Observable.empty()
}
}
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { debug("ChangeColor=$it", TAG) }
.subscribe(colorHolderSource::change)
Expand All @@ -49,10 +80,39 @@ class MainPresenter(

override fun unbindIntents() {
super.unbindIntents()
disposable?.takeUnless { it.isDisposed }?.dispose()
disposable?.dispose()
colorCache.clear()
}

private companion object {
private const val TAG = "__main__"
}
}

@WorkerThread
private fun getColors(
resource: Bitmap,
@ColorInt defaultStatusBarColor: Int,
@ColorInt defaultBackgroundColor: Int,
): ColorHolderSource.Colors {
return Palette
.from(resource)
.generate()
.let { palette ->
@ColorInt val statusBarColor = listOf(
palette.getSwatchForTarget(Target.DARK_VIBRANT)?.rgb,
palette.getSwatchForTarget(Target.VIBRANT)?.rgb,
palette.getSwatchForTarget(Target.LIGHT_VIBRANT)?.rgb,
palette.getSwatchForTarget(Target.DARK_MUTED)?.rgb,
palette.getSwatchForTarget(Target.MUTED)?.rgb,
palette.getSwatchForTarget(Target.DARK_MUTED)?.rgb
).find { it !== null } ?: defaultStatusBarColor

@ColorInt val backgroundColor = listOf(
palette.getSwatchForTarget(Target.LIGHT_VIBRANT)?.rgb,
palette.getSwatchForTarget(Target.LIGHT_MUTED)?.rgb
).find { it !== null } ?: defaultBackgroundColor

ColorHolderSource.Colors(statusBarColor, backgroundColor)
}
}
Loading
Loading