From b067096fc70aa5d81fb90d8eec7c5f17dcf3de08 Mon Sep 17 00:00:00 2001 From: inorichi Date: Sun, 11 Dec 2016 12:43:44 +0100 Subject: [PATCH] Add drawer to filter and sort the library (#570) * Add additional drawer to filter and sort the library * Tint icon when there's a filter active * Comments and minor changes --- .../java/eu/kanade/tachiyomi/Constants.kt | 3 - .../data/preference/PreferencesHelper.kt | 2 + .../ui/catalogue/CatalogueFragment.kt | 1 + .../tachiyomi/ui/library/LibraryFragment.kt | 134 +++---- .../ui/library/LibraryNavigationView.kt | 187 +++++++++ .../tachiyomi/ui/library/LibraryPresenter.kt | 28 +- .../tachiyomi/ui/library/LibrarySort.kt | 9 + .../widget/ExtendedNavigationView.kt | 363 ++++++++++++++++++ .../main/res/drawable/empty_drawable_32dp.xml | 8 + .../ic_keyboard_arrow_down_black_32dp.xml | 9 + .../ic_keyboard_arrow_up_black_32dp.xml | 9 + app/src/main/res/layout/library_drawer.xml | 8 + .../res/layout/navigation_view_checkbox.xml | 23 ++ .../layout/navigation_view_checkedtext.xml | 21 + .../main/res/layout/navigation_view_radio.xml | 23 ++ app/src/main/res/menu/library.xml | 44 +-- app/src/main/res/values/strings.xml | 3 + 17 files changed, 743 insertions(+), 132 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt create mode 100644 app/src/main/res/drawable/empty_drawable_32dp.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_arrow_down_black_32dp.xml create mode 100644 app/src/main/res/drawable/ic_keyboard_arrow_up_black_32dp.xml create mode 100644 app/src/main/res/layout/library_drawer.xml create mode 100644 app/src/main/res/layout/navigation_view_checkbox.xml create mode 100644 app/src/main/res/layout/navigation_view_checkedtext.xml create mode 100644 app/src/main/res/layout/navigation_view_radio.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/Constants.kt b/app/src/main/java/eu/kanade/tachiyomi/Constants.kt index fcfd4b56c3..48fec85954 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Constants.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Constants.kt @@ -7,7 +7,4 @@ object Constants { const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4 const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5 - const val SORT_LIBRARY_ALPHA = 0 - const val SORT_LIBRARY_LAST_READ = 1 - const val SORT_LIBRARY_LAST_UPDATED = 2 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 2163dd7781..e2c61388c3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -130,6 +130,8 @@ class PreferencesHelper(context: Context) { fun librarySortingMode() = rxPrefs.getInteger(keys.librarySortingMode, 0) + fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true) + fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false) fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt index 5fc11962c9..68f2ee1283 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt @@ -221,6 +221,7 @@ open class CatalogueFragment : BaseRxFragment(), FlexibleVie // Setup filters button menu.findItem(R.id.action_set_filter).apply { + icon.mutate() if (presenter.source.filters.isEmpty()) { isEnabled = false icon.alpha = 128 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt index c531daa6fa..de9b522677 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt @@ -3,25 +3,27 @@ package eu.kanade.tachiyomi.ui.library import android.app.Activity import android.content.Intent import android.content.res.Configuration +import android.graphics.Color import android.os.Bundle import android.support.design.widget.TabLayout +import android.support.v4.graphics.drawable.DrawableCompat import android.support.v4.view.ViewPager +import android.support.v4.widget.DrawerLayout import android.support.v7.view.ActionMode import android.support.v7.widget.SearchView import android.view.* import com.afollestad.materialdialogs.MaterialDialog import com.f2prateek.rx.preferences.Preference -import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault -import eu.kanade.tachiyomi.data.preference.invert import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.category.CategoryActivity import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.toast import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.fragment_library.* @@ -81,6 +83,30 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback var mangaPerRow = 0 private set + /** + * Navigation view containing filter/sort/display items. + */ + private lateinit var navView: LibraryNavigationView + + /** + * Drawer listener to allow swipe only for closing the drawer. + */ + private val drawerListener by lazy { + object : DrawerLayout.SimpleDrawerListener() { + override fun onDrawerClosed(drawerView: View) { + if (drawerView == navView) { + activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) + } + } + + override fun onDrawerOpened(drawerView: View) { + if (drawerView == navView) { + activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, navView) + } + } + } + } + /** * Subscription for the number of manga per row. */ @@ -149,6 +175,25 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback .skip(1) // Set again the adapter to recalculate the covers height .subscribe { reattachAdapter() } + + + // Inflate and prepare drawer + navView = activity.drawer.inflate(R.layout.library_drawer) as LibraryNavigationView + activity.drawer.addView(navView) + activity.drawer.addDrawerListener(drawerListener) + + navView.post { + if (isAdded && !activity.drawer.isDrawerOpen(navView)) + activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView) + } + + navView.onGroupClicked = { group -> + when (group) { + is LibraryNavigationView.FilterGroup -> onFilterChanged() + is LibraryNavigationView.SortGroup -> onSortChanged() + is LibraryNavigationView.DisplayGroup -> reattachAdapter() + } + } } override fun onResume() { @@ -157,6 +202,8 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback } override fun onDestroyView() { + activity.drawer.removeDrawerListener(drawerListener) + activity.drawer.removeView(navView) numColumnsSubscription?.unsubscribe() tabs.setupWithViewPager(null) tabs.visibility = View.GONE @@ -169,34 +216,6 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback super.onSaveInstanceState(outState) } - /** - * Prepare the Fragment host's standard options menu to be displayed. This is - * called right before the menu is shown, every time it is shown. You can - * use this method to efficiently enable/disable items or otherwise - * dynamically modify the contents. - * - * @param menu The options menu as last shown or first initialized by - */ - override fun onPrepareOptionsMenu(menu: Menu) { - // Initialize search menu - val filterDownloadedItem = menu.findItem(R.id.action_filter_downloaded) - val filterUnreadItem = menu.findItem(R.id.action_filter_unread) - val sortModeAlpha = menu.findItem(R.id.action_sort_alpha) - val sortModeLastRead = menu.findItem(R.id.action_sort_last_read) - val sortModeLastUpdated = menu.findItem(R.id.action_sort_last_updated) - - // Set correct checkbox filter - filterDownloadedItem.isChecked = preferences.filterDownloaded().getOrDefault() - filterUnreadItem.isChecked = preferences.filterUnread().getOrDefault() - - // Set correct radio button sort - when (preferences.librarySortingMode().getOrDefault()) { - Constants.SORT_LIBRARY_ALPHA -> sortModeAlpha.isChecked = true - Constants.SORT_LIBRARY_LAST_READ -> sortModeLastRead.isChecked = true - Constants.SORT_LIBRARY_LAST_UPDATED -> sortModeLastUpdated.isChecked = true - } - } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.library, menu) @@ -209,6 +228,9 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback searchView.clearFocus() } + // Mutate the filter icon because it needs to be tinted and the resource is shared. + menu.findItem(R.id.action_filter).icon.mutate() + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { onSearchTextChange(query) @@ -223,40 +245,19 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback } + override fun onPrepareOptionsMenu(menu: Menu) { + val filterItem = menu.findItem(R.id.action_filter) + + // Tint icon if there's a filter active + val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE + DrawableCompat.setTint(filterItem.icon, filterColor) + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.action_filter_unread -> { - // Update settings. - preferences.filterUnread().invert() - // Apply filter. - onFilterOrSortChanged() + R.id.action_filter -> { + activity.drawer.openDrawer(Gravity.END) } - R.id.action_filter_downloaded -> { - // Update settings. - preferences.filterDownloaded().invert() - // Apply filter. - onFilterOrSortChanged() - } - R.id.action_filter_empty -> { - // Update settings. - preferences.filterUnread().set(false) - preferences.filterDownloaded().set(false) - // Apply filter - onFilterOrSortChanged() - } - R.id.action_sort_alpha -> { - preferences.librarySortingMode().set(Constants.SORT_LIBRARY_ALPHA) - onFilterOrSortChanged() - } - R.id.action_sort_last_read -> { - preferences.librarySortingMode().set(Constants.SORT_LIBRARY_LAST_READ) - onFilterOrSortChanged() - } - R.id.action_sort_last_updated -> { - preferences.librarySortingMode().set(Constants.SORT_LIBRARY_LAST_UPDATED) - onFilterOrSortChanged() - } - R.id.action_library_display_mode -> swapDisplayMode() R.id.action_update_library -> { LibraryUpdateService.start(activity) } @@ -271,19 +272,18 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback } /** - * Applies filter change + * Called when a filter is changed. */ - private fun onFilterOrSortChanged() { + private fun onFilterChanged() { presenter.requestLibraryUpdate() activity.supportInvalidateOptionsMenu() } /** - * Swap display mode + * Called when the sorting mode is changed. */ - private fun swapDisplayMode() { - presenter.swapDisplayMode() - reattachAdapter() + private fun onSortChanged() { + presenter.requestLibraryUpdate() } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt new file mode 100644 index 0000000000..5a63ad587d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryNavigationView.kt @@ -0,0 +1,187 @@ +package eu.kanade.tachiyomi.ui.library + +import android.content.Context +import android.util.AttributeSet +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.widget.ExtendedNavigationView +import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_ASC +import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_DESC +import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_NONE +import uy.kohesive.injekt.injectLazy + +/** + * The navigation view shown in a drawer with the different options to show the library. + */ +class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) +: ExtendedNavigationView(context, attrs) { + + /** + * Preferences helper. + */ + private val preferences: PreferencesHelper by injectLazy() + + /** + * List of groups shown in the view. + */ + private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup()) + + /** + * Adapter instance. + */ + private val adapter = Adapter(groups.map { it.createItems() }.flatten()) + + /** + * Click listener to notify the parent fragment when an item from a group is clicked. + */ + var onGroupClicked: (Group) -> Unit = {} + + init { + recycler.adapter = adapter + + groups.forEach { it.initModels() } + } + + /** + * Returns true if there's at least one filter from [FilterGroup] active. + */ + fun hasActiveFilters(): Boolean { + return (groups[0] as FilterGroup).items.any { it.checked } + } + + /** + * Adapter of the recycler view. + */ + inner class Adapter(items: List) : ExtendedNavigationView.Adapter(items) { + + override fun onItemClicked(item: Item) { + if (item is GroupedItem) { + item.group.onItemClicked(item) + onGroupClicked(item.group) + } + } + + } + + /** + * Filters group (unread, downloaded, ...). + */ + inner class FilterGroup : Group { + + private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this) + + private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this) + + override val items = listOf(downloaded, unread) + + override val header = Item.Header(R.string.action_filter) + + override val footer = Item.Separator() + + override fun initModels() { + downloaded.checked = preferences.filterDownloaded().getOrDefault() + unread.checked = preferences.filterUnread().getOrDefault() + } + + override fun onItemClicked(item: Item) { + item as Item.CheckboxGroup + item.checked = !item.checked + when (item) { + downloaded -> preferences.filterDownloaded().set(item.checked) + unread -> preferences.filterUnread().set(item.checked) + } + + adapter.notifyItemChanged(item) + } + + } + + /** + * Sorting group (alphabetically, by last read, ...) and ascending or descending. + */ + inner class SortGroup : Group { + + private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this) + + private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this) + + private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this) + + override val items = listOf(alphabetically, lastRead, lastUpdated) + + override val header = Item.Header(R.string.action_sort) + + override val footer = Item.Separator() + + override fun initModels() { + val sorting = preferences.librarySortingMode().getOrDefault() + val order = if (preferences.librarySortingAscending().getOrDefault()) + SORT_ASC else SORT_DESC + + alphabetically.state = if (sorting == LibrarySort.ALPHA) order else SORT_NONE + lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE + lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE + } + + override fun onItemClicked(item: Item) { + item as Item.MultiStateGroup + val prevState = item.state + + item.group.items.forEach { (it as Item.MultiStateGroup).state = SORT_NONE } + item.state = when (prevState) { + SORT_NONE -> SORT_ASC + SORT_ASC -> SORT_DESC + SORT_DESC -> SORT_ASC + else -> throw Exception("Unknown state") + } + + preferences.librarySortingMode().set(when (item) { + alphabetically -> LibrarySort.ALPHA + lastRead -> LibrarySort.LAST_READ + lastUpdated -> LibrarySort.LAST_UPDATED + else -> throw Exception("Unknown sorting") + }) + preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false) + + item.group.items.forEach { adapter.notifyItemChanged(it) } + } + + } + + /** + * Display group, to show the library as a list or a grid. + */ + inner class DisplayGroup : Group { + + private val grid = Item.Radio(R.string.action_display_grid, this) + + private val list = Item.Radio(R.string.action_display_list, this) + + override val items = listOf(grid, list) + + override val header = Item.Header(R.string.action_display) + + override val footer = null + + override fun initModels() { + val asList = preferences.libraryAsList().getOrDefault() + grid.checked = !asList + list.checked = asList + } + + override fun onItemClicked(item: Item) { + item as Item.Radio + if (item.checked) return + + item.group.items.forEach { (it as Item.Radio).checked = false } + item.checked = true + + preferences.libraryAsList().set(if (item == list) true else false) + + item.group.items.forEach { adapter.notifyItemChanged(it) } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index be253b19a1..0206005638 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -4,7 +4,6 @@ import android.os.Bundle import android.util.Pair import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category @@ -111,9 +110,12 @@ class LibraryPresenter : BasePresenter() { } private fun applyFilters(map: Map>): Map> { + val isAscending = preferences.librarySortingAscending().getOrDefault() + val comparator = Comparator { m1, m2 -> sortManga(m1, m2) } + return map.mapValues { entry -> entry.value .filter { filterManga(it) } - .sortedWith(Comparator { m1, m2 -> sortManga(m1, m2) }) + .sortedWith(if (isAscending) comparator else Collections.reverseOrder(comparator)) } } @@ -172,19 +174,15 @@ class LibraryPresenter : BasePresenter() { */ fun sortManga(manga1: Manga, manga2: Manga): Int { when (preferences.librarySortingMode().getOrDefault()) { - Constants.SORT_LIBRARY_ALPHA -> return manga1.title.compareTo(manga2.title) - Constants.SORT_LIBRARY_LAST_READ -> { + LibrarySort.ALPHA -> return manga1.title.compareTo(manga2.title) + LibrarySort.LAST_READ -> { var a = 0L var b = 0L - manga1.id?.let { manga1Id -> - manga2.id?.let { manga2Id -> - db.getLastHistoryByMangaId(manga1Id).executeAsBlocking()?.let { a = it.last_read } - db.getLastHistoryByMangaId(manga2Id).executeAsBlocking()?.let { b = it.last_read } - } - } + db.getLastHistoryByMangaId(manga1.id!!).executeAsBlocking()?.let { a = it.last_read } + db.getLastHistoryByMangaId(manga2.id!!).executeAsBlocking()?.let { b = it.last_read } return b.compareTo(a) } - Constants.SORT_LIBRARY_LAST_UPDATED -> return manga2.last_update.compareTo(manga1.last_update) + LibrarySort.LAST_UPDATED -> return manga2.last_update.compareTo(manga1.last_update) else -> return manga1.title.compareTo(manga2.title) } } @@ -326,12 +324,4 @@ class LibraryPresenter : BasePresenter() { return false } - /** - * Changes the active display mode. - */ - fun swapDisplayMode() { - val displayAsList = preferences.libraryAsList().getOrDefault() - preferences.libraryAsList().set(!displayAsList) - } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt new file mode 100644 index 0000000000..a69541070e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt @@ -0,0 +1,9 @@ +package eu.kanade.tachiyomi.ui.library + +object LibrarySort { + + const val ALPHA = 0 + const val LAST_READ = 1 + const val LAST_UPDATED = 2 + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt new file mode 100644 index 0000000000..16ce785957 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt @@ -0,0 +1,363 @@ +package eu.kanade.tachiyomi.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.Drawable +import android.support.annotation.CallSuper +import android.support.design.R +import android.support.design.internal.ScrimInsetsFrameLayout +import android.support.graphics.drawable.VectorDrawableCompat +import android.support.v4.content.ContextCompat +import android.support.v4.view.ViewCompat +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.TintTypedArray +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.CheckedTextView +import android.widget.RadioButton +import android.widget.TextView +import eu.kanade.tachiyomi.util.getResourceColor +import eu.kanade.tachiyomi.util.inflate +import eu.kanade.tachiyomi.R as TR + +/** + * An alternative implementation of [android.support.design.widget.NavigationView], without menu + * inflation and allowing customizable items (multiple selections, custom views, etc). + */ +@Suppress("LeakingThis") +@SuppressLint("PrivateResource") +open class ExtendedNavigationView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) +: ScrimInsetsFrameLayout(context, attrs, defStyleAttr) { + + /** + * Max width of the navigation view. + */ + private var maxWidth: Int + + /** + * Recycler view containing all the items. + */ + protected val recycler = RecyclerView(context) + + init { + // Custom attributes + val a = TintTypedArray.obtainStyledAttributes(context, attrs, + R.styleable.NavigationView, defStyleAttr, + R.style.Widget_Design_NavigationView) + + ViewCompat.setBackground( + this, a.getDrawable(R.styleable.NavigationView_android_background)) + + if (a.hasValue(R.styleable.NavigationView_elevation)) { + ViewCompat.setElevation(this, a.getDimensionPixelSize( + R.styleable.NavigationView_elevation, 0).toFloat()) + } + + ViewCompat.setFitsSystemWindows(this, + a.getBoolean(R.styleable.NavigationView_android_fitsSystemWindows, false)) + + maxWidth = a.getDimensionPixelSize(R.styleable.NavigationView_android_maxWidth, 0) + + a.recycle() + + recycler.layoutManager = LinearLayoutManager(context) + addView(recycler) + } + + /** + * Overriden to measure the width of the navigation view. + */ + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + val width = when (MeasureSpec.getMode(widthSpec)) { + MeasureSpec.AT_MOST -> MeasureSpec.makeMeasureSpec( + Math.min(MeasureSpec.getSize(widthSpec), maxWidth), MeasureSpec.EXACTLY) + MeasureSpec.UNSPECIFIED -> MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY) + else -> widthSpec + } + // Let super sort out the height + super.onMeasure(width, heightSpec) + } + + /** + * Every item of the nav view. Generic items must belong to this list, custom items could be + * implemented by an abstract class. If more customization is needed in the future, this can be + * changed to an interface instead of sealed class. + */ + sealed class Item { + /** + * A view separator. + */ + class Separator(val paddingTop: Int = 0, val paddingBottom: Int = 0) : Item() + + /** + * A header with a title. + */ + class Header(val resTitle: Int) : Item() + + /** + * A checkbox. + */ + open class Checkbox(val resTitle: Int, var checked: Boolean = false) : Item() + + /** + * A checkbox belonging to a group. The group must handle selections and restrictions. + */ + class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false) + : Checkbox(resTitle, checked), GroupedItem + + /** + * A radio belonging to a group (a sole radio makes no sense). The group must handle + * selections and restrictions. + */ + class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false) + : Item(), GroupedItem + + /** + * An item with which needs more than two states (selected/deselected). + */ + abstract class MultiState(val resTitle: Int, var state: Int = 0) : Item() { + + /** + * Returns the drawable associated to every possible each state. + */ + abstract fun getStateDrawable(context: Context): Drawable? + + /** + * Creates a vector tinted with the accent color. + * + * @param context any context. + * @param resId the vector resource to load and tint + */ + fun tintVector(context: Context, resId: Int): Drawable { + return VectorDrawableCompat.create(context.resources, resId, context.theme)!!.apply { + setTint(context.theme.getResourceColor(TR.attr.colorAccent)) + } + } + } + + /** + * An item with which needs more than two states (selected/deselected) belonging to a group. + * The group must handle selections and restrictions. + */ + abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0) + : MultiState(resTitle, state), GroupedItem + + /** + * A multistate item for sorting lists (unselected, ascending, descending). + */ + class MultiSort(resId: Int, group: Group) : MultiStateGroup(resId, group) { + + companion object { + const val SORT_NONE = 0 + const val SORT_ASC = 1 + const val SORT_DESC = 2 + } + + override fun getStateDrawable(context: Context): Drawable? { + return when (state) { + SORT_ASC -> tintVector(context, TR.drawable.ic_keyboard_arrow_up_black_32dp) + SORT_DESC -> tintVector(context, TR.drawable.ic_keyboard_arrow_down_black_32dp) + SORT_NONE -> ContextCompat.getDrawable(context, TR.drawable.empty_drawable_32dp) + else -> null + } + } + + } + } + + /** + * Interface for an item belonging to a group. + */ + interface GroupedItem { + val group: Group + } + + /** + * A group containing a list of items. + */ + interface Group { + + /** + * An optional header for the group, typically a [Item.Header]. + */ + val header: Item? + + /** + * An optional footer for the group, typically a [Item.Separator]. + */ + val footer: Item? + + /** + * The items of the group, excluding header and footer. + */ + val items: List + + /** + * Creates all the elements of this group. Implementations can override this method for more + * customization. + */ + fun createItems() = (mutableListOf() + header + items + footer).filterNotNull() + + /** + * Called after creating the list of items. Implementations should load the current values + * into the models. + */ + fun initModels() + + /** + * Called when an item of this group is clicked. The group is responsible for all the + * selections of its items. + */ + fun onItemClicked(item: Item) + + } + + /** + * Base view holder. + */ + abstract class Holder(view: View) : RecyclerView.ViewHolder(view) + + /** + * Separator view holder. + */ + class SeparatorHolder(parent: ViewGroup) + : Holder(parent.inflate(R.layout.design_navigation_item_separator)) + + /** + * Header view holder. + */ + class HeaderHolder(parent: ViewGroup) + : Holder(parent.inflate(R.layout.design_navigation_item_subheader)) + + /** + * Clickable view holder. + */ + abstract class ClickableHolder(view: View, listener: View.OnClickListener?) : Holder(view) { + init { + itemView.setOnClickListener(listener) + } + } + + /** + * Radio view holder. + */ + class RadioHolder(parent: ViewGroup, listener: View.OnClickListener?) + : ClickableHolder(parent.inflate(TR.layout.navigation_view_radio), listener) { + + val radio = itemView.findViewById(TR.id.nav_view_item) as RadioButton + } + + /** + * Checkbox view holder. + */ + class CheckboxHolder(parent: ViewGroup, listener: View.OnClickListener?) + : ClickableHolder(parent.inflate(TR.layout.navigation_view_checkbox), listener) { + + val check = itemView.findViewById(TR.id.nav_view_item) as CheckBox + } + + /** + * Multi state view holder. + */ + class MultiStateHolder(parent: ViewGroup, listener: View.OnClickListener?) + : ClickableHolder(parent.inflate(TR.layout.navigation_view_checkedtext), listener) { + + val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView + } + + /** + * Base adapter for the navigation view. It knows how to create and render every subclass of + * [Item]. + */ + abstract inner class Adapter(private val items: List) : RecyclerView.Adapter() { + + private val onClick = View.OnClickListener { + val pos = recycler.getChildAdapterPosition(it) + val item = items[pos] + onItemClicked(item) + } + + fun notifyItemChanged(item: Item) { + val pos = items.indexOf(item) + if (pos != -1) notifyItemChanged(pos) + } + + override fun getItemCount(): Int { + return items.size + } + + @CallSuper + override fun getItemViewType(position: Int): Int { + val item = items[position] + return when (item) { + is Item.Header -> VIEW_TYPE_HEADER + is Item.Separator -> VIEW_TYPE_SEPARATOR + is Item.Radio -> VIEW_TYPE_RADIO + is Item.Checkbox -> VIEW_TYPE_CHECKBOX + is Item.MultiState -> VIEW_TYPE_MULTISTATE + } + } + + @CallSuper + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + return when (viewType) { + VIEW_TYPE_HEADER -> HeaderHolder(parent) + VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent) + VIEW_TYPE_RADIO -> RadioHolder(parent, onClick) + VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, onClick) + VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, onClick) + else -> throw Exception("Unknown view type") + } + } + + @CallSuper + override fun onBindViewHolder(holder: Holder, position: Int) { + when (holder) { + is HeaderHolder -> { + val view = holder.itemView as TextView + val item = items[position] as Item.Header + view.setText(item.resTitle) + } + is SeparatorHolder -> { + val view = holder.itemView + val item = items[position] as Item.Separator + view.setPadding(0, item.paddingTop, 0, item.paddingBottom) + } + is RadioHolder -> { + val item = items[position] as Item.Radio + holder.radio.setText(item.resTitle) + holder.radio.isChecked = item.checked + } + is CheckboxHolder -> { + val item = items[position] as Item.CheckboxGroup + holder.check.setText(item.resTitle) + holder.check.isChecked = item.checked + } + is MultiStateHolder -> { + val item = items[position] as Item.MultiStateGroup + val drawable = item.getStateDrawable(context) + holder.text.setText(item.resTitle) + holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) + } + } + } + + abstract fun onItemClicked(item: Item) + + } + + companion object { + private const val VIEW_TYPE_HEADER = 100 + private const val VIEW_TYPE_SEPARATOR = 101 + private const val VIEW_TYPE_RADIO = 102 + private const val VIEW_TYPE_CHECKBOX = 103 + private const val VIEW_TYPE_MULTISTATE = 104 + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/empty_drawable_32dp.xml b/app/src/main/res/drawable/empty_drawable_32dp.xml new file mode 100644 index 0000000000..de7699cab7 --- /dev/null +++ b/app/src/main/res/drawable/empty_drawable_32dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_down_black_32dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_down_black_32dp.xml new file mode 100644 index 0000000000..8a9f899145 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_down_black_32dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_up_black_32dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_up_black_32dp.xml new file mode 100644 index 0000000000..7d7c892949 --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_up_black_32dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/library_drawer.xml b/app/src/main/res/layout/library_drawer.xml new file mode 100644 index 0000000000..659d66bb9a --- /dev/null +++ b/app/src/main/res/layout/library_drawer.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/layout/navigation_view_checkbox.xml b/app/src/main/res/layout/navigation_view_checkbox.xml new file mode 100644 index 0000000000..2121f53660 --- /dev/null +++ b/app/src/main/res/layout/navigation_view_checkbox.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/layout/navigation_view_checkedtext.xml b/app/src/main/res/layout/navigation_view_checkedtext.xml new file mode 100644 index 0000000000..6761cf1a1d --- /dev/null +++ b/app/src/main/res/layout/navigation_view_checkedtext.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/res/layout/navigation_view_radio.xml b/app/src/main/res/layout/navigation_view_radio.xml new file mode 100644 index 0000000000..196ad31682 --- /dev/null +++ b/app/src/main/res/layout/navigation_view_radio.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/menu/library.xml b/app/src/main/res/menu/library.xml index e128d87699..5e7bf7ad5e 100644 --- a/app/src/main/res/menu/library.xml +++ b/app/src/main/res/menu/library.xml @@ -14,44 +14,7 @@ android:id="@+id/action_filter" android:icon="@drawable/ic_filter_list_white_24dp" android:title="@string/action_filter" - app:showAsAction="ifRoom"> - - - - - - - - - - - - - - - - + app:showAsAction="ifRoom"/> - - Open in browser Add to home screen Change display mode + Display + Grid + List Set filter Cancel Sort