improved timeline position after load

This commit is contained in:
Mariotaku Lee 2017-12-30 17:26:14 +08:00
parent d728a86f17
commit e8774425a7
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
6 changed files with 164 additions and 76 deletions

View File

@ -0,0 +1,26 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 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 android.arch.paging
fun <T> PagedListAdapterHelper<T>.setPagedListListener(listener: ((list: PagedList<T>?) -> Unit)?) {
mListener = if (listener != null) PagedListAdapterHelper.PagedListListener { pagedList ->
listener(pagedList)
} else null
}

View File

@ -22,6 +22,7 @@ package org.mariotaku.twidere.adapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.arch.paging.PagedList import android.arch.paging.PagedList
import android.arch.paging.PagedListAdapterHelper import android.arch.paging.PagedListAdapterHelper
import android.arch.paging.setPagedListListener
import android.content.Context import android.content.Context
import android.support.v7.recyclerview.extensions.ListAdapterConfig import android.support.v7.recyclerview.extensions.ListAdapterConfig
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
@ -44,7 +45,11 @@ import org.mariotaku.twidere.annotation.TimelineStyle
import org.mariotaku.twidere.constant.newDocumentApiKey import org.mariotaku.twidere.constant.newDocumentApiKey
import org.mariotaku.twidere.exception.UnsupportedCountIndexException import org.mariotaku.twidere.exception.UnsupportedCountIndexException
import org.mariotaku.twidere.extension.model.activityStatus import org.mariotaku.twidere.extension.model.activityStatus
import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.ItemCounts
import org.mariotaku.twidere.model.ObjectId
import org.mariotaku.twidere.model.ParcelableActivity
import org.mariotaku.twidere.model.ParcelableMedia
import org.mariotaku.twidere.model.placeholder.ParcelableActivityPlaceholder
import org.mariotaku.twidere.util.IntentUtils import org.mariotaku.twidere.util.IntentUtils
import org.mariotaku.twidere.util.OnLinkClickHandler import org.mariotaku.twidere.util.OnLinkClickHandler
import org.mariotaku.twidere.util.TwidereLinkify import org.mariotaku.twidere.util.TwidereLinkify
@ -119,6 +124,12 @@ class ParcelableActivitiesAdapter(
var activityClickListener: ActivityAdapterListener? = null var activityClickListener: ActivityAdapterListener? = null
var pagedListListener: ((list: PagedList<ParcelableActivity>?) -> Unit)? = null
set(value) {
field = value
pagedActivitiesHelper.setPagedListListener(value)
}
private val inflater = LayoutInflater.from(context) private val inflater = LayoutInflater.from(context)
private val twidereLinkify = TwidereLinkify(OnLinkClickHandler(context, null, preferences)) private val twidereLinkify = TwidereLinkify(OnLinkClickHandler(context, null, preferences))
private val statusAdapterDelegate = DummyItemAdapter(context, twidereLinkify, this, requestManager) private val statusAdapterDelegate = DummyItemAdapter(context, twidereLinkify, this, requestManager)
@ -400,18 +411,6 @@ class ParcelableActivitiesAdapter(
} }
} }
object ParcelableActivityPlaceholder : ParcelableActivity() {
init {
id = "none"
account_key = UserKey.INVALID
user_key = UserKey.INVALID
}
override fun hashCode(): Int {
return -1
}
}
companion object { companion object {
const val ITEM_VIEW_TYPE_STUB = 0 const val ITEM_VIEW_TYPE_STUB = 0
const val ITEM_VIEW_TYPE_GAP = 1 const val ITEM_VIEW_TYPE_GAP = 1

View File

@ -21,6 +21,7 @@ package org.mariotaku.twidere.adapter
import android.arch.paging.PagedList import android.arch.paging.PagedList
import android.arch.paging.PagedListAdapterHelper import android.arch.paging.PagedListAdapterHelper
import android.arch.paging.setPagedListListener
import android.content.Context import android.content.Context
import android.support.v4.widget.Space import android.support.v4.widget.Space
import android.support.v7.recyclerview.extensions.ListAdapterConfig import android.support.v7.recyclerview.extensions.ListAdapterConfig
@ -99,6 +100,20 @@ class ParcelableStatusesAdapter(
override val itemCounts = ItemCounts(5) override val itemCounts = ItemCounts(5)
override var loadMoreIndicatorPosition: Int
get() = super.loadMoreIndicatorPosition
set(value) {
super.loadMoreIndicatorPosition = value
updateItemCounts()
}
override var loadMoreSupportedPosition: Int
get() = super.loadMoreSupportedPosition
set(value) {
super.loadMoreSupportedPosition = value
updateItemCounts()
}
var isShowInReplyTo: Boolean = false var isShowInReplyTo: Boolean = false
set(value) { set(value) {
if (field == value) return if (field == value) return
@ -134,18 +149,10 @@ class ParcelableStatusesAdapter(
val statusStartIndex: Int val statusStartIndex: Int
get() = getItemStartPosition(ITEM_INDEX_STATUS) get() = getItemStartPosition(ITEM_INDEX_STATUS)
override var loadMoreIndicatorPosition: Int var pagedListListener: ((list: PagedList<ParcelableStatus>?) -> Unit)? = null
get() = super.loadMoreIndicatorPosition
set(value) { set(value) {
super.loadMoreIndicatorPosition = value field = value
updateItemCounts() pagedStatusesHelper.setPagedListListener(value)
}
override var loadMoreSupportedPosition: Int
get() = super.loadMoreSupportedPosition
set(value) {
super.loadMoreSupportedPosition = value
updateItemCounts()
} }
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)

View File

@ -60,7 +60,9 @@ import org.mariotaku.twidere.data.processor.DataSourceItemProcessor
import org.mariotaku.twidere.extension.model.activityStatus import org.mariotaku.twidere.extension.model.activityStatus
import org.mariotaku.twidere.extension.queryOne import org.mariotaku.twidere.extension.queryOne
import org.mariotaku.twidere.extension.showContextMenuForChild import org.mariotaku.twidere.extension.showContextMenuForChild
import org.mariotaku.twidere.extension.view.PositionWithOffset
import org.mariotaku.twidere.extension.view.firstVisibleItemPosition import org.mariotaku.twidere.extension.view.firstVisibleItemPosition
import org.mariotaku.twidere.extension.view.firstVisibleItemPositionWithOffset
import org.mariotaku.twidere.extension.view.lastVisibleItemPosition import org.mariotaku.twidere.extension.view.lastVisibleItemPosition
import org.mariotaku.twidere.fragment.AbsContentRecyclerViewFragment import org.mariotaku.twidere.fragment.AbsContentRecyclerViewFragment
import org.mariotaku.twidere.fragment.timeline.AbsTimelineFragment import org.mariotaku.twidere.fragment.timeline.AbsTimelineFragment
@ -82,6 +84,7 @@ import org.mariotaku.twidere.view.ExtendedRecyclerView
import org.mariotaku.twidere.view.holder.ActivityTitleSummaryViewHolder import org.mariotaku.twidere.view.holder.ActivityTitleSummaryViewHolder
import org.mariotaku.twidere.view.holder.GapViewHolder import org.mariotaku.twidere.view.holder.GapViewHolder
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
import java.util.concurrent.atomic.AtomicReference
abstract class AbsActivitiesFragment : AbsContentRecyclerViewFragment<ParcelableActivitiesAdapter, RecyclerView.LayoutManager>() { abstract class AbsActivitiesFragment : AbsContentRecyclerViewFragment<ParcelableActivitiesAdapter, RecyclerView.LayoutManager>() {
@ -125,12 +128,15 @@ abstract class AbsActivitiesFragment : AbsContentRecyclerViewFragment<Parcelable
private val busEventHandler = BusEventHandler() private val busEventHandler = BusEventHandler()
private val scrollHandler = ScrollHandler() private val scrollHandler = ScrollHandler()
private val timelineBoundaryCallback = ActivitiesBoundaryCallback()
private val positionBackup: AtomicReference<PositionWithOffset> = AtomicReference()
private var dataController: ExtendedPagedListProvider.DataController? = null private var dataController: ExtendedPagedListProvider.DataController? = null
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
registerForContextMenu(recyclerView) registerForContextMenu(recyclerView)
adapter.activityClickListener = ActivityClickHandler() adapter.activityClickListener = ActivityClickHandler()
adapter.pagedListListener = this::onPagedListChanged
adapter.loadMoreSupportedPosition = if (isStandalone) { adapter.loadMoreSupportedPosition = if (isStandalone) {
LoadMorePosition.NONE LoadMorePosition.NONE
} else { } else {
@ -235,26 +241,7 @@ abstract class AbsActivitiesFragment : AbsContentRecyclerViewFragment<Parcelable
} }
override fun onLoadMoreContents(position: Int) { override fun onLoadMoreContents(position: Int) {
if (isStandalone || !refreshEnabled || position != LoadMorePosition.END) return // No-op
val started = getActivities(object : ContentRefreshParam {
override val accountKeys by lazy {
this@AbsActivitiesFragment.accountKeys
}
override val pagination by lazy {
val keys = accountKeys.toNulls()
val maxIds = DataStoreUtils.getRefreshOldestActivityMaxPositions(context!!, contentUri, keys)
val maxSortIds = DataStoreUtils.getRefreshOldestActivityMaxSortPositions(context!!, contentUri, keys)
return@lazy Array(keys.size) { idx ->
SinceMaxPagination.maxId(maxIds[idx], maxSortIds[idx])
}
}
override val tabId: Long
get() = this@AbsActivitiesFragment.tabId
})
if (!started) return
super.onLoadMoreContents(position)
} }
override fun scrollToPositionWithOffset(position: Int, offset: Int) { override fun scrollToPositionWithOffset(position: Int, offset: Int) {
@ -269,6 +256,9 @@ abstract class AbsActivitiesFragment : AbsContentRecyclerViewFragment<Parcelable
} }
} }
/**
* Scroll to start of the timeline. This also updates read position
*/
override fun scrollToStart(): Boolean { override fun scrollToStart(): Boolean {
val result = super.scrollToStart() val result = super.scrollToStart()
if (result) saveReadPosition(0) if (result) saveReadPosition(0)
@ -288,7 +278,7 @@ abstract class AbsActivitiesFragment : AbsContentRecyclerViewFragment<Parcelable
protected open fun onDataLoaded(data: PagedList<ParcelableActivity>?) { protected open fun onDataLoaded(data: PagedList<ParcelableActivity>?) {
val firstVisiblePosition = layoutManager.firstVisibleItemPosition val firstVisiblePosition = layoutManager.firstVisibleItemPositionWithOffset
adapter.showAccountsColor = accountKeys.size > 1 adapter.showAccountsColor = accountKeys.size > 1
adapter.activities = data adapter.activities = data
when { when {
@ -299,12 +289,15 @@ abstract class AbsActivitiesFragment : AbsContentRecyclerViewFragment<Parcelable
showContent() showContent()
} }
} }
if (firstVisiblePosition == 0 && !preferences[readFromBottomKey]) { positionBackup.set(firstVisiblePosition)
val weakThis by weak(this) }
recyclerView.post {
val f = weakThis?.takeIf { !it.isDetached } ?: return@post protected open fun onPagedListChanged(data: PagedList<ParcelableActivity>?) {
f.scrollToStart() val firstVisiblePosition = positionBackup.getAndSet(null) ?: return
} if (firstVisiblePosition.position == 0 && !preferences[readFromBottomKey]) {
scrollToPositionWithOffset(0, 0)
} else {
scrollToPositionWithOffset(firstVisiblePosition.position, firstVisiblePosition.offset)
} }
} }
@ -533,6 +526,31 @@ abstract class AbsActivitiesFragment : AbsContentRecyclerViewFragment<Parcelable
} }
} }
private inner class ActivitiesBoundaryCallback : PagedList.BoundaryCallback<ParcelableStatus>() {
override fun onItemAtEndLoaded(itemAtEnd: ParcelableStatus) {
val started = getActivities(object : ContentRefreshParam {
override val accountKeys by lazy {
this@AbsActivitiesFragment.accountKeys
}
override val pagination by lazy {
val keys = accountKeys.toNulls()
val maxIds = DataStoreUtils.getRefreshOldestActivityMaxPositions(context!!, contentUri, keys)
val maxSortIds = DataStoreUtils.getRefreshOldestActivityMaxSortPositions(context!!, contentUri, keys)
return@lazy Array(keys.size) { idx ->
SinceMaxPagination.maxId(maxIds[idx], maxSortIds[idx])
}
}
override val tabId: Long
get() = this@AbsActivitiesFragment.tabId
})
if (started) {
adapter.loadMoreIndicatorPosition = LoadMorePosition.END
}
}
}
companion object { companion object {
val activityColumnsLite = Activities.COLUMNS - arrayOf(Activities.SOURCES, Activities.TARGETS, val activityColumnsLite = Activities.COLUMNS - arrayOf(Activities.SOURCES, Activities.TARGETS,
Activities.TARGET_OBJECTS, Activities.MENTIONS_JSON, Activities.CARD, Activities.TARGET_OBJECTS, Activities.MENTIONS_JSON, Activities.CARD,

View File

@ -70,6 +70,7 @@ import org.mariotaku.twidere.data.fetcher.StatusesFetcher
import org.mariotaku.twidere.extension.* import org.mariotaku.twidere.extension.*
import org.mariotaku.twidere.extension.adapter.removeStatuses import org.mariotaku.twidere.extension.adapter.removeStatuses
import org.mariotaku.twidere.extension.data.observe import org.mariotaku.twidere.extension.data.observe
import org.mariotaku.twidere.extension.view.PositionWithOffset
import org.mariotaku.twidere.extension.view.firstVisibleItemPosition import org.mariotaku.twidere.extension.view.firstVisibleItemPosition
import org.mariotaku.twidere.extension.view.firstVisibleItemPositionWithOffset import org.mariotaku.twidere.extension.view.firstVisibleItemPositionWithOffset
import org.mariotaku.twidere.extension.view.lastVisibleItemPosition import org.mariotaku.twidere.extension.view.lastVisibleItemPosition
@ -99,6 +100,7 @@ import org.mariotaku.twidere.view.holder.GapViewHolder
import org.mariotaku.twidere.view.holder.TimelineFilterHeaderViewHolder import org.mariotaku.twidere.view.holder.TimelineFilterHeaderViewHolder
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
import org.mariotaku.twidere.view.holder.status.StatusViewHolder import org.mariotaku.twidere.view.holder.status.StatusViewHolder
import java.util.concurrent.atomic.AtomicReference
abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableStatusesAdapter, LayoutManager>(), abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableStatusesAdapter, LayoutManager>(),
IFloatingActionButtonFragment { IFloatingActionButtonFragment {
@ -154,12 +156,14 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
private val busEventHandler = BusEventHandler() private val busEventHandler = BusEventHandler()
private val scrollHandler = ScrollHandler() private val scrollHandler = ScrollHandler()
private val timelineBoundaryCallback = StatusesBoundaryCallback() private val timelineBoundaryCallback = StatusesBoundaryCallback()
private val positionBackup: AtomicReference<PositionWithOffset> = AtomicReference()
private var dataController: ExtendedPagedListProvider.DataController? = null private var dataController: ExtendedPagedListProvider.DataController? = null
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
registerForContextMenu(recyclerView) registerForContextMenu(recyclerView)
adapter.statusClickListener = StatusClickHandler() adapter.statusClickListener = StatusClickHandler()
adapter.pagedListListener = this::onPagedListChanged
adapter.loadMoreSupportedPosition = if (isStandalone) { adapter.loadMoreSupportedPosition = if (isStandalone) {
LoadMorePosition.NONE LoadMorePosition.NONE
} else { } else {
@ -285,6 +289,9 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
} }
} }
/**
* Scroll to start of the timeline. This also updates read position
*/
override fun scrollToStart(): Boolean { override fun scrollToStart(): Boolean {
val result = super.scrollToStart() val result = super.scrollToStart()
if (result) saveReadPosition(0) if (result) saveReadPosition(0)
@ -317,7 +324,6 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
showProgress() showProgress()
} }
protected open fun onDataLoaded(data: PagedList<ParcelableStatus>?) { protected open fun onDataLoaded(data: PagedList<ParcelableStatus>?) {
val firstVisiblePosition = layoutManager.firstVisibleItemPositionWithOffset val firstVisiblePosition = layoutManager.firstVisibleItemPositionWithOffset
adapter.showAccountsColor = accountKeys.size > 1 adapter.showAccountsColor = accountKeys.size > 1
@ -331,18 +337,15 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
showContent() showContent()
} }
} }
if (firstVisiblePosition == null) return positionBackup.set(firstVisiblePosition)
}
protected open fun onPagedListChanged(data: PagedList<ParcelableStatus>?) {
val firstVisiblePosition = positionBackup.getAndSet(null) ?: return
if (firstVisiblePosition.position == 0 && !preferences[readFromBottomKey]) { if (firstVisiblePosition.position == 0 && !preferences[readFromBottomKey]) {
val weakThis by weak(this) scrollToPositionWithOffset(0, 0)
recyclerView.post {
val f = weakThis?.takeIf { !it.isDetached && it.view != null } ?: return@post
f.scrollToStart()
}
} else { } else {
val lm = loaderManager scrollToPositionWithOffset(firstVisiblePosition.position, firstVisiblePosition.offset)
if (lm is LinearLayoutManager) {
lm.scrollToPositionWithOffset(firstVisiblePosition.position, firstVisiblePosition.offset)
}
} }
} }
@ -400,7 +403,7 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
private fun setupLiveData() { private fun setupLiveData() {
statuses = if (isStandalone) onCreateStandaloneLiveData() else onCreateDatabaseLiveData() statuses = if (isStandalone) onCreateStandaloneLiveData() else onCreateDatabaseLiveData()
statuses?.observe(this, success = { onDataLoaded(it) }, fail = { statuses?.observe(this, success = this::onDataLoaded, fail = {
showError(R.drawable.ic_info_error_generic, it.getErrorMessage(context!!)) showError(R.drawable.ic_info_error_generic, it.getErrorMessage(context!!))
}) })
} }
@ -487,6 +490,19 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
} }
} }
class DefaultOnLikedListener(
private val context: Context,
private val status: ParcelableStatus,
private val accountKey: UserKey? = null
) : LikeAnimationDrawable.OnLikedListener {
override fun onLiked(): Boolean {
if (status.is_favorite) return false
CreateFavoriteTask(context, accountKey ?: status.account_key, status).promise()
return true
}
}
internal inner class BusEventHandler { internal inner class BusEventHandler {
@Subscribe @Subscribe
@ -634,19 +650,6 @@ abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableSt
} }
} }
class DefaultOnLikedListener(
private val context: Context,
private val status: ParcelableStatus,
private val accountKey: UserKey? = null
) : LikeAnimationDrawable.OnLikedListener {
override fun onLiked(): Boolean {
if (status.is_favorite) return false
CreateFavoriteTask(context, accountKey ?: status.account_key, status).promise()
return true
}
}
companion object { companion object {
const val REQUEST_FAVORITE_SELECT_ACCOUNT = 101 const val REQUEST_FAVORITE_SELECT_ACCOUNT = 101

View File

@ -0,0 +1,35 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 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.model.placeholder
import org.mariotaku.twidere.model.ParcelableActivity
import org.mariotaku.twidere.model.UserKey
object ParcelableActivityPlaceholder : ParcelableActivity() {
init {
id = "none"
account_key = UserKey.INVALID
user_key = UserKey.INVALID
}
override fun hashCode(): Int {
return -1
}
}