Set status bar foreground when app bar is hidden
parent
455351e3a8
commit
310d4e58bb
@ -0,0 +1,151 @@
|
|||||||
|
package com.google.android.material.appbar
|
||||||
|
|
||||||
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.FloatRange
|
||||||
|
import com.google.android.material.animation.AnimationUtils
|
||||||
|
import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener
|
||||||
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [AppBarLayout] with our own lift state handler and custom title alpha.
|
||||||
|
*
|
||||||
|
* Inside this package to access some package-private methods.
|
||||||
|
*/
|
||||||
|
class KotatsuAppBarLayout @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 val offsetListener = OnOffsetChangedListener { appBarLayout, verticalOffset ->
|
||||||
|
// Show status bar foreground when offset
|
||||||
|
val foreground = (appBarLayout?.statusBarForeground as? MaterialShapeDrawable) ?: return@OnOffsetChangedListener
|
||||||
|
val start = foreground.alpha
|
||||||
|
val end = if (verticalOffset != 0) 255 else 0
|
||||||
|
|
||||||
|
statusBarForegroundAnimator?.cancel()
|
||||||
|
if (animatorSet?.isRunning == true) {
|
||||||
|
foreground.alpha = end
|
||||||
|
return@OnOffsetChangedListener
|
||||||
|
}
|
||||||
|
if (start != end) {
|
||||||
|
statusBarForegroundAnimator = ValueAnimator.ofInt(start, end).apply {
|
||||||
|
duration = resources.getInteger(materialR.integer.app_bar_elevation_anim_duration).toLong()
|
||||||
|
interpolator = AnimationUtils.LINEAR_INTERPOLATOR
|
||||||
|
addUpdateListener {
|
||||||
|
foreground.alpha = it.animatedValue as Int
|
||||||
|
}
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTransparentWhenNotLifted = false
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
updateStates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
addOnOffsetChangedListener(offsetListener)
|
||||||
|
toolbar.background.alpha = 0 // Use app bar background
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
removeOnOffsetChangedListener(offsetListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("Recycle")
|
||||||
|
private fun updateStates() {
|
||||||
|
val animators = mutableListOf<ValueAnimator>()
|
||||||
|
|
||||||
|
val fromElevation = elevation
|
||||||
|
val toElevation = if (lifted) {
|
||||||
|
resources.getDimension(materialR.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(materialR.integer.app_bar_elevation_anim_duration).toLong()
|
||||||
|
interpolator = AnimationUtils.LINEAR_INTERPOLATOR
|
||||||
|
playTogether(*animators.toTypedArray())
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
statusBarForeground = MaterialShapeDrawable.createWithElevationOverlay(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.widgets
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
|
||||||
|
import org.koitharu.kotatsu.utils.ext.findChild
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||||
|
context: Context? = null,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
) : CoordinatorLayout.Behavior<BottomNavigationView>(context, attrs) {
|
||||||
|
|
||||||
|
@ViewCompat.NestedScrollType
|
||||||
|
private var lastStartedType: Int = 0
|
||||||
|
|
||||||
|
private var offsetAnimator: ValueAnimator? = null
|
||||||
|
|
||||||
|
private var dyRatio = 1F
|
||||||
|
|
||||||
|
override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean {
|
||||||
|
return dependency is AppBarLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDependentViewChanged(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
child: BottomNavigationView,
|
||||||
|
dependency: View,
|
||||||
|
): Boolean {
|
||||||
|
val toolbarSize = (dependency as ViewGroup).findChild<Toolbar>()?.height ?: 0
|
||||||
|
dyRatio = if (toolbarSize > 0) {
|
||||||
|
child.height.toFloat() / toolbarSize
|
||||||
|
} else {
|
||||||
|
1F
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartNestedScroll(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: BottomNavigationView,
|
||||||
|
directTargetChild: View,
|
||||||
|
target: View,
|
||||||
|
axes: Int,
|
||||||
|
type: Int,
|
||||||
|
): Boolean {
|
||||||
|
if (axes != ViewCompat.SCROLL_AXIS_VERTICAL) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
lastStartedType = type
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNestedPreScroll(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: BottomNavigationView,
|
||||||
|
target: View,
|
||||||
|
dx: Int,
|
||||||
|
dy: Int,
|
||||||
|
consumed: IntArray,
|
||||||
|
type: Int,
|
||||||
|
) {
|
||||||
|
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
|
||||||
|
child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopNestedScroll(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: BottomNavigationView,
|
||||||
|
target: View,
|
||||||
|
type: Int,
|
||||||
|
) {
|
||||||
|
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
||||||
|
animateBottomNavigationVisibility(child, child.translationY < child.height / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateBottomNavigationVisibility(child: BottomNavigationView, isVisible: Boolean) {
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
offsetAnimator = ValueAnimator().apply {
|
||||||
|
interpolator = DecelerateInterpolator()
|
||||||
|
duration = (150 * child.context.animatorDurationScale).roundToLong()
|
||||||
|
addUpdateListener {
|
||||||
|
child.translationY = it.animatedValue as Float
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offsetAnimator?.setFloatValues(
|
||||||
|
child.translationY,
|
||||||
|
if (isVisible) 0F else child.height.toFloat(),
|
||||||
|
)
|
||||||
|
offsetAnimator?.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,156 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.widgets
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.animation.TimeInterpolator
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.ViewPropertyAnimator
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.doOnLayout
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.customview.view.AbsSavedState
|
||||||
|
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
||||||
|
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||||
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
import org.koitharu.kotatsu.utils.ext.applySystemAnimatorScale
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
class KotatsuBottomNavigationView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = materialR.attr.bottomNavigationStyle,
|
||||||
|
defStyleRes: Int = materialR.style.Widget_Design_BottomNavigationView,
|
||||||
|
) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
|
||||||
|
private var currentAnimator: ViewPropertyAnimator? = null
|
||||||
|
|
||||||
|
private var currentState = STATE_UP
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Hide on scroll
|
||||||
|
doOnLayout {
|
||||||
|
findViewTreeLifecycleOwner()?.lifecycleScope?.let {
|
||||||
|
updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||||
|
behavior = HideBottomNavigationOnScrollBehavior()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(): Parcelable {
|
||||||
|
val superState = super.onSaveInstanceState()
|
||||||
|
return SavedState(superState).also {
|
||||||
|
it.currentState = currentState
|
||||||
|
it.translationY = translationY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||||
|
if (state is SavedState) {
|
||||||
|
super.onRestoreInstanceState(state.superState)
|
||||||
|
super.setTranslationY(state.translationY)
|
||||||
|
currentState = state.currentState
|
||||||
|
} else {
|
||||||
|
super.onRestoreInstanceState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTranslationY(translationY: Float) {
|
||||||
|
// Disallow translation change when state down
|
||||||
|
if (currentState == STATE_DOWN) return
|
||||||
|
super.setTranslationY(translationY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows this view up.
|
||||||
|
*/
|
||||||
|
fun slideUp() = post {
|
||||||
|
currentAnimator?.cancel()
|
||||||
|
clearAnimation()
|
||||||
|
|
||||||
|
currentState = STATE_UP
|
||||||
|
animateTranslation(
|
||||||
|
0F,
|
||||||
|
SLIDE_UP_ANIMATION_DURATION,
|
||||||
|
LinearOutSlowInInterpolator(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides this view down. [setTranslationY] won't work until [slideUp] is called.
|
||||||
|
*/
|
||||||
|
fun slideDown() = post {
|
||||||
|
currentAnimator?.cancel()
|
||||||
|
clearAnimation()
|
||||||
|
|
||||||
|
currentState = STATE_DOWN
|
||||||
|
animateTranslation(
|
||||||
|
height.toFloat(),
|
||||||
|
SLIDE_DOWN_ANIMATION_DURATION,
|
||||||
|
FastOutLinearInInterpolator(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) {
|
||||||
|
currentAnimator = animate()
|
||||||
|
.translationY(targetY)
|
||||||
|
.setInterpolator(interpolator)
|
||||||
|
.setDuration(duration)
|
||||||
|
.applySystemAnimatorScale(context)
|
||||||
|
.setListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
currentAnimator = null
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SavedState : AbsSavedState {
|
||||||
|
var currentState = STATE_UP
|
||||||
|
var translationY = 0F
|
||||||
|
|
||||||
|
constructor(superState: Parcelable) : super(superState)
|
||||||
|
|
||||||
|
constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
|
||||||
|
currentState = source.readInt()
|
||||||
|
translationY = source.readFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(out, flags)
|
||||||
|
out.writeInt(currentState)
|
||||||
|
out.writeFloat(translationY)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val CREATOR: Parcelable.ClassLoaderCreator<SavedState> = object : Parcelable.ClassLoaderCreator<SavedState> {
|
||||||
|
override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState {
|
||||||
|
return SavedState(source, loader)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFromParcel(source: Parcel): SavedState {
|
||||||
|
return SavedState(source, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<SavedState> {
|
||||||
|
return newArray(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val STATE_DOWN = 1
|
||||||
|
private const val STATE_UP = 2
|
||||||
|
|
||||||
|
private const val SLIDE_UP_ANIMATION_DURATION = 225L
|
||||||
|
private const val SLIDE_DOWN_ANIMATION_DURATION = 175L
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue