Attach some FABs and snackbars to root CoordinatorLayout

Fixes some issues around snackbars sometimes being out of view.
This commit is contained in:
arkon 2020-07-10 10:44:54 -04:00
parent 962d8e5fd2
commit 479eb1ba71
10 changed files with 181 additions and 105 deletions

View file

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.ui.base.controller
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
interface FabController {
fun configureFab(fab: ExtendedFloatingActionButton) {}
fun cleanupFab(fab: ExtendedFloatingActionButton) {}
}

View file

@ -15,6 +15,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.tfcporciuncula.flow.Preference
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -30,10 +31,10 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.connectivityManager
@ -62,6 +63,7 @@ import uy.kohesive.injekt.injectLazy
*/
open class BrowseSourceController(bundle: Bundle) :
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
FabController,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener,
@ -84,6 +86,9 @@ open class BrowseSourceController(bundle: Bundle) :
*/
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
/**
* Snackbar containing an error message when a request fails.
*/
@ -162,13 +167,27 @@ open class BrowseSourceController(bundle: Bundle) :
filterSheet?.setFilters(presenter.filterItems)
// TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly
filterSheet?.setOnShowListener { binding.fabFilter.gone() }
filterSheet?.setOnDismissListener { binding.fabFilter.visible() }
filterSheet?.setOnShowListener { actionFab?.gone() }
filterSheet?.setOnDismissListener { actionFab?.visible() }
binding.fabFilter.setOnClickListener { filterSheet?.show() }
actionFab?.setOnClickListener { filterSheet?.show() }
binding.fabFilter.offsetAppbarHeight(activity!!)
binding.fabFilter.visible()
actionFab?.visible()
}
override fun configureFab(fab: ExtendedFloatingActionButton) {
actionFab = fab
// Controlled by initFilterSheet()
fab.gone()
fab.setText(R.string.action_filter)
fab.setIconResource(R.drawable.ic_filter_list_24dp)
}
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
actionFabScrollListener?.let { recycler?.removeOnScrollListener(it) }
actionFab = null
}
override fun onDestroyView(view: View) {
@ -228,7 +247,7 @@ open class BrowseSourceController(bundle: Bundle) :
)
recycler.clipToPadding = false
binding.fabFilter.shrinkOnScroll(recycler)
actionFab?.shrinkOnScroll(recycler)
}
recycler.setHasFixedSize(true)

View file

@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
@ -16,9 +17,10 @@ import eu.davidea.flexibleadapter.helpers.UndoHelper
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
@ -28,6 +30,7 @@ import reactivecircus.flowbinding.android.view.clicks
*/
class CategoryController :
NucleusController<CategoriesControllerBinding, CategoryPresenter>(),
FabController,
ActionMode.Callback,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
@ -46,6 +49,9 @@ class CategoryController :
*/
private var adapter: CategoryAdapter? = null
private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
/**
* Undo helper used for restoring a deleted category.
*/
@ -89,13 +95,23 @@ class CategoryController :
adapter?.isHandleDragEnabled = true
adapter?.isPermanentDelete = false
binding.fab.clicks()
actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler)
}
override fun configureFab(fab: ExtendedFloatingActionButton) {
actionFab = fab
fab.setText(R.string.action_add)
fab.setIconResource(R.drawable.ic_add_24dp)
fab.clicks()
.onEach {
CategoryCreateDialog(this@CategoryController).showDialog(router, null)
}
.launchIn(scope)
}
binding.fab.offsetAppbarHeight(activity!!)
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
actionFab = null
}
/**
@ -181,7 +197,7 @@ class CategoryController :
R.id.action_delete -> {
undoHelper = UndoHelper(adapter, this)
undoHelper?.start(
adapter.selectedPositions, view!!,
adapter.selectedPositions, activity!!.findViewById(R.id.root_coordinator),
R.string.snack_categories_deleted, R.string.action_undo, 3000
)

View file

@ -7,13 +7,18 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.databinding.DownloadControllerBinding
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.visible
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -28,6 +33,7 @@ import rx.android.schedulers.AndroidSchedulers
*/
class DownloadController :
NucleusController<DownloadControllerBinding, DownloadPresenter>(),
FabController,
DownloadAdapter.DownloadItemListener {
/**
@ -35,6 +41,9 @@ class DownloadController :
*/
private var adapter: DownloadAdapter? = null
private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
/**
* Map of subscriptions for active downloads.
*/
@ -78,22 +87,7 @@ class DownloadController :
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)
binding.fab.clicks()
.onEach {
val context = applicationContext ?: return@onEach
if (isRunning) {
DownloadService.stop(context)
presenter.pauseDownloads()
} else {
DownloadService.start(context)
}
setInformationView()
}
.launchIn(scope)
binding.fab.offsetAppbarHeight(activity!!)
actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler)
// Subscribe to changes
DownloadService.runningRelay
@ -109,6 +103,29 @@ class DownloadController :
.subscribeUntilDestroy { onUpdateDownloadedPages(it) }
}
override fun configureFab(fab: ExtendedFloatingActionButton) {
actionFab = fab
fab.clicks()
.onEach {
val context = applicationContext ?: return@onEach
if (isRunning) {
DownloadService.stop(context)
presenter.pauseDownloads()
} else {
DownloadService.start(context)
}
setInformationView()
}
.launchIn(scope)
}
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
actionFab = null
}
override fun onDestroyView(view: View) {
for (subscription in progressSubscriptions.values) {
subscription.unsubscribe()
@ -267,18 +284,28 @@ class DownloadController :
private fun setInformationView() {
if (presenter.downloadQueue.isEmpty()) {
binding.emptyView.show(R.string.information_no_downloads)
binding.fab.hide()
actionFab?.gone()
} else {
binding.emptyView.hide()
binding.fab.show()
actionFab?.apply {
visible()
binding.fab.setImageResource(
if (isRunning) {
R.drawable.ic_pause_24dp
} else {
R.drawable.ic_play_arrow_24dp
}
)
setText(
if (isRunning) {
R.string.action_pause
} else {
R.string.action_resume
}
)
setIconResource(
if (isRunning) {
R.drawable.ic_pause_24dp
} else {
R.drawable.ic_play_arrow_24dp
}
)
}
}
}

View file

@ -27,6 +27,7 @@ import eu.kanade.tachiyomi.databinding.MainActivityBinding
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
@ -42,7 +43,9 @@ import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.visible
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.delay
@ -151,20 +154,7 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
if (savedInstanceState == null) {
// Show changelog prompt on update
if (Migrations.upgrade(preferences) && !BuildConfig.DEBUG) {
binding.controllerContainer.snack(getString(R.string.updated_version, BuildConfig.VERSION_NAME), Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.whats_new) {
val url = "https://github.com/inorichi/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
// Ensure the snackbar sits above the bottom nav
val layoutParams = view.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.anchorId = binding.bottomNav.id
layoutParams.anchorGravity = Gravity.TOP
layoutParams.gravity = Gravity.TOP
view.layoutParams = layoutParams
}
showUpdateInfoSnackbar()
}
}
@ -357,6 +347,15 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
binding.tabs.setupWithViewPager(null)
}
if (from is FabController) {
binding.rootFab.gone()
from.cleanupFab(binding.rootFab)
}
if (to is FabController) {
binding.rootFab.visible()
to.configureFab(binding.rootFab)
}
if (to is NoToolbarElevationController) {
binding.appbar.disableElevation()
} else {
@ -382,6 +381,32 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
}
}
private fun showUpdateInfoSnackbar() {
val snack = binding.rootCoordinator.snack(
getString(R.string.updated_version, BuildConfig.VERSION_NAME),
Snackbar.LENGTH_INDEFINITE
) {
setAction(R.string.whats_new) {
val url = "https://github.com/inorichi/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
}
// Ensure the snackbar sits above the bottom nav
val layoutParams = view.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.anchorId = binding.bottomNav.id
layoutParams.anchorGravity = Gravity.TOP
layoutParams.gravity = Gravity.TOP
view.layoutParams = layoutParams
}
// Manually handle dismiss delay since Snackbar.LENGTH_LONG is a too short
launchIO {
delay(5000)
snack.dismiss()
}
}
companion object {
// Shortcut actions
const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"

View file

@ -93,8 +93,8 @@ inline fun View.toggle() {
*
* @param recycler [RecyclerView] that the FAB should shrink/extend in response to.
*/
fun ExtendedFloatingActionButton.shrinkOnScroll(recycler: RecyclerView) {
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
fun ExtendedFloatingActionButton.shrinkOnScroll(recycler: RecyclerView): RecyclerView.OnScrollListener {
val listener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy <= 0) {
extend()
@ -102,7 +102,9 @@ fun ExtendedFloatingActionButton.shrinkOnScroll(recycler: RecyclerView) {
shrink()
}
}
})
}
recycler.addOnScrollListener(listener)
return listener
}
/**

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -14,12 +13,6 @@
android:paddingBottom="@dimen/fab_list_padding"
tools:listitem="@layout/categories_item" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
style="@style/Theme.Widget.FAB"
app:layout_anchor="@id/recycler"
app:srcCompat="@drawable/ic_add_24dp" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
@ -27,4 +20,4 @@
android:layout_gravity="center"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</FrameLayout>

View file

@ -22,13 +22,6 @@
app:fastScrollerBubbleEnabled="false"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
style="@style/Theme.Widget.FAB"
android:visibility="gone"
app:layout_anchor="@id/recycler"
app:srcCompat="@drawable/ic_pause_24dp" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="wrap_content"

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@ -32,6 +33,11 @@
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/root_fab"
style="@style/Theme.Widget.FAB"
android:visibility="gone" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
style="@style/Widget.MaterialComponents.BottomNavigationView.Colored"

View file

@ -1,46 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
<LinearLayout
android:id="@+id/catalogue_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.browse.source.browse.BrowseSourceController">
<LinearLayout
android:id="@+id/catalogue_view"
android:layout_width="match_parent"
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.browse.source.browse.BrowseSourceController">
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab_filter"
style="@style/Theme.Widget.FAB"
android:text="@string/action_filter"
android:visibility="gone"
app:icon="@drawable/ic_filter_list_24dp"
app:layout_anchor="@id/catalogue_view" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="gone" />
</FrameLayout>