@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(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() 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() 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() } }