Update category adapter

This commit is contained in:
len 2017-01-13 19:35:20 +01:00
parent e4d4dbbeb6
commit 7b9f5d0e9f
10 changed files with 159 additions and 296 deletions

View file

@ -1,36 +0,0 @@
package eu.kanade.tachiyomi.ui.base.adapter
/**
* Interface to listen for a move or dismissal event from a [ItemTouchHelper.Callback].
*
* @author Paul Burke (ipaulpro)
*/
interface ItemTouchHelperAdapter {
/**
* Called when an item has been dragged far enough to trigger a move. This is called every time
* an item is shifted, and **not** at the end of a "drop" event.
*
* Implementations should call [RecyclerView.Adapter.notifyItemMoved] after
* adjusting the underlying data to reflect this move.
*
* @param fromPosition The start position of the moved item.
* @param toPosition Then resolved position of the moved item.
* @see [RecyclerView.getAdapterPositionFor]
* @see [RecyclerView.ViewHolder.getAdapterPosition]
*/
fun onItemMove(fromPosition: Int, toPosition: Int)
/**
* Called when an item has been dismissed by a swipe.
*
* Implementations should call [RecyclerView.Adapter.notifyItemRemoved] after
* adjusting the underlying data to reflect this removal.
*
* @param position The position of the item dismissed.
* @see RecyclerView.getAdapterPositionFor
* @see RecyclerView.ViewHolder.getAdapterPosition
*/
fun onItemDismiss(position: Int)
}

View file

@ -1,13 +0,0 @@
package eu.kanade.tachiyomi.ui.base.adapter
import android.support.v7.widget.RecyclerView
interface OnStartDragListener {
/**
* Called when a view is requesting a start of a drag.
*
* @param viewHolder The holder of the view to drag.
*/
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
}

View file

@ -1,28 +0,0 @@
package eu.kanade.tachiyomi.ui.base.adapter
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.helper.ItemTouchHelper
open class SimpleItemTouchHelperCallback(private val adapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
override fun isLongPressDragEnabled() = true
override fun isItemViewSwipeEnabled() = true
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean {
adapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
adapter.onItemDismiss(viewHolder.adapterPosition)
}
}

View file

@ -6,16 +6,15 @@ import android.os.Bundle
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.support.v7.widget.helper.ItemTouchHelper
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import eu.davidea.flexibleadapter4.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.adapter.OnStartDragListener
import kotlinx.android.synthetic.main.activity_edit_categories.* import kotlinx.android.synthetic.main.activity_edit_categories.*
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
@ -29,7 +28,10 @@ import nucleus.factory.RequiresPresenter
@RequiresPresenter(CategoryPresenter::class) @RequiresPresenter(CategoryPresenter::class)
class CategoryActivity : class CategoryActivity :
BaseRxActivity<CategoryPresenter>(), BaseRxActivity<CategoryPresenter>(),
ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener, OnStartDragListener { ActionMode.Callback,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
UndoHelper.OnUndoListener {
/** /**
* Object used to show actionMode toolbar. * Object used to show actionMode toolbar.
@ -41,11 +43,6 @@ class CategoryActivity :
*/ */
private lateinit var adapter: CategoryAdapter private lateinit var adapter: CategoryAdapter
/**
* TouchHelper used for reorder animation and movement.
*/
private lateinit var touchHelper: ItemTouchHelper
companion object { companion object {
/** /**
* Create new CategoryActivity intent. * Create new CategoryActivity intent.
@ -75,27 +72,17 @@ class CategoryActivity :
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
// Touch helper to drag and reorder categories adapter.isHandleDragEnabled = true
touchHelper = ItemTouchHelper(CategoryItemTouchHelper(adapter))
touchHelper.attachToRecyclerView(recycler)
// Create OnClickListener for creating new category // Create OnClickListener for creating new category
fab.setOnClickListener({ v -> fab.setOnClickListener {
MaterialDialog.Builder(this) MaterialDialog.Builder(this)
.title(R.string.action_add_category) .title(R.string.action_add_category)
.negativeText(android.R.string.cancel) .negativeText(android.R.string.cancel)
.input(R.string.name, 0, false) .input(R.string.name, 0, false)
{ dialog, input -> presenter.createCategory(input.toString()) } { dialog, input -> presenter.createCategory(input.toString()) }
.show() .show()
})
} }
/**
* Finishes action mode.
* Call this when action mode action is finished.
*/
fun destroyActionModeIfNeeded() {
actionMode?.finish()
} }
/** /**
@ -103,19 +90,13 @@ class CategoryActivity :
* *
* @param categories list containing categories * @param categories list containing categories
*/ */
fun setCategories(categories: List<Category>) { fun setCategories(categories: List<CategoryItem>) {
destroyActionModeIfNeeded() actionMode?.finish()
adapter.setItems(categories) adapter.updateDataSet(categories)
val selected = categories.filter { it.isSelected }
if (selected.isNotEmpty()) {
selected.forEach { onItemLongClick(categories.indexOf(it)) }
} }
/**
* Returns the selected categories
*
* @return list of selected categories
*/
private fun getSelectedCategories(): List<Category> {
// Create a list of the selected categories
return adapter.selectedItems.map { adapter.getItem(it) }
} }
/** /**
@ -127,51 +108,11 @@ class CategoryActivity :
MaterialDialog.Builder(this) MaterialDialog.Builder(this)
.title(R.string.action_rename_category) .title(R.string.action_rename_category)
.negativeText(android.R.string.cancel) .negativeText(android.R.string.cancel)
.onNegative { materialDialog, dialogAction -> destroyActionModeIfNeeded() }
.input(getString(R.string.name), category.name, false) .input(getString(R.string.name), category.name, false)
{ dialog, input -> presenter.renameCategory(category, input.toString()) } { dialog, input -> presenter.renameCategory(category, input.toString()) }
.show() .show()
} }
/**
* Toggle actionMode selection
*
* @param position position of selected item
*/
private fun toggleSelection(position: Int) {
adapter.toggleSelection(position, false)
// Get selected item count
val count = adapter.selectedItemCount
// If no item is selected finish action mode
if (count == 0) {
actionMode?.finish()
} else {
// This block will only run if actionMode is not null
actionMode?.let {
// Set title equal to selected item
it.title = getString(R.string.label_selected, count)
it.invalidate()
// Show edit button only when one item is selected
val editItem = it.menu.findItem(R.id.action_edit)
editItem.isVisible = count == 1
}
}
}
/**
* Called each time the action mode is shown.
* Always called after onCreateActionMode
*
* @return false
*/
override fun onPrepareActionMode(actionMode: ActionMode, menu: Menu): Boolean {
return false
}
/** /**
* Called when action mode item clicked. * Called when action mode item clicked.
* *
@ -183,12 +124,26 @@ class CategoryActivity :
override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean { override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean {
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.action_delete -> { R.id.action_delete -> {
// Delete select categories. UndoHelper(adapter, this)
presenter.deleteCategories(getSelectedCategories()) .withAction(UndoHelper.ACTION_REMOVE, object : UndoHelper.OnActionListener {
override fun onPreAction(): Boolean {
adapter.selectedPositions.forEach { adapter.getItem(it).isSelected = false }
return false
}
override fun onPostAction() {
actionMode.finish()
}
})
.remove(adapter.selectedPositions, recycler.parent as View,
R.string.snack_categories_deleted, R.string.action_undo, 3000)
} }
R.id.action_edit -> { R.id.action_edit -> {
// Edit selected category // Edit selected category
editCategory(getSelectedCategories()[0]) if (adapter.selectedItemCount == 1) {
val position = adapter.selectedPositions.first()
editCategory(adapter.getItem(position).category)
}
} }
else -> return false else -> return false
} }
@ -211,6 +166,22 @@ class CategoryActivity :
return true return true
} }
/**
* Called each time the action mode is shown.
* Always called after onCreateActionMode
*
* @return false
*/
override fun onPrepareActionMode(actionMode: ActionMode, menu: Menu): Boolean {
val count = adapter.selectedItemCount
actionMode.title = getString(R.string.label_selected, count)
// Show edit button only when one item is selected
val editItem = actionMode.menu.findItem(R.id.action_edit)
editItem.isVisible = count == 1
return true
}
/** /**
* Called when action mode destroyed. * Called when action mode destroyed.
* *
@ -218,8 +189,7 @@ class CategoryActivity :
*/ */
override fun onDestroyActionMode(mode: ActionMode?) { override fun onDestroyActionMode(mode: ActionMode?) {
// Reset adapter to single selection // Reset adapter to single selection
adapter.mode = FlexibleAdapter.MODE_SINGLE adapter.mode = FlexibleAdapter.MODE_IDLE
// Clear selected items
adapter.clearSelection() adapter.clearSelection()
actionMode = null actionMode = null
} }
@ -229,11 +199,9 @@ class CategoryActivity :
* *
* @param position position of clicked item. * @param position position of clicked item.
*/ */
override fun onListItemClick(position: Int): Boolean { override fun onItemClick(position: Int): Boolean {
// Check if action mode is initialized and selected item exist. // Check if action mode is initialized and selected item exist.
if (position == -1) { if (actionMode != null && position != RecyclerView.NO_POSITION) {
return false
} else if (actionMode != null) {
toggleSelection(position) toggleSelection(position)
return true return true
} else { } else {
@ -246,24 +214,52 @@ class CategoryActivity :
* *
* @param position position of clicked item. * @param position position of clicked item.
*/ */
override fun onListItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
// Check if action mode is initialized. // Check if action mode is initialized.
if (actionMode == null) if (actionMode == null) {
// Initialize action mode // Initialize action mode
actionMode = startSupportActionMode(this) actionMode = startSupportActionMode(this)
}
// Set item as selected // Set item as selected
toggleSelection(position) toggleSelection(position)
} }
/** /**
* Called when item is dragged * Toggle the selection state of an item.
* * If the item was the last one in the selection and is unselected, the ActionMode is finished.
* @param viewHolder view that contains dragged item
*/ */
override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) { private fun toggleSelection(position: Int) {
// Notify touchHelper //Mark the position selected
touchHelper.startDrag(viewHolder) adapter.toggleSelection(position)
if (adapter.selectedItemCount == 0) {
actionMode?.finish()
} else {
actionMode?.invalidate()
}
}
/**
* Called when an item is released from a drag.
*/
fun onItemReleased() {
val categories = (0..adapter.itemCount-1).map { adapter.getItem(it).category }
presenter.reorderCategories(categories)
}
/**
* Called when the undo action is clicked in the snackbar.
*/
override fun onUndoConfirmed(action: Int) {
adapter.restoreDeletedItems()
}
/**
* Called when the time to restore the items expires.
*/
override fun onDeleteConfirmed(action: Int) {
presenter.deleteCategories(adapter.deletedItems.map { it.category })
} }
} }

View file

@ -1,12 +1,6 @@
package eu.kanade.tachiyomi.ui.category package eu.kanade.tachiyomi.ui.category
import android.view.ViewGroup import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter4.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.adapter.ItemTouchHelperAdapter
import eu.kanade.tachiyomi.util.inflate
import java.util.*
/** /**
* Adapter of CategoryHolder. * Adapter of CategoryHolder.
@ -17,85 +11,24 @@ import java.util.*
* @constructor Creates a CategoryAdapter object * @constructor Creates a CategoryAdapter object
*/ */
class CategoryAdapter(private val activity: CategoryActivity) : class CategoryAdapter(private val activity: CategoryActivity) :
FlexibleAdapter<CategoryHolder, Category>(), ItemTouchHelperAdapter { FlexibleAdapter<CategoryItem>(null, activity, true) {
init {
// Set unique id's
setHasStableIds(true)
}
/** /**
* Called when ViewHolder is created * Called when item is released.
*
* @param parent parent View
* @param viewType int containing viewType
*/ */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryHolder { fun onItemReleased() {
// Inflate layout with item_edit_categories.xml
val view = parent.inflate(R.layout.item_edit_categories)
return CategoryHolder(view, this, activity, activity)
}
/**
* Called when ViewHolder is bind
*
* @param holder bind holder
* @param position position of holder
*/
override fun onBindViewHolder(holder: CategoryHolder, position: Int) {
// Update holder values.
val category = getItem(position)
holder.onSetValues(category)
//When user scrolls this bind the correct selection status
holder.itemView.isActivated = isSelected(position)
}
/**
* Update items with list of categories
*
* @param items list of categories
*/
fun setItems(items: List<Category>) {
mItems = ArrayList(items)
notifyDataSetChanged()
}
/**
* Get category by position
*
* @param position position of item
*/
override fun getItemId(position: Int): Long {
return mItems[position].id!!.toLong()
}
/**
* Called when item is moved
*
* @param fromPosition previous position of item.
* @param toPosition new position of item.
*/
override fun onItemMove(fromPosition: Int, toPosition: Int) {
// Move items and notify touch helper
Collections.swap(mItems, fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
// Update database // Update database
activity.presenter.reorderCategories(mItems) activity.onItemReleased()
} }
/** override fun clearSelection() {
* Must be implemented, not used super.clearSelection()
*/ (0..itemCount-1).forEach { getItem(it).isSelected = false }
override fun onItemDismiss(position: Int) {
// Empty method.
} }
/** override fun toggleSelection(position: Int) {
* Must be implemented, not used super.toggleSelection(position)
*/ getItem(position).isSelected = isSelected(position)
override fun updateDataSet(p0: String?) {
// Empty method.
} }
} }

View file

@ -2,14 +2,11 @@ package eu.kanade.tachiyomi.ui.category
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.support.v4.view.MotionEventCompat
import android.view.MotionEvent
import android.view.View import android.view.View
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.adapter.OnStartDragListener
import kotlinx.android.synthetic.main.item_edit_categories.view.* import kotlinx.android.synthetic.main.item_edit_categories.view.*
/** /**
@ -19,17 +16,10 @@ import kotlinx.android.synthetic.main.item_edit_categories.view.*
* *
* @param view view of category item. * @param view view of category item.
* @param adapter adapter belonging to holder. * @param adapter adapter belonging to holder.
* @param listener called when item clicked.
* @param dragListener called when item dragged.
* *
* @constructor Create CategoryHolder object * @constructor Create CategoryHolder object
*/ */
class CategoryHolder( class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHolder(view, adapter) {
view: View,
adapter: CategoryAdapter,
listener: FlexibleViewHolder.OnListItemClickListener,
dragListener: OnStartDragListener
) : FlexibleViewHolder(view, adapter, listener) {
init { init {
// Create round letter image onclick to simulate long click // Create round letter image onclick to simulate long click
@ -38,13 +28,7 @@ class CategoryHolder(
onLongClick(view) onLongClick(view)
} }
// Set on touch listener for reorder image setDragHandleView(itemView.reorder)
itemView.reorder.setOnTouchListener { v, event ->
if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
dragListener.onStartDrag(this)
}
false
}
} }
/** /**
@ -52,7 +36,7 @@ class CategoryHolder(
* *
* @param category category of item. * @param category category of item.
*/ */
fun onSetValues(category: Category) { fun bind(category: Category) {
// Set capitalized title. // Set capitalized title.
itemView.title.text = category.name.capitalize() itemView.title.text = category.name.capitalize()
@ -78,4 +62,10 @@ class CategoryHolder(
.endConfig() .endConfig()
.buildRound(text, ColorGenerator.MATERIAL.getColor(text)) .buildRound(text, ColorGenerator.MATERIAL.getColor(text))
} }
override fun onItemReleased(position: Int) {
super.onItemReleased(position)
adapter.onItemReleased()
}
} }

View file

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.ui.category
import android.view.LayoutInflater
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.util.inflate
class CategoryItem(val category: Category) : AbstractFlexibleItem<CategoryHolder>() {
var isSelected = false
override fun getLayoutRes(): Int {
return R.layout.item_edit_categories
}
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
parent: ViewGroup): CategoryHolder {
return CategoryHolder(parent.inflate(layoutRes), adapter as CategoryAdapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CategoryHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(category)
}
override fun isDraggable(): Boolean {
return true
}
override fun equals(other: Any?): Boolean {
if (other is CategoryItem) {
return category.id == other.category.id
}
return false
}
override fun hashCode(): Int {
return category.id!!
}
}

View file

@ -1,26 +0,0 @@
package eu.kanade.tachiyomi.ui.category
import eu.kanade.tachiyomi.ui.base.adapter.ItemTouchHelperAdapter
import eu.kanade.tachiyomi.ui.base.adapter.SimpleItemTouchHelperCallback
class CategoryItemTouchHelper(adapter: ItemTouchHelperAdapter) : SimpleItemTouchHelperCallback(adapter) {
/**
* Disable items swipe remove
*
* @return false
*/
override fun isItemViewSwipeEnabled(): Boolean {
return false
}
/**
* Disable long press item drag
*
* @return false
*/
override fun isLongPressDragEnabled(): Boolean {
return false
}
}

View file

@ -31,6 +31,7 @@ class CategoryPresenter : BasePresenter<CategoryActivity>() {
db.getCategories().asRxObservable() db.getCategories().asRxObservable()
.doOnNext { categories = it } .doOnNext { categories = it }
.map { it.map(::CategoryItem) }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(CategoryActivity::setCategories) .subscribeLatestCache(CategoryActivity::setCategories)
} }

View file

@ -71,6 +71,7 @@
<string name="action_share">Share</string> <string name="action_share">Share</string>
<string name="action_save">Save</string> <string name="action_save">Save</string>
<string name="action_reset">Reset</string> <string name="action_reset">Reset</string>
<string name="action_undo">Undo</string>
<!-- Operations --> <!-- Operations -->
<string name="deleting">Deleting…</string> <string name="deleting">Deleting…</string>
@ -286,6 +287,7 @@
<!-- Category activity --> <!-- Category activity -->
<string name="error_category_exists">A category with this name already exists!</string> <string name="error_category_exists">A category with this name already exists!</string>
<string name="snack_categories_deleted">Categories deleted</string>
<!-- Dialog option with checkbox view --> <!-- Dialog option with checkbox view -->
<string name="dialog_with_checkbox_remove_description">This will remove the read date of this chapter. Are you sure?</string> <string name="dialog_with_checkbox_remove_description">This will remove the read date of this chapter. Are you sure?</string>