Refactor MainActivity navigation and AppBars behavior
This commit is contained in:
@@ -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.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 org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
context: Context? = null,
|
||||
@@ -90,7 +87,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
offsetAnimator?.cancel()
|
||||
offsetAnimator = ValueAnimator().apply {
|
||||
interpolator = DecelerateInterpolator()
|
||||
duration = (150 * child.context.animatorDurationScale).roundToLong()
|
||||
duration = child.context.getAnimationDuration(R.integer.config_shorterAnimTime)
|
||||
addUpdateListener {
|
||||
child.translationY = it.animatedValue as Float
|
||||
}
|
||||
@@ -101,4 +98,4 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
|
||||
)
|
||||
offsetAnimator?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.util.AttributeSet
|
||||
import android.view.ViewPropertyAnimator
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StyleRes
|
||||
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.R as materialR
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import org.koitharu.kotatsu.utils.ext.applySystemAnimatorScale
|
||||
import com.google.android.material.R as materialR
|
||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||
|
||||
class KotatsuBottomNavigationView @JvmOverloads constructor(
|
||||
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 SlidingBottomNavigationView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = materialR.attr.bottomNavigationStyle,
|
||||
defStyleRes: Int = materialR.style.Widget_Design_BottomNavigationView,
|
||||
) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes) {
|
||||
@AttrRes defStyleAttr: Int = materialR.attr.bottomNavigationStyle,
|
||||
@StyleRes defStyleRes: Int = materialR.style.Widget_Design_BottomNavigationView,
|
||||
) : BottomNavigationView(context, attrs, defStyleAttr, defStyleRes),
|
||||
CoordinatorLayout.AttachedBehavior {
|
||||
|
||||
private var currentAnimator: ViewPropertyAnimator? = null
|
||||
|
||||
private var currentState = STATE_UP
|
||||
private var behavior = HideBottomNavigationOnScrollBehavior()
|
||||
|
||||
init {
|
||||
// Hide on scroll
|
||||
doOnLayout {
|
||||
findViewTreeLifecycleOwner()?.lifecycleScope?.let {
|
||||
updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
behavior = HideBottomNavigationOnScrollBehavior()
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun getBehavior(): CoordinatorLayout.Behavior<*> {
|
||||
return behavior
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superState = super.onSaveInstanceState()
|
||||
return SavedState(superState).also {
|
||||
it.currentState = currentState
|
||||
it.translationY = translationY
|
||||
}
|
||||
return SavedState(superState, currentState, translationY)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
@@ -62,14 +58,12 @@ class KotatsuBottomNavigationView @JvmOverloads constructor(
|
||||
|
||||
override fun setTranslationY(translationY: Float) {
|
||||
// Disallow translation change when state down
|
||||
if (currentState == STATE_DOWN) return
|
||||
super.setTranslationY(translationY)
|
||||
if (currentState != STATE_DOWN) {
|
||||
super.setTranslationY(translationY)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows this view up.
|
||||
*/
|
||||
fun slideUp() = post {
|
||||
fun show() {
|
||||
currentAnimator?.cancel()
|
||||
clearAnimation()
|
||||
|
||||
@@ -81,16 +75,17 @@ class KotatsuBottomNavigationView @JvmOverloads constructor(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides this view down. [setTranslationY] won't work until [slideUp] is called.
|
||||
*/
|
||||
fun slideDown() = post {
|
||||
fun hide() {
|
||||
currentAnimator?.cancel()
|
||||
clearAnimation()
|
||||
|
||||
currentState = STATE_DOWN
|
||||
val target = measureHeight()
|
||||
if (target == 0) {
|
||||
return
|
||||
}
|
||||
animateTranslation(
|
||||
height.toFloat(),
|
||||
target.toFloat(),
|
||||
SLIDE_DOWN_ANIMATION_DURATION,
|
||||
FastOutLinearInInterpolator(),
|
||||
)
|
||||
@@ -102,22 +97,26 @@ class KotatsuBottomNavigationView @JvmOverloads constructor(
|
||||
.setInterpolator(interpolator)
|
||||
.setDuration(duration)
|
||||
.applySystemAnimatorScale(context)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
currentAnimator = null
|
||||
postInvalidate()
|
||||
}
|
||||
},
|
||||
.setListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
currentAnimator = null
|
||||
postInvalidate()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal class SavedState : AbsSavedState {
|
||||
internal class SavedState : BaseSavedState {
|
||||
var currentState = STATE_UP
|
||||
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()
|
||||
translationY = source.readFloat()
|
||||
}
|
||||
@@ -129,28 +128,14 @@ class KotatsuBottomNavigationView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.ClassLoaderCreator<SavedState> = object : Parcelable.ClassLoaderCreator<SavedState> {
|
||||
override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState {
|
||||
return SavedState(source, loader)
|
||||
}
|
||||
val CREATOR: Parcelable.Creator<SavedState> = object : Parcelable.Creator<SavedState> {
|
||||
override fun createFromParcel(`in`: Parcel) = SavedState(`in`)
|
||||
|
||||
override fun createFromParcel(source: Parcel): SavedState {
|
||||
return SavedState(source, null)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SavedState> {
|
||||
return newArray(size)
|
||||
}
|
||||
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
@@ -21,8 +20,7 @@ class WindowInsetHolder @JvmOverloads constructor(
|
||||
private var desiredHeight = 0
|
||||
private var desiredWidth = 0
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||
val barsInsets = WindowInsetsCompat.toWindowInsetsCompat(insets, this)
|
||||
.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val gravity = getLayoutGravity()
|
||||
@@ -41,24 +39,26 @@ class WindowInsetHolder @JvmOverloads constructor(
|
||||
desiredHeight = newHeight
|
||||
requestLayout()
|
||||
}
|
||||
return super.dispatchApplyWindowInsets(insets)
|
||||
return super.onApplyWindowInsets(insets)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
||||
super.onMeasure(
|
||||
if (desiredWidth == 0 || widthMode == MeasureSpec.EXACTLY) {
|
||||
widthMeasureSpec
|
||||
} else {
|
||||
MeasureSpec.makeMeasureSpec(desiredWidth, widthMode)
|
||||
},
|
||||
if (desiredHeight == 0 || heightMode == MeasureSpec.EXACTLY) {
|
||||
heightMeasureSpec
|
||||
} else {
|
||||
MeasureSpec.makeMeasureSpec(desiredHeight, heightMode)
|
||||
},
|
||||
)
|
||||
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
|
||||
|
||||
val width: Int = when (widthMode) {
|
||||
MeasureSpec.EXACTLY -> widthSize
|
||||
MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
|
||||
else -> desiredWidth
|
||||
}
|
||||
val height = when (heightMode) {
|
||||
MeasureSpec.EXACTLY -> heightSize
|
||||
MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)
|
||||
else -> desiredHeight
|
||||
}
|
||||
setMeasuredDimension(width, height)
|
||||
}
|
||||
|
||||
private fun getLayoutGravity(): Int {
|
||||
@@ -69,4 +69,4 @@ class WindowInsetHolder @JvmOverloads constructor(
|
||||
else -> Gravity.NO_GRAVITY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,16 +201,11 @@ abstract class MangaListFragment :
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
binding.recyclerView.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
binding.recyclerView.fastScroller.updateLayoutParams<MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom
|
||||
marginEnd = insets.end(binding.recyclerView)
|
||||
}
|
||||
if (activity is MainActivity) {
|
||||
val headerHeight = (activity as? AppBarOwner)?.appBar?.measureHeight() ?: insets.top
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.koitharu.kotatsu.main.ui
|
||||
|
||||
import org.koitharu.kotatsu.base.ui.widgets.KotatsuBottomNavigationView
|
||||
import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
|
||||
|
||||
interface BottomNavOwner {
|
||||
|
||||
val bottomNav: KotatsuBottomNavigationView?
|
||||
val bottomNav: SlidingBottomNavigationView?
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.util.size
|
||||
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.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.AppBarLayout.LayoutParams.*
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -29,11 +28,9 @@ import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.base.ui.widgets.KotatsuBottomNavigationView
|
||||
import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
|
||||
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||
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.parsers.model.Manga
|
||||
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.settings.newsources.NewSourcesDialogFragment
|
||||
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.tracker.ui.FeedFragment
|
||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||
import org.koitharu.kotatsu.utils.VoiceInputContract
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
private const val TAG_PRIMARY = "primary"
|
||||
private const val TAG_SEARCH = "search"
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -64,25 +58,23 @@ class MainActivity :
|
||||
View.OnClickListener,
|
||||
View.OnFocusChangeListener,
|
||||
SearchSuggestionListener,
|
||||
NavigationBarView.OnItemSelectedListener,
|
||||
NavigationBarView.OnItemReselectedListener {
|
||||
MainNavigationDelegate.OnFragmentChangedListener {
|
||||
|
||||
private val viewModel by viewModels<MainViewModel>()
|
||||
private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>()
|
||||
private val voiceInputLauncher = registerForActivityResult(VoiceInputContract(), VoiceInputCallback())
|
||||
private lateinit var navBar: NavigationBarView
|
||||
private lateinit var navigationDelegate: MainNavigationDelegate
|
||||
|
||||
override val appBar: AppBarLayout
|
||||
get() = binding.appbar
|
||||
|
||||
override val bottomNav: KotatsuBottomNavigationView?
|
||||
override val bottomNav: SlidingBottomNavigationView?
|
||||
get() = binding.bottomNav
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityMainBinding.inflate(layoutInflater))
|
||||
|
||||
navBar = checkNotNull(bottomNav ?: binding.navRail)
|
||||
if (bottomNav != null) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
|
||||
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
|
||||
@@ -98,18 +90,17 @@ class MainActivity :
|
||||
onFocusChangeListener = 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.navRail?.headerView?.setOnClickListener(this)
|
||||
binding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null
|
||||
|
||||
onBackPressedDispatcher.addCallback(ExitCallback(this, binding.container))
|
||||
supportFragmentManager.findFragmentByTag(TAG_PRIMARY)?.let {
|
||||
if (it is LibraryFragment) binding.fab?.show() else binding.fab?.hide()
|
||||
} ?: onNavigationItemSelected(navBar.selectedItemId)
|
||||
navigationDelegate = MainNavigationDelegate(checkNotNull(bottomNav ?: binding.navRail), supportFragmentManager)
|
||||
navigationDelegate.addOnFragmentChangedListener(this)
|
||||
navigationDelegate.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
onFirstStart()
|
||||
}
|
||||
@@ -123,17 +114,7 @@ class MainActivity :
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
val isSearchOpened = isSearchOpened()
|
||||
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)
|
||||
adjustSearchUI(isSearchOpened(), animate = false)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
@@ -149,8 +130,15 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
return onNavigationItemSelected(item.itemId)
|
||||
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
|
||||
if (fragment is LibraryFragment) {
|
||||
binding.fab?.show()
|
||||
} else {
|
||||
binding.fab?.hide()
|
||||
}
|
||||
if (fromUser) {
|
||||
binding.appbar.setExpanded(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -248,36 +236,9 @@ class MainActivity :
|
||||
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) {
|
||||
val options = binding.fab?.let {
|
||||
val fab = binding.fab ?: binding.navRail?.headerView
|
||||
val options = fab?.let {
|
||||
scaleUpActivityOptionsOf(it).toBundle()
|
||||
}
|
||||
startActivity(ReaderActivity.newIntent(this, manga), options)
|
||||
@@ -292,17 +253,7 @@ class MainActivity :
|
||||
repeat(counters.size) { i ->
|
||||
val id = counters.keyAt(i)
|
||||
val counter = counters.valueAt(i)
|
||||
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
|
||||
}
|
||||
navigationDelegate.setCounter(id, counter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,55 +265,28 @@ class MainActivity :
|
||||
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() {
|
||||
TransitionManager.beginDelayedTransition(binding.appbar)
|
||||
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)
|
||||
adjustSearchUI(isOpened = true, animate = true)
|
||||
}
|
||||
|
||||
private fun onSearchClosed() {
|
||||
binding.searchView.hideKeyboard()
|
||||
TransitionManager.beginDelayedTransition(binding.appbar)
|
||||
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)
|
||||
adjustSearchUI(isOpened = false, animate = true)
|
||||
}
|
||||
|
||||
private fun showNav(visible: Boolean) {
|
||||
bottomNav?.run {
|
||||
if (visible) {
|
||||
slideUp()
|
||||
show()
|
||||
} else {
|
||||
slideDown()
|
||||
hide()
|
||||
}
|
||||
}
|
||||
binding.navRail?.isVisible = visible
|
||||
}
|
||||
|
||||
private fun isSearchOpened(): Boolean {
|
||||
return supportFragmentManager.findFragmentByTag(TAG_SEARCH)?.isVisible == true
|
||||
return supportFragmentManager.findFragmentByTag(TAG_SEARCH) != null
|
||||
}
|
||||
|
||||
private fun onFirstStart() {
|
||||
@@ -382,7 +306,7 @@ class MainActivity :
|
||||
|
||||
private fun adjustFabVisibility(
|
||||
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true,
|
||||
topFragment: Fragment? = supportFragmentManager.findFragmentByTag(TAG_PRIMARY),
|
||||
topFragment: Fragment? = navigationDelegate.primaryFragment,
|
||||
isSearchOpened: Boolean = isSearchOpened(),
|
||||
) {
|
||||
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?> {
|
||||
|
||||
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.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
@@ -53,6 +54,7 @@ class MultiSearchActivity :
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivitySearchMultiBinding.inflate(layoutInflater))
|
||||
window.statusBarColor = ContextCompat.getColor(this, R.color.dim_statusbar)
|
||||
|
||||
val itemCLickListener = object : OnListItemClickListener<MultiSearchListModel> {
|
||||
override fun onItemClick(item: MultiSearchListModel, view: View) {
|
||||
|
||||
@@ -1,28 +1,51 @@
|
||||
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
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_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:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:clipToPadding="false"
|
||||
android:fitsSystemWindows="false"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:clipToPadding="false"
|
||||
android:stateListAnimator="@null"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/navRail"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
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
|
||||
android:id="@+id/toolbar_card"
|
||||
@@ -30,7 +53,8 @@
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginVertical="8dp"
|
||||
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
|
||||
android:id="@id/toolbar"
|
||||
@@ -51,7 +75,7 @@
|
||||
android:background="@null"
|
||||
android:gravity="center_vertical"
|
||||
android:hint="@string/search_manga"
|
||||
android:imeOptions="actionSearch"
|
||||
android:imeOptions="actionSearch|flagNoFullscreen"
|
||||
android:importantForAutofill="no"
|
||||
android:singleLine="true"
|
||||
tools:drawableEnd="@drawable/abc_ic_clear_material" />
|
||||
@@ -60,28 +84,8 @@
|
||||
|
||||
</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.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<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>
|
||||
|
||||
</org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
@@ -14,14 +14,22 @@
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
tools:layout="@layout/fragment_list" />
|
||||
|
||||
<com.google.android.material.appbar.KotatsuAppBarLayout
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:fitsSystemWindows="true"
|
||||
android:fitsSystemWindows="false"
|
||||
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
|
||||
android:id="@+id/toolbar_card"
|
||||
@@ -60,7 +68,7 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.appbar.KotatsuAppBarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
@@ -68,6 +76,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="-4dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/_continue"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="?attr/colorContainer"
|
||||
@@ -75,12 +84,11 @@
|
||||
app:layout_anchor="@id/bottomNav"
|
||||
app:layout_anchorGravity="top|end"
|
||||
app:layout_behavior="org.koitharu.kotatsu.base.ui.util.ShrinkOnScrollBehavior"
|
||||
app:layout_insetEdge="bottom"
|
||||
app:layout_dodgeInsetEdges="bottom"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_insetEdge="bottom"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.koitharu.kotatsu.base.ui.widgets.KotatsuBottomNavigationView
|
||||
<org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
|
||||
android:id="@+id/bottomNav"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -90,4 +98,4 @@
|
||||
app:menu="@menu/nav_bottom"
|
||||
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"?>
|
||||
<org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.KotatsuAppBarLayout
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.KotatsuAppBarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@id/container"
|
||||
@@ -37,4 +37,4 @@
|
||||
android:layout_height="match_parent"
|
||||
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"?>
|
||||
<org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.KotatsuAppBarLayout
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:elevation="0dp">
|
||||
android:fitsSystemWindows="false"
|
||||
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
|
||||
android:id="@id/toolbar"
|
||||
@@ -18,7 +25,7 @@
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
|
||||
</com.google.android.material.appbar.KotatsuAppBarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@id/recyclerView"
|
||||
@@ -28,4 +35,4 @@
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
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:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/railFab"
|
||||
android:theme="@style/ThemeOverlay.Material3.FloatingActionButton.Tertiary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_read" />
|
||||
android:contentDescription="@string/_continue"
|
||||
app:srcCompat="@drawable/ic_read" />
|
||||
|
||||
@@ -8,5 +8,6 @@
|
||||
<color name="scrollbar">#66FFFFFF</color>
|
||||
<color name="selector_foreground">#29FFFFFF</color>
|
||||
<color name="divider_default">#1FFFFFFF</color>
|
||||
<color name="dim_statusbar">#99000000</color>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
<color name="grey">#424242</color>
|
||||
<color name="grey_dark">#212121</color>
|
||||
<color name="dim">#99000000</color>
|
||||
<color name="dim_statusbar">#99FFFFFF</color>
|
||||
<color name="scrollbar">#66000000</color>
|
||||
<color name="selector_foreground">#29000000</color>
|
||||
<color name="divider_default">#1F000000</color>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="config_defaultAnimTime">300</integer>
|
||||
<integer name="config_shorterAnimTime">150</integer>
|
||||
<integer name="config_tinyAnimTime">50</integer>
|
||||
|
||||
<integer name="manga_badge_max_character_count">3</integer>
|
||||
|
||||
Reference in New Issue
Block a user