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 } }