mihon/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiAppBarLayout.kt
2022-06-20 22:54:42 -04:00

215 lines
7 KiB
Kotlin

@file:Suppress("PackageDirectoryMismatch")
package com.google.android.material.appbar
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.animation.LinearInterpolator
import android.widget.TextView
import androidx.annotation.FloatRange
import androidx.core.graphics.drawable.updateBounds
import androidx.core.graphics.withTranslation
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.findViewTreeLifecycleOwner
import com.google.android.material.shape.MaterialShapeDrawable
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.view.findChild
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.HierarchyChangeEvent
import reactivecircus.flowbinding.android.view.hierarchyChangeEvents
/**
* [AppBarLayout] with our own lift state handler and custom title alpha.
*
* Inside this package to access some package-private methods.
*/
class TachiyomiAppBarLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : AppBarLayout(context, attrs) {
private var lifted = true
private val toolbar by lazy { findViewById<MaterialToolbar>(R.id.toolbar) }
@FloatRange(from = 0.0, to = 1.0)
var titleTextAlpha = 1F
set(value) {
field = value
titleTextView?.alpha = field
}
private var titleTextView: TextView? = null
set(value) {
field = value
field?.alpha = titleTextAlpha
}
private var animatorSet: AnimatorSet? = null
private var statusBarForegroundAnimator: ValueAnimator? = null
private var currentOffset = 0
var isTransparentWhenNotLifted = false
set(value) {
if (field != value) {
field = value
updateStates()
}
}
/**
* Disabled. Lift on scroll is handled manually with [eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout]
*/
override fun isLiftOnScroll(): Boolean = false
override fun isLifted(): Boolean = lifted
override fun setLifted(lifted: Boolean): Boolean {
return if (this.lifted != lifted) {
this.lifted = lifted
updateStates()
true
} else {
false
}
}
override fun setLiftedState(lifted: Boolean, force: Boolean): Boolean = false
override fun draw(canvas: Canvas) {
super.draw(canvas)
canvas.withTranslation(y = -currentOffset.toFloat()) {
statusBarForeground?.draw(this)
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
statusBarForeground?.updateBounds(right = width, bottom = paddingTop)
}
override fun onOffsetChanged(offset: Int) {
currentOffset = offset
super.onOffsetChanged(offset)
// Show status bar foreground when offset
val foreground = (statusBarForeground as? MaterialShapeDrawable) ?: return
val start = foreground.alpha
val end = if (offset != 0) 255 else 0
statusBarForegroundAnimator?.cancel()
if (animatorSet?.isRunning == true) {
foreground.alpha = end
return
}
if (start != end) {
statusBarForegroundAnimator = ValueAnimator.ofInt(start, end).apply {
duration = resources.getInteger(R.integer.app_bar_elevation_anim_duration).toLong()
interpolator = LINEAR_INTERPOLATOR
addUpdateListener {
foreground.alpha = it.animatedValue as Int
}
start()
}
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
toolbar.background.alpha = 0 // Use app bar background
titleTextView = toolbar.findChild<TextView>()
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.let { scope ->
toolbar.hierarchyChangeEvents()
.onEach {
when (it) {
is HierarchyChangeEvent.ChildAdded -> {
if (it.child is TextView) {
titleTextView = it.child as TextView
}
}
is HierarchyChangeEvent.ChildRemoved -> {
if (it.child == titleTextView) {
titleTextView = null
}
}
}
}
.launchIn(scope)
}
}
override fun setStatusBarForeground(drawable: Drawable?) {
super.setStatusBarForeground(drawable)
setWillNotDraw(statusBarForeground == null)
}
@SuppressLint("Recycle")
private fun updateStates() {
val animators = mutableListOf<ValueAnimator>()
val fromElevation = elevation
val toElevation = if (lifted) {
resources.getDimension(R.dimen.design_appbar_elevation)
} else {
0F
}
if (fromElevation != toElevation) {
ValueAnimator.ofFloat(fromElevation, toElevation).apply {
addUpdateListener {
elevation = it.animatedValue as Float
(statusBarForeground as? MaterialShapeDrawable)?.elevation = it.animatedValue as Float
}
animators.add(this)
}
}
val transparent = if (lifted) false else isTransparentWhenNotLifted
val fromAlpha = (background as? MaterialShapeDrawable)?.alpha ?: background.alpha
val toAlpha = if (transparent) 0 else 255
if (fromAlpha != toAlpha) {
ValueAnimator.ofInt(fromAlpha, toAlpha).apply {
addUpdateListener {
val value = it.animatedValue as Int
background.alpha = value
}
animators.add(this)
}
}
if (animators.isNotEmpty()) {
animatorSet?.cancel()
animatorSet = AnimatorSet().apply {
duration = resources.getInteger(R.integer.app_bar_elevation_anim_duration).toLong()
interpolator = LINEAR_INTERPOLATOR
playTogether(*animators.toTypedArray())
start()
}
}
}
init {
statusBarForeground = MaterialShapeDrawable.createWithElevationOverlay(context)
applyInsetter {
type(navigationBars = true) {
margin(horizontal = true)
}
type(statusBars = true) {
padding(top = true)
}
ignoreVisibility(true)
}
}
companion object {
private val LINEAR_INTERPOLATOR = LinearInterpolator()
}
}