1
0
mirror of https://github.com/tateisu/SubwayTooter synced 2025-01-28 01:29:23 +01:00

TL更新時のちらつきをなくした

This commit is contained in:
tateisu 2018-01-20 15:51:14 +09:00
parent 67a6d87777
commit 4ab6f78121
21 changed files with 500 additions and 261 deletions

View File

@ -12,8 +12,8 @@ android {
minSdkVersion 21
targetSdkVersion 27
versionCode 209
versionName "2.0.9"
versionCode 210
versionName "2.1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

View File

@ -27,6 +27,7 @@ import android.support.design.widget.NavigationView
import android.support.v4.view.GravityCompat
import android.support.v4.widget.DrawerLayout
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.DefaultItemAnimator
import android.view.Menu
import android.view.MenuItem
import android.view.Window
@ -297,10 +298,10 @@ class ActMain : AppCompatActivity()
override fun run() {
handler.removeCallbacks(this)
if(! bStart) return
for(c in app_state.column_list) {
c.fireShowContent()
}
if(Pref.bpRelativeTimestamp(pref)) {
for(c in app_state.column_list) {
c.fireRelativeTime()
}
handler.postDelayed(this, 10000L)
}
}
@ -453,21 +454,14 @@ class ActMain : AppCompatActivity()
// アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない
run {
val new_order = ArrayList<Int>()
var i = 0
val ie = app_state.column_list.size
while(i < ie) {
for( i in 0 until app_state.column_list.size){
val column = app_state.column_list[i]
if(! column.access_info.isNA) {
val sa = SavedAccount.loadAccount(this@ActMain, column.access_info.db_id)
if(sa == null) {
++ i
continue
}
if(sa == null) continue
}
new_order.add(i)
++ i
}
if(new_order.size != app_state.column_list.size) {
@ -489,6 +483,10 @@ class ActMain : AppCompatActivity()
// カラムの表示範囲インジケータを更新
updateColumnStripSelection(- 1, - 1f)
for(c in app_state.column_list) {
c.fireShowContent(reason="ActMain onStart",reset=true)
}
// 相対時刻表示
proc_updateRelativeTime.run()
@ -740,7 +738,7 @@ class ActMain : AppCompatActivity()
val idx = data.getIntExtra(ActColumnCustomize.EXTRA_COLUMN_INDEX, 0)
if(idx >= 0 && idx < app_state.column_list.size) {
app_state.column_list[idx].fireColumnColor()
app_state.column_list[idx].fireShowContent()
app_state.column_list[idx].fireShowContent(reason="ActMain column color changed",reset=true)
}
updateColumnStrip()
}
@ -1247,6 +1245,12 @@ class ActMain : AppCompatActivity()
}
})
env.tablet_pager.itemAnimator = null
// val animator = env.tablet_pager.itemAnimator
// if( animator is DefaultItemAnimator){
// animator.supportsChangeAnimations = false
// }
env.tablet_snap_helper = GravitySnapHelper(Gravity.START)
env.tablet_snap_helper.attachToRecyclerView(env.tablet_pager)
@ -1952,7 +1956,7 @@ class ActMain : AppCompatActivity()
fun showColumnMatchAccount(account : SavedAccount) {
for(column in app_state.column_list) {
if(account.acct == column.access_info.acct) {
column.fireShowContent()
column.fireRebindAdapterItems()
}
}
}

View File

@ -0,0 +1,9 @@
package jp.juggler.subwaytooter
enum class AdapterChangeType {
RangeInsert,
RangeRemove,
RangeChange,
}
class AdapterChange(val type : AdapterChangeType, val listIndex : Int, val count : Int = 1)

View File

@ -343,7 +343,7 @@ class Column(
internal var task_progress : String? = null
internal val list_data = BucketList<Any>()
internal val list_data = BucketList<TimelineItem>()
private val duplicate_map = DuplicateMap()
private val isFilterEnabled : Boolean
@ -369,7 +369,7 @@ class Column(
// ListViewの表示更新が追いつかないとスクロール位置が崩れるので
// 一定時間より短期間にはデータ更新しないようにする
private var last_show_stream_data : Long = 0
private val stream_data_queue = LinkedList<Any>()
private val stream_data_queue = LinkedList<TimelineItem>()
private var bPutGap : Boolean = false
@ -668,7 +668,7 @@ class Column(
}
}
if(bChanged) {
fireShowContent()
fireShowContent(reason="findStatus",reset=true)
}
}
}
@ -680,7 +680,7 @@ class Column(
val INVALID_ACCOUNT = - 1L
val tmp_list = ArrayList<Any>(list_data.size)
val tmp_list = ArrayList<TimelineItem>(list_data.size)
for(o in list_data) {
if(o is TootStatus) {
if(who_id == (o.account.id)) continue
@ -698,7 +698,7 @@ class Column(
if(tmp_list.size != list_data.size) {
list_data.clear()
list_data.addAll(tmp_list)
fireShowContent()
fireShowContent(reason="removeAccountInTimeline")
}
}
@ -706,7 +706,7 @@ class Column(
// ミュート解除が成功した時に呼ばれる
fun removeFromMuteList(target_account : SavedAccount, who_id : Long) {
if(column_type == TYPE_MUTES && target_account.acct == access_info.acct) {
val tmp_list = ArrayList<Any>(list_data.size)
val tmp_list = ArrayList<TimelineItem>(list_data.size)
for(o in list_data) {
if(o is TootAccount) {
if(o.id == who_id) continue
@ -716,7 +716,7 @@ class Column(
if(tmp_list.size != list_data.size) {
list_data.clear()
list_data.addAll(tmp_list)
fireShowContent()
fireShowContent(reason="removeFromMuteList")
}
}
}
@ -724,7 +724,7 @@ class Column(
// ブロック解除が成功したので、ブロックリストから削除する
fun removeFromBlockList(target_account : SavedAccount, who_id : Long) {
if(column_type == TYPE_BLOCKS && target_account.acct == access_info.acct) {
val tmp_list = ArrayList<Any>(list_data.size)
val tmp_list = ArrayList<TimelineItem>(list_data.size)
for(o in list_data) {
if(o is TootAccount) {
if(o.id == who_id) continue
@ -734,7 +734,7 @@ class Column(
if(tmp_list.size != list_data.size) {
list_data.clear()
list_data.addAll(tmp_list)
fireShowContent()
fireShowContent(reason="removeFromBlockList")
}
}
@ -744,7 +744,7 @@ class Column(
if(target_account.acct != access_info.acct) return
if(column_type == TYPE_FOLLOW_REQUESTS) {
val tmp_list = ArrayList<Any>(list_data.size)
val tmp_list = ArrayList<TimelineItem>(list_data.size)
for(o in list_data) {
if(o is TootAccount) {
if(o.id == who_id) continue
@ -754,11 +754,11 @@ class Column(
if(tmp_list.size != list_data.size) {
list_data.clear()
list_data.addAll(tmp_list)
fireShowContent()
fireShowContent(reason="removeFollowRequest 1")
}
} else {
// 他のカラムでもフォロー状態の表示更新が必要
fireShowContent()
fireShowContent(reason="removeFollowRequest 2",reset=true)
}
}
@ -767,7 +767,7 @@ class Column(
if(target_account.host != access_info.host) return
val tmp_list = ArrayList<Any>(list_data.size)
val tmp_list = ArrayList<TimelineItem>(list_data.size)
for(o in list_data) {
if(o is TootStatus) {
if(status_id == o.id) continue
@ -783,22 +783,23 @@ class Column(
if(tmp_list.size != list_data.size) {
list_data.clear()
list_data.addAll(tmp_list)
fireShowContent()
fireShowContent(reason="removeStatus")
}
}
fun removeNotifications() {
cancelLastTask()
list_data.clear()
mRefreshLoadingError = ""
bRefreshLoading = false
mInitialLoadingError = ""
bInitialLoading = false
max_id = ""
since_id = ""
fireShowContent()
list_data.clear()
duplicate_map.clear()
fireShowContent(reason="removeNotifications",reset=true)
PollingWorker.queueNotificationCleared(context, access_info.db_id)
}
@ -807,7 +808,7 @@ class Column(
if(column_type != TYPE_NOTIFICATIONS) return
if(access_info.acct != target_account.acct) return
val tmp_list = ArrayList<Any>(list_data.size)
val tmp_list = ArrayList<TimelineItem>(list_data.size)
for(o in list_data) {
if(o is TootNotification) {
if(o.id == notification.id) continue
@ -819,12 +820,12 @@ class Column(
if(tmp_list.size != list_data.size) {
list_data.clear()
list_data.addAll(tmp_list)
fireShowContent()
fireShowContent(reason="removeNotificationOne")
}
}
fun onMuteAppUpdated() {
val tmp_list = ArrayList<Any>(list_data.size)
val tmp_list = ArrayList<TimelineItem>(list_data.size)
val muted_app = MutedApp.nameSet
val muted_word = MutedWord.nameSet
@ -843,7 +844,7 @@ class Column(
if(tmp_list.size != list_data.size) {
list_data.clear()
list_data.addAll(tmp_list)
fireShowContent()
fireShowContent(reason="onMuteAppUpdated")
}
}
@ -863,7 +864,7 @@ class Column(
val checker =
{ acct : String? -> if(acct == null) false else reDomain.matcher(acct).find() }
val tmp_list = ArrayList<Any>(list_data.size)
val tmp_list = ArrayList<TimelineItem>(list_data.size)
for(o in list_data) {
if(o is TootStatus) {
@ -879,7 +880,7 @@ class Column(
if(tmp_list.size != list_data.size) {
list_data.clear()
list_data.addAll(tmp_list)
fireShowContent()
fireShowContent(reason="onDomainBlockChanged")
}
}
@ -943,30 +944,46 @@ class Column(
return _holder_list.size > 1
}
internal fun fireShowContent() {
internal fun fireShowContent(
reason:String,
changeList : List<AdapterChange>? = null,
reset : Boolean = false
) {
if(! Utils.isMainThread) {
throw RuntimeException("fireShowColumnHeader: not on main thread.")
throw RuntimeException("fireShowContent: not on main thread.")
}
val holder = viewHolder
holder?.showContent()
viewHolder?.showContent(reason,changeList,reset)
}
internal fun fireShowColumnHeader() {
if(! Utils.isMainThread) {
throw RuntimeException("fireShowColumnHeader: not on main thread.")
}
val holder = viewHolder
holder?.showColumnHeader()
viewHolder?.showColumnHeader()
}
internal fun fireColumnColor() {
if(! Utils.isMainThread) {
throw RuntimeException("fireShowColumnHeader: not on main thread.")
throw RuntimeException("fireColumnColor: not on main thread.")
}
val holder = viewHolder
holder?.showColumnColor()
viewHolder?.showColumnColor()
}
fun fireRelativeTime() {
if(! Utils.isMainThread) {
throw RuntimeException("fireRelativeTime: not on main thread.")
}
viewHolder?.updateRelativeTime()
}
fun fireRebindAdapterItems() {
if(! Utils.isMainThread) {
throw RuntimeException("fireRelativeTime: not on main thread.")
}
viewHolder?.rebindAdapterItems()
}
private fun cancelLastTask() {
if(last_task != null) {
last_task?.cancel(true)
@ -1028,27 +1045,30 @@ class Column(
}
private inline fun <reified T> addAll(
dstArg : ArrayList<Any>?,
private inline fun <reified T : TimelineItem> addAll(
dstArg : ArrayList<TimelineItem>?,
src : ArrayList<T>
) : ArrayList<Any> {
) : ArrayList<TimelineItem> {
val dst = dstArg ?: ArrayList()
for(item in src) {
dst.add(item as Any)
dst.add(item as TimelineItem)
}
return dst
}
private fun addOne(dstArg : ArrayList<Any>?, item : Any) : ArrayList<Any> {
private fun addOne(
dstArg : ArrayList<TimelineItem>?,
item : TimelineItem
) : ArrayList<TimelineItem> {
val dst = dstArg ?: ArrayList()
dst.add(item)
return dst
}
private fun addWithFilterStatus(
dstArg : ArrayList<Any>?,
dstArg : ArrayList<TimelineItem>?,
src : ArrayList<TootStatus>
) : ArrayList<Any> {
) : ArrayList<TimelineItem> {
val dst = dstArg ?: ArrayList()
for(status in src) {
if(! isFiltered(status)) {
@ -1059,9 +1079,9 @@ class Column(
}
private fun addWithFilterNotification(
dstArg : ArrayList<Any>?,
dstArg : ArrayList<TimelineItem>?,
src : ArrayList<TootNotification>
) : ArrayList<Any> {
) : ArrayList<TimelineItem> {
val dst = dstArg ?: ArrayList()
for(item in src) {
if(! isFiltered(item)) dst.add(item)
@ -1232,7 +1252,7 @@ class Column(
//
private fun updateRelation(
client : TootApiClient,
list : ArrayList<Any>?,
list : ArrayList<TimelineItem>?,
who : TootAccount?
) {
if(access_info.isPseudo) return
@ -1268,7 +1288,7 @@ class Column(
duplicate_map.clear()
list_data.clear()
fireShowContent()
fireShowContent(reason="loading start",reset=true)
val task = @SuppressLint("StaticFieldLeak")
object : AsyncTask<Void, Void, TootApiResult?>() {
@ -1276,9 +1296,9 @@ class Column(
internal var instance_tmp : TootInstance? = null
internal var list_pinned : ArrayList<Any>? = null
internal var list_pinned : ArrayList<TimelineItem>? = null
internal var list_tmp : ArrayList<Any>? = null
internal var list_tmp : ArrayList<TimelineItem>? = null
internal fun getInstanceInformation(
client : TootApiClient,
@ -1498,7 +1518,7 @@ class Column(
Utils.runOnMainThread {
if(isCancelled) return@runOnMainThread
task_progress = s
fireShowContent()
fireShowContent(reason="loading progress",changeList = ArrayList())
}
}
})
@ -1782,6 +1802,7 @@ class Column(
if(result.error != null) {
this@Column.mInitialLoadingError = result.error ?: ""
} else {
duplicate_map.clear()
list_data.clear()
val list_tmp = this.list_tmp
if(list_tmp != null) {
@ -1796,14 +1817,10 @@ class Column(
resumeStreaming(false)
}
fireShowContent()
fireShowContent(reason="loading updated",reset=true)
// 初期ロードの直後は先頭に移動する
try {
viewHolder?.listLayoutManager?.scrollToPositionWithOffset(0, 0)
} catch(ignored : Throwable) {
}
viewHolder?.scrollToTop()
}
}
this.last_task = task
@ -1915,7 +1932,7 @@ class Column(
object : AsyncTask<Void, Void, TootApiResult?>() {
internal var parser = TootParser(context, access_info, highlightTrie = highlight_trie)
internal var list_tmp : ArrayList<Any>? = null
internal var list_tmp : ArrayList<TimelineItem>? = null
internal fun getAccountList(
client : TootApiClient,
@ -2419,7 +2436,7 @@ class Column(
Utils.runOnMainThread {
if(isCancelled) return@runOnMainThread
task_progress = s
fireShowContent()
fireShowContent(reason="refresh progress",changeList = ArrayList())
}
}
})
@ -2593,20 +2610,13 @@ class Column(
val error = result.error
if(error != null) {
mRefreshLoadingError = error
fireShowContent()
return
}
val list_tmp = this.list_tmp
if(list_tmp == null || list_tmp.isEmpty()) {
fireShowContent()
fireShowContent(reason="refresh error",changeList = ArrayList())
return
}
val list_new = duplicate_map.filterDuplicate(list_tmp)
if(list_new.isEmpty()) {
fireShowContent()
if(list_new.isEmpty() ) {
fireShowContent(reason="refresh list_new is empty",changeList = ArrayList())
return
}
@ -2617,9 +2627,12 @@ class Column(
sp = holder.scrollPosition
}
val added = list_new.size
if(bBottom) {
val changeList = listOf(AdapterChange(AdapterChangeType.RangeInsert,list_data.size,added))
list_data.addAll(list_new)
fireShowContent()
fireShowContent(reason="refresh updated bottom",changeList = changeList)
// 新着が少しだけ見えるようにスクロール位置を移動する
if(sp != null) {
@ -2637,23 +2650,19 @@ class Column(
}
}
// 投稿後のリフレッシュなら当該投稿の位置を探す
var status_index = - 1
var i = 0
val ie = list_new.size
while(i < ie) {
for( i in 0 until added){
val o = list_new[i]
if(o is TootStatus) {
if(o.id == posted_status_id) {
status_index = i
break
}
if(o is TootStatus && o.id == posted_status_id) {
status_index = i
break
}
++ i
}
val added = list_new.size
val changeList = listOf(AdapterChange(AdapterChangeType.RangeInsert,0,added))
list_data.addAll(0, list_new)
fireShowContent()
fireShowContent(reason="refresh updated head",changeList=changeList)
if(status_index >= 0 && refresh_after_toot == Pref.RAT_REFRESH_SCROLL) {
// 投稿後にその投稿にスクロールする
@ -2714,7 +2723,7 @@ class Column(
object : AsyncTask<Void, Void, TootApiResult?>() {
internal var max_id = gap.max_id
internal val since_id = gap.since_id
internal var list_tmp : ArrayList<Any>? = null
internal var list_tmp : ArrayList<TimelineItem>? = null
internal var parser = TootParser(context, access_info, highlightTrie = highlight_trie)
@ -2940,7 +2949,7 @@ class Column(
Utils.runOnMainThread {
if(isCancelled) return@runOnMainThread
task_progress = s
fireShowContent()
fireShowContent(reason="gap progress",changeList = ArrayList())
}
}
})
@ -3035,23 +3044,26 @@ class Column(
val error = result.error
if(error != null) {
mRefreshLoadingError = error
fireShowContent()
fireShowContent(reason="gap error",changeList = ArrayList())
return
}
val position = list_data.indexOf(gap)
if(position == - 1) {
log.d("gap not found..")
fireShowContent(reason="gap not found",changeList = ArrayList())
return
}
val list_tmp = this.list_tmp
if(list_tmp == null) {
fireShowContent()
fireShowContent(reason="gap list_tmp is null",changeList = ArrayList())
return
}
// 0個でもギャップを消すために以下の処理を続ける
val position = list_data.indexOf(gap)
if(position == - 1) {
log.d("gap is not found..")
return
}
val list_new = duplicate_map.filterDuplicate(list_tmp)
// idx番目の要素がListViewのtopから何ピクセル下にあるか
@ -3074,7 +3086,13 @@ class Column(
val added = list_new.size // may 0
list_data.removeAt(position)
list_data.addAll(position, list_new)
fireShowContent()
val changeList = ArrayList<AdapterChange>()
changeList.add(AdapterChange( AdapterChangeType.RangeRemove,position))
if( added > 0 ){
changeList.add(AdapterChange(AdapterChangeType.RangeInsert,position,added))
}
fireShowContent(reason="gap updated",changeList = changeList)
if(holder != null) {
if(restore_idx >= 0) {
@ -3192,13 +3210,12 @@ class Column(
&& ! Pref.bpDontRefreshOnResume(App1.getAppState(context).pref)
&& ! dont_auto_refresh
) {
// リフレッシュしてからストリーミング開始
log.d("onStart: start auto refresh.")
startRefresh(true, false, - 1L, - 1)
} else if(isSearchColumn) {
// 検索カラムはリフレッシュもストリーミングもないが、表示開始のタイミングでリストの再描画を行いたい
fireShowContent()
fireShowContent(reason="Column onStart isSearchColumn",reset=true)
} else {
// ギャップつきでストリーミング開始
log.d("onStart: start streaming with gap.")
@ -3322,28 +3339,29 @@ class Column(
private val onStreamingMessage = fun(event_type : String, item : Any?) {
if(is_dispose.get()) return
if("delete" == event_type) {
if(item is Long) {
removeStatus(access_info, item)
if(item is Long) {
if("delete" == event_type) {
removeStatus(access_info, item )
}
return
}
if(item is TootNotification) {
if(column_type != TYPE_NOTIFICATIONS) return
if(isFiltered(item)) return
} else if(item is TootStatus) {
if(column_type == TYPE_NOTIFICATIONS) return
if(column_type == TYPE_LOCAL && item.account.acct.indexOf('@') != - 1) return
if(isFiltered(item)) return
if(this.enable_speech) {
App1.getAppState(context).addSpeech(item.reblog ?: item)
} else if(item is TimelineItem) {
if(item is TootNotification) {
if(column_type != TYPE_NOTIFICATIONS) return
if(isFiltered(item)) return
} else if(item is TootStatus) {
if(column_type == TYPE_NOTIFICATIONS) return
if(column_type == TYPE_LOCAL && item.account.acct.indexOf('@') != - 1) return
if(isFiltered(item)) return
if(this.enable_speech) {
App1.getAppState(context).addSpeech(item.reblog ?: item)
}
}
stream_data_queue.addFirst(item)
mergeStreamingMessage.run()
}
stream_data_queue.addFirst(item)
mergeStreamingMessage.run()
}
private val mergeStreamingMessage = object : Runnable {
@ -3446,21 +3464,26 @@ class Column(
}
}
list_data.addAll(0, list_new)
fireShowContent()
val added = list_new.size
val changeList = listOf(AdapterChange(AdapterChangeType.RangeInsert,0,added))
list_data.addAll(0, list_new)
fireShowContent(reason="mergeStreamingMessage",changeList = changeList)
if(holder != null) {
if(holder_sp == null) {
// スクロール位置が先頭なら先頭のまま
// スクロール位置が先頭なら先頭にする
log.d("mergeStreamingMessage: has VH. missing scroll position.")
viewHolder?.scrollToTop()
} else if(holder_sp.adapterIndex == 0 && holder_sp.offset == 0) {
// スクロール位置が先頭なら先頭のまま
// スクロール位置が先頭なら先頭にする
log.d(
"mergeStreamingMessage: has VH. keep head. pos=%s,offset=%s"
, holder_sp.adapterIndex
, holder_sp.offset
)
holder.setScrollPosition(ScrollPosition(0,0))
} else if(restore_idx < - 1) {
// 可視範囲の検出に失敗
log.d("mergeStreamingMessage: has VH. can't find visible range.")
@ -3481,4 +3504,5 @@ class Column(
}
}
}

View File

@ -195,6 +195,12 @@ class ColumnViewHolder(
if(Pref.bpShareViewPool(activity.pref)) {
listView.recycledViewPool = activity.viewPool
}
listView.itemAnimator = null
//
// val animator = listView.itemAnimator
// if( animator is DefaultItemAnimator){
// animator.supportsChangeAnimations = false
// }
btnSearch = root.findViewById(R.id.btnSearch)
etSearch = root.findViewById(R.id.etSearch)
@ -483,7 +489,7 @@ class ColumnViewHolder(
showColumnColor()
showContent()
showContent(reason="onPageCreate",reset=true)
} finally {
loading_busy = false
}
@ -738,7 +744,7 @@ class ColumnViewHolder(
R.id.cbHideMediaDefault -> {
column.hide_media_default = isChecked
activity.app_state.saveColumnList()
column.fireShowContent()
column.fireShowContent(reason="HideMediaDefault in ColumnSetting",reset=true)
}
R.id.cbEnableSpeech -> {
@ -782,7 +788,7 @@ class ColumnViewHolder(
R.id.llColumnHeader -> {
if(status_adapter.itemCount > 0) {
listLayoutManager.scrollToPositionWithOffset(0, 0)
scrollToTop()
}
}
@ -827,6 +833,17 @@ class ColumnViewHolder(
btnColumnClose.alpha = if(dont_close) 0.3f else 1f
}
// 相対時刻を更新する
fun updateRelativeTime() = rebindAdapterItems()
fun rebindAdapterItems() {
for( childIndex in 0 until listView.childCount){
val adapterIndex = listView.getChildAdapterPosition(listView.getChildAt(childIndex))
if( adapterIndex == RecyclerView.NO_POSITION) continue
status_adapter?.notifyItemChanged( adapterIndex )
}
}
// カラムヘッダなど、負荷が低い部分の表示更新
fun showColumnHeader() {
val column = this.column ?: return
@ -861,12 +878,15 @@ class ColumnViewHolder(
}
fun showContent() {
internal fun showContent(
reason:String,
changeList : List<AdapterChange>? = null ,
reset : Boolean = false
) {
// クラッシュレポートにadapterとリストデータの状態不整合が多かったので、
// とりあえずリストデータ変更の通知だけは最優先で行っておく
try {
status_adapter?.notifyDataSetChanged()
status_adapter?.notifyChange(reason,changeList,reset)
} catch(ex : Throwable) {
log.trace(ex)
}
@ -920,7 +940,7 @@ class ColumnViewHolder(
}
// 表示状態が変わった後にもう一度呼び出す必要があるらしい。。。
status_adapter.notifyDataSetChanged()
// 試しにやめてみる status_adapter.notifyChange()
proc_restoreScrollPosition.run()
}
@ -961,7 +981,7 @@ class ColumnViewHolder(
}
}
fun setScrollPosition(sp : ScrollPosition, deltaDp : Float) {
fun setScrollPosition(sp : ScrollPosition, deltaDp : Float =0f) {
val last_adapter = listView.adapter
if(column == null || last_adapter == null) return
@ -1074,4 +1094,12 @@ class ColumnViewHolder(
?: throw IndexOutOfBoundsException()
}
fun scrollToTop() {
try {
listLayoutManager.scrollToPositionWithOffset(0, 0)
} catch(ignored : Throwable) {
}
}
}

View File

@ -1,10 +1,13 @@
package jp.juggler.subwaytooter
import android.os.Handler
import android.os.SystemClock
import android.support.v7.util.DiffUtil
import android.support.v7.util.ListUpdateCallback
import android.support.v7.widget.RecyclerView
import android.view.ViewGroup
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TimelineItem
import jp.juggler.subwaytooter.util.LogCategory
internal class ItemListAdapter(
private val activity : ActMain,
@ -12,40 +15,69 @@ internal class ItemListAdapter(
internal val columnVh : ColumnViewHolder,
private val bSimpleList : Boolean
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val list : List<Any>
companion object {
private val log = LogCategory("ItemListAdapter")
}
private inner class DiffCallback(
val oldList : List<TimelineItem>,
val newList : List<TimelineItem>,
val biasListIndex : Int
) : DiffUtil.Callback() {
override fun getOldListSize() : Int {
return oldList.size - biasListIndex
}
override fun getNewListSize() : Int {
return newList.size - biasListIndex
}
override fun areItemsTheSame(oldAdapterIndex : Int, newAdapterIndex : Int) : Boolean {
val oldListIndex = oldAdapterIndex + biasListIndex
val newListIndex = newAdapterIndex + biasListIndex
// header?
if(oldListIndex < 0 || newListIndex < 0) return oldListIndex == newListIndex
// compare object address
return oldList[oldListIndex] === newList[newListIndex]
}
override fun areContentsTheSame(oldAdapterIndex : Int, newAdapterIndex : Int) : Boolean {
val oldListIndex = oldAdapterIndex + biasListIndex
val newListIndex = newAdapterIndex + biasListIndex
// headerは毎回更新する
return ! (oldListIndex < 0 || newListIndex < 0)
}
}
private var list : ArrayList<TimelineItem>
private val handler : Handler
init {
this.list = column.list_data
setHasStableIds(false)
this.list = ArrayList()
this.handler = activity.handler
setHasStableIds(true)
}
override fun getItemCount() : Int {
return column.toAdapterIndex(column.list_data.size)
}
private fun getItemIdForListIndex(position : Int):Long{
val o = list[position]
return when(o){
is TootAccount -> o.id
is TootStatus -> o.id
is TootNotification -> o.id
else-> 0L
}
}
override fun getItemId(position : Int) : Long {
val headerType = column.headerType
if( headerType != null){
if(position==0) return 0
return getItemIdForListIndex(position-1)
return try {
list[column.toListIndex(position)].listViewItemId
} catch(ignored : Throwable) {
0L
}
return getItemIdForListIndex(position)
}
override fun getItemViewType(position : Int) : Int {
val headerType = column.headerType
if( headerType == null || position>0 ) return 0
if(headerType == null || position > 0) return 0
return headerType.viewType
}
@ -56,41 +88,46 @@ internal class ItemListAdapter(
holder.viewRoot.tag = holder
return ViewHolderItem(holder)
}
Column.HeaderType.Profile.viewType -> {
val viewRoot = activity.layoutInflater.inflate(R.layout.lv_header_profile, parent, false)
val holder = ViewHolderHeaderProfile(activity,viewRoot)
val viewRoot =
activity.layoutInflater.inflate(R.layout.lv_header_profile, parent, false)
val holder = ViewHolderHeaderProfile(activity, viewRoot)
viewRoot.tag = holder
return holder
}
Column.HeaderType.Search.viewType -> {
val viewRoot = activity.layoutInflater.inflate(R.layout.lv_header_search_desc, parent, false)
val holder = ViewHolderHeaderSearch(activity,viewRoot)
val viewRoot =
activity.layoutInflater.inflate(R.layout.lv_header_search_desc, parent, false)
val holder = ViewHolderHeaderSearch(activity, viewRoot)
viewRoot.tag = holder
return holder
}
Column.HeaderType.Instance.viewType -> {
val viewRoot = activity.layoutInflater.inflate(R.layout.lv_header_instance, parent, false)
val holder = ViewHolderHeaderInstance(activity,viewRoot)
val viewRoot =
activity.layoutInflater.inflate(R.layout.lv_header_instance, parent, false)
val holder = ViewHolderHeaderInstance(activity, viewRoot)
viewRoot.tag = holder
return holder
}
else -> throw RuntimeException("unknown viewType: $viewType")
}
}
fun findHeaderViewHolder(listView:RecyclerView): ViewHolderHeaderBase?{
return when(column.headerType){
null-> null
else-> listView.findViewHolderForAdapterPosition(0) as? ViewHolderHeaderBase
fun findHeaderViewHolder(listView : RecyclerView) : ViewHolderHeaderBase? {
return when(column.headerType) {
null -> null
else -> listView.findViewHolderForAdapterPosition(0) as? ViewHolderHeaderBase
}
}
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, positionArg : Int) {
val headerType = column.headerType
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, adapterIndex : Int) {
if(holder is ViewHolderItem) {
val position = if(headerType != null) positionArg - 1 else positionArg
val o = if(position >= 0 && position < list.size) list[position] else null
holder.ivh.bind(this, column, bSimpleList, o)
val listIndex = column.toListIndex(adapterIndex)
holder.ivh.bind(this, column, bSimpleList, list[listIndex])
} else if(holder is ViewHolderHeaderBase) {
holder.bindData(column)
}
@ -104,45 +141,93 @@ internal class ItemListAdapter(
}
}
// override fun getViewTypeCount() : Int {
// return if(header != null) 2 else 1
// }
fun notifyChange(
reason : String,
changeList : List<AdapterChange>? = null,
reset : Boolean = false
) {
val time_start = SystemClock.elapsedRealtime()
// カラムから最新データをコピーする
val new_list = ArrayList<TimelineItem>()
new_list.ensureCapacity(column.list_data.size)
new_list.addAll(column.list_data)
when {
// 変更リストが指定された場合はヘッダ部分とリスト要素を通知する
changeList != null -> {
log.d("notifyChange: changeList=${changeList.size},reason=$reason")
// override fun getItem(positionArg : Int) : Any? {
// var position = positionArg
// if(header != null) {
// if(position == 0) return header
// -- position
// }
// return if(position >= 0 && position < column.list_data.size) list[position] else null
// }
this.list = new_list
// override fun hasStableIds():Boolean = false
// override fun getView(positionArg : Int, viewOld : View?, parent : ViewGroup) : View {
// var position = positionArg
// val header = this.header
// if(header != null) {
// if(position == 0) return header.viewRoot
// -- position
// }
//
// val o = if(position >= 0 && position < list.size) list[position] else null
//
// val holder : ItemViewHolder
// val view : View
// if(viewOld == null) {
//
// } else {
// view = viewOld
// holder = view.tag as ItemViewHolder
// }
// holder.bind(o)
// return view
// }
// ヘッダは毎回更新する
// (ヘッダだけ更新するためにカラのchangeListが渡される)
if(column.headerType != null){
notifyItemRangeChanged(0, 1)
}
// 変更リストを順番に通知する
for(c in changeList) {
val adapterIndex = column.toAdapterIndex(c.listIndex)
log.d("notifyChange: ChangeType=${c.type} pos=$adapterIndex,count=${c.count}")
when(c.type) {
AdapterChangeType.RangeInsert -> notifyItemRangeInserted(adapterIndex, c.count)
AdapterChangeType.RangeRemove -> notifyItemRangeRemoved(adapterIndex, c.count)
AdapterChangeType.RangeChange -> notifyItemRangeChanged(adapterIndex, c.count)
}
}
}
}
reset -> {
log.d("notifyChange: DataSetChanged! reason=$reason")
this.list = new_list
notifyDataSetChanged()
}
else -> {
val diffResult = DiffUtil.calculateDiff(
DiffCallback(
oldList = this.list, // 比較対象の古いデータ
newList = new_list,
biasListIndex = column.toListIndex(0)
),
false // ログを見た感じ、移動なんてなかった
)
val time = SystemClock.elapsedRealtime() - time_start
log.d("notifyChange: size=${new_list.size},time=${time}ms,reason=$reason")
this.list = new_list
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
override fun onInserted(position : Int, count : Int) {
log.d("notifyChange: notifyItemRangeInserted pos=$position,count=$count")
notifyItemRangeInserted(position, count)
}
override fun onRemoved(position : Int, count : Int) {
log.d("notifyChange: notifyItemRangeRemoved pos=$position,count=$count")
notifyItemRangeRemoved(position, count)
}
override fun onChanged(position : Int, count : Int, payload : Any?) {
log.d("notifyChange: notifyItemRangeChanged pos=$position,count=$count")
notifyItemRangeChanged(position, count, payload)
}
override fun onMoved(fromPosition : Int, toPosition : Int) {
log.d("notifyChange: notifyItemMoved from=$fromPosition,to=$toPosition")
notifyItemMoved(fromPosition, toPosition)
}
})
}
}
// diffを取る部分をワーカースレッドで実行したいが、直後にスクロール位置の処理があるので差支えがある…
}
}

View File

@ -120,13 +120,15 @@ internal class ItemViewHolder(
private var buttons_for_status : StatusButtons? = null
private var item : Any? = null
private var item : TimelineItem? = null
private var status_showing : TootStatus? = null
private var status_account : TootAccount? = null
private var boost_account : TootAccount? = null
private var follow_account : TootAccount? = null
private var boost_time :Long = 0L
private val content_color_default : Int
private var acct_color : Int = 0
@ -213,7 +215,7 @@ internal class ItemViewHolder(
}
fun bind(list_adapter : ItemListAdapter, column : Column, bSimpleList : Boolean, item : Any?) {
fun bind(list_adapter : ItemListAdapter, column : Column, bSimpleList : Boolean, item : TimelineItem) {
this.list_adapter = list_adapter
this.column = column
this.bSimpleList = bSimpleList
@ -293,12 +295,11 @@ internal class ItemViewHolder(
)
}
this.item = null
this.status_showing = null
this.status_account = null
this.boost_account = null
this.follow_account = null
this.boost_time = 0L
llBoosted.visibility = View.GONE
llFollow.visibility = View.GONE
@ -306,11 +307,9 @@ internal class ItemViewHolder(
llSearchTag.visibility = View.GONE
llList.visibility = View.GONE
llExtra.removeAllViews()
if(item == null) return
var c : Int
c = if(column.content_color != 0) column.content_color else content_color_default
tvBoosted.setTextColor(c)
tvFollowerName.setTextColor(c)
@ -331,10 +330,11 @@ internal class ItemViewHolder(
// tvBoostedAcct.setTextColor( c );
// tvFollowerAcct.setTextColor( c );
// tvAcct.setTextColor( c );
this.item = item
when(item) {
is String -> showSearchTag(item)
is TootTag -> showSearchTag(item)
is TootAccount -> showAccount(item)
is TootNotification -> showNotification(item)
is TootGap -> showGap()
@ -461,9 +461,9 @@ internal class ItemViewHolder(
btnSearchTag.text = domain_block.domain
}
private fun showSearchTag(tag : String) {
private fun showSearchTag(tag : TootTag) {
llSearchTag.visibility = View.VISIBLE
btnSearchTag.text = "#" + tag
btnSearchTag.text = "#" + tag.name
}
private fun showGap() {
@ -473,6 +473,7 @@ internal class ItemViewHolder(
private fun showBoost(who : TootAccount, time : Long, icon_attr_id : Int, text : Spannable) {
boost_account = who
boost_time = time
llBoosted.visibility = View.VISIBLE
ivBoosted.setImageResource(Styler.getAttributeResourceId(activity, icon_attr_id))
tvBoostedTime.text = TootStatus.formatTime(tvBoostedTime.context, time, true)
@ -695,6 +696,18 @@ internal class ItemViewHolder(
tvTime.text = sb
}
fun updateRelativeTime() {
val boost_time = this.boost_time
if( boost_time != 0L ){
tvBoostedTime.text = TootStatus.formatTime(tvBoostedTime.context, boost_time, true)
}
val status_showing = this.status_showing
if( status_showing != null ){
showStatusTime(activity, status_showing)
}
}
private fun setAcct(tv : TextView, acctLong : String, acctShort : String?) {
val ac = AcctColor.load(acctLong)
@ -819,7 +832,10 @@ internal class ItemViewHolder(
btnContentWarning -> status_showing?.let { status ->
val new_shown = llContents.visibility == View.GONE
ContentWarning.save(status, new_shown)
list_adapter.notifyDataSetChanged()
// 1個だけ開閉するのではなく、例えば通知TLにある複数の要素をまとめて開閉するなどある
list_adapter.notifyChange(reason="ContentWarning onClick", reset=true)
}
ivThumbnail -> status_account?.let { who ->
@ -868,13 +884,12 @@ internal class ItemViewHolder(
.show()
}
is String -> {
// search_tag は#を含まない
is TootTag -> {
Action_HashTag.timeline(
activity,
activity.nextPosition(column),
access_info,
item
item.name // #を含まない
)
}
}
@ -982,9 +997,9 @@ internal class ItemViewHolder(
// .show()
// }
is String -> {
is TootTag -> {
// search_tag は#を含まない
val tagEncoded = Uri.encode(item)
val tagEncoded = Uri.encode(item.name)
val host = access_info.host
val url = "https://$host/tags/$tagEncoded"
Action_HashTag.timelineOtherInstance(
@ -992,7 +1007,7 @@ internal class ItemViewHolder(
pos = activity.nextPosition(column),
url = url,
host = host,
tag_without_sharp = item
tag_without_sharp = item.name
)
}
@ -1682,6 +1697,7 @@ internal class ItemViewHolder(
}
}
}

View File

@ -349,4 +349,11 @@ internal class ViewHolderHeaderProfile(
override fun onViewRecycled() {
}
fun updateRelativeTime(){
val who = this.who
if( who != null ) {
tvCreated.text = TootStatus.formatTime(tvCreated.context, who.time_created_at, true)
}
}
}

View File

@ -1,13 +1,9 @@
package jp.juggler.subwaytooter.api
import jp.juggler.subwaytooter.api.entity.*
import java.util.ArrayList
import java.util.HashSet
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.api.entity.TootReport
import jp.juggler.subwaytooter.api.entity.TootStatus
class DuplicateMap {
private val set_status_id = HashSet<Long>()
@ -24,7 +20,7 @@ class DuplicateMap {
set_status_uri.clear()
}
fun isDuplicate(o : Any) : Boolean {
fun isDuplicate(o : TimelineItem) : Boolean {
when(o) {
@ -69,11 +65,13 @@ class DuplicateMap {
return false
}
fun filterDuplicate(src : Collection<Any>) : ArrayList<Any> {
val list_new = ArrayList<Any>()
for(o in src) {
if(isDuplicate(o)) continue
list_new.add(o)
fun filterDuplicate(src : Collection<TimelineItem>?) : ArrayList<TimelineItem> {
val list_new = ArrayList<TimelineItem>()
if( src != null ) {
for(o in src) {
if(isDuplicate(o)) continue
list_new.add(o)
}
}
return list_new
}

View File

@ -0,0 +1,14 @@
package jp.juggler.subwaytooter.api.entity
import java.util.concurrent.atomic.AtomicLong
// カラムに表示する要素全てのベースクラス
open class TimelineItem{
companion object {
val idSeed = AtomicLong(3) // ヘッダ用にいくつか空けておく
}
// AdapterView用のIDを採番する
val listViewItemId :Long = idSeed.incrementAndGet()
}

View File

@ -21,7 +21,7 @@ open class TootAccount(
accessInfo : LinkClickContext,
src : JSONObject,
serviceType : ServiceType
) {
) :TimelineItem(){
//URL of the user's profile page (can be remote)
// https://mastodon.juggler.jp/@tateisu

View File

@ -6,7 +6,7 @@ import java.util.ArrayList
class TootDomainBlock(
val domain : String
) {
):TimelineItem() {
companion object {

View File

@ -3,7 +3,7 @@ package jp.juggler.subwaytooter.api.entity
class TootGap(
val max_id : String,
val since_id : String
) {
) :TimelineItem(){
constructor(max_id : Long, since_id : Long) : this(max_id.toString(), since_id.toString())
}

View File

@ -12,7 +12,7 @@ import jp.juggler.subwaytooter.util.Utils
class TootList(
val id : Long,
val title : String?
) : Comparable<TootList> {
) : TimelineItem(), Comparable<TootList> {
// タイトルの数字列部分は数字の大小でソートされるようにしたい
private val title_for_sort : ArrayList<Any>?

View File

@ -12,7 +12,7 @@ class TootNotification(
private val created_at : String?, // The time the notification was created
val account : TootAccount?, // The Account sending the notification to the user
val status : TootStatus? // The Status associated with the notification, if applicable
) {
) :TimelineItem(){
val time_created_at : Long

View File

@ -7,7 +7,7 @@ import jp.juggler.subwaytooter.util.Utils
class TootReport(
val id : Long,
private val action_taken : String? // The action taken in response to the report
) {
) :TimelineItem() {
constructor(src : JSONObject) : this(
id = Utils.optLongX(src, "id"),

View File

@ -5,18 +5,21 @@ import org.json.JSONObject
import java.util.ArrayList
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.util.Utils
class TootResults(
val accounts : ArrayList<TootAccount>, // An array of matched Accounts
val statuses : TootStatus.List, // An array of matched Statuses
val hashtags : ArrayList<String> // An array of matched hashtags, as strings
val hashtags : ArrayList<TootTag> // An array of matched hashtags
) {
constructor(parser : TootParser, src : JSONObject) : this(
accounts = TootAccount.parseList(parser.context, parser.accessInfo, src.optJSONArray("accounts")),
accounts = TootAccount.parseList(
parser.context,
parser.accessInfo,
src.optJSONArray("accounts")
),
statuses = TootStatus.parseList(parser, src.optJSONArray("statuses")),
hashtags = Utils.parseStringArray(src.optJSONArray("hashtags"))
hashtags = TootTag.parseStringArray(src.optJSONArray("hashtags"))
)
}

View File

@ -22,7 +22,7 @@ import java.text.SimpleDateFormat
import java.util.*
@Suppress("MemberVisibilityCanPrivate")
class TootStatus(parser : TootParser, src : JSONObject, serviceType : ServiceType) {
class TootStatus(parser : TootParser, src : JSONObject, serviceType : ServiceType) :TimelineItem(){
val json : JSONObject

View File

@ -1,13 +1,37 @@
package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.util.Utils
import org.json.JSONArray
import org.json.JSONObject
class TootTag(
val name : String, // The hashtag, not including the preceding #
val url : String // The URL of the hashtag
) {
// The hashtag, not including the preceding #
val name : String,
// The URL of the hashtag. may null if generated from TootContext
val url : String? =null
) :TimelineItem(){
constructor(src : JSONObject):this(
name = src.notEmptyOrThrow("name"),
url = src.notEmptyOrThrow("url")
url = Utils.optStringX(src,"url")
)
companion object {
// 検索結果のhashtagリストから生成する
fun parseStringArray( array: JSONArray?):ArrayList<TootTag>{
val result = ArrayList<TootTag>()
if(array != null) {
for( i in 0 until array.length() ){
val sv = Utils.optStringX(array, i)
if( sv?.isNotEmpty() == true ) {
result.add(TootTag(name = sv))
}
}
}
return result
}
}
}

View File

@ -22,7 +22,7 @@ class CustomEmojiCache(internal val context : Context) {
private val log = LogCategory("CustomEmojiCache")
internal const val DEBUG = true
internal const val DEBUG = false
internal const val CACHE_MAX = 512 // 使用中のビットマップは掃除しないので、頻度によってはこれより多くなることもある
internal const val ERROR_EXPIRE = 60000L * 10
@ -243,12 +243,20 @@ class CustomEmojiCache(internal val context : Context) {
private fun decodeAPNG(data : ByteArray, url : String) : APNGFrames? {
try {
val frames = APNGFrames.parseAPNG(ByteArrayInputStream(data), 64)
if(frames?.hasMultipleFrame == true) return frames
if(DEBUG) log.d("parseAPNG returns null or single frame.")
// mastodonのstatic_urlが返すPNG画像はAPNGだと透明になってる場合がある。BitmapFactoryでデコードしなおすべき
frames?.dispose()
// PNGヘッダを確認
if( data.size >= 8
&& (data[0].toInt() and 0xff) == 0x89
&& (data[1].toInt() and 0xff) == 0x50
){
// APNGをデコード
val frames = APNGFrames.parseAPNG(ByteArrayInputStream(data), 64)
if(frames?.hasMultipleFrame == true) return frames
frames?.dispose()
// mastodonのstatic_urlが返すPNG画像はAPNGだと透明になってる場合がある。BitmapFactoryでデコードしなおすべき
if(DEBUG) log.d("parseAPNG returns null or single frame.")
}
// fall thru
} catch(ex : Throwable) {
if(DEBUG) log.trace(ex)

View File

@ -454,4 +454,23 @@ class TestKotlinFeature {
var e2 = arrayOf( null,1,null,2)
}
@Test fun testOutProjectedType(){
fun foo( args: Array<out Number>){
val sb = StringBuilder()
for(s in args){
if(sb.isNotEmpty()) sb.append(',')
sb
.append(s.toString())
.append('#')
.append( s.javaClass.simpleName)
}
println(sb)
println(args.contains(6)) // 禁止されていない。inポジションって何だ…
// arggs[0]=6 //禁止されている
}
foo( arrayOf(1,2,3))
foo( arrayOf(1f,2f,3f))
}
}