Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt

466 lines
18 KiB
Kotlin
Raw Normal View History

2016-06-29 15:47:52 +02:00
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.adapter
import android.content.Context
import android.database.Cursor
import android.database.CursorIndexOutOfBoundsException
2016-06-29 15:47:52 +02:00
import android.support.v4.widget.Space
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
2017-03-01 15:12:25 +01:00
import com.bumptech.glide.RequestManager
2017-01-07 07:16:02 +01:00
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.contains
import org.mariotaku.ktextension.findPositionByItemId
import org.mariotaku.ktextension.rangeOfSize
2016-06-29 15:47:52 +02:00
import org.mariotaku.ktextension.safeMoveToPosition
import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter
2016-12-06 04:08:56 +01:00
import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter.Companion.ITEM_VIEW_TYPE_GAP
2016-08-19 16:25:27 +02:00
import org.mariotaku.twidere.adapter.iface.IItemCountsAdapter
2016-06-29 15:47:52 +02:00
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
2016-12-06 04:08:56 +01:00
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.Companion.ITEM_VIEW_TYPE_LOAD_INDICATOR
2016-06-29 15:47:52 +02:00
import org.mariotaku.twidere.adapter.iface.IStatusesAdapter
import org.mariotaku.twidere.annotation.PreviewStyle
import org.mariotaku.twidere.constant.*
2017-01-07 07:16:02 +01:00
import org.mariotaku.twidere.constant.SharedPreferenceConstants.KEY_DISPLAY_SENSITIVE_CONTENTS
2017-01-31 14:10:20 +01:00
import org.mariotaku.twidere.model.*
2016-06-29 15:47:52 +02:00
import org.mariotaku.twidere.util.StatusAdapterLinkClickHandler
import org.mariotaku.twidere.util.TwidereLinkify
import org.mariotaku.twidere.util.Utils
import org.mariotaku.twidere.view.holder.EmptyViewHolder
import org.mariotaku.twidere.view.holder.GapViewHolder
import org.mariotaku.twidere.view.holder.LoadIndicatorViewHolder
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
2016-12-06 04:08:56 +01:00
import java.util.*
2016-06-29 15:47:52 +02:00
/**
* Created by mariotaku on 15/10/26.
*/
abstract class ParcelableStatusesAdapter(
2017-03-01 15:12:25 +01:00
context: Context,
2017-03-02 07:59:19 +01:00
requestManager: RequestManager
) : LoadMoreSupportAdapter<RecyclerView.ViewHolder>(context, requestManager), IStatusesAdapter<List<ParcelableStatus>>,
2016-08-19 16:25:27 +02:00
IItemCountsAdapter {
2016-08-18 15:02:49 +02:00
2017-01-07 07:16:02 +01:00
protected val inflater: LayoutInflater = LayoutInflater.from(context)
2016-08-18 15:02:49 +02:00
2016-08-19 16:25:27 +02:00
final override val twidereLinkify: TwidereLinkify
@PreviewStyle
2017-01-07 07:16:02 +01:00
final override val mediaPreviewStyle: Int = preferences[mediaPreviewStyleKey]
final override val nameFirst: Boolean = preferences[nameFirstKey]
final override val useStarsForLikes: Boolean = preferences[iWantMyStarsBackKey]
@TwidereLinkify.HighlightStyle
final override val linkHighlightingStyle: Int = preferences[linkHighlightOptionKey]
final override val lightFont: Boolean = preferences[lightFontKey]
final override val mediaPreviewEnabled: Boolean = Utils.isMediaPreviewEnabled(context, preferences)
final override val sensitiveContentEnabled: Boolean = preferences.getBoolean(KEY_DISPLAY_SENSITIVE_CONTENTS, false)
2017-01-07 07:16:02 +01:00
private val showCardActions: Boolean = !preferences[hideCardActionsKey]
2016-06-29 15:47:52 +02:00
2016-12-06 04:08:56 +01:00
private val gapLoadingIds: MutableSet<ObjectId> = HashSet()
override var statusClickListener: IStatusViewHolder.StatusClickListener? = null
2016-06-29 15:47:52 +02:00
override val gapClickListener: IGapSupportedAdapter.GapClickListener?
get() = statusClickListener
2016-08-19 16:25:27 +02:00
val hasPinnedStatuses: Boolean
get() = pinnedStatuses != null
2016-06-29 15:47:52 +02:00
override var showAccountsColor: Boolean = false
set(value) {
if (field == value) return
field = value
notifyDataSetChanged()
}
var isShowInReplyTo: Boolean = false
set(value) {
if (field == value) return
field = value
notifyDataSetChanged()
}
2016-08-18 15:02:49 +02:00
2016-08-19 16:25:27 +02:00
var pinnedStatuses: List<ParcelableStatus>? = null
set(value) {
field = value
value?.forEach { it.is_pinned_status = true }
2017-02-28 07:35:31 +01:00
updateItemCount()
2016-08-19 16:25:27 +02:00
notifyDataSetChanged()
}
2016-06-29 15:47:52 +02:00
private var data: List<ParcelableStatus>? = null
2017-02-28 07:35:31 +01:00
private var displayPositions: IntArray? = null
private var displayDataCount: Int = 0
2016-06-29 15:47:52 +02:00
private var showingActionCardId = RecyclerView.NO_ID
2017-01-31 14:10:20 +01:00
override val itemCounts = ItemCounts(4)
2016-08-19 16:25:27 +02:00
2017-02-28 07:35:31 +01:00
val statusStartIndex: Int
get() = getItemStartPosition(ITEM_INDEX_STATUS)
override var loadMoreIndicatorPosition: Long
get() = super.loadMoreIndicatorPosition
set(value) {
super.loadMoreIndicatorPosition = value
updateItemCount()
}
override var loadMoreSupportedPosition: Long
get() = super.loadMoreSupportedPosition
set(value) {
super.loadMoreSupportedPosition = value
updateItemCount()
}
2016-06-29 15:47:52 +02:00
init {
val handler = StatusAdapterLinkClickHandler<List<ParcelableStatus>>(context, preferences)
twidereLinkify = TwidereLinkify(handler)
handler.setAdapter(this)
isShowInReplyTo = true
setHasStableIds(true)
}
override fun isGapItem(position: Int): Boolean {
val dataPosition = position - statusStartIndex
val statusCount = getStatusCount(false)
2016-06-29 15:47:52 +02:00
if (dataPosition < 0 || dataPosition >= statusCount) return false
// Don't show gap if it's last item
if (dataPosition == statusCount - 1) return false
if (data is ObjectCursor) {
val cursor = (data as ObjectCursor).cursor
if (!cursor.moveToPosition(dataPosition)) return false
val indices = (data as ObjectCursor).indices as ParcelableStatusCursorIndices
return cursor.getShort(indices.is_gap).toInt() == 1
}
return getStatus(position).is_gap
2016-06-29 15:47:52 +02:00
}
override fun getStatus(position: Int, raw: Boolean): ParcelableStatus {
return getStatus(position, getItemCountIndex(position, raw), raw)
2016-06-29 15:47:52 +02:00
}
override fun getStatusCount(raw: Boolean): Int {
if (raw) return data?.size ?: 0
return displayDataCount
}
2016-06-29 15:47:52 +02:00
2017-02-28 07:35:31 +01:00
override fun setData(data: List<ParcelableStatus>?): Boolean {
var changed = true
if (data == null) {
displayPositions = null
displayDataCount = 0
} else if (data is ObjectCursor) {
displayPositions = null
displayDataCount = data.size
} else {
var filteredCount = 0
displayPositions = IntArray(data.size).apply {
data.forEachIndexed { i, item ->
if (!item.is_gap && item.is_filtered) {
filteredCount++
} else {
this[i - filteredCount] = i
}
}
}
displayDataCount = data.size - filteredCount
changed = this.data != data
2017-02-28 07:35:31 +01:00
}
this.data = data
gapLoadingIds.clear()
updateItemCount()
notifyDataSetChanged()
return changed
}
fun getData(): List<ParcelableStatus>? {
return data
}
2016-06-29 15:47:52 +02:00
override fun getItemId(position: Int): Long {
2017-02-28 07:35:31 +01:00
val countIndex = getItemCountIndex(position)
when (countIndex) {
ITEM_INDEX_PINNED_STATUS -> {
val status = pinnedStatuses!![position - getItemStartPosition(ITEM_INDEX_PINNED_STATUS)]
val mask = ITEM_INDEX_PINNED_STATUS.toLong() shl 32
return mask + status.hashCode()
}
ITEM_INDEX_STATUS -> return getFieldValue(position, { cursor, indices ->
val accountKey = UserKey.valueOf(cursor.getString(indices.account_key))
val id = cursor.getString(indices.id)
return@getFieldValue ParcelableStatus.calculateHashCode(accountKey, id).toLong()
}, { status ->
return@getFieldValue status.hashCode().toLong()
}, -1L)
else -> return (countIndex.toLong() shl 32) + position
}
2016-06-29 15:47:52 +02:00
}
override fun getStatusId(position: Int, raw: Boolean): String {
2017-02-28 07:35:31 +01:00
return getFieldValue(position, { cursor, indices ->
return@getFieldValue cursor.getString(indices.id)
}, { status ->
return@getFieldValue status.id
}, "")
}
fun getStatusSortId(position: Int): Long {
return getFieldValue(position, { cursor, indices ->
return@getFieldValue cursor.getLong(indices.sort_id)
}, { status ->
return@getFieldValue status.sort_id
}, -1L)
2016-06-29 15:47:52 +02:00
}
override fun getStatusTimestamp(position: Int, raw: Boolean): Long {
return getFieldValue(position, { cursor, indices ->
return@getFieldValue cursor.getLong(indices.timestamp)
}, { status ->
return@getFieldValue status.timestamp
}, -1L)
2016-06-29 15:47:52 +02:00
}
override fun getStatusPositionKey(position: Int, raw: Boolean): Long {
return getFieldValue(position, { cursor, indices ->
2016-06-29 15:47:52 +02:00
val positionKey = cursor.getLong(indices.position_key)
if (positionKey > 0) return@getFieldValue positionKey
return@getFieldValue cursor.getLong(indices.timestamp)
}, { status ->
val positionKey = status.position_key
if (positionKey > 0) return@getFieldValue positionKey
return@getFieldValue status.timestamp
}, -1L)
2016-06-29 15:47:52 +02:00
}
override fun getAccountKey(position: Int, raw: Boolean): UserKey {
2017-02-28 07:35:31 +01:00
val def: UserKey? = null
return getFieldValue(position, { cursor, indices ->
return@getFieldValue UserKey.valueOf(cursor.getString(indices.account_key))
}, { status ->
return@getFieldValue status.account_key
}, def, raw)!!
2016-06-29 15:47:52 +02:00
}
override fun isCardActionsShown(position: Int): Boolean {
if (position == RecyclerView.NO_POSITION) return showCardActions
return showCardActions || showingActionCardId == getItemId(position)
}
override fun showCardActions(position: Int) {
if (showingActionCardId != RecyclerView.NO_ID) {
val pos = findPositionByItemId(showingActionCardId)
if (pos != RecyclerView.NO_POSITION) {
notifyItemChanged(pos)
}
}
showingActionCardId = getItemId(position)
if (position != RecyclerView.NO_POSITION) {
notifyItemChanged(position)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (viewType) {
2017-02-28 07:35:31 +01:00
VIEW_TYPE_STATUS -> {
2016-06-29 15:47:52 +02:00
return onCreateStatusViewHolder(parent) as RecyclerView.ViewHolder
}
2016-12-06 04:08:56 +01:00
ITEM_VIEW_TYPE_GAP -> {
val view = inflater.inflate(GapViewHolder.layoutResource, parent, false)
2016-06-29 15:47:52 +02:00
return GapViewHolder(this, view)
}
2016-12-06 04:08:56 +01:00
ITEM_VIEW_TYPE_LOAD_INDICATOR -> {
2017-01-31 14:10:20 +01:00
val view = inflater.inflate(R.layout.list_item_load_indicator, parent, false)
2016-06-29 15:47:52 +02:00
return LoadIndicatorViewHolder(view)
}
2017-02-28 07:35:31 +01:00
VIEW_TYPE_EMPTY -> {
2016-06-29 15:47:52 +02:00
return EmptyViewHolder(Space(context))
}
}
throw IllegalStateException("Unknown view type " + viewType)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
2017-02-28 07:35:31 +01:00
when (holder.itemViewType) {
VIEW_TYPE_STATUS -> {
val countIdx = getItemCountIndex(position)
val status = getStatus(position, countIdx)
2017-02-28 07:35:31 +01:00
(holder as IStatusViewHolder).displayStatus(status, displayInReplyTo = isShowInReplyTo,
displayPinned = countIdx == ITEM_INDEX_PINNED_STATUS)
2016-08-19 16:25:27 +02:00
}
2017-02-28 07:35:31 +01:00
ITEM_VIEW_TYPE_GAP -> {
val status = getStatus(position)
2017-02-28 07:35:31 +01:00
val loading = gapLoadingIds.any { it.accountKey == status.account_key && it.id == status.id }
(holder as GapViewHolder).display(loading)
2016-06-29 15:47:52 +02:00
}
}
2016-12-06 04:08:56 +01:00
}
2016-06-29 15:47:52 +02:00
override fun getItemViewType(position: Int): Int {
if (position == 0 && ILoadMoreSupportAdapter.START in loadMoreIndicatorPosition) {
2016-12-06 04:08:56 +01:00
return ITEM_VIEW_TYPE_LOAD_INDICATOR
2016-06-29 15:47:52 +02:00
}
2016-08-19 16:25:27 +02:00
when (getItemCountIndex(position)) {
2017-02-28 07:35:31 +01:00
ITEM_INDEX_LOAD_START_INDICATOR, ITEM_INDEX_LOAD_END_INDICATOR -> {
2016-12-06 04:08:56 +01:00
return ITEM_VIEW_TYPE_LOAD_INDICATOR
2016-08-19 16:25:27 +02:00
}
2017-02-28 07:35:31 +01:00
ITEM_INDEX_PINNED_STATUS -> {
return VIEW_TYPE_STATUS
2016-08-19 16:25:27 +02:00
}
2017-02-28 07:35:31 +01:00
ITEM_INDEX_STATUS -> {
2016-08-19 16:25:27 +02:00
if (isGapItem(position)) {
2016-12-06 04:08:56 +01:00
return ITEM_VIEW_TYPE_GAP
2016-08-19 16:25:27 +02:00
} else {
2017-02-28 07:35:31 +01:00
return VIEW_TYPE_STATUS
2016-08-19 16:25:27 +02:00
}
}
2016-06-29 15:47:52 +02:00
}
2016-08-19 16:25:27 +02:00
throw AssertionError()
2016-06-29 15:47:52 +02:00
}
2017-02-28 07:35:31 +01:00
protected abstract fun onCreateStatusViewHolder(parent: ViewGroup): IStatusViewHolder
override fun addGapLoadingId(id: ObjectId) {
gapLoadingIds.add(id)
}
override fun removeGapLoadingId(id: ObjectId) {
gapLoadingIds.remove(id)
}
fun isStatus(position: Int, raw: Boolean = false): Boolean {
return position < getStatusCount(raw)
2017-02-28 07:35:31 +01:00
}
2016-06-29 15:47:52 +02:00
override fun getItemCount(): Int {
2017-01-31 14:10:20 +01:00
return itemCounts.itemCount
2016-06-29 15:47:52 +02:00
}
override fun findStatusById(accountKey: UserKey, statusId: String): ParcelableStatus? {
for (i in 0 until getStatusCount(true)) {
if (accountKey == getAccountKey(i, true) && statusId == getStatusId(i, true)) {
return getStatus(i, true)
2016-06-29 15:47:52 +02:00
}
}
return null
}
fun findPositionByPositionKey(positionKey: Long, raw: Boolean = false): Int {
2017-02-28 07:35:31 +01:00
// Assume statuses are descend sorted by id, so break at first status with id
// lesser equals than read position
if (positionKey <= 0) return RecyclerView.NO_POSITION
val range = rangeOfSize(statusStartIndex, getStatusCount(raw))
2017-02-28 07:35:31 +01:00
if (range.isEmpty()) return RecyclerView.NO_POSITION
if (positionKey < getStatusPositionKey(range.last)) {
return range.last
}
return range.indexOfFirst { positionKey >= getStatusPositionKey(it) }
}
fun findPositionBySortId(sortId: Long, raw: Boolean = false): Int {
2017-02-28 07:35:31 +01:00
// Assume statuses are descend sorted by id, so break at first status with id
// lesser equals than read position
if (sortId <= 0) return RecyclerView.NO_POSITION
val range = rangeOfSize(statusStartIndex, getStatusCount(raw))
2017-02-28 07:35:31 +01:00
if (range.isEmpty()) return RecyclerView.NO_POSITION
if (sortId < getStatusSortId(range.last)) {
return range.last
}
return range.indexOfFirst { sortId >= getStatusSortId(it) }
}
2016-06-29 15:47:52 +02:00
private fun getItemCountIndex(position: Int, raw: Boolean): Int {
if (!raw) return itemCounts.getItemCountIndex(position)
var sum = 0
for (i in 0 until itemCounts.size) {
sum += when (i) {
ITEM_INDEX_STATUS -> data!!.size
else -> itemCounts[i]
}
if (position < sum) {
return i
}
}
return -1
}
private inline fun <T> getFieldValue(position: Int,
readCursorValueAction: (cursor: Cursor, indices: ParcelableStatusCursorIndices) -> T,
readStatusValueAction: (status: ParcelableStatus) -> T,
defValue: T, raw: Boolean = false): T {
if (data is ObjectCursor) {
val dataPosition = position - statusStartIndex
if (dataPosition < 0 || dataPosition >= getStatusCount(true)) {
throw CursorIndexOutOfBoundsException("index: $position, valid range is $0..${getStatusCount(true)}")
}
val cursor = (data as ObjectCursor).cursor
if (!cursor.safeMoveToPosition(dataPosition)) return defValue
val indices = (data as ObjectCursor).indices as ParcelableStatusCursorIndices
return readCursorValueAction(cursor, indices)
}
2017-03-15 09:06:55 +01:00
return readStatusValueAction(getStatus(position, raw))
}
private fun getStatus(position: Int, countIndex: Int, raw: Boolean = false): ParcelableStatus {
2017-02-28 07:35:31 +01:00
when (countIndex) {
ITEM_INDEX_PINNED_STATUS -> {
return pinnedStatuses!![position - getItemStartPosition(ITEM_INDEX_PINNED_STATUS)]
}
ITEM_INDEX_STATUS -> {
val data = this.data!!
val dataPosition = position - statusStartIndex
2017-02-28 07:35:31 +01:00
val positions = displayPositions
if (positions != null && !raw) {
2017-02-28 07:35:31 +01:00
return data[positions[dataPosition]]
} else {
return data[dataPosition]
}
}
}
val validStart = getItemStartPosition(ITEM_INDEX_PINNED_STATUS)
val validEnd = getItemStartPosition(ITEM_INDEX_STATUS) + getStatusCount(raw) - 1
throw IndexOutOfBoundsException("index: $position, valid range is $validStart..$validEnd")
2016-06-29 15:47:52 +02:00
}
2017-02-28 07:35:31 +01:00
private fun updateItemCount() {
itemCounts[ITEM_INDEX_LOAD_START_INDICATOR] = if (ILoadMoreSupportAdapter.START in loadMoreIndicatorPosition) 1 else 0
2017-02-28 07:35:31 +01:00
itemCounts[ITEM_INDEX_PINNED_STATUS] = pinnedStatuses?.size ?: 0
itemCounts[ITEM_INDEX_STATUS] = getStatusCount(false)
itemCounts[ITEM_INDEX_LOAD_END_INDICATOR] = if (ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition) 1 else 0
2016-06-29 15:47:52 +02:00
}
2017-02-28 07:35:31 +01:00
companion object {
const val VIEW_TYPE_STATUS = 2
const val VIEW_TYPE_EMPTY = 3
2017-02-28 07:35:31 +01:00
const val ITEM_INDEX_LOAD_START_INDICATOR = 0
const val ITEM_INDEX_PINNED_STATUS = 1
const val ITEM_INDEX_STATUS = 2
const val ITEM_INDEX_LOAD_END_INDICATOR = 3
}
2016-08-19 16:25:27 +02:00
2016-06-29 15:47:52 +02:00
}