@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.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) val saveCount = canvas.save() canvas.translate(0f, -currentOffset.toFloat()) statusBarForeground?.draw(canvas) canvas.restoreToCount(saveCount) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { super.onLayout(changed, l, t, r, b) statusBarForeground?.setBounds(0, 0, width, 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() } }