Refactor MainActivity navigation and AppBars behavior

This commit is contained in:
Koitharu
2022-08-01 10:33:58 +03:00
parent 656405edbc
commit 8b0f221eef
21 changed files with 350 additions and 666 deletions

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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

View File

@@ -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?
}

View File

@@ -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?) {

View File

@@ -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)
}
}

View File

@@ -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) {