Set status bar foreground when app bar is hidden
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -81,22 +81,11 @@ class FavouritesContainerFragment :
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
|
||||
binding.root.updatePadding(
|
||||
top = headerHeight - insets.top
|
||||
)
|
||||
binding.pager.updatePadding(
|
||||
// 8 dp is needed so that the top of the list is not attached to tabs (visible when ActionMode is active)
|
||||
top = -headerHeight + resources.resolveDp(8)
|
||||
)
|
||||
binding.tabs.apply {
|
||||
updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right
|
||||
)
|
||||
updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,10 +185,6 @@ abstract class MangaListFragment :
|
||||
right = insets.right,
|
||||
)
|
||||
if (activity is MainActivity) {
|
||||
binding.recyclerView.updatePadding(
|
||||
top = headerHeight,
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
binding.swipeRefreshLayout.setProgressViewOffset(
|
||||
true,
|
||||
headerHeight + resources.resolveDp(-72),
|
||||
|
||||
@@ -76,12 +76,9 @@ class FeedFragment :
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
|
||||
binding.recyclerView.updatePadding(
|
||||
top = headerHeight + paddingVertical,
|
||||
left = insets.left + paddingHorizontal,
|
||||
right = insets.right + paddingHorizontal,
|
||||
bottom = insets.bottom + paddingVertical,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,19 @@ import android.net.Network
|
||||
import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.Window
|
||||
import android.view.animation.Animation
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.constraintlayout.motion.widget.MotionScene
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import androidx.work.CoroutineWorker
|
||||
import com.google.android.material.elevation.ElevationOverlayProvider
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -111,4 +116,15 @@ fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float
|
||||
elevation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val Context.animatorDurationScale: Float
|
||||
get() = Settings.Global.getFloat(this.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
|
||||
|
||||
fun ViewPropertyAnimator.applySystemAnimatorScale(context: Context): ViewPropertyAnimator = apply {
|
||||
this.duration = (this.duration * context.animatorDurationScale).toLong()
|
||||
}
|
||||
|
||||
inline fun <reified T> ViewGroup.findChild(): T? {
|
||||
return children.find { it is T } as? T
|
||||
}
|
||||
Reference in New Issue
Block a user