improved load more gesture

This commit is contained in:
Mariotaku Lee 2017-02-11 02:01:31 +08:00
parent 23dd621445
commit 7522bf04eb
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
14 changed files with 155 additions and 41 deletions

View File

@ -76,6 +76,10 @@ public class ParcelableMessage {
@CursorField(Messages.LOCAL_TIMESTAMP)
public long local_timestamp;
@JsonField(name = "sort_id")
@CursorField(value = Messages.SORT_ID)
public long sort_id;
@JsonField(name = "text_unescaped")
@CursorField(Messages.TEXT_UNESCAPED)
public String text_unescaped;
@ -119,9 +123,13 @@ public class ParcelableMessage {
", conversation_id='" + conversation_id + '\'' +
", message_type='" + message_type + '\'' +
", message_timestamp=" + message_timestamp +
", local_timestamp=" + local_timestamp +
", sort_id=" + sort_id +
", text_unescaped='" + text_unescaped + '\'' +
", media=" + Arrays.toString(media) +
", spans=" + Arrays.toString(spans) +
", extras=" + extras +
", internalExtras=" + internalExtras +
", sender_key=" + sender_key +
", recipient_key=" + recipient_key +
", is_outgoing=" + is_outgoing +
@ -129,7 +137,6 @@ public class ParcelableMessage {
'}';
}
@OnPreJsonSerialize
void beforeJsonSerialize() {
internalExtras = InternalExtras.from(extras);

View File

@ -76,6 +76,10 @@ public class ParcelableMessageConversation {
@CursorField(value = Conversations.LOCAL_TIMESTAMP)
public long local_timestamp;
@JsonField(name = "sort_id")
@CursorField(value = Conversations.SORT_ID)
public long sort_id;
@JsonField(name = "text_unescaped")
@CursorField(Conversations.TEXT_UNESCAPED)
public String text_unescaped;
@ -121,11 +125,13 @@ public class ParcelableMessageConversation {
return "ParcelableMessageConversation{" +
"_id=" + _id +
", account_key=" + account_key +
", account_color=" + account_color +
", id='" + id + '\'' +
", conversation_type='" + conversation_type + '\'' +
", message_type='" + message_type + '\'' +
", message_timestamp=" + message_timestamp +
", local_timestamp=" + local_timestamp +
", sort_id=" + sort_id +
", text_unescaped='" + text_unescaped + '\'' +
", media=" + Arrays.toString(media) +
", spans=" + Arrays.toString(spans) +

View File

@ -361,6 +361,7 @@ public interface TwidereDataStore {
String MESSAGE_TYPE = "message_type";
String MESSAGE_TIMESTAMP = "message_timestamp";
String LOCAL_TIMESTAMP = "local_timestamp";
String SORT_ID = "sort_id";
String TEXT_UNESCAPED = "text_unescaped";
String MEDIA = "media";
String SPANS = "spans";
@ -384,6 +385,7 @@ public interface TwidereDataStore {
String CONVERSATION_TYPE = "conversation_type";
String MESSAGE_TYPE = "message_type";
String MESSAGE_TIMESTAMP = "message_timestamp";
String SORT_ID = "sort_id";
String LOCAL_TIMESTAMP = "local_timestamp";
String TEXT_UNESCAPED = "text_unescaped";
String MEDIA = "media";

View File

@ -34,7 +34,7 @@ import static org.mariotaku.twidere.annotation.PreferenceType.STRING;
public interface Constants extends TwidereConstants {
String DATABASES_NAME = "twidere.sqlite";
int DATABASES_VERSION = 169;
int DATABASES_VERSION = 170;
int EXTRA_FEATURES_NOTICE_VERSION = 0;

View File

@ -16,4 +16,8 @@ fun Array<*>?.isNullOrEmpty(): Boolean {
fun <T : Any> Array<T>.toNulls(): Array<T?> {
@Suppress("UNCHECKED_CAST")
return this as Array<T?>
}
}
fun <T : Any> Array<T>.toStringArray(): Array<String> {
return Array(size) { this[it].toString() }
}

View File

@ -5,8 +5,10 @@ import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import org.mariotaku.twidere.adapter.iface.IItemCountsAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
import org.mariotaku.twidere.model.ItemCounts
import org.mariotaku.twidere.model.ParcelableMessageConversation
import org.mariotaku.twidere.view.holder.LoadIndicatorViewHolder
import org.mariotaku.twidere.view.holder.message.MessageEntryViewHolder
/**
@ -15,7 +17,7 @@ import org.mariotaku.twidere.view.holder.message.MessageEntryViewHolder
class MessagesEntriesAdapter(context: Context) : LoadMoreSupportAdapter<RecyclerView.ViewHolder>(context),
IItemCountsAdapter {
override val itemCounts: ItemCounts = ItemCounts(1)
override val itemCounts: ItemCounts = ItemCounts(2)
var conversations: List<ParcelableMessageConversation>? = null
set(value) {
@ -33,6 +35,7 @@ class MessagesEntriesAdapter(context: Context) : LoadMoreSupportAdapter<Recycler
override fun getItemCount(): Int {
itemCounts[0] = conversations?.size ?: 0
itemCounts[1] = if (loadMoreIndicatorPosition and ILoadMoreSupportAdapter.END != 0L) 1 else 0
return itemCounts.itemCount
}
@ -47,12 +50,26 @@ class MessagesEntriesAdapter(context: Context) : LoadMoreSupportAdapter<Recycler
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val itemView = inflater.inflate(MessageEntryViewHolder.layoutResource, parent, false)
return MessageEntryViewHolder(itemView, this)
when (viewType) {
ITEM_TYPE_MESSAGE_ENTRY -> {
val itemView = inflater.inflate(MessageEntryViewHolder.layoutResource, parent, false)
return MessageEntryViewHolder(itemView, this)
}
ITEM_VIEW_TYPE_LOAD_INDICATOR -> {
val itemView = inflater.inflate(LoadIndicatorViewHolder.layoutResource, parent, false)
return LoadIndicatorViewHolder(itemView)
}
}
throw UnsupportedOperationException()
}
override fun getItemViewType(position: Int): Int {
return ITEM_TYPE_MESSAGE_ENTRY
when (itemCounts.getItemCountIndex(position)) {
0 -> return ITEM_TYPE_MESSAGE_ENTRY
1 -> return ITEM_VIEW_TYPE_LOAD_INDICATOR
}
throw UnsupportedOperationException()
}
fun getConversation(position: Int): ParcelableMessageConversation? {
@ -67,7 +84,7 @@ class MessagesEntriesAdapter(context: Context) : LoadMoreSupportAdapter<Recycler
companion object {
const val ITEM_TYPE_MESSAGE_ENTRY = 1
const val ITEM_VIEW_TYPE_LOAD_INDICATOR = 2
}
}

View File

@ -452,12 +452,12 @@ class ParcelableActivitiesAdapter(
}
companion object {
val ITEM_VIEW_TYPE_STUB = 0
val ITEM_VIEW_TYPE_GAP = 1
val ITEM_VIEW_TYPE_LOAD_INDICATOR = 2
val ITEM_VIEW_TYPE_TITLE_SUMMARY = 3
val ITEM_VIEW_TYPE_STATUS = 4
val ITEM_VIEW_TYPE_EMPTY = 5
const val ITEM_VIEW_TYPE_STUB = 0
const val ITEM_VIEW_TYPE_GAP = 1
const val ITEM_VIEW_TYPE_LOAD_INDICATOR = 2
const val ITEM_VIEW_TYPE_TITLE_SUMMARY = 3
const val ITEM_VIEW_TYPE_STATUS = 4
const val ITEM_VIEW_TYPE_EMPTY = 5
}
}

View File

@ -178,11 +178,15 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
swipeLayout.setProgressBackgroundColorSchemeResource(colorRes)
adapter = onCreateAdapter(context)
layoutManager = onCreateLayoutManager(context)
scrollListener = RecyclerViewScrollHandler(this, RecyclerViewScrollHandler.RecyclerViewCallback(recyclerView))
recyclerView.layoutManager = layoutManager
recyclerView.setHasFixedSize(true)
val swipeLayout = swipeLayout
if (swipeLayout is ExtendedSwipeRefreshLayout) {
(swipeLayout as ExtendedSwipeRefreshLayout).setTouchInterceptor(object : IExtendedView.TouchInterceptor {
swipeLayout.setTouchInterceptor(object : IExtendedView.TouchInterceptor {
override fun dispatchTouchEvent(view: View, event: MotionEvent): Boolean {
scrollListener.touchListener.onTouch(view, event)
return false
}
@ -198,13 +202,14 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
}
})
} else {
recyclerView.setOnTouchListener(scrollListener.touchListener)
}
setupRecyclerView(context, recyclerView)
recyclerView.adapter = adapter
scrollListener = RecyclerViewScrollHandler(this, RecyclerViewScrollHandler.RecyclerViewCallback(recyclerView))
scrollListener.touchSlop = ViewConfiguration.get(context).scaledTouchSlop
recyclerView.setOnTouchListener(scrollListener.touchListener)
}
protected open fun setupRecyclerView(context: Context, recyclerView: RecyclerView) {

View File

@ -6,6 +6,8 @@ import android.support.v4.app.LoaderManager
import android.support.v4.content.Loader
import com.squareup.otto.Subscribe
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.toStringArray
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.sqliteqb.library.OrderBy
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.MessagesEntriesAdapter
@ -30,8 +32,9 @@ import org.mariotaku.twidere.util.Utils
class MessagesEntriesFragment : AbsContentListRecyclerViewFragment<MessagesEntriesAdapter>(),
LoaderManager.LoaderCallbacks<List<ParcelableMessageConversation>?>, MessagesEntriesAdapter.MessageConversationClickListener {
private val accountKeys: Array<UserKey>
get() = Utils.getAccountKeys(context, arguments) ?: DataStoreUtils.getActivatedAccountKeys(context)
private val accountKeys: Array<UserKey> by lazy {
Utils.getAccountKeys(context, arguments) ?: DataStoreUtils.getActivatedAccountKeys(context)
}
private val errorInfoKey: String = ErrorInfoStore.KEY_DIRECT_MESSAGES
@ -55,6 +58,8 @@ class MessagesEntriesFragment : AbsContentListRecyclerViewFragment<MessagesEntri
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ParcelableMessageConversation>?> {
val loader = ObjectCursorLoader(context, ParcelableMessageConversationCursorIndices::class.java)
loader.uri = Conversations.CONTENT_URI
loader.selection = Expression.inArgs(Conversations.ACCOUNT_KEY, accountKeys.size).sql
loader.selectionArgs = accountKeys.toStringArray()
loader.projection = Conversations.COLUMNS
loader.sortOrder = OrderBy(Conversations.LOCAL_TIMESTAMP, false).sql
return loader
@ -67,6 +72,7 @@ class MessagesEntriesFragment : AbsContentListRecyclerViewFragment<MessagesEntri
override fun onLoadFinished(loader: Loader<List<ParcelableMessageConversation>?>?, data: List<ParcelableMessageConversation>?) {
adapter.conversations = data
adapter.drawAccountColors = accountKeys.size > 1
setLoadMoreIndicatorPosition(ILoadMoreSupportAdapter.NONE)
showContentOrError()
}
@ -83,7 +89,13 @@ class MessagesEntriesFragment : AbsContentListRecyclerViewFragment<MessagesEntri
}
override fun onLoadMoreContents(position: Long) {
super.onLoadMoreContents(position)
if (position != ILoadMoreSupportAdapter.END) {
return
}
setLoadMoreIndicatorPosition(ILoadMoreSupportAdapter.END)
twitterWrapper.getMessagesAsync(GetMessagesTask.LoadMoreTaskParam(context) {
this@MessagesEntriesFragment.accountKeys
})
}
override fun onConversationClick(position: Int) {

View File

@ -221,16 +221,10 @@ class GetMessagesTask(context: Context) : BaseAbstractTask<RefreshTaskParam, Uni
)
class RefreshNewTaskParam(
val context: Context,
val getAccountKeys: () -> Array<UserKey>
) : SimpleRefreshTaskParam() {
context: Context,
getAccountKeys: () -> Array<UserKey>
) : RefreshMessagesTaskParam(context, getAccountKeys) {
private val accounts by lazy {
AccountUtils.getAllAccountDetails(AccountManager.get(context), accountKeys, false)
}
override val accountKeys: Array<UserKey>
get() = getAccountKeys()
override val sinceIds: Array<String?>?
get() {
@ -252,5 +246,46 @@ class GetMessagesTask(context: Context) : BaseAbstractTask<RefreshTaskParam, Uni
override val hasSinceIds: Boolean = true
override val hasMaxIds: Boolean = false
}
class LoadMoreTaskParam(
context: Context,
getAccountKeys: () -> Array<UserKey>
) : RefreshMessagesTaskParam(context, getAccountKeys) {
override val maxIds: Array<String?>?
get() {
val keys = accounts.map { account ->
when (account?.type) {
AccountType.FANFOU -> {
return@map null
}
}
return@map account?.key
}.toTypedArray()
val incomingIds = DataStoreUtils.getOldestMessageIds(context, Messages.CONTENT_URI,
keys, false)
val outgoingIds = DataStoreUtils.getOldestMessageIds(context, Messages.CONTENT_URI,
keys, true)
return incomingIds + outgoingIds
}
override val hasSinceIds: Boolean = false
override val hasMaxIds: Boolean = true
}
open class RefreshMessagesTaskParam(
val context: Context,
val getAccountKeys: () -> Array<UserKey>
) : SimpleRefreshTaskParam() {
protected val accounts: Array<AccountDetails?> by lazy {
AccountUtils.getAllAccountDetails(AccountManager.get(context), accountKeys, false)
}
override final val accountKeys: Array<UserKey>
get() = getAccountKeys()
}
}

View File

@ -74,10 +74,10 @@ open class ContentScrollHandler<A>(
if (!contentListSupport.refreshing && adapter.loadMoreSupportedPosition != ILoadMoreSupportAdapter.NONE
&& adapter.loadMoreIndicatorPosition == ILoadMoreSupportAdapter.NONE) {
var position: Long = 0
if (contentListSupport.reachingEnd && scrollDirection > 0) {
if (contentListSupport.reachingEnd && scrollDirection < 0) {
position = position or ILoadMoreSupportAdapter.END
}
if (contentListSupport.reachingStart && scrollDirection < 0) {
if (contentListSupport.reachingStart && scrollDirection > 0) {
position = position or ILoadMoreSupportAdapter.START
}
resetScrollDirection()
@ -133,13 +133,16 @@ open class ContentScrollHandler<A>(
lastY = Float.NaN
}
MotionEvent.ACTION_MOVE -> {
if (!java.lang.Float.isNaN(lastY)) {
val delta = lastY - event.rawY
listener.setScrollDirection(if (delta < 0) -1 else 1)
if (lastY.isNaN()) {
lastY = event.y
} else {
lastY = event.rawY
val delta = event.y - lastY
listener.setScrollDirection(if (delta < 0) -1 else 1)
}
}
MotionEvent.ACTION_UP -> {
lastY = Float.NaN
}
}
return false
}

View File

@ -171,6 +171,12 @@ object DataStoreUtils {
OrderBy(SQLFunctions.MAX(Messages.LOCAL_TIMESTAMP)), having, null)
}
fun getOldestMessageIds(context: Context, uri: Uri, accountKeys: Array<UserKey?>, outgoing: Boolean): Array<String?> {
val having: Expression = Expression.equals(Messages.IS_OUTGOING, if (outgoing) 1 else 0)
return getStringFieldArray(context, uri, accountKeys, Messages.ACCOUNT_KEY, Messages.MESSAGE_ID,
OrderBy(SQLFunctions.MIN(Messages.LOCAL_TIMESTAMP)), having, null)
}
fun getNewestStatusSortIds(context: Context, uri: Uri, accountKeys: Array<UserKey?>): LongArray {
return getLongFieldArray(context, uri, accountKeys, Statuses.ACCOUNT_KEY, Statuses.SORT_ID,

View File

@ -37,6 +37,7 @@ import org.mariotaku.twidere.model.Tab
import org.mariotaku.twidere.model.TabValuesCreator
import org.mariotaku.twidere.model.tab.TabConfiguration
import org.mariotaku.twidere.provider.TwidereDataStore.*
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
import org.mariotaku.twidere.util.content.DatabaseUpgradeHelper.safeUpgrade
import org.mariotaku.twidere.util.migrateAccounts
import java.util.*
@ -79,9 +80,10 @@ class TwidereSQLiteOpenHelper(
db.endTransaction()
db.beginTransaction()
db.execSQL(createTable(Messages.TABLE_NAME, Messages.COLUMNS, Messages.TYPES, true))
db.execSQL(createTable(Messages.Conversations.TABLE_NAME, Messages.Conversations.COLUMNS,
Messages.Conversations.TYPES, true))
db.execSQL(createTable(Messages.TABLE_NAME, Messages.COLUMNS, Messages.TYPES, true,
messagesConstraint()))
db.execSQL(createTable(Conversations.TABLE_NAME, Conversations.COLUMNS,
Conversations.TYPES, true, messageConversationsConstraint()))
db.setTransactionSuccessful()
db.endTransaction()
@ -101,6 +103,7 @@ class TwidereSQLiteOpenHelper(
setupDefaultTabs(db)
}
private fun setupDefaultTabs(db: SQLiteDatabase) {
db.beginTransaction()
@CustomTabType
@ -239,9 +242,10 @@ class TwidereSQLiteOpenHelper(
safeUpgrade(db, SavedSearches.TABLE_NAME, SavedSearches.COLUMNS, SavedSearches.TYPES, true, null)
// DM columns
safeUpgrade(db, Messages.TABLE_NAME, Messages.COLUMNS, Messages.TYPES, true, null)
safeUpgrade(db, Messages.Conversations.TABLE_NAME, Messages.Conversations.COLUMNS,
Messages.Conversations.TYPES, true, null)
safeUpgrade(db, Messages.TABLE_NAME, Messages.COLUMNS, Messages.TYPES, true, null,
messagesConstraint())
safeUpgrade(db, Conversations.TABLE_NAME, Conversations.COLUMNS,
Conversations.TYPES, true, null, messageConversationsConstraint())
if (oldVersion < 131) {
migrateFilteredUsers(db)
@ -312,4 +316,13 @@ class TwidereSQLiteOpenHelper(
return qb.buildSQL()
}
private fun messagesConstraint(): Constraint {
return Constraint.unique("unique_message", Columns(Messages.ACCOUNT_KEY, Messages.CONVERSATION_ID,
Messages.MESSAGE_ID), OnConflict.REPLACE)
}
private fun messageConversationsConstraint(): Constraint {
return Constraint.unique("unique_message_conversations", Columns(Conversations.ACCOUNT_KEY,
Conversations.CONVERSATION_ID), OnConflict.REPLACE)
}
}

View File

@ -22,6 +22,7 @@ package org.mariotaku.twidere.view.holder
import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.synthetic.main.list_item_load_indicator.view.*
import org.mariotaku.twidere.R
/**
* Created by mariotaku on 14/11/19.
@ -32,4 +33,7 @@ class LoadIndicatorViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun setLoadProgressVisible(visible: Boolean) {
loadProgress.visibility = if (visible) View.VISIBLE else View.GONE
}
companion object {
const val layoutResource = R.layout.list_item_load_indicator
}
}