fixed read position

added parallel load for image page
This commit is contained in:
Mariotaku Lee 2016-08-22 15:26:43 +08:00
parent ada7965a75
commit d7a34d82a1
7 changed files with 189 additions and 237 deletions

View File

@ -20,6 +20,7 @@
package org.mariotaku.twidere.model;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.mariotaku.twidere.util.TwidereArrayUtils;
@ -69,8 +70,8 @@ public class StringLongPair {
return key + ":" + value;
}
public static StringLongPair valueOf(String s) throws NumberFormatException {
if (s == null) return null;
@NonNull
public static StringLongPair valueOf(@NonNull String s) throws NumberFormatException {
final String[] segs = s.split(":");
if (segs.length != 2) throw new NumberFormatException();
return new StringLongPair(segs[0], Long.parseLong(segs[1]));

View File

@ -1,194 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2014 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.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.mariotaku.twidere.annotation.CustomTabType;
import org.mariotaku.twidere.annotation.NotificationType;
import org.mariotaku.twidere.annotation.ReadPositionTag;
import org.mariotaku.twidere.model.StringLongPair;
import org.mariotaku.twidere.util.collection.CompactHashSet;
import java.util.Set;
import static org.mariotaku.twidere.TwidereConstants.TIMELINE_POSITIONS_PREFERENCES_NAME;
public class ReadStateManager {
private final SharedPreferencesWrapper mPreferences;
public ReadStateManager(final Context context) {
mPreferences = SharedPreferencesWrapper.getInstance(context,
TIMELINE_POSITIONS_PREFERENCES_NAME, Context.MODE_PRIVATE);
}
public long getPosition(final String key) {
if (TextUtils.isEmpty(key)) return -1;
return mPreferences.getLong(key, -1);
}
@NonNull
public StringLongPair[] getPositionPairs(final String key) {
if (TextUtils.isEmpty(key)) return new StringLongPair[0];
final Set<String> set = mPreferences.getStringSet(key, null);
if (set == null) return new StringLongPair[0];
final StringLongPair[] pairs = new StringLongPair[set.size()];
int count = 0;
for (String entry : set.toArray(new String[set.size()])) {
try {
pairs[count++] = StringLongPair.valueOf(entry);
} catch (NumberFormatException e) {
return new StringLongPair[0];
}
}
return pairs;
}
public long getPosition(final String key, final String keyId) {
if (TextUtils.isEmpty(key)) return -1;
final Set<String> set = mPreferences.getStringSet(key, null);
if (set == null) return -1;
final String prefix = keyId + ":";
for (String entry : set) {
if (entry.startsWith(prefix)) return StringLongPair.valueOf(entry).getValue();
}
return -1;
}
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
mPreferences.registerOnSharedPreferenceChangeListener(listener);
}
public boolean setPosition(String key, String keyId, long position) {
return setPosition(key, keyId, position, false);
}
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
mPreferences.unregisterOnSharedPreferenceChangeListener(listener);
}
public boolean setPosition(final String key, final String keyId, final long position, boolean acceptOlder) {
if (TextUtils.isEmpty(key)) return false;
Set<String> set = mPreferences.getStringSet(key, null);
if (set == null) {
set = new CompactHashSet<>();
}
String keyValue = null;
final String prefix = keyId + ":";
for (String entry : set) {
if (entry.startsWith(prefix)) {
keyValue = entry;
break;
}
}
final StringLongPair pair;
if (keyValue != null) {
// Found value
pair = StringLongPair.valueOf(keyValue);
if (!acceptOlder && pair.getValue() > position) return false;
set.remove(keyValue);
pair.setValue(position);
} else {
pair = new StringLongPair(keyId, position);
}
set.add(pair.toString());
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putStringSet(key, set);
editor.apply();
return true;
}
public boolean setPositionPairs(final String key, @Nullable final StringLongPair[] pairs) {
if (TextUtils.isEmpty(key)) return false;
final SharedPreferences.Editor editor = mPreferences.edit();
if (pairs == null) {
editor.remove(key);
} else {
final Set<String> set = new CompactHashSet<>();
for (StringLongPair pair : pairs) {
set.add(pair.toString());
}
editor.putStringSet(key, set);
}
editor.apply();
return true;
}
public boolean setPosition(final String key, final long position) {
return setPosition(key, position, false);
}
public boolean setPosition(final String key, final long position, boolean acceptOlder) {
if (TextUtils.isEmpty(key) || !acceptOlder && getPosition(key) >= position) return false;
final SharedPreferences.Editor editor = mPreferences.edit();
editor.putLong(key, position);
editor.apply();
return true;
}
public interface OnReadStateChangeListener {
void onReadStateChanged();
}
@Nullable
@ReadPositionTag
public static String getReadPositionTagForNotificationType(@NotificationType String notificationType) {
if (notificationType == null) return null;
switch (notificationType) {
case NotificationType.HOME_TIMELINE: {
return ReadPositionTag.HOME_TIMELINE;
}
case NotificationType.DIRECT_MESSAGES: {
return ReadPositionTag.DIRECT_MESSAGES;
}
case NotificationType.INTERACTIONS: {
return ReadPositionTag.ACTIVITIES_ABOUT_ME;
}
}
return null;
}
@Nullable
@ReadPositionTag
public static String getReadPositionTagForTabType(@CustomTabType String tabType) {
if (tabType == null) return null;
switch (tabType) {
case CustomTabType.HOME_TIMELINE: {
return ReadPositionTag.HOME_TIMELINE;
}
case CustomTabType.NOTIFICATIONS_TIMELINE: {
return ReadPositionTag.ACTIVITIES_ABOUT_ME;
}
case CustomTabType.DIRECT_MESSAGES: {
return ReadPositionTag.DIRECT_MESSAGES;
}
}
return null;
}
}

View File

@ -838,10 +838,9 @@ public final class Utils implements Constants {
return accountKeys[0];
}
@Nullable
public static String getReadPositionTagWithAccount(@Nullable final String tag,
@NonNull
public static String getReadPositionTagWithAccount(@NonNull final String tag,
@Nullable final UserKey accountKey) {
if (tag == null) return null;
if (accountKey == null) return tag;
return tag + "_" + accountKey;
}

View File

@ -112,13 +112,6 @@ abstract class AbsActivitiesFragment protected constructor() : AbsContentListRec
}
}
private val onScrollListener = object : OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
saveReadPosition()
}
}
}
private var navigationHelper: RecyclerViewNavigationHelper? = null
private var pauseOnScrollListener: OnScrollListener? = null
private var activeHotMobiScrollTracker: OnScrollListener? = null
@ -213,14 +206,6 @@ abstract class AbsActivitiesFragment protected constructor() : AbsContentListRec
return onCreateActivitiesLoader(activity, args, fromUser)
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser) {
saveReadPosition()
}
}
protected fun saveReadPosition() {
val layoutManager = layoutManager
if (layoutManager != null) {
@ -380,7 +365,6 @@ abstract class AbsActivitiesFragment protected constructor() : AbsContentListRec
override fun onStart() {
super.onStart()
recyclerView.addOnScrollListener(onScrollListener)
recyclerView.addOnScrollListener(pauseOnScrollListener)
val task = object : AbstractTask<Any?, Boolean, RecyclerView>() {
public override fun doLongOperation(params: Any?): Boolean {
@ -411,7 +395,6 @@ abstract class AbsActivitiesFragment protected constructor() : AbsContentListRec
}
activeHotMobiScrollTracker = null
recyclerView.removeOnScrollListener(pauseOnScrollListener)
recyclerView.removeOnScrollListener(onScrollListener)
if (userVisibleHint) {
saveReadPosition()
}
@ -489,17 +472,21 @@ abstract class AbsActivitiesFragment protected constructor() : AbsContentListRec
protected abstract fun onLoadingFinished()
protected fun saveReadPosition(position: Int) {
if (context == null) return
if (host == null) return
if (position == RecyclerView.NO_POSITION) return
val item = adapter!!.getActivity(position) ?: return
var positionUpdated = false
readPositionTag?.let {
for (accountKey in accountKeys) {
val tag = Utils.getReadPositionTagWithAccount(readPositionTag, accountKey)
val tag = Utils.getReadPositionTagWithAccount(it, accountKey)
if (readStateManager.setPosition(tag, item.timestamp)) {
positionUpdated = true
}
}
}
currentReadPositionTag?.let {
readStateManager.setPosition(it, item.timestamp, true)
}
if (positionUpdated) {
twitterWrapper.setActivitiesAboutMeUnreadAsync(accountKeys, item.timestamp)

View File

@ -305,14 +305,6 @@ abstract class AbsStatusesFragment protected constructor() :
return onCreateStatusesLoader(activity, args, fromUser)
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser) {
saveReadPosition()
}
}
override fun onLoadFinished(loader: Loader<List<ParcelableStatus>?>, data: List<ParcelableStatus>?) {
val adapter = adapter ?: return
val rememberPosition = preferences.getBoolean(SharedPreferenceConstants.KEY_REMEMBER_POSITION, false)
@ -508,15 +500,21 @@ abstract class AbsStatusesFragment protected constructor() :
protected fun saveReadPosition(position: Int) {
if (host == null) return
if (position == RecyclerView.NO_POSITION) return
val adapter = adapter
val status = adapter!!.getStatus(position) ?: return
val adapter = adapter ?: return
val status = adapter.getStatus(position) ?: return
val positionKey = if (status.position_key > 0) status.position_key else status.timestamp
readPositionTagWithArguments?.let {
for (accountKey in accountKeys) {
val tag = Utils.getReadPositionTagWithAccount(readPositionTagWithArguments, accountKey)
val tag = Utils.getReadPositionTagWithAccount(it, accountKey)
readStateManager.setPosition(tag, positionKey)
}
}
currentReadPositionTag?.let {
readStateManager.setPosition(it, positionKey, true)
}
}
protected abstract fun hasMoreData(data: List<ParcelableStatus>?): Boolean

View File

@ -70,9 +70,10 @@ class ImagePageFragment : SubsampleImageViewerFragment() {
}
}
override fun setupImageView(imageView: SubsamplingScaleImageView?) {
imageView!!.maxScale = resources.displayMetrics.density
override fun setupImageView(imageView: SubsamplingScaleImageView) {
imageView.maxScale = resources.displayMetrics.density
imageView.setBitmapDecoderClass(PreviewBitmapDecoder::class.java)
imageView.setParallelLoadingEnabled(true)
}
override fun getImageSource(data: CacheDownloadLoader.Result): ImageSource {

View File

@ -0,0 +1,160 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2014 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.util
import android.content.Context
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.text.TextUtils
import org.mariotaku.twidere.TwidereConstants.TIMELINE_POSITIONS_PREFERENCES_NAME
import org.mariotaku.twidere.annotation.CustomTabType
import org.mariotaku.twidere.annotation.NotificationType
import org.mariotaku.twidere.annotation.ReadPositionTag
import org.mariotaku.twidere.model.StringLongPair
import org.mariotaku.twidere.util.collection.CompactHashSet
class ReadStateManager(context: Context) {
private val preferences: SharedPreferencesWrapper
init {
preferences = SharedPreferencesWrapper.getInstance(context,
TIMELINE_POSITIONS_PREFERENCES_NAME, Context.MODE_PRIVATE)
}
fun getPosition(key: String): Long {
if (TextUtils.isEmpty(key)) return -1
return preferences.getLong(key, -1)
}
fun getPositionPairs(key: String): Array<StringLongPair> {
if (TextUtils.isEmpty(key)) return emptyArray()
val set = preferences.getStringSet(key, null) ?: return emptyArray()
try {
return set.map { StringLongPair.valueOf(it) }.toTypedArray()
} catch (e: NumberFormatException) {
return emptyArray()
}
}
fun getPosition(key: String, keyId: String): Long {
if (TextUtils.isEmpty(key)) return -1
val set = preferences.getStringSet(key, null) ?: return -1
val prefix = keyId + ":"
val first = set.firstOrNull { it.startsWith(prefix) } ?: return -1
return StringLongPair.valueOf(first).value
}
fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
preferences.registerOnSharedPreferenceChangeListener(listener)
}
fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
preferences.unregisterOnSharedPreferenceChangeListener(listener)
}
@JvmOverloads fun setPosition(key: String, keyId: String, position: Long, acceptOlder: Boolean = false): Boolean {
if (TextUtils.isEmpty(key)) return false
val set: MutableSet<String> = preferences.getStringSet(key, null) ?: CompactHashSet<String>()
val prefix = keyId + ":"
val keyValue: String? = set.firstOrNull { it.startsWith(prefix) }
val pair: StringLongPair
if (keyValue != null) {
// Found value
pair = StringLongPair.valueOf(keyValue)
if (!acceptOlder && pair.value > position) return false
set.remove(keyValue)
pair.value = position
} else {
pair = StringLongPair(keyId, position)
}
set.add(pair.toString())
val editor = preferences.edit()
editor.putStringSet(key, set)
editor.apply()
return true
}
fun setPositionPairs(key: String, pairs: Array<StringLongPair>?): Boolean {
if (TextUtils.isEmpty(key)) return false
val editor = preferences.edit()
if (pairs == null) {
editor.remove(key)
} else {
val set = CompactHashSet<String>()
for (pair in pairs) {
set.add(pair.toString())
}
editor.putStringSet(key, set)
}
editor.apply()
return true
}
@JvmOverloads fun setPosition(key: String, position: Long, acceptOlder: Boolean = false): Boolean {
if (TextUtils.isEmpty(key) || !acceptOlder && getPosition(key) >= position) return false
val editor = preferences.edit()
editor.putLong(key, position)
editor.apply()
return true
}
interface OnReadStateChangeListener {
fun onReadStateChanged()
}
companion object {
@ReadPositionTag
fun getReadPositionTagForNotificationType(@NotificationType notificationType: String?): String? {
if (notificationType == null) return null
when (notificationType) {
NotificationType.HOME_TIMELINE -> {
return ReadPositionTag.HOME_TIMELINE
}
NotificationType.DIRECT_MESSAGES -> {
return ReadPositionTag.DIRECT_MESSAGES
}
NotificationType.INTERACTIONS -> {
return ReadPositionTag.ACTIVITIES_ABOUT_ME
}
}
return null
}
@ReadPositionTag
fun getReadPositionTagForTabType(@CustomTabType tabType: String?): String? {
if (tabType == null) return null
when (tabType) {
CustomTabType.HOME_TIMELINE -> {
return ReadPositionTag.HOME_TIMELINE
}
CustomTabType.NOTIFICATIONS_TIMELINE -> {
return ReadPositionTag.ACTIVITIES_ABOUT_ME
}
CustomTabType.DIRECT_MESSAGES -> {
return ReadPositionTag.DIRECT_MESSAGES
}
}
return null
}
}
}