Read button tip in DetailsActivity
This commit is contained in:
@@ -45,7 +45,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
protected val exceptionResolver = ExceptionResolver(this)
|
||||
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
|
||||
@JvmField
|
||||
val actionModeDelegate = ActionModeDelegate()
|
||||
@@ -62,6 +62,7 @@ abstract class BaseActivity<B : ViewBinding> :
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
insetsDelegate.handleImeInsets = true
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
putDataToExtras(intent)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ abstract class BaseFragment<B : ViewBinding> :
|
||||
protected val exceptionResolver = ExceptionResolver(this)
|
||||
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
|
||||
protected val actionModeDelegate: ActionModeDelegate
|
||||
get() = (requireActivity() as BaseActivity<*>).actionModeDelegate
|
||||
@@ -44,11 +44,13 @@ abstract class BaseFragment<B : ViewBinding> :
|
||||
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
insetsDelegate.onViewCreated(view)
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
onViewBindingCreated(requireViewBinding(), savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewBinding = null
|
||||
insetsDelegate.removeInsetsListener(this)
|
||||
insetsDelegate.onDestroyView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
@JvmField
|
||||
protected val insetsDelegate = WindowInsetsDelegate(this)
|
||||
protected val insetsDelegate = WindowInsetsDelegate()
|
||||
|
||||
override val recyclerView: RecyclerView
|
||||
get() = listView
|
||||
@@ -35,9 +35,11 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
listView.clipToPadding = false
|
||||
insetsDelegate.onViewCreated(view)
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
insetsDelegate.removeInsetsListener(this)
|
||||
insetsDelegate.onDestroyView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
|
||||
get() = activeActionMode != null
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
activeActionMode?.finish()
|
||||
finishActionMode()
|
||||
}
|
||||
|
||||
fun onSupportActionModeStarted(mode: ActionMode) {
|
||||
@@ -45,6 +45,10 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
|
||||
owner.lifecycle.addObserver(ListenerLifecycleObserver(listener))
|
||||
}
|
||||
|
||||
fun finishActionMode() {
|
||||
activeActionMode?.finish()
|
||||
}
|
||||
|
||||
private inner class ListenerLifecycleObserver(
|
||||
private val listener: ActionModeListener,
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
@@ -5,10 +5,9 @@ import androidx.core.graphics.Insets
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import java.util.LinkedList
|
||||
|
||||
class WindowInsetsDelegate(
|
||||
private val listener: WindowInsetsListener,
|
||||
) : OnApplyWindowInsetsListener, View.OnLayoutChangeListener {
|
||||
class WindowInsetsDelegate : OnApplyWindowInsetsListener, View.OnLayoutChangeListener {
|
||||
|
||||
@JvmField
|
||||
var handleImeInsets: Boolean = false
|
||||
@@ -16,6 +15,7 @@ class WindowInsetsDelegate(
|
||||
@JvmField
|
||||
var interceptingWindowInsetsListener: OnApplyWindowInsetsListener? = null
|
||||
|
||||
private val listeners = LinkedList<WindowInsetsListener>()
|
||||
private var lastInsets: Insets? = null
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
@@ -29,7 +29,7 @@ class WindowInsetsDelegate(
|
||||
handledInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
if (newInsets != lastInsets) {
|
||||
listener.onWindowInsetsChanged(newInsets)
|
||||
listeners.forEach { it.onWindowInsetsChanged(newInsets) }
|
||||
lastInsets = newInsets
|
||||
}
|
||||
return handledInsets
|
||||
@@ -52,6 +52,15 @@ class WindowInsetsDelegate(
|
||||
}
|
||||
}
|
||||
|
||||
fun addInsetsListener(listener: WindowInsetsListener) {
|
||||
listeners.add(listener)
|
||||
lastInsets?.let { listener.onWindowInsetsChanged(it) }
|
||||
}
|
||||
|
||||
fun removeInsetsListener(listener: WindowInsetsListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
fun onViewCreated(view: View) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view, this)
|
||||
view.addOnLayoutChangeListener(this)
|
||||
|
||||
@@ -92,6 +92,14 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun showOrHide(show: Boolean) {
|
||||
if (show) {
|
||||
show()
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) {
|
||||
currentAnimator = animate()
|
||||
.translationY(targetY)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.transition.TransitionManager
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.setMargins
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelSize
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.databinding.ItemTipBinding
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class ButtonTip(
|
||||
private val root: ViewGroup,
|
||||
private val insetsDelegate: WindowInsetsDelegate,
|
||||
private val viewModel: DetailsViewModel,
|
||||
) : View.OnClickListener, WindowInsetsDelegate.WindowInsetsListener {
|
||||
|
||||
private var selfBinding = ItemTipBinding.inflate(LayoutInflater.from(root.context), root, false)
|
||||
private val actionBarSize = root.context.getThemeDimensionPixelSize(materialR.attr.actionBarSize)
|
||||
|
||||
init {
|
||||
selfBinding.textView.setText(R.string.details_button_tip)
|
||||
selfBinding.imageViewIcon.setImageResource(R.drawable.ic_tap)
|
||||
selfBinding.root.id = R.id.layout_tip
|
||||
selfBinding.buttonClose.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
remove()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
if (root is CoordinatorLayout) {
|
||||
selfBinding.root.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||
bottomMargin = topMargin + insets.bottom + insets.top + actionBarSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addToRoot() {
|
||||
val lp: ViewGroup.LayoutParams = when (root) {
|
||||
is CoordinatorLayout -> CoordinatorLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
).apply {
|
||||
// anchorId = R.id.layout_bottom
|
||||
// anchorGravity = Gravity.TOP
|
||||
gravity = Gravity.BOTTOM
|
||||
setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal))
|
||||
bottomMargin += actionBarSize
|
||||
}
|
||||
|
||||
is ConstraintLayout -> ConstraintLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
).apply {
|
||||
width = root.resources.getDimensionPixelSize(R.dimen.m3_side_sheet_width)
|
||||
setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal))
|
||||
}
|
||||
|
||||
else -> ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
root.addView(selfBinding.root, lp)
|
||||
if (root is ConstraintLayout) {
|
||||
val cs = ConstraintSet()
|
||||
cs.clone(root)
|
||||
cs.connect(R.id.layout_tip, ConstraintSet.TOP, R.id.appbar, ConstraintSet.BOTTOM)
|
||||
cs.connect(R.id.layout_tip, ConstraintSet.START, R.id.card_chapters, ConstraintSet.START)
|
||||
cs.connect(R.id.layout_tip, ConstraintSet.END, R.id.card_chapters, ConstraintSet.END)
|
||||
cs.applyTo(root)
|
||||
}
|
||||
insetsDelegate.addInsetsListener(this)
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
if (root.context.isAnimationsEnabled) {
|
||||
TransitionManager.beginDelayedTransition(root)
|
||||
}
|
||||
insetsDelegate.removeInsetsListener(this)
|
||||
root.removeView(selfBinding.root)
|
||||
viewModel.onButtonTipClosed()
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -70,6 +71,7 @@ class DetailsActivity :
|
||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||
|
||||
private lateinit var viewBadge: ViewBadge
|
||||
private var buttonTip: WeakReference<ButtonTip>? = null
|
||||
|
||||
private val viewModel: DetailsViewModel by viewModels()
|
||||
private lateinit var chaptersMenuProvider: ChaptersMenuProvider
|
||||
@@ -122,6 +124,7 @@ class DetailsActivity :
|
||||
viewModel.onShowToast.observeEvent(this) {
|
||||
makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
viewModel.onShowTip.observeEvent(this) { showTip() }
|
||||
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
||||
viewModel.selectedBranch.observe(this) {
|
||||
viewBinding.toolbarChapters?.subtitle = it
|
||||
@@ -162,6 +165,8 @@ class DetailsActivity :
|
||||
|
||||
override fun onLongClick(v: View): Boolean = when (v.id) {
|
||||
R.id.button_read -> {
|
||||
buttonTip?.get()?.remove()
|
||||
buttonTip = null
|
||||
val menu = PopupMenu(v.context, v)
|
||||
menu.inflate(R.menu.popup_read)
|
||||
menu.setOnMenuItemClickListener(this)
|
||||
@@ -345,8 +350,16 @@ class DetailsActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun showTip() {
|
||||
val tip = ButtonTip(viewBinding.root as ViewGroup, insetsDelegate, viewModel)
|
||||
tip.addToRoot()
|
||||
buttonTip = WeakReference(tip)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TIP_BUTTON = "btn_read"
|
||||
|
||||
fun newIntent(context: Context, manga: Manga): Intent {
|
||||
return Intent(context, DetailsActivity::class.java)
|
||||
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true))
|
||||
|
||||
@@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -78,6 +80,7 @@ class DetailsViewModel @Inject constructor(
|
||||
private var loadingJob: Job
|
||||
|
||||
val onShowToast = MutableEventFlow<Int>()
|
||||
val onShowTip = MutableEventFlow<Unit>()
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
|
||||
val manga = doubleManga.map { it?.any }
|
||||
@@ -191,6 +194,12 @@ class DetailsViewModel @Inject constructor(
|
||||
localStorageChanges
|
||||
.collect { onDownloadComplete(it) }
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
if (settings.isTipEnabled(DetailsActivity.TIP_BUTTON)) {
|
||||
manga.filterNot { it?.chapters.isNullOrEmpty() }.first()
|
||||
onShowTip.call(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
@@ -277,6 +286,10 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onButtonTipClosed() {
|
||||
settings.closeTip(DetailsActivity.TIP_BUTTON)
|
||||
}
|
||||
|
||||
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
|
||||
val result = doubleMangaLoadUseCase(intent)
|
||||
val manga = result.requireAny()
|
||||
|
||||
@@ -126,8 +126,10 @@ class MainActivity :
|
||||
viewBinding.navRail?.headerView?.setOnClickListener(this)
|
||||
viewBinding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null
|
||||
|
||||
navigationDelegate =
|
||||
MainNavigationDelegate(checkNotNull(bottomNav ?: viewBinding.navRail), supportFragmentManager)
|
||||
navigationDelegate = MainNavigationDelegate(
|
||||
navBar = checkNotNull(bottomNav ?: viewBinding.navRail),
|
||||
fragmentManager = supportFragmentManager,
|
||||
)
|
||||
navigationDelegate.addOnFragmentChangedListener(this)
|
||||
navigationDelegate.onCreate()
|
||||
|
||||
@@ -156,6 +158,8 @@ class MainActivity :
|
||||
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
|
||||
adjustFabVisibility(topFragment = fragment)
|
||||
if (fromUser) {
|
||||
actionModeDelegate.finishActionMode()
|
||||
closeSearchCallback.handleOnBackPressed()
|
||||
viewBinding.appbar.setExpanded(true)
|
||||
}
|
||||
}
|
||||
@@ -243,13 +247,13 @@ class MainActivity :
|
||||
override fun onSupportActionModeStarted(mode: ActionMode) {
|
||||
super.onSupportActionModeStarted(mode)
|
||||
adjustFabVisibility()
|
||||
showNav(false)
|
||||
bottomNav?.hide()
|
||||
}
|
||||
|
||||
override fun onSupportActionModeFinished(mode: ActionMode) {
|
||||
super.onSupportActionModeFinished(mode)
|
||||
adjustFabVisibility()
|
||||
showNav(true)
|
||||
bottomNav?.show()
|
||||
}
|
||||
|
||||
private fun onOpenReader(manga: Manga) {
|
||||
@@ -301,17 +305,6 @@ class MainActivity :
|
||||
closeSearchCallback.isEnabled = false
|
||||
}
|
||||
|
||||
private fun showNav(visible: Boolean) {
|
||||
bottomNav?.run {
|
||||
if (visible) {
|
||||
show()
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
viewBinding.navRail?.isVisible = visible
|
||||
}
|
||||
|
||||
private fun isSearchOpened(): Boolean {
|
||||
return supportFragmentManager.findFragmentByTag(TAG_SEARCH) != null
|
||||
}
|
||||
@@ -340,7 +333,7 @@ class MainActivity :
|
||||
}
|
||||
|
||||
private fun adjustFabVisibility(
|
||||
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true,
|
||||
isResumeEnabled: Boolean = viewModel.isResumeEnabled.value,
|
||||
topFragment: Fragment? = navigationDelegate.primaryFragment,
|
||||
isSearchOpened: Boolean = isSearchOpened(),
|
||||
) {
|
||||
@@ -383,7 +376,7 @@ class MainActivity :
|
||||
supportActionBar?.setHomeAsUpIndicator(
|
||||
if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material,
|
||||
)
|
||||
showNav(!isOpened)
|
||||
bottomNav?.showOrHide(!isOpened)
|
||||
}
|
||||
|
||||
private fun requestNotificationsPermission() {
|
||||
|
||||
12
app/src/main/res/drawable/ic_tap.xml
Normal file
12
app/src/main/res/drawable/ic_tap.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M10,9A1,1 0 0,1 11,8A1,1 0 0,1 12,9V13.47L13.21,13.6L18.15,15.79C18.68,16.03 19,16.56 19,17.14V21.5C18.97,22.32 18.32,22.97 17.5,23H11C10.62,23 10.26,22.85 10,22.57L5.1,18.37L5.84,17.6C6.03,17.39 6.3,17.28 6.58,17.28H6.8L10,19V9M11,5A4,4 0 0,1 15,9C15,10.5 14.2,11.77 13,12.46V11.24C13.61,10.69 14,9.89 14,9A3,3 0 0,0 11,6A3,3 0 0,0 8,9C8,9.89 8.39,10.69 9,11.24V12.46C7.8,11.77 7,10.5 7,9A4,4 0 0,1 11,5Z" />
|
||||
</vector>
|
||||
@@ -5,4 +5,5 @@
|
||||
<item name="action_leaks" type="id" />
|
||||
<item name="fast_scroller" type="id" />
|
||||
<item name="group_branches" type="id" />
|
||||
<item name="layout_tip" type="id" />
|
||||
</resources>
|
||||
|
||||
@@ -436,4 +436,5 @@
|
||||
<string name="reader_info_bar_summary">Show the current time and reading progress at the top of the screen</string>
|
||||
<string name="show_pages_numbers_summary">Show page numbers in bottom corner</string>
|
||||
<string name="pages_animation_summary">Animate page switching</string>
|
||||
<string name="details_button_tip">Press and hold the Read button to see more options</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user