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
|
||||
|
||||
enum class SortOrder {
|
||||
ALPHABETICAL, POPULARITY, UPDATED, NEWEST, RATING
|
||||
import androidx.annotation.StringRes
|
||||
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.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)
|
||||
}
|
||||
@ -1,53 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
tools:openDrawer="end">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_holder"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_manga_list" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_holder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:text="?android:textColorSecondary"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
tools:text="@tools:sample/lorem[3]" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:text="?android:textColorSecondary"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
tools:text="@tools:sample/lorem[3]" />
|
||||
android:indeterminate="true" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true" />
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView_filter"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="?android:windowBackground"
|
||||
android:orientation="vertical"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_category_checkable" />
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
@ -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