Refactor MainActivity navigation and AppBars behavior

pull/189/head
Koitharu 4 years ago
parent 656405edbc
commit 8b0f221eef
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -1,151 +0,0 @@
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,42 @@
package org.koitharu.kotatsu.base.ui.util
import android.animation.ValueAnimator
import android.view.animation.AccelerateDecelerateInterpolator
import com.google.android.material.R as materialR
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.shape.MaterialShapeDrawable
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
class StatusBarDimHelper : AppBarLayout.OnOffsetChangedListener {
private var animator: ValueAnimator? = null
private val interpolator = AccelerateDecelerateInterpolator()
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
val foreground = appBarLayout.statusBarForeground ?: return
val start = foreground.alpha
val collapsed = verticalOffset != 0
val end = if (collapsed) 255 else 0
animator?.cancel()
if (start == end) {
animator = null
return
}
animator = ValueAnimator.ofInt(start, end).apply {
duration = appBarLayout.context.getAnimationDuration(materialR.integer.app_bar_elevation_anim_duration)
interpolator = this@StatusBarDimHelper.interpolator
addUpdateListener {
foreground.alpha = it.animatedValue as Int
}
start()
}
}
fun attachToAppBar(appBarLayout: AppBarLayout) {
appBarLayout.addOnOffsetChangedListener(this)
appBarLayout.statusBarForeground =
MaterialShapeDrawable.createWithElevationOverlay(appBarLayout.context).apply {
alpha = 0
}
}
}

@ -1,134 +0,0 @@
/*
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.FrameLayout
import androidx.annotation.ColorInt
import androidx.annotation.StringRes
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.postDelayed
import com.google.android.material.color.MaterialColors
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.FadingSnackbarLayoutBinding
import org.koitharu.kotatsu.utils.ext.getThemeColorStateList
import com.google.android.material.R as materialR
private const val SHORT_DURATION_MS = 1_500L
private const val LONG_DURATION_MS = 2_750L
/**
* A custom snackbar implementation allowing more control over placement and entry/exit animations.
*
* Xtimms: Well, my sufferings over the Snackbar in [DetailsActivity] will go away forever... Thanks, Google.
*
* https://github.com/google/iosched/blob/main/mobile/src/main/java/com/google/samples/apps/iosched/widget/FadingSnackbar.kt
*/
class FadingSnackbar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr) {
private val binding = FadingSnackbarLayoutBinding.inflate(LayoutInflater.from(context), this)
private val enterDuration = context.resources.getInteger(R.integer.config_defaultAnimTime).toLong()
private val exitDuration = context.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
init {
binding.snackbarLayout.background = createThemedBackground()
}
fun dismiss() {
if (visibility == VISIBLE && alpha == 1f) {
animate()
.alpha(0f)
.withEndAction { visibility = GONE }
.duration = exitDuration
}
}
fun show(
messageText: CharSequence?,
@StringRes actionId: Int = 0,
duration: Int = Snackbar.LENGTH_SHORT,
onActionClick: (FadingSnackbar.() -> Unit)? = null,
onDismiss: (() -> Unit)? = null,
) {
binding.snackbarText.text = messageText
if (actionId != 0) {
with(binding.snackbarAction) {
visibility = VISIBLE
text = context.getString(actionId)
setOnClickListener {
onActionClick?.invoke(this@FadingSnackbar) ?: dismiss()
}
}
} else {
binding.snackbarAction.visibility = GONE
}
alpha = 0f
visibility = VISIBLE
animate()
.alpha(1f)
.duration = enterDuration
if (duration == Snackbar.LENGTH_INDEFINITE) {
return
}
val durationMs = enterDuration + if (duration == Snackbar.LENGTH_LONG) LONG_DURATION_MS else SHORT_DURATION_MS
postDelayed(durationMs) {
dismiss()
onDismiss?.invoke()
}
}
private fun createThemedBackground(): Drawable {
val backgroundColor = MaterialColors.layer(this, materialR.attr.colorSurface, materialR.attr.colorOnSurface, 1f)
val shapeAppearanceModel = ShapeAppearanceModel.builder(
context,
materialR.style.ShapeAppearance_Material3_Corner_ExtraSmall,
0
).build()
val background = createMaterialShapeDrawableBackground(
backgroundColor,
shapeAppearanceModel,
)
val backgroundTint = context.getThemeColorStateList(materialR.attr.colorSurfaceInverse)
return if (backgroundTint != null) {
val wrappedDrawable = DrawableCompat.wrap(background)
DrawableCompat.setTintList(wrappedDrawable, backgroundTint)
wrappedDrawable
} else {
DrawableCompat.wrap(background)
}
}
private fun createMaterialShapeDrawableBackground(
@ColorInt backgroundColor: Int,
shapeAppearanceModel: ShapeAppearanceModel,
): MaterialShapeDrawable {
val background = MaterialShapeDrawable(shapeAppearanceModel)
background.fillColor = ColorStateList.valueOf(backgroundColor)
return background
}
}

@ -4,17 +4,14 @@ import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import org.koitharu.kotatsu.utils.ext.animatorDurationScale import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.findChild import org.koitharu.kotatsu.utils.ext.getAnimationDuration
import org.koitharu.kotatsu.utils.ext.measureHeight import org.koitharu.kotatsu.utils.ext.measureHeight
import kotlin.math.roundToLong
class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor( class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
context: Context? = null, context: Context? = null,
@ -90,7 +87,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
offsetAnimator?.cancel() offsetAnimator?.cancel()
offsetAnimator = ValueAnimator().apply { offsetAnimator = ValueAnimator().apply {
interpolator = DecelerateInterpolator() interpolator = DecelerateInterpolator()
duration = (150 * child.context.animatorDurationScale).roundToLong() duration = child.context.getAnimationDuration(R.integer.config_shorterAnimTime)
addUpdateListener { addUpdateListener {
child.translationY = it.animatedValue as Float child.translationY = it.animatedValue as Float
} }

@ -1,111 +0,0 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.doOnLayout
import androidx.customview.view.AbsSavedState
import com.google.android.material.appbar.AppBarLayout
import org.koitharu.kotatsu.utils.ext.findChild
class KotatsuCoordinatorLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = androidx.coordinatorlayout.R.attr.coordinatorLayoutStyle
) : CoordinatorLayout(context, attrs, defStyleAttr) {
private var appBarLayout: AppBarLayout? = null
/**
* If true, [AppBarLayout] child will be lifted on nested scroll.
*/
var isLiftAppBarOnScroll = true
/**
* Internal check
*/
private val canLiftAppBarOnScroll
get() = isLiftAppBarOnScroll
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed)
if (canLiftAppBarOnScroll) {
appBarLayout?.isLifted = dyConsumed != 0 || dyUnconsumed >= 0
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
appBarLayout = findChild()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
appBarLayout = null
}
override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState()
return if (superState != null) {
SavedState(superState).also {
it.appBarLifted = appBarLayout?.isLifted ?: false
}
} else {
superState
}
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state is SavedState) {
super.onRestoreInstanceState(state.superState)
doOnLayout {
appBarLayout?.isLifted = state.appBarLifted
}
} else {
super.onRestoreInstanceState(state)
}
}
internal class SavedState : AbsSavedState {
var appBarLifted = false
constructor(superState: Parcelable) : super(superState)
constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
appBarLifted = source.readByte().toInt() == 1
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeByte((if (appBarLifted) 1 else 0).toByte())
}
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)
}
}
}
}
}

@ -8,46 +8,42 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.ViewPropertyAnimator import android.view.ViewPropertyAnimator
import androidx.annotation.AttrRes
import androidx.annotation.StyleRes
import androidx.coordinatorlayout.widget.CoordinatorLayout 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.FastOutLinearInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import androidx.lifecycle.findViewTreeLifecycleOwner import com.google.android.material.R as materialR
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import org.koitharu.kotatsu.utils.ext.applySystemAnimatorScale import org.koitharu.kotatsu.utils.ext.applySystemAnimatorScale
import com.google.android.material.R as materialR import org.koitharu.kotatsu.utils.ext.measureHeight
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
class KotatsuBottomNavigationView @JvmOverloads constructor( class SlidingBottomNavigationView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = materialR.attr.bottomNavigationStyle, @AttrRes defStyleAttr: Int = materialR.attr.bottomNavigationStyle,
defStyleRes: Int = materialR.style.Widget_Design_BottomNavigationView, @StyleRes defStyleRes: Int = materialR.style.Widget_Design_BottomNavigationView,
) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes) { ) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes),
CoordinatorLayout.AttachedBehavior {
private var currentAnimator: ViewPropertyAnimator? = null private var currentAnimator: ViewPropertyAnimator? = null
private var currentState = STATE_UP private var currentState = STATE_UP
private var behavior = HideBottomNavigationOnScrollBehavior()
init { override fun getBehavior(): CoordinatorLayout.Behavior<*> {
// Hide on scroll return behavior
doOnLayout {
findViewTreeLifecycleOwner()?.lifecycleScope?.let {
updateLayoutParams<CoordinatorLayout.LayoutParams> {
behavior = HideBottomNavigationOnScrollBehavior()
}
}
}
} }
override fun onSaveInstanceState(): Parcelable { override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState() val superState = super.onSaveInstanceState()
return SavedState(superState).also { return SavedState(superState, currentState, translationY)
it.currentState = currentState
it.translationY = translationY
}
} }
override fun onRestoreInstanceState(state: Parcelable?) { override fun onRestoreInstanceState(state: Parcelable?) {
@ -62,14 +58,12 @@ class KotatsuBottomNavigationView @JvmOverloads constructor(
override fun setTranslationY(translationY: Float) { override fun setTranslationY(translationY: Float) {
// Disallow translation change when state down // Disallow translation change when state down
if (currentState == STATE_DOWN) return if (currentState != STATE_DOWN) {
super.setTranslationY(translationY) super.setTranslationY(translationY)
}
} }
/** fun show() {
* Shows this view up.
*/
fun slideUp() = post {
currentAnimator?.cancel() currentAnimator?.cancel()
clearAnimation() clearAnimation()
@ -81,16 +75,17 @@ class KotatsuBottomNavigationView @JvmOverloads constructor(
) )
} }
/** fun hide() {
* Hides this view down. [setTranslationY] won't work until [slideUp] is called.
*/
fun slideDown() = post {
currentAnimator?.cancel() currentAnimator?.cancel()
clearAnimation() clearAnimation()
currentState = STATE_DOWN currentState = STATE_DOWN
val target = measureHeight()
if (target == 0) {
return
}
animateTranslation( animateTranslation(
height.toFloat(), target.toFloat(),
SLIDE_DOWN_ANIMATION_DURATION, SLIDE_DOWN_ANIMATION_DURATION,
FastOutLinearInInterpolator(), FastOutLinearInInterpolator(),
) )
@ -102,22 +97,26 @@ class KotatsuBottomNavigationView @JvmOverloads constructor(
.setInterpolator(interpolator) .setInterpolator(interpolator)
.setDuration(duration) .setDuration(duration)
.applySystemAnimatorScale(context) .applySystemAnimatorScale(context)
.setListener(object : AnimatorListenerAdapter() { .setListener(
override fun onAnimationEnd(animation: Animator?) { object : AnimatorListenerAdapter() {
currentAnimator = null override fun onAnimationEnd(animation: Animator?) {
postInvalidate() currentAnimator = null
} postInvalidate()
}, }
},
) )
} }
internal class SavedState : AbsSavedState { internal class SavedState : BaseSavedState {
var currentState = STATE_UP var currentState = STATE_UP
var translationY = 0F var translationY = 0F
constructor(superState: Parcelable) : super(superState) constructor(superState: Parcelable, currentState: Int, translationY: Float) : super(superState) {
this.currentState = currentState
this.translationY = translationY
}
constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) { constructor(source: Parcel) : super(source) {
currentState = source.readInt() currentState = source.readInt()
translationY = source.readFloat() translationY = source.readFloat()
} }
@ -129,28 +128,14 @@ class KotatsuBottomNavigationView @JvmOverloads constructor(
} }
companion object { companion object {
@Suppress("unused")
@JvmField @JvmField
val CREATOR: Parcelable.ClassLoaderCreator<SavedState> = object : Parcelable.ClassLoaderCreator<SavedState> { val CREATOR: Parcelable.Creator<SavedState> = object : Parcelable.Creator<SavedState> {
override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState { override fun createFromParcel(`in`: Parcel) = SavedState(`in`)
return SavedState(source, loader)
} override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
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
}
} }

@ -1,14 +0,0 @@
package org.koitharu.kotatsu.base.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
class SquareLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
public override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
}
}

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.base.ui.widgets package org.koitharu.kotatsu.base.ui.widgets
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity import android.view.Gravity
@ -21,8 +20,7 @@ class WindowInsetHolder @JvmOverloads constructor(
private var desiredHeight = 0 private var desiredHeight = 0
private var desiredWidth = 0 private var desiredWidth = 0
@SuppressLint("RtlHardcoded") override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
val barsInsets = WindowInsetsCompat.toWindowInsetsCompat(insets, this) val barsInsets = WindowInsetsCompat.toWindowInsetsCompat(insets, this)
.getInsets(WindowInsetsCompat.Type.systemBars()) .getInsets(WindowInsetsCompat.Type.systemBars())
val gravity = getLayoutGravity() val gravity = getLayoutGravity()
@ -41,24 +39,26 @@ class WindowInsetHolder @JvmOverloads constructor(
desiredHeight = newHeight desiredHeight = newHeight
requestLayout() requestLayout()
} }
return super.dispatchApplyWindowInsets(insets) return super.onApplyWindowInsets(insets)
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec) val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec) val heightMode = MeasureSpec.getMode(heightMeasureSpec)
super.onMeasure( val heightSize = MeasureSpec.getSize(heightMeasureSpec)
if (desiredWidth == 0 || widthMode == MeasureSpec.EXACTLY) {
widthMeasureSpec val width: Int = when (widthMode) {
} else { MeasureSpec.EXACTLY -> widthSize
MeasureSpec.makeMeasureSpec(desiredWidth, widthMode) MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
}, else -> desiredWidth
if (desiredHeight == 0 || heightMode == MeasureSpec.EXACTLY) { }
heightMeasureSpec val height = when (heightMode) {
} else { MeasureSpec.EXACTLY -> heightSize
MeasureSpec.makeMeasureSpec(desiredHeight, heightMode) MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)
}, else -> desiredHeight
) }
setMeasuredDimension(width, height)
} }
private fun getLayoutGravity(): Int { private fun getLayoutGravity(): Int {

@ -201,16 +201,11 @@ abstract class MangaListFragment :
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding(
left = insets.left,
right = insets.right,
)
binding.recyclerView.updatePadding( binding.recyclerView.updatePadding(
bottom = insets.bottom, bottom = insets.bottom,
) )
binding.recyclerView.fastScroller.updateLayoutParams<MarginLayoutParams> { binding.recyclerView.fastScroller.updateLayoutParams<MarginLayoutParams> {
bottomMargin = insets.bottom bottomMargin = insets.bottom
marginEnd = insets.end(binding.recyclerView)
} }
if (activity is MainActivity) { if (activity is MainActivity) {
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top

@ -1,8 +1,8 @@
package org.koitharu.kotatsu.main.ui package org.koitharu.kotatsu.main.ui
import org.koitharu.kotatsu.base.ui.widgets.KotatsuBottomNavigationView import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
interface BottomNavOwner { interface BottomNavOwner {
val bottomNav: KotatsuBottomNavigationView? val bottomNav: SlidingBottomNavigationView?
} }

@ -7,9 +7,9 @@ import android.view.View
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.IdRes
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.util.size import androidx.core.util.size
import androidx.core.view.* import androidx.core.view.*
@ -21,7 +21,6 @@ import androidx.transition.TransitionManager
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.* import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -29,11 +28,9 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
import org.koitharu.kotatsu.base.ui.widgets.KotatsuBottomNavigationView
import org.koitharu.kotatsu.databinding.ActivityMainBinding import org.koitharu.kotatsu.databinding.ActivityMainBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.explore.ui.ExploreFragment
import org.koitharu.kotatsu.library.ui.LibraryFragment import org.koitharu.kotatsu.library.ui.LibraryFragment
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@ -46,14 +43,11 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import org.koitharu.kotatsu.settings.tools.ToolsFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
import org.koitharu.kotatsu.tracker.ui.FeedFragment
import org.koitharu.kotatsu.tracker.work.TrackWorker import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.VoiceInputContract import org.koitharu.kotatsu.utils.VoiceInputContract
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
private const val TAG_PRIMARY = "primary"
private const val TAG_SEARCH = "search" private const val TAG_SEARCH = "search"
@AndroidEntryPoint @AndroidEntryPoint
@ -64,25 +58,23 @@ class MainActivity :
View.OnClickListener, View.OnClickListener,
View.OnFocusChangeListener, View.OnFocusChangeListener,
SearchSuggestionListener, SearchSuggestionListener,
NavigationBarView.OnItemSelectedListener, MainNavigationDelegate.OnFragmentChangedListener {
NavigationBarView.OnItemReselectedListener {
private val viewModel by viewModels<MainViewModel>() private val viewModel by viewModels<MainViewModel>()
private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>() private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>()
private val voiceInputLauncher = registerForActivityResult(VoiceInputContract(), VoiceInputCallback()) private val voiceInputLauncher = registerForActivityResult(VoiceInputContract(), VoiceInputCallback())
private lateinit var navBar: NavigationBarView private lateinit var navigationDelegate: MainNavigationDelegate
override val appBar: AppBarLayout override val appBar: AppBarLayout
get() = binding.appbar get() = binding.appbar
override val bottomNav: KotatsuBottomNavigationView? override val bottomNav: SlidingBottomNavigationView?
get() = binding.bottomNav get() = binding.bottomNav
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityMainBinding.inflate(layoutInflater)) setContentView(ActivityMainBinding.inflate(layoutInflater))
navBar = checkNotNull(bottomNav ?: binding.navRail)
if (bottomNav != null) { if (bottomNav != null) {
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets -> ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) { if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
@ -98,18 +90,17 @@ class MainActivity :
onFocusChangeListener = this@MainActivity onFocusChangeListener = this@MainActivity
searchSuggestionListener = this@MainActivity searchSuggestionListener = this@MainActivity
} }
window.statusBarColor = ContextCompat.getColor(this, R.color.dim_statusbar)
binding.root.isLiftAppBarOnScroll = false
navBar.setOnItemSelectedListener(this)
navBar.setOnItemReselectedListener(this)
binding.fab?.setOnClickListener(this) binding.fab?.setOnClickListener(this)
binding.navRail?.headerView?.setOnClickListener(this) binding.navRail?.headerView?.setOnClickListener(this)
binding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null binding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null
onBackPressedDispatcher.addCallback(ExitCallback(this, binding.container)) onBackPressedDispatcher.addCallback(ExitCallback(this, binding.container))
supportFragmentManager.findFragmentByTag(TAG_PRIMARY)?.let { navigationDelegate = MainNavigationDelegate(checkNotNull(bottomNav ?: binding.navRail), supportFragmentManager)
if (it is LibraryFragment) binding.fab?.show() else binding.fab?.hide() navigationDelegate.addOnFragmentChangedListener(this)
} ?: onNavigationItemSelected(navBar.selectedItemId) navigationDelegate.onCreate(savedInstanceState)
if (savedInstanceState == null) { if (savedInstanceState == null) {
onFirstStart() onFirstStart()
} }
@ -123,17 +114,7 @@ class MainActivity :
override fun onRestoreInstanceState(savedInstanceState: Bundle) { override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState) super.onRestoreInstanceState(savedInstanceState)
val isSearchOpened = isSearchOpened() adjustSearchUI(isSearchOpened(), animate = false)
if (isSearchOpened) {
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
scrollFlags = SCROLL_FLAG_NO_SCROLL
}
binding.toolbarCard.background = null
binding.appbar.setBackgroundColor(getThemeColor(materialR.attr.colorSurfaceVariant))
binding.appbar.updatePadding(left = 0, right = 0)
supportActionBar?.setHomeAsUpIndicator(materialR.drawable.abc_ic_ab_back_material)
}
adjustFabVisibility(isSearchOpened = isSearchOpened)
} }
override fun onBackPressed() { override fun onBackPressed() {
@ -149,8 +130,15 @@ class MainActivity :
} }
} }
override fun onNavigationItemSelected(item: MenuItem): Boolean { override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
return onNavigationItemSelected(item.itemId) if (fragment is LibraryFragment) {
binding.fab?.show()
} else {
binding.fab?.hide()
}
if (fromUser) {
binding.appbar.setExpanded(true)
}
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -248,36 +236,9 @@ class MainActivity :
showNav(true) showNav(true)
} }
private fun onNavigationItemSelected(@IdRes itemId: Int): Boolean {
when (itemId) {
R.id.nav_library -> {
setPrimaryFragment(LibraryFragment.newInstance())
}
R.id.nav_explore -> {
setPrimaryFragment(ExploreFragment.newInstance())
}
R.id.nav_feed -> {
setPrimaryFragment(FeedFragment.newInstance())
}
R.id.nav_tools -> {
setPrimaryFragment(ToolsFragment.newInstance())
}
else -> return false
}
appBar.setExpanded(true)
appBar.isLifted = false
return true
}
override fun onNavigationItemReselected(item: MenuItem) {
val fragment = supportFragmentManager.findFragmentById(R.id.container) as? RecyclerViewOwner ?: return
val recyclerView = fragment.recyclerView
recyclerView.smoothScrollToPosition(0)
binding.appbar.isLifted = false
}
private fun onOpenReader(manga: Manga) { private fun onOpenReader(manga: Manga) {
val options = binding.fab?.let { val fab = binding.fab ?: binding.navRail?.headerView
val options = fab?.let {
scaleUpActivityOptionsOf(it).toBundle() scaleUpActivityOptionsOf(it).toBundle()
} }
startActivity(ReaderActivity.newIntent(this, manga), options) startActivity(ReaderActivity.newIntent(this, manga), options)
@ -292,17 +253,7 @@ class MainActivity :
repeat(counters.size) { i -> repeat(counters.size) { i ->
val id = counters.keyAt(i) val id = counters.keyAt(i)
val counter = counters.valueAt(i) val counter = counters.valueAt(i)
if (counter == 0) { navigationDelegate.setCounter(id, counter)
navBar.getBadge(id)?.isVisible = false
} else {
val badge = navBar.getOrCreateBadge(id)
if (counter < 0) {
badge.clearNumber()
} else {
badge.number = counter
}
badge.isVisible = true
}
} }
} }
@ -314,55 +265,28 @@ class MainActivity :
adjustFabVisibility(isResumeEnabled = isEnabled) adjustFabVisibility(isResumeEnabled = isEnabled)
} }
private fun setPrimaryFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment, TAG_PRIMARY)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit()
adjustFabVisibility(topFragment = fragment)
}
private fun onSearchOpened() { private fun onSearchOpened() {
TransitionManager.beginDelayedTransition(binding.appbar) adjustSearchUI(isOpened = true, animate = true)
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
scrollFlags = SCROLL_FLAG_NO_SCROLL
}
binding.toolbarCard.background = null
binding.appbar.isLifted = true
binding.appbar.updatePadding(left = 0, right = 0)
adjustFabVisibility(isSearchOpened = true)
supportActionBar?.setHomeAsUpIndicator(materialR.drawable.abc_ic_ab_back_material)
showNav(false)
} }
private fun onSearchClosed() { private fun onSearchClosed() {
binding.searchView.hideKeyboard() binding.searchView.hideKeyboard()
TransitionManager.beginDelayedTransition(binding.appbar) adjustSearchUI(isOpened = false, animate = true)
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP
}
binding.toolbarCard.setBackgroundResource(R.drawable.toolbar_background)
binding.appbar.isLifted = false
val padding = resources.getDimensionPixelOffset(R.dimen.margin_normal)
binding.appbar.updatePadding(left = padding, right = padding)
adjustFabVisibility(isSearchOpened = false)
supportActionBar?.setHomeAsUpIndicator(materialR.drawable.abc_ic_search_api_material)
showNav(true)
} }
private fun showNav(visible: Boolean) { private fun showNav(visible: Boolean) {
bottomNav?.run { bottomNav?.run {
if (visible) { if (visible) {
slideUp() show()
} else { } else {
slideDown() hide()
} }
} }
binding.navRail?.isVisible = visible binding.navRail?.isVisible = visible
} }
private fun isSearchOpened(): Boolean { private fun isSearchOpened(): Boolean {
return supportFragmentManager.findFragmentByTag(TAG_SEARCH)?.isVisible == true return supportFragmentManager.findFragmentByTag(TAG_SEARCH) != null
} }
private fun onFirstStart() { private fun onFirstStart() {
@ -382,7 +306,7 @@ class MainActivity :
private fun adjustFabVisibility( private fun adjustFabVisibility(
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true, isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true,
topFragment: Fragment? = supportFragmentManager.findFragmentByTag(TAG_PRIMARY), topFragment: Fragment? = navigationDelegate.primaryFragment,
isSearchOpened: Boolean = isSearchOpened(), isSearchOpened: Boolean = isSearchOpened(),
) { ) {
val fab = binding.fab val fab = binding.fab
@ -402,6 +326,31 @@ class MainActivity :
} }
} }
private fun adjustSearchUI(isOpened: Boolean, animate: Boolean) {
if (animate) {
TransitionManager.beginDelayedTransition(binding.appbar)
}
val appBarScrollFlags = if (isOpened) {
SCROLL_FLAG_NO_SCROLL
} else {
SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP
}
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> { scrollFlags = appBarScrollFlags }
binding.insetsHolder.updateLayoutParams<AppBarLayout.LayoutParams> { scrollFlags = appBarScrollFlags }
binding.toolbarCard.background = if (isOpened) {
null
} else {
ContextCompat.getDrawable(this, R.drawable.toolbar_background)
}
val padding = if (isOpened) 0 else resources.getDimensionPixelOffset(R.dimen.margin_normal)
binding.appbar.updatePadding(left = padding, right = padding)
adjustFabVisibility(isSearchOpened = isOpened)
supportActionBar?.setHomeAsUpIndicator(
if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material,
)
showNav(!isOpened)
}
private inner class VoiceInputCallback : ActivityResultCallback<String?> { private inner class VoiceInputCallback : ActivityResultCallback<String?> {
override fun onActivityResult(result: String?) { override fun onActivityResult(result: String?) {

@ -0,0 +1,102 @@
package org.koitharu.kotatsu.main.ui
import android.os.Bundle
import android.view.MenuItem
import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import com.google.android.material.navigation.NavigationBarView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.explore.ui.ExploreFragment
import org.koitharu.kotatsu.library.ui.LibraryFragment
import org.koitharu.kotatsu.settings.tools.ToolsFragment
import org.koitharu.kotatsu.tracker.ui.FeedFragment
import java.util.*
private const val TAG_PRIMARY = "primary"
class MainNavigationDelegate(
private val navBar: NavigationBarView,
private val fragmentManager: FragmentManager,
) : NavigationBarView.OnItemSelectedListener, NavigationBarView.OnItemReselectedListener {
private val listeners = LinkedList<OnFragmentChangedListener>()
val primaryFragment: Fragment?
get() = fragmentManager.findFragmentByTag(TAG_PRIMARY)
init {
navBar.setOnItemSelectedListener(this)
navBar.setOnItemReselectedListener(this)
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
return onNavigationItemSelected(item.itemId)
}
override fun onNavigationItemReselected(item: MenuItem) {
val fragment = fragmentManager.findFragmentByTag(TAG_PRIMARY) as? RecyclerViewOwner ?: return
val recyclerView = fragment.recyclerView
recyclerView.smoothScrollToPosition(0)
}
fun onCreate(savedInstanceState: Bundle?) {
primaryFragment?.let {
onFragmentChanged(it, fromUser = false)
} ?: onNavigationItemSelected(navBar.selectedItemId)
}
fun setCounter(@IdRes id: Int, counter: Int) {
if (counter == 0) {
navBar.getBadge(id)?.isVisible = false
} else {
val badge = navBar.getOrCreateBadge(id)
if (counter < 0) {
badge.clearNumber()
} else {
badge.number = counter
}
badge.isVisible = true
}
}
fun addOnFragmentChangedListener(listener: OnFragmentChangedListener) {
listeners.add(listener)
}
fun removeOnFragmentChangedListener(listener: OnFragmentChangedListener) {
listeners.remove(listener)
}
private fun onNavigationItemSelected(@IdRes itemId: Int): Boolean {
setPrimaryFragment(
when (itemId) {
R.id.nav_library -> LibraryFragment.newInstance()
R.id.nav_explore -> ExploreFragment.newInstance()
R.id.nav_feed -> FeedFragment.newInstance()
R.id.nav_tools -> ToolsFragment.newInstance()
else -> return false
},
)
return true
}
private fun setPrimaryFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.replace(R.id.container, fragment, TAG_PRIMARY)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit()
onFragmentChanged(fragment, fromUser = true)
}
private fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
listeners.forEach { it.onFragmentChanged(fragment, fromUser) }
}
interface OnFragmentChangedListener {
fun onFragmentChanged(fragment: Fragment, fromUser: Boolean)
}
}

@ -7,6 +7,7 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.ImageLoader import coil.ImageLoader
@ -53,6 +54,7 @@ class MultiSearchActivity :
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivitySearchMultiBinding.inflate(layoutInflater)) setContentView(ActivitySearchMultiBinding.inflate(layoutInflater))
window.statusBarColor = ContextCompat.getColor(this, R.color.dim_statusbar)
val itemCLickListener = object : OnListItemClickListener<MultiSearchListModel> { val itemCLickListener = object : OnListItemClickListener<MultiSearchListModel> {
override fun onItemClick(item: MultiSearchListModel, view: View) { override fun onItemClick(item: MultiSearchListModel, view: View) {

@ -1,28 +1,51 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".main.ui.MainActivity"> tools:context=".main.ui.MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout <com.google.android.material.navigationrail.NavigationRailView
android:id="@+id/navRail"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:elevation="1dp"
app:headerLayout="@layout/navigation_rail_fab"
app:labelVisibilityMode="labeled"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/nav_bottom" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.KotatsuAppBarLayout <androidx.fragment.app.FragmentContainerView
android:id="@id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:layout="@layout/fragment_list" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar" android:id="@+id/appbar"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:clipToPadding="false"
android:fitsSystemWindows="false"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingRight="16dp" android:paddingRight="16dp"
android:clipToPadding="false"
android:stateListAnimator="@null" android:stateListAnimator="@null"
app:layout_constraintEnd_toEndOf="parent" app:liftOnScroll="false">
app:layout_constraintStart_toEndOf="@id/navRail"
app:layout_constraintTop_toTopOf="parent"> <org.koitharu.kotatsu.base.ui.widgets.WindowInsetHolder
android:id="@+id/insetsHolder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
app:layout_scrollFlags="scroll|enterAlways|snap" />
<FrameLayout <FrameLayout
android:id="@+id/toolbar_card" android:id="@+id/toolbar_card"
@ -30,7 +53,8 @@
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginVertical="8dp" android:layout_marginVertical="8dp"
android:background="@drawable/toolbar_background" android:background="@drawable/toolbar_background"
android:theme="@style/ThemeOverlay.Kotatsu.MainToolbar"> android:theme="@style/ThemeOverlay.Kotatsu.MainToolbar"
app:layout_scrollFlags="scroll|enterAlways|snap">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar" android:id="@id/toolbar"
@ -51,7 +75,7 @@
android:background="@null" android:background="@null"
android:gravity="center_vertical" android:gravity="center_vertical"
android:hint="@string/search_manga" android:hint="@string/search_manga"
android:imeOptions="actionSearch" android:imeOptions="actionSearch|flagNoFullscreen"
android:importantForAutofill="no" android:importantForAutofill="no"
android:singleLine="true" android:singleLine="true"
tools:drawableEnd="@drawable/abc_ic_clear_material" /> tools:drawableEnd="@drawable/abc_ic_clear_material" />
@ -60,28 +84,8 @@
</FrameLayout> </FrameLayout>
</com.google.android.material.appbar.KotatsuAppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.navigationrail.NavigationRailView
android:id="@+id/navRail"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:elevation="1dp"
app:headerLayout="@layout/navigation_rail_fab"
app:labelVisibilityMode="labeled"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/nav_bottom" />
<androidx.fragment.app.FragmentContainerView
android:id="@id/container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/navRail"
app:layout_constraintTop_toBottomOf="@id/appbar"
tools:layout="@layout/fragment_list" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout> </LinearLayout>

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@ -14,14 +14,22 @@
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:layout="@layout/fragment_list" /> tools:layout="@layout/fragment_list" />
<com.google.android.material.appbar.KotatsuAppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar" android:id="@+id/appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
android:fitsSystemWindows="true" android:fitsSystemWindows="false"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:stateListAnimator="@null"> android:stateListAnimator="@null"
app:liftOnScroll="false">
<org.koitharu.kotatsu.base.ui.widgets.WindowInsetHolder
android:id="@+id/insetsHolder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
app:layout_scrollFlags="scroll|enterAlways|snap" />
<FrameLayout <FrameLayout
android:id="@+id/toolbar_card" android:id="@+id/toolbar_card"
@ -60,7 +68,7 @@
</FrameLayout> </FrameLayout>
</com.google.android.material.appbar.KotatsuAppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab" android:id="@+id/fab"
@ -68,6 +76,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginBottom="-4dp" android:layout_marginBottom="-4dp"
android:paddingBottom="8dp"
android:text="@string/_continue" android:text="@string/_continue"
android:visibility="gone" android:visibility="gone"
app:backgroundTint="?attr/colorContainer" app:backgroundTint="?attr/colorContainer"
@ -75,12 +84,11 @@
app:layout_anchor="@id/bottomNav" app:layout_anchor="@id/bottomNav"
app:layout_anchorGravity="top|end" app:layout_anchorGravity="top|end"
app:layout_behavior="org.koitharu.kotatsu.base.ui.util.ShrinkOnScrollBehavior" app:layout_behavior="org.koitharu.kotatsu.base.ui.util.ShrinkOnScrollBehavior"
app:layout_insetEdge="bottom"
app:layout_dodgeInsetEdges="bottom" app:layout_dodgeInsetEdges="bottom"
android:paddingBottom="8dp" app:layout_insetEdge="bottom"
tools:visibility="visible" /> tools:visibility="visible" />
<org.koitharu.kotatsu.base.ui.widgets.KotatsuBottomNavigationView <org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
android:id="@+id/bottomNav" android:id="@+id/bottomNav"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -90,4 +98,4 @@
app:menu="@menu/nav_bottom" app:menu="@menu/nav_bottom"
tools:ignore="KeyboardInaccessibleWidget" /> tools:ignore="KeyboardInaccessibleWidget" />
</org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.KotatsuAppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar" android:id="@+id/appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -29,7 +29,7 @@
</com.google.android.material.appbar.MaterialToolbar> </com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.KotatsuAppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@id/container" android:id="@id/container"
@ -37,4 +37,4 @@
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -1,16 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.KotatsuAppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar" android:id="@+id/appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:fitsSystemWindows="false"
app:elevation="0dp"> app:elevation="0dp"
app:liftOnScroll="false">
<org.koitharu.kotatsu.base.ui.widgets.WindowInsetHolder
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
app:layout_scrollFlags="scroll|enterAlways|snap" />
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar" android:id="@id/toolbar"
@ -18,7 +25,7 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways" /> app:layout_scrollFlags="scroll|enterAlways" />
</com.google.android.material.appbar.KotatsuAppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@id/recyclerView" android:id="@id/recyclerView"
@ -28,4 +35,4 @@
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/railFab" android:id="@+id/railFab"
android:theme="@style/ThemeOverlay.Material3.FloatingActionButton.Tertiary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/_continue"
app:srcCompat="@drawable/ic_read" /> app:srcCompat="@drawable/ic_read" />

@ -8,5 +8,6 @@
<color name="scrollbar">#66FFFFFF</color> <color name="scrollbar">#66FFFFFF</color>
<color name="selector_foreground">#29FFFFFF</color> <color name="selector_foreground">#29FFFFFF</color>
<color name="divider_default">#1FFFFFFF</color> <color name="divider_default">#1FFFFFFF</color>
<color name="dim_statusbar">#99000000</color>
</resources> </resources>

@ -18,6 +18,7 @@
<color name="grey">#424242</color> <color name="grey">#424242</color>
<color name="grey_dark">#212121</color> <color name="grey_dark">#212121</color>
<color name="dim">#99000000</color> <color name="dim">#99000000</color>
<color name="dim_statusbar">#99FFFFFF</color>
<color name="scrollbar">#66000000</color> <color name="scrollbar">#66000000</color>
<color name="selector_foreground">#29000000</color> <color name="selector_foreground">#29000000</color>
<color name="divider_default">#1F000000</color> <color name="divider_default">#1F000000</color>

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<integer name="config_defaultAnimTime">300</integer> <integer name="config_defaultAnimTime">300</integer>
<integer name="config_shorterAnimTime">150</integer>
<integer name="config_tinyAnimTime">50</integer> <integer name="config_tinyAnimTime">50</integer>
<integer name="manga_badge_max_character_count">3</integer> <integer name="manga_badge_max_character_count">3</integer>

Loading…
Cancel
Save