SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/view/HeaderGridView.kt

434 lines
12 KiB
Kotlin

package jp.juggler.subwaytooter.view
import android.content.Context
import android.database.DataSetObservable
import android.database.DataSetObserver
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.Filter
import android.widget.Filterable
import android.widget.FrameLayout
import android.widget.GridView
import android.widget.ListAdapter
import android.widget.WrapperListAdapter
import java.util.ArrayList
class HeaderGridView : GridView {
private inner class FullWidthFixedViewLayout(context : Context) : FrameLayout(context) {
override fun onMeasure(widthMeasureSpecArg : Int, heightMeasureSpec : Int) {
val targetWidth = (this@HeaderGridView.measuredWidth
- this@HeaderGridView.paddingLeft
- this@HeaderGridView.paddingRight)
val widthMeasureSpec = MeasureSpec.makeMeasureSpec(
targetWidth,
MeasureSpec.getMode(widthMeasureSpecArg)
)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
class Header(
val rangeStart : Int,
val rangeLength : Int,
val itemHeight : Int,
val view : View,
val viewContainer : ViewGroup,
val data : Any?,
val isSelectable : Boolean
) {
var rowStart = 0
var rowLength = 0 // includes header itself
}
companion object {
@Suppress("unused")
private val TAG = "HeaderGridView"
private fun areAllListInfosSelectable(infos : ArrayList<Header>?) : Boolean {
val hasNotSelectable = null != infos?.find { ! it.isSelectable }
return ! hasNotSelectable
}
}
private val headers = ArrayList<Header>()
private var willCalculateRows = true
private var lastNumColumns = -1
private var rowEnd = 0
private fun updateRows() :Boolean{
if(!willCalculateRows) return true
val numColumns = numColumns
if( numColumns < 1) return false
willCalculateRows = false
var row = 0
for(header in headers) {
header.rowStart = row
val rowLength = 1 + (header.rangeLength + numColumns - 1) / numColumns
header.rowLength = rowLength
row += rowLength
}
rowEnd = row
lastNumColumns = numColumns
return true
}
private fun findHeader(pos : Int) : Pair<Header, Int> {
val row = pos / lastNumColumns
var start = 0
var end = headers.size
while(end - start > 0) {
val mid = (start + end) shr 1
val header = headers[mid]
when {
row < header.rowStart -> end = mid
row >= header.rowStart + header.rowLength -> start = mid + 1
else -> {
val offset = pos - header.rowStart * lastNumColumns
return Pair(header, offset)
}
}
}
throw ArrayIndexOutOfBoundsException("pos=$pos,row=$row,start=$start,end=$end,headers.size=${headers.size}")
}
fun findListItemIndex(position : Int) : Int {
return if(adapter is HeaderViewGridAdapter) {
if(!updateRows()) return -1
val (header, idx) = findHeader(position)
val offset = idx - lastNumColumns
if(offset in (0 until header.rangeLength)) {
offset + header.rangeStart
} else {
- 1
}
} else {
position
}
}
constructor(context : Context) : super(context) {
init()
}
constructor(context : Context, attrs : AttributeSet) : super(context, attrs) {
init()
}
constructor(context : Context, attrs : AttributeSet, defStyle : Int) :
super(context, attrs, defStyle) {
init()
}
override fun setClipChildren(clipChildren : Boolean) {
// Ignore, since the header rows depend on not being clipped
}
private fun init() {
super.setClipChildren(false)
}
override fun onMeasure(widthMeasureSpec : Int, heightMeasureSpec : Int) {
val oldNumColumns = numColumns
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val numColumns = numColumns
if(numColumns != oldNumColumns) {
(adapter as? HeaderViewGridAdapter)?.update()
}
}
/**
* Add a fixed view to appear at the top of the grid. If addHeaderView is
* called more than once, the views will appear in the order they were
* added. Views added using this call can take focus if they want.
*
*
* NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
* the supplied cursor with one that will also account for header views.
*
* @param v The view to add.
* @param data Data to associate with this view
* @param isSelectable whether the item is selectable
*/
@JvmOverloads
@Suppress("unused")
fun addHeaderView(
rangeStart : Int,
rangeLength : Int,
itemHeight : Int,
v : View,
data : Any? = null,
isSelectable : Boolean = true
) {
if(adapter != null && adapter !is HeaderViewGridAdapter) {
error("Cannot add header view to grid -- setAdapter has already been called.")
}
headers.add(
Header(
rangeStart = rangeStart,
rangeLength = rangeLength,
itemHeight = itemHeight,
view = v,
viewContainer = FullWidthFixedViewLayout(context)
.apply {
try {
// remove from old parent
(v.parent as? ViewGroup)?.removeView(v)
}catch(ex:Throwable){
Log.w(TAG,"can't remove from old parent",ex)
}
addView(v)
},
data = data,
isSelectable = isSelectable
)
)
(adapter as? HeaderViewGridAdapter)?.update()
}
/**
* Removes a previously-added header view.
*
* @param v The view to remove
* @return true if the view was removed, false if the view was not a header
* view
*/
@Suppress("unused")
fun removeHeaderView(v : View) : Boolean {
willCalculateRows = true
val it = headers.iterator()
while(it.hasNext()) {
val info = it.next()
if(info.view === v) {
it.remove()
(adapter as? HeaderViewGridAdapter)?.update()
return true
}
}
return false
}
fun reset() {
headers.clear()
adapter = null
}
override fun setAdapter(adapter : ListAdapter?) {
if(adapter !=null && headers.size > 0) {
willCalculateRows = true
super.setAdapter(HeaderViewGridAdapter(adapter))
} else {
super.setAdapter(adapter)
}
}
/**
* ListAdapter used when a HeaderGridView has header views. This ListAdapter
* wraps another one and also keeps track of the header views and their
* associated data objects.
*
* This is intended as a base class; you will probably not need to
* use this class directly in your own code.
*/
private inner class HeaderViewGridAdapter(private val mAdapter : ListAdapter) :
WrapperListAdapter, Filterable {
// This is used to notify the container of updates relating to number of columns
// or headers changing, which changes the number of placeholders needed
private val mDataSetObservable = DataSetObservable()
internal var mAreAllFixedViewsSelectable : Boolean = false
private val mIsFilterable : Boolean
init {
mIsFilterable = mAdapter is Filterable
mAreAllFixedViewsSelectable = areAllListInfosSelectable(headers)
}
override fun isEmpty() : Boolean {
return (mAdapter.isEmpty) && headers.size == 0
}
override fun areAllItemsEnabled() : Boolean {
return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled()
}
override fun hasStableIds() : Boolean {
return mAdapter.hasStableIds()
}
override fun registerDataSetObserver(observer : DataSetObserver) {
mDataSetObservable.registerObserver(observer)
mAdapter.registerDataSetObserver(observer)
}
override fun unregisterDataSetObserver(observer : DataSetObserver) {
mDataSetObservable.unregisterObserver(observer)
mAdapter.unregisterDataSetObserver(observer)
}
override fun getFilter() : Filter? {
return if(mIsFilterable) {
(mAdapter as Filterable).filter
} else null
}
override fun getWrappedAdapter() : ListAdapter? {
return mAdapter
}
fun update() {
willCalculateRows = true
mAreAllFixedViewsSelectable = areAllListInfosSelectable(headers)
mDataSetObservable.notifyChanged()
}
override fun getCount() : Int {
if(!updateRows()) return 0
return rowEnd * lastNumColumns
}
override fun isEnabled(position : Int) : Boolean {
if(!updateRows()) return false
val (header, idx) = findHeader(position)
return when {
// Header
idx == 0 -> header.isSelectable
// right of header
idx < lastNumColumns -> false
// data
else -> {
val offset = idx - lastNumColumns
if(offset in (0 until header.rangeLength)) {
mAdapter.isEnabled(offset + header.rangeStart)
} else {
false
}
}
}
}
override fun getItem(position : Int) : Any? {
if(!updateRows()) return null
val (header, idx) = findHeader(position)
return when {
// Header
idx == 0 -> header.data
// right of header
idx < lastNumColumns -> null
// data
else -> {
val offset = idx - lastNumColumns
if(offset in (0 until header.rangeLength)) {
mAdapter.getItem(offset + header.rangeStart)
} else {
null
}
}
}
}
override fun getItemId(position : Int) : Long {
if(!updateRows()) return -1L
val (header, idx) = findHeader(position)
return when {
// Header, right of header
idx < lastNumColumns -> - 1L
// data
else -> {
val offset = idx - lastNumColumns
if(offset in (0 until header.rangeLength)) {
mAdapter.getItemId(offset + header.rangeStart)
} else {
- 1L
}
}
}
}
override fun getViewTypeCount() : Int {
return mAdapter.viewTypeCount + 1
}
override fun getItemViewType(position : Int) : Int {
if(!updateRows()) error("view required before layout")
val (header, idx) = findHeader(position)
return when {
// Header
idx == 0 -> AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER
// right of header
// Placeholders get the last view type number
idx < lastNumColumns -> mAdapter.viewTypeCount
// data
else -> {
val offset = idx - lastNumColumns
if(offset in (0 until header.rangeLength)) {
mAdapter.getItemViewType(offset + header.rangeStart)
} else {
// Placeholders get the last view type number
mAdapter.viewTypeCount
}
}
}
}
override fun getView(position : Int, convertViewArg : View?, parent : ViewGroup) : View? {
if(!updateRows()) error("view required before layout.")
val (header, idx) = findHeader(position)
return when {
// Header
idx == 0 -> header.viewContainer
// right of header
// Placeholders get the last view type number
idx < lastNumColumns -> (convertViewArg ?: View(parent.context))
.apply {
// We need to do this because GridView uses the height of the last item
// in a row to determine the height for the entire row.
visibility = View.INVISIBLE
minimumHeight = header.viewContainer.height
}
// data
else -> {
val offset = idx - lastNumColumns
if(offset in (0 until header.rangeLength)) {
mAdapter.getView(offset + header.rangeStart, convertViewArg, parent)
} else {
// Placeholders get the last view type number
(convertViewArg ?: View(parent.context))
.apply {
// We need to do this because GridView uses the height of the last item
// in a row to determine the height for the entire row.
visibility = View.INVISIBLE
minimumHeight = header.itemHeight
}
}
}
}
}
}
}