diff options
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt')
-rw-r--r-- | app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt | 112 |
1 files changed, 73 insertions, 39 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt index c30ee199..16c28c02 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -22,7 +22,6 @@ import android.util.AttributeSet import android.view.View import android.widget.FrameLayout import android.widget.ProgressBar -import ca.allanwang.kau.utils.ContextHelper import ca.allanwang.kau.utils.bindView import ca.allanwang.kau.utils.circularReveal import ca.allanwang.kau.utils.fadeIn @@ -39,17 +38,27 @@ import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.WEB_LOAD_DELAY import com.pitchedapps.frost.injectors.ThemeProvider -import com.pitchedapps.frost.kotlin.subscribeDuringJob import com.pitchedapps.frost.prefs.Prefs import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.web.FrostEmitter +import com.pitchedapps.frost.web.asFrostEmitter import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.ConflatedBroadcastChannel -import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.flow.transformWhile import javax.inject.Inject +@ExperimentalCoroutinesApi class FrostContentWeb @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -60,6 +69,7 @@ class FrostContentWeb @JvmOverloads constructor( override val layoutRes: Int = R.layout.view_content_base_web } +@ExperimentalCoroutinesApi class FrostContentRecycler @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -70,7 +80,7 @@ class FrostContentRecycler @JvmOverloads constructor( override val layoutRes: Int = R.layout.view_content_base_recycler } -@UseExperimental(ExperimentalCoroutinesApi::class) +@ExperimentalCoroutinesApi abstract class FrostContentView<out T> @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -88,8 +98,8 @@ abstract class FrostContentView<out T> @JvmOverloads constructor( /** * Subsection of [FrostContentView] that is [AndroidEntryPoint] friendly (no generics) */ -@UseExperimental(ExperimentalCoroutinesApi::class) @AndroidEntryPoint +@ExperimentalCoroutinesApi abstract class FrostContentViewBase( context: Context, attrs: AttributeSet?, @@ -119,13 +129,35 @@ abstract class FrostContentViewBase( private val refresh: SwipeRefreshLayout by bindView(R.id.content_refresh) private val progress: ProgressBar by bindView(R.id.content_progress) + private val coreView: View by bindView(R.id.content_core) + /** * While this can be conflated, there exist situations where we wish to watch refresh cycles. * Here, we'd need to make sure we don't skip events + * + * TODO ensure there is only one flow provider is this is still separated in login + * Use case for shared flow is to avoid emitting before subscribing; buffer can probably be size 1 */ - override val refreshChannel: BroadcastChannel<Boolean> = BroadcastChannel(10) - override val progressChannel: BroadcastChannel<Int> = ConflatedBroadcastChannel() - override val titleChannel: BroadcastChannel<String> = ConflatedBroadcastChannel() + private val refreshMutableFlow = MutableSharedFlow<Boolean>( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + override val refreshFlow: SharedFlow<Boolean> = refreshMutableFlow.asSharedFlow() + + override val refreshEmit: FrostEmitter<Boolean> = refreshMutableFlow.asFrostEmitter() + + private val progressMutableFlow = MutableStateFlow(0) + + override val progressFlow: SharedFlow<Int> = progressMutableFlow.asSharedFlow() + + override val progressEmit: FrostEmitter<Int> = progressMutableFlow.asFrostEmitter() + + private val titleMutableFlow = MutableStateFlow("") + + override val titleFlow: SharedFlow<String> = titleMutableFlow.asSharedFlow() + + override val titleEmit: FrostEmitter<String> = titleMutableFlow.asFrostEmitter() override lateinit var scope: CoroutineScope @@ -160,7 +192,6 @@ abstract class FrostContentViewBase( */ protected fun init() { inflate(context, layoutRes, this) - core.parent = this reloadThemeSelf() } @@ -169,23 +200,23 @@ abstract class FrostContentViewBase( baseEnum = container.baseEnum init() scope = container - core.bind(container) + core.bind(this, container) refresh.setOnRefreshListener { core.reload(true) } - refreshChannel.subscribeDuringJob(scope, ContextHelper.coroutineContext) { r -> + refreshFlow.distinctUntilChanged().onEach { r -> L.v { "Refreshing $r" } refresh.isRefreshing = r - } + }.launchIn(scope) - progressChannel.subscribeDuringJob(scope, ContextHelper.coroutineContext) { p -> + progressFlow.onEach { p -> progress.invisibleIf(p == 100) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progress.setProgress(p, true) else progress.progress = p - } + }.launchIn(scope) } override fun reloadTheme() { @@ -212,7 +243,6 @@ abstract class FrostContentViewBase( } private var transitionStart: Long = -1 - private var refreshReceiver: ReceiveChannel<Boolean>? = null /** * Hook onto the refresh observable for one cycle @@ -220,33 +250,37 @@ abstract class FrostContentViewBase( * The cycle only starts on the first load since there may have been another process when this is registered */ override fun registerTransition(urlChanged: Boolean, animate: Boolean): Boolean { - if (!urlChanged && refreshReceiver != null) { + if (!urlChanged && transitionStart != -1L) { L.v { "Consuming url load" } return false // still in progress; do not bother with load } + coreView.transition(animate) + return true + } + + private fun View.transition(animate: Boolean) { L.v { "Registered transition" } - with(core) { - refreshReceiver = refreshChannel.openSubscription().also { receiver -> - scope.launchMain { - var loading = false - for (r in receiver) { - if (r) { - loading = true - transitionStart = System.currentTimeMillis() - clearAnimation() - if (isVisible) - fadeOut(duration = 200L) - } else if (loading) { - if (animate && prefs.animate) circularReveal(offset = WEB_LOAD_DELAY) - else fadeIn(duration = 200L, offset = WEB_LOAD_DELAY) - L.v { "Transition loaded in ${System.currentTimeMillis() - transitionStart} ms" } - receiver.cancel() - refreshReceiver = null - } + transitionStart = 0L // Marker for pending transition + scope.launchMain { + refreshFlow.distinctUntilChanged() + // Pseudo windowed mode + .runningFold(false to false) { (_, prev), curr -> prev to curr } + // Take until prev was loading and current is not loading + // Unlike takeWhile, we include the last state (first non matching) + .transformWhile { emit(it); it != (true to false) } + .onEach { (prev, curr) -> + if (curr) { + transitionStart = System.currentTimeMillis() + clearAnimation() + if (isVisible) + fadeOut(duration = 200L) + } else if (prev) { // prev && !curr + if (animate && prefs.animate) circularReveal(offset = WEB_LOAD_DELAY) + else fadeIn(duration = 200L, offset = WEB_LOAD_DELAY) + L.v { "Transition loaded in ${System.currentTimeMillis() - transitionStart} ms" } } - } - } + }.collect() + transitionStart = -1L } - return true } } |