Filter
parent
315aea8b5c
commit
2309d2465b
@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MangaFilter(
|
||||||
|
val sortOrder: SortOrder,
|
||||||
|
val tag: MangaTag?
|
||||||
|
) : Parcelable
|
||||||
@ -1,5 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.core.model
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
enum class SortOrder {
|
import androidx.annotation.StringRes
|
||||||
ALPHABETICAL, POPULARITY, UPDATED, NEWEST, RATING
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
|
enum class SortOrder(@StringRes val titleRes: Int) {
|
||||||
|
UPDATED(R.string.updated),
|
||||||
|
POPULARITY(R.string.popular),
|
||||||
|
RATING(R.string.by_rating),
|
||||||
|
NEWEST(R.string.newest),
|
||||||
|
ALPHABETICAL(R.string.by_name)
|
||||||
}
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.common
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import moxy.MvpBottomSheetDialogFragment
|
||||||
|
|
||||||
|
abstract class BaseBottomSheet(@LayoutRes private val layoutResId: Int) : MvpBottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? = inflater.inflate(layoutResId, container, false)
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.common.list.decor
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getThemeDrawable
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class ItemTypeDividerDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
private val divider = context.getThemeDrawable(android.R.attr.listDivider)
|
||||||
|
private val bounds = Rect()
|
||||||
|
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect, view: View,
|
||||||
|
parent: RecyclerView, state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
outRect.set(0, divider?.intrinsicHeight ?: 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas, parent: RecyclerView, s: RecyclerView.State) {
|
||||||
|
if (parent.layoutManager == null || divider == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val adapter = parent.adapter ?: return
|
||||||
|
canvas.save()
|
||||||
|
val left: Int
|
||||||
|
val right: Int
|
||||||
|
if (parent.clipToPadding) {
|
||||||
|
left = parent.paddingLeft
|
||||||
|
right = parent.width - parent.paddingRight
|
||||||
|
canvas.clipRect(
|
||||||
|
left, parent.paddingTop, right,
|
||||||
|
parent.height - parent.paddingBottom
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
left = 0
|
||||||
|
right = parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastItemType = -1
|
||||||
|
for (child in parent.children) {
|
||||||
|
val itemType = adapter.getItemViewType(parent.getChildAdapterPosition(child))
|
||||||
|
if (lastItemType != -1 && itemType != lastItemType) {
|
||||||
|
parent.getDecoratedBoundsWithMargins(child, bounds)
|
||||||
|
val top: Int = bounds.top + child.translationY.roundToInt()
|
||||||
|
val bottom: Int = top + divider.intrinsicHeight
|
||||||
|
divider.setBounds(left, top, right, bottom)
|
||||||
|
divider.draw(canvas)
|
||||||
|
}
|
||||||
|
lastItemType = itemType
|
||||||
|
}
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.common.list.decor
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.utils.ext.inflate
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/paetztm/recycler_view_headers
|
||||||
|
*/
|
||||||
|
class SectionItemDecoration(
|
||||||
|
private val isSticky: Boolean,
|
||||||
|
private val callback: Callback
|
||||||
|
) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
private var headerView: TextView? = null
|
||||||
|
private var headerOffset: Int = 0
|
||||||
|
|
||||||
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
|
if (headerOffset == 0) {
|
||||||
|
headerOffset = parent.resources.getDimensionPixelSize(R.dimen.header_height)
|
||||||
|
}
|
||||||
|
val pos = parent.getChildAdapterPosition(view)
|
||||||
|
outRect.set(0, if (callback.isSection(pos)) headerOffset else 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
super.onDrawOver(c, parent, state)
|
||||||
|
val textView = headerView ?: parent.inflate<TextView>(R.layout.item_header).also {
|
||||||
|
headerView = it
|
||||||
|
}
|
||||||
|
fixLayoutSize(textView, parent)
|
||||||
|
|
||||||
|
for (child in parent.children) {
|
||||||
|
val pos = parent.getChildAdapterPosition(child)
|
||||||
|
if (callback.isSection(pos)) {
|
||||||
|
textView.text = callback.getSectionTitle(pos) ?: continue
|
||||||
|
c.save()
|
||||||
|
if (isSticky) {
|
||||||
|
c.translate(
|
||||||
|
0f,
|
||||||
|
max(0f, (child.top - textView.height).toFloat())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
c.translate(
|
||||||
|
0f,
|
||||||
|
(child.top - textView.height).toFloat()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
textView.draw(c)
|
||||||
|
c.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the header view to make sure its size is greater than 0 and will be drawn
|
||||||
|
* https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations
|
||||||
|
*/
|
||||||
|
private fun fixLayoutSize(view: View, parent: ViewGroup) {
|
||||||
|
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
|
||||||
|
val heightSpec =
|
||||||
|
View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
|
||||||
|
val childWidth = ViewGroup.getChildMeasureSpec(
|
||||||
|
widthSpec,
|
||||||
|
parent.paddingLeft + parent.paddingRight,
|
||||||
|
view.layoutParams.width
|
||||||
|
)
|
||||||
|
val childHeight = ViewGroup.getChildMeasureSpec(
|
||||||
|
heightSpec,
|
||||||
|
parent.paddingTop + parent.paddingBottom,
|
||||||
|
view.layoutParams.height
|
||||||
|
)
|
||||||
|
view.measure(childWidth, childHeight)
|
||||||
|
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
|
||||||
|
fun isSection(position: Int): Boolean
|
||||||
|
|
||||||
|
fun getSectionTitle(position: Int): CharSequence?
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package org.koitharu.kotatsu.ui.common.list
|
package org.koitharu.kotatsu.ui.common.list.decor
|
||||||
|
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.main.list.filter
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaFilter
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.core.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
class FilterAdapter(
|
||||||
|
sortOrders: List<SortOrder> = emptyList(),
|
||||||
|
tags: List<MangaTag> = emptyList(),
|
||||||
|
state: MangaFilter?,
|
||||||
|
private val listener: OnFilterChangedListener
|
||||||
|
) : RecyclerView.Adapter<BaseViewHolder<*, Boolean>>() {
|
||||||
|
|
||||||
|
private val sortOrders = ArrayList<SortOrder>(sortOrders)
|
||||||
|
private val tags = ArrayList(Collections.singletonList(null) + tags)
|
||||||
|
|
||||||
|
private var currentState = state ?: MangaFilter(sortOrders.first(), null)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
|
||||||
|
VIEW_TYPE_SORT -> FilterSortHolder(parent).apply {
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
setCheckedSort(requireData())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VIEW_TYPE_TAG -> FilterTagHolder(parent).apply {
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
setCheckedTag(boundData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("Unknown viewType $viewType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = sortOrders.size + tags.size
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BaseViewHolder<*, Boolean>, position: Int) {
|
||||||
|
when (holder) {
|
||||||
|
is FilterSortHolder -> {
|
||||||
|
val item = sortOrders[position]
|
||||||
|
holder.bind(item, item == currentState.sortOrder)
|
||||||
|
}
|
||||||
|
is FilterTagHolder -> {
|
||||||
|
val item = tags[position - sortOrders.size]
|
||||||
|
holder.bind(item, item == currentState.tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int) = when (position) {
|
||||||
|
in sortOrders.indices -> VIEW_TYPE_SORT
|
||||||
|
else -> VIEW_TYPE_TAG
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCheckedTag(tag: MangaTag?) {
|
||||||
|
if (tag != currentState.tag) {
|
||||||
|
val oldItemPos = tags.indexOf(currentState.tag)
|
||||||
|
val newItemPos = tags.indexOf(tag)
|
||||||
|
currentState = currentState.copy(tag = tag)
|
||||||
|
if (oldItemPos in tags.indices) {
|
||||||
|
notifyItemChanged(sortOrders.size + oldItemPos)
|
||||||
|
}
|
||||||
|
if (newItemPos in tags.indices) {
|
||||||
|
notifyItemChanged(sortOrders.size + newItemPos)
|
||||||
|
}
|
||||||
|
listener.onFilterChanged(currentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCheckedSort(sort: SortOrder) {
|
||||||
|
if (sort != currentState.sortOrder) {
|
||||||
|
val oldItemPos = sortOrders.indexOf(currentState.sortOrder)
|
||||||
|
val newItemPos = sortOrders.indexOf(sort)
|
||||||
|
currentState = currentState.copy(sortOrder = sort)
|
||||||
|
if (oldItemPos in sortOrders.indices) {
|
||||||
|
notifyItemChanged(oldItemPos)
|
||||||
|
}
|
||||||
|
if (newItemPos in sortOrders.indices) {
|
||||||
|
notifyItemChanged(newItemPos)
|
||||||
|
}
|
||||||
|
listener.onFilterChanged(currentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val VIEW_TYPE_SORT = 0
|
||||||
|
const val VIEW_TYPE_TAG = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.main.list.filter
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import kotlinx.android.synthetic.main.item_checkable_single.*
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||||
|
|
||||||
|
class FilterSortHolder(parent: ViewGroup) :
|
||||||
|
BaseViewHolder<SortOrder, Boolean>(parent, R.layout.item_checkable_single) {
|
||||||
|
|
||||||
|
override fun onBind(data: SortOrder, extra: Boolean) {
|
||||||
|
radio.setText(data.titleRes)
|
||||||
|
radio.isChecked = extra
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.main.list.filter
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import kotlinx.android.synthetic.main.item_checkable_single.*
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||||
|
|
||||||
|
class FilterTagHolder(parent: ViewGroup) :
|
||||||
|
BaseViewHolder<MangaTag?, Boolean>(parent, R.layout.item_checkable_single) {
|
||||||
|
|
||||||
|
override fun onBind(data: MangaTag?, extra: Boolean) {
|
||||||
|
radio.text = data?.title ?: context.getString(R.string.all)
|
||||||
|
radio.isChecked = extra
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.main.list.filter
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaFilter
|
||||||
|
|
||||||
|
interface OnFilterChangedListener {
|
||||||
|
|
||||||
|
fun onFilterChanged(filter: MangaFilter)
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<CheckedTextView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/radio"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||||
|
android:background="?android:selectableItemBackground"
|
||||||
|
android:drawableStart="?android:listChoiceIndicatorSingle"
|
||||||
|
android:drawablePadding="12dp"
|
||||||
|
android:gravity="center_vertical|start"
|
||||||
|
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||||
|
tools:text="@tools:sample/full_names" />
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/header_height"
|
||||||
|
android:background="?android:windowBackground"
|
||||||
|
android:gravity="center_vertical|start"
|
||||||
|
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="@tools:sample/lorem[2]" />
|
||||||
Loading…
Reference in New Issue