SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt

1601 lines
48 KiB
Kotlin

package jp.juggler.subwaytooter
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.AsyncTask
import android.support.v4.content.ContextCompat
import android.support.v4.view.ViewCompat
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.text.Editable
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.TextWatcher
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo
import android.widget.*
import jp.juggler.subwaytooter.action.Action_List
import jp.juggler.subwaytooter.action.Action_Notification
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.util.ScrollPosition
import jp.juggler.util.createResizedBitmap
import jp.juggler.subwaytooter.view.ListDivider
import jp.juggler.util.*
import org.jetbrains.anko.textColor
import java.io.Closeable
import java.lang.reflect.Field
import java.util.regex.Pattern
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayout
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection
@SuppressLint("ClickableViewAccessibility")
class ColumnViewHolder(
val activity : ActMain,
val viewRoot : View
) : View.OnClickListener,
SwipyRefreshLayout.OnRefreshListener,
CompoundButton.OnCheckedChangeListener, View.OnLongClickListener {
companion object {
private val log = LogCategory("ColumnViewHolder")
val fieldRecycler : Field by lazy {
val field = RecyclerView::class.java.getDeclaredField("mRecycler")
field.isAccessible = true
field
}
val fieldState : Field by lazy {
val field = RecyclerView::class.java.getDeclaredField("mState")
field.isAccessible = true
field
}
val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
// var lastRefreshError : String = ""
// var lastRefreshErrorShown : Long = 0L
}
var column : Column? = null
private var status_adapter : ItemListAdapter? = null
private var page_idx : Int = 0
private val tvLoading : TextView
val listView : RecyclerView
val refreshLayout : SwipyRefreshLayout
lateinit var listLayoutManager : LinearLayoutManager
private val llColumnHeader : View
private val tvColumnIndex : TextView
private val tvColumnStatus : TextView
private val tvColumnContext : TextView
private val ivColumnIcon : ImageView
private val tvColumnName : TextView
private val llColumnSetting : View
private val btnSearch : View
private val etSearch : EditText
private val cbResolve : CheckBox
private val etRegexFilter : EditText
private val tvRegexFilterError : TextView
private val btnColumnSetting : ImageButton
private val btnColumnReload : ImageButton
private val btnColumnClose : ImageButton
private val flColumnBackground : View
private val ivColumnBackgroundImage : ImageView
private val llSearch : View
private val cbDontCloseColumn : CheckBox
private val cbWithAttachment : CheckBox
private val cbWithHighlight : CheckBox
private val cbDontShowBoost : CheckBox
private val cbDontShowFollow : CheckBox
private val cbDontShowFavourite : CheckBox
private val cbDontShowReply : CheckBox
private val cbDontShowReaction : CheckBox
private val cbDontShowVote : CheckBox
private val cbDontShowNormalToot : CheckBox
private val cbInstanceLocal : CheckBox
private val cbDontStreaming : CheckBox
private val cbDontAutoRefresh : CheckBox
private val cbHideMediaDefault : CheckBox
private val cbSystemNotificationNotRelated : CheckBox
private val cbEnableSpeech : CheckBox
private val cbOldApi : CheckBox
private val llRegexFilter : View
private val btnDeleteNotification : Button
private val svQuickFilter : HorizontalScrollView
private val btnQuickFilterAll : Button
private val btnQuickFilterMention : ImageButton
private val btnQuickFilterFavourite : ImageButton
private val btnQuickFilterBoost : ImageButton
private val btnQuickFilterFollow : ImageButton
private val btnQuickFilterReaction : ImageButton
private val btnQuickFilterVote : ImageButton
private val llRefreshError : FrameLayout
private val ivRefreshError : ImageView
private val tvRefreshError : TextView
private val llListList : View
private val etListName : EditText
private val btnListAdd : View
private val isPageDestroyed : Boolean
get() = column == null || activity.isFinishing
private var loading_busy : Boolean = false
private var last_image_uri : String? = null
private var last_image_bitmap : Bitmap? = null
private var last_image_task : AsyncTask<Void, Void, Bitmap?>? = null
private fun checkRegexFilterError(src : String) : String? {
try {
if(src.isEmpty()) {
return null
}
val m = Pattern.compile(src).matcher("")
if(m.find()) {
// 空文字列にマッチする正規表現はエラー扱いにする
// そうしないとCWの警告テキストにマッチしてしまう
return activity.getString(R.string.regex_filter_matches_empty_string)
}
return null
} catch(ex : Throwable) {
val message = ex.message
return if(message != null && message.isNotEmpty()) {
message
} else {
ex.withCaption(activity.resources, R.string.regex_error)
}
}
}
private fun isRegexValid() : Boolean {
val s = etRegexFilter.text.toString()
val error = checkRegexFilterError(s)
tvRegexFilterError.text = error ?: ""
return error == null
}
val isColumnSettingShown : Boolean
get() = llColumnSetting.visibility == View.VISIBLE
// val headerView : HeaderViewHolderBase?
// get() = status_adapter?.header
val scrollPosition : ScrollPosition
get() = ScrollPosition(this)
inner class ErrorFlickListener(
context : Context
) : View.OnTouchListener, GestureDetector.OnGestureListener {
private val gd = GestureDetector(context, this)
private val density = context.resources.displayMetrics.density
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v : View?, event : MotionEvent?) : Boolean {
return gd.onTouchEvent(event)
}
override fun onShowPress(e : MotionEvent?) {
}
override fun onLongPress(e : MotionEvent?) {
}
override fun onSingleTapUp(e : MotionEvent?) : Boolean {
return true
}
override fun onDown(e : MotionEvent?) : Boolean {
return true
}
override fun onScroll(
e1 : MotionEvent?,
e2 : MotionEvent?,
distanceX : Float,
distanceY : Float
) : Boolean {
return true
}
override fun onFling(
e1 : MotionEvent?,
e2 : MotionEvent?,
velocityX : Float,
velocityY : Float
) : Boolean {
val vx = velocityX.abs()
val vy = velocityY.abs()
if(vy < vx * 1.5f) {
// フリック方向が上下ではない
log.d("fling? not vertical view. $vx $vy")
} else {
val vydp = vy / density
val limit = 1024f
log.d("fling? $vydp/$limit")
if(vydp >= limit) {
val column = column
if(column != null && column.lastTask == null) {
column.startLoading()
}
}
}
return true
}
}
@SuppressLint("ClickableViewAccessibility")
private fun initLoadingTextView() {
tvLoading.setOnTouchListener(ErrorFlickListener(activity))
}
init {
viewRoot.scan { v ->
try {
if(v is Button) {
// ボタンは触らない
} else if(v is TextView) {
v.typeface = ActMain.timeline_font
}
} catch(ex : Throwable) {
log.trace(ex)
}
}
flColumnBackground = viewRoot.findViewById(R.id.flColumnBackground)
ivColumnBackgroundImage = viewRoot.findViewById(R.id.ivColumnBackgroundImage)
llColumnHeader = viewRoot.findViewById(R.id.llColumnHeader)
tvColumnIndex = viewRoot.findViewById(R.id.tvColumnIndex)
tvColumnStatus = viewRoot.findViewById(R.id.tvColumnStatus)
tvColumnName = viewRoot.findViewById(R.id.tvColumnName)
tvColumnContext = viewRoot.findViewById(R.id.tvColumnContext)
ivColumnIcon = viewRoot.findViewById(R.id.ivColumnIcon)
btnColumnSetting = viewRoot.findViewById(R.id.btnColumnSetting)
btnColumnReload = viewRoot.findViewById(R.id.btnColumnReload)
btnColumnClose = viewRoot.findViewById(R.id.btnColumnClose)
tvLoading = viewRoot.findViewById(R.id.tvLoading)
listView = viewRoot.findViewById(R.id.listView)
if(Pref.bpShareViewPool(activity.pref)) {
listView.setRecycledViewPool(activity.viewPool)
}
listView.itemAnimator = null
//
// val animator = listView.itemAnimator
// if( animator is DefaultItemAnimator){
// animator.supportsChangeAnimations = false
// }
btnSearch = viewRoot.findViewById(R.id.btnSearch)
etSearch = viewRoot.findViewById(R.id.etSearch)
cbResolve = viewRoot.findViewById(R.id.cbResolve)
llSearch = viewRoot.findViewById(R.id.llSearch)
llListList = viewRoot.findViewById(R.id.llListList)
btnListAdd = viewRoot.findViewById(R.id.btnListAdd)
etListName = viewRoot.findViewById(R.id.etListName)
btnListAdd.setOnClickListener(this)
etListName.setOnEditorActionListener { _, actionId, _ ->
var handled = false
if(actionId == EditorInfo.IME_ACTION_SEND) {
btnListAdd.performClick()
handled = true
}
handled
}
llColumnSetting = viewRoot.findViewById(R.id.llColumnSetting)
cbDontCloseColumn = viewRoot.findViewById(R.id.cbDontCloseColumn)
cbWithAttachment = viewRoot.findViewById(R.id.cbWithAttachment)
cbWithHighlight = viewRoot.findViewById(R.id.cbWithHighlight)
cbDontShowBoost = viewRoot.findViewById(R.id.cbDontShowBoost)
cbDontShowFollow = viewRoot.findViewById(R.id.cbDontShowFollow)
cbDontShowFavourite = viewRoot.findViewById(R.id.cbDontShowFavourite)
cbDontShowReply = viewRoot.findViewById(R.id.cbDontShowReply)
cbDontShowReaction = viewRoot.findViewById(R.id.cbDontShowReaction)
cbDontShowVote = viewRoot.findViewById(R.id.cbDontShowVote)
cbDontShowNormalToot = viewRoot.findViewById(R.id.cbDontShowNormalToot)
cbInstanceLocal = viewRoot.findViewById(R.id.cbInstanceLocal)
cbDontStreaming = viewRoot.findViewById(R.id.cbDontStreaming)
cbDontAutoRefresh = viewRoot.findViewById(R.id.cbDontAutoRefresh)
cbHideMediaDefault = viewRoot.findViewById(R.id.cbHideMediaDefault)
cbSystemNotificationNotRelated = viewRoot.findViewById(R.id.cbSystemNotificationNotRelated)
cbEnableSpeech = viewRoot.findViewById(R.id.cbEnableSpeech)
cbOldApi = viewRoot.findViewById(R.id.cbOldApi)
etRegexFilter = viewRoot.findViewById(R.id.etRegexFilter)
llRegexFilter = viewRoot.findViewById(R.id.llRegexFilter)
tvRegexFilterError = viewRoot.findViewById(R.id.tvRegexFilterError)
btnDeleteNotification = viewRoot.findViewById(R.id.btnDeleteNotification)
svQuickFilter = viewRoot.findViewById(R.id.svQuickFilter)
btnQuickFilterAll = viewRoot.findViewById(R.id.btnQuickFilterAll)
btnQuickFilterMention = viewRoot.findViewById(R.id.btnQuickFilterMention)
btnQuickFilterFavourite = viewRoot.findViewById(R.id.btnQuickFilterFavourite)
btnQuickFilterBoost = viewRoot.findViewById(R.id.btnQuickFilterBoost)
btnQuickFilterFollow = viewRoot.findViewById(R.id.btnQuickFilterFollow)
btnQuickFilterReaction = viewRoot.findViewById(R.id.btnQuickFilterReaction)
btnQuickFilterVote = viewRoot.findViewById(R.id.btnQuickFilterVote)
val llColumnSettingInside : LinearLayout = viewRoot.findViewById(R.id.llColumnSettingInside)
btnQuickFilterAll.setOnClickListener(this)
btnQuickFilterMention.setOnClickListener(this)
btnQuickFilterFavourite.setOnClickListener(this)
btnQuickFilterBoost.setOnClickListener(this)
btnQuickFilterFollow.setOnClickListener(this)
btnQuickFilterReaction.setOnClickListener(this)
btnQuickFilterVote.setOnClickListener(this)
llColumnHeader.setOnClickListener(this)
btnColumnSetting.setOnClickListener(this)
btnColumnReload.setOnClickListener(this)
btnColumnClose.setOnClickListener(this)
btnColumnClose.setOnLongClickListener(this)
btnDeleteNotification.setOnClickListener(this)
viewRoot.findViewById<View>(R.id.btnColor).setOnClickListener(this)
this.refreshLayout = viewRoot.findViewById(R.id.swipyRefreshLayout)
refreshLayout.setOnRefreshListener(this)
refreshLayout.setDistanceToTriggerSync((0.5f + 20f * activity.density).toInt())
llRefreshError = viewRoot.findViewById(R.id.llRefreshError)
ivRefreshError = viewRoot.findViewById(R.id.ivRefreshError)
tvRefreshError = viewRoot.findViewById(R.id.tvRefreshError)
llRefreshError.setOnClickListener(this)
setIconDrawableId(activity, ivRefreshError, R.drawable.ic_error, Color.RED)
cbDontCloseColumn.setOnCheckedChangeListener(this)
cbWithAttachment.setOnCheckedChangeListener(this)
cbWithHighlight.setOnCheckedChangeListener(this)
cbDontShowBoost.setOnCheckedChangeListener(this)
cbDontShowFollow.setOnCheckedChangeListener(this)
cbDontShowFavourite.setOnCheckedChangeListener(this)
cbDontShowReply.setOnCheckedChangeListener(this)
cbDontShowReaction.setOnCheckedChangeListener(this)
cbDontShowVote.setOnCheckedChangeListener(this)
cbDontShowNormalToot.setOnCheckedChangeListener(this)
cbInstanceLocal.setOnCheckedChangeListener(this)
cbDontStreaming.setOnCheckedChangeListener(this)
cbDontAutoRefresh.setOnCheckedChangeListener(this)
cbHideMediaDefault.setOnCheckedChangeListener(this)
cbSystemNotificationNotRelated.setOnCheckedChangeListener(this)
cbEnableSpeech.setOnCheckedChangeListener(this)
cbOldApi.setOnCheckedChangeListener(this)
if(Pref.bpMoveNotificationsQuickFilter(activity.pref)) {
(svQuickFilter.parent as? ViewGroup)?.removeView(svQuickFilter)
llColumnSettingInside.addView(svQuickFilter, 0)
svQuickFilter.setOnTouchListener { v, event ->
val action = event.action
if(action == MotionEvent.ACTION_DOWN) {
val sv = v as? HorizontalScrollView
if(sv != null && sv.getChildAt(0).width > sv.width) {
sv.requestDisallowInterceptTouchEvent(true)
}
}
v.onTouchEvent(event)
}
}
if(! activity.header_text_size_sp.isNaN()) {
tvColumnName.textSize = activity.header_text_size_sp
val acctSize = activity.header_text_size_sp * 0.857f
tvColumnContext.textSize = acctSize
tvColumnStatus.textSize = acctSize
tvColumnIndex.textSize = acctSize
}
initLoadingTextView()
var pad = 0
var wh = ActMain.headerIconSize + pad * 2
ivColumnIcon.layoutParams.width = wh
ivColumnIcon.layoutParams.height = wh
ivColumnIcon.setPaddingRelative(pad, pad, pad, pad)
pad = (ActMain.headerIconSize * 0.125f + 0.5f).toInt()
wh = ActMain.headerIconSize + pad * 2
btnColumnSetting.layoutParams.width = wh
btnColumnSetting.layoutParams.height = wh
btnColumnSetting.setPaddingRelative(pad, pad, pad, pad)
btnColumnReload.layoutParams.width = wh
btnColumnReload.layoutParams.height = wh
btnColumnReload.setPaddingRelative(pad, pad, pad, pad)
btnColumnClose.layoutParams.width = wh
btnColumnClose.layoutParams.height = wh
btnColumnClose.setPaddingRelative(pad, pad, pad, pad)
// 入力の追跡
etRegexFilter.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s : CharSequence,
start : Int,
count : Int,
after : Int
) {
}
override fun onTextChanged(s : CharSequence, start : Int, before : Int, count : Int) {}
override fun afterTextChanged(s : Editable) {
if(loading_busy) return
activity.handler.removeCallbacks(proc_start_filter)
if(isRegexValid()) {
activity.handler.postDelayed(proc_start_filter, 666L)
}
}
})
btnSearch.setOnClickListener(this)
etSearch.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ ->
if(! loading_busy) {
if(actionId == EditorInfo.IME_ACTION_SEARCH) {
btnSearch.performClick()
return@OnEditorActionListener true
}
}
false
})
}
private val proc_start_filter = Runnable {
if(! isPageDestroyed && isRegexValid()) {
val column = this.column ?: return@Runnable
column.regex_text = etRegexFilter.text.toString()
activity.app_state.saveColumnList()
column.startLoading()
}
}
private val proc_restoreScrollPosition = object : Runnable {
override fun run() {
activity.handler.removeCallbacks(this)
if(isPageDestroyed) {
log.d("restoreScrollPosition [%d], page is destroyed.")
return
}
val column = this@ColumnViewHolder.column
if(column == null) {
log.d("restoreScrollPosition [%d], column==null", page_idx)
return
}
if(column.is_dispose.get()) {
log.d("restoreScrollPosition [%d], column is disposed", page_idx)
return
}
if(column.hasMultipleViewHolder()) {
log.d(
"restoreScrollPosition [%d] %s , column has multiple view holder. retry later.",
page_idx,
column.getColumnName(true)
)
// タブレットモードでカラムを追加/削除した際に発生する。
// このタイミングでスクロール位置を復元してもうまくいかないので延期する
activity.handler.postDelayed(this, 100L)
return
}
//復元後にもここを通るがこれは正常である
val sp = column.scroll_save
if(sp == null) {
// val lvi = column.last_viewing_item_id
// if( lvi != null ){
// column.last_viewing_item_id = null
// val listIndex = column.findListIndexByTimelineId(lvi)
// if( listIndex != null){
// log.d(
// "restoreScrollPosition [$page_idx] %s , restore from last_viewing_item_id %d"
// , column.getColumnName( true )
// ,listIndex
// )
// ScrollPosition(column.toAdapterIndex(listIndex),0).restore(this@ColumnViewHolder)
// return
// }
// }
log.d(
"restoreScrollPosition [$page_idx] %s , column has no saved scroll position."
, column.getColumnName(true)
)
return
}
column.scroll_save = null
if(listView.visibility != View.VISIBLE) {
log.d(
"restoreScrollPosition [$page_idx] %s , listView is not visible. saved position %s,%s is dropped."
, column.getColumnName(true)
, sp.adapterIndex
, sp.offset
)
} else {
log.d(
"restoreScrollPosition [%d] %s , listView is visible. resume %s,%s"
, page_idx
, column.getColumnName(true)
, sp.adapterIndex
, sp.offset
)
sp.restore(this@ColumnViewHolder)
}
}
}
fun onPageDestroy(page_idx : Int) {
// タブレットモードの場合、onPageCreateより前に呼ばれる
val column = this@ColumnViewHolder.column
if(column != null) {
log.d("onPageDestroy [%d] %s", page_idx, tvColumnName.text)
saveScrollPosition()
listView.adapter = null
column.removeColumnViewHolder(this)
this@ColumnViewHolder.column = null
}
closeBitmaps()
activity.closeListItemPopup()
}
fun onPageCreate(column : Column, page_idx : Int, page_count : Int) {
loading_busy = true
try {
this.column = column
this.page_idx = page_idx
log.d("onPageCreate [%d] %s", page_idx, column.getColumnName(true))
val bSimpleList =
column.column_type != Column.TYPE_CONVERSATION && Pref.bpSimpleList(activity.pref)
tvColumnIndex.text = activity.getString(R.string.column_index, page_idx + 1, page_count)
tvColumnStatus.text = "?"
listView.adapter = null
if(listView.itemDecorationCount == 0) {
listView.addItemDecoration(ListDivider(activity))
}
val status_adapter = ItemListAdapter(activity, column, this, bSimpleList)
this.status_adapter = status_adapter
val isNotificationColumn = column.column_type == Column.TYPE_NOTIFICATIONS
// 添付メディアや正規表現のフィルタ
val bAllowFilter = column.canStatusFilter()
llColumnSetting.visibility = View.GONE
cbDontCloseColumn.isChecked = column.dont_close
cbWithAttachment.isChecked = column.with_attachment
cbWithHighlight.isChecked = column.with_highlight
cbDontShowBoost.isChecked = column.dont_show_boost
cbDontShowFollow.isChecked = column.dont_show_follow
cbDontShowFavourite.isChecked = column.dont_show_favourite
cbDontShowReply.isChecked = column.dont_show_reply
cbDontShowReaction.isChecked = column.dont_show_reaction
cbDontShowVote.isChecked = column.dont_show_vote
cbDontShowNormalToot.isChecked = column.dont_show_normal_toot
cbInstanceLocal.isChecked = column.instance_local
cbDontStreaming.isChecked = column.dont_streaming
cbDontAutoRefresh.isChecked = column.dont_auto_refresh
cbHideMediaDefault.isChecked = column.hide_media_default
cbSystemNotificationNotRelated.isChecked = column.system_notification_not_related
cbEnableSpeech.isChecked = column.enable_speech
cbOldApi.isChecked = column.use_old_api
etRegexFilter.setText(column.regex_text)
etSearch.setText(column.search_query)
cbResolve.isChecked = column.search_resolve
vg(cbWithAttachment, bAllowFilter)
vg(cbWithHighlight, bAllowFilter)
vg(etRegexFilter, bAllowFilter)
vg(llRegexFilter, bAllowFilter)
vg(cbDontShowBoost, column.canFilterBoost())
vg(cbDontShowReply, column.canFilterReply())
vg(cbDontShowNormalToot, column.canFilterNormalToot())
vg(cbDontShowReaction, isNotificationColumn && column.isMisskey)
vg(cbDontShowVote, isNotificationColumn && column.isMisskey)
vg(cbDontShowFavourite, isNotificationColumn && ! column.isMisskey)
vg(cbDontShowFollow, isNotificationColumn)
vg(cbInstanceLocal, column.column_type == Column.TYPE_HASHTAG)
vg(cbDontStreaming, column.canStreaming())
vg(cbDontAutoRefresh, column.canAutoRefresh())
vg(cbHideMediaDefault, column.canNSFWDefault())
vg(cbSystemNotificationNotRelated, column.column_type == Column.TYPE_NOTIFICATIONS)
vg(cbEnableSpeech, column.canSpeech())
vg(cbOldApi, column.column_type == Column.TYPE_DIRECT_MESSAGES)
vg(btnDeleteNotification, column.column_type == Column.TYPE_NOTIFICATIONS)
vg(llSearch, column.isSearchColumn)
vg(llListList, column.column_type == Column.TYPE_LIST_LIST)
vg(cbResolve, column.column_type == Column.TYPE_SEARCH)
// tvRegexFilterErrorの表示を更新
if(bAllowFilter) {
isRegexValid()
}
val canRefreshTop = column.canRefreshTopBySwipe()
val canRefreshBottom = column.canRefreshBottomBySwipe()
refreshLayout.isEnabled = canRefreshTop || canRefreshBottom
refreshLayout.direction = if(canRefreshTop && canRefreshBottom) {
SwipyRefreshLayoutDirection.BOTH
} else if(canRefreshTop) {
SwipyRefreshLayoutDirection.TOP
} else {
SwipyRefreshLayoutDirection.BOTTOM
}
bRefreshErrorWillShown = false
llRefreshError.clearAnimation()
llRefreshError.visibility = View.GONE
//
listLayoutManager = LinearLayoutManager(activity)
listView.layoutManager = listLayoutManager
listView.adapter = status_adapter
//XXX FastScrollerのサポートを諦める。ライブラリはいくつかあるんだけど、設定でON/OFFできなかったり頭文字バブルを無効にできなかったり
// listView.isFastScrollEnabled = ! Pref.bpDisableFastScroller(Pref.pref(activity))
column.addColumnViewHolder(this)
showQuickFilter()
showColumnColor()
showContent(reason = "onPageCreate", reset = true)
} finally {
loading_busy = false
}
}
private val procShowColumnStatus : Runnable = Runnable {
val column = this.column
if(column == null || column.is_dispose.get()) return@Runnable
val sb = SpannableStringBuilder()
try {
val task = column.lastTask
if(task != null) {
sb.append(
when(task.ctType) {
ColumnTaskType.LOADING -> 'L'
ColumnTaskType.REFRESH_TOP -> 'T'
ColumnTaskType.REFRESH_BOTTOM -> 'B'
ColumnTaskType.GAP -> 'G'
}
)
sb.append(
when {
task.isCancelled -> "~"
task.ctClosed.get() -> "!"
task.ctStarted.get() -> ""
else -> "?"
}
)
}
when(column.getStreamingStatus()) {
StreamingIndicatorState.NONE -> {
}
StreamingIndicatorState.REGISTERED -> {
sb.appendColorShadeIcon(activity, R.drawable.ic_pulse, "Streaming")
sb.append("?")
}
StreamingIndicatorState.LISTENING -> {
sb.appendColorShadeIcon(activity, R.drawable.ic_pulse, "Streaming")
}
}
} finally {
log.d("showColumnStatus ${sb}")
tvColumnStatus.text = sb
}
}
fun showColumnStatus() {
activity.handler.removeCallbacks(procShowColumnStatus)
activity.handler.postDelayed(procShowColumnStatus, 50L)
}
fun showColumnColor() {
val column = this.column
if(column == null || column.is_dispose.get()) return
// カラムヘッダ背景
column.setHeaderBackground(llColumnHeader)
// カラムヘッダ文字色(A)
var c = column.getHeaderNameColor()
tvColumnName.textColor = c
setIconDrawableId(
activity,
ivColumnIcon,
column.getIconId(column.column_type),
c
)
setIconDrawableId(activity, btnColumnSetting, R.drawable.ic_tune, c)
setIconDrawableId(activity, btnColumnReload, R.drawable.ic_refresh, c)
setIconDrawableId(activity, btnColumnClose, R.drawable.ic_close, c)
// カラムヘッダ文字色(B)
c = column.getHeaderPageNumberColor()
tvColumnIndex.textColor = c
tvColumnStatus.textColor = c
// カラム内部の背景色
c = column.column_bg_color
if(c == 0) c = Column.defaultColorContentBg
if(c == 0) {
ViewCompat.setBackground(flColumnBackground, null)
} else {
flColumnBackground.setBackgroundColor(c)
}
// カラム内部の背景画像
ivColumnBackgroundImage.alpha = column.column_bg_image_alpha
loadBackgroundImage(ivColumnBackgroundImage, column.column_bg_image)
// エラー表示
tvLoading.textColor = column.getContentColor()
status_adapter?.findHeaderViewHolder(listView)?.showColor()
}
private fun closeBitmaps() {
try {
ivColumnBackgroundImage.visibility = View.GONE
ivColumnBackgroundImage.setImageDrawable(null)
last_image_bitmap?.recycle()
last_image_bitmap = null
last_image_task?.cancel(true)
last_image_task = null
last_image_uri = null
} catch(ex : Throwable) {
log.trace(ex)
}
}
@SuppressLint("StaticFieldLeak")
private fun loadBackgroundImage(iv : ImageView, url : String?) {
try {
if(url == null || url.isEmpty()) {
// 指定がないなら閉じる
closeBitmaps()
return
}
if(url == last_image_uri) {
// 今表示してるのと同じ
return
}
// 直前の処理をキャンセルする。Bitmapも破棄する
closeBitmaps()
// ロード開始
last_image_uri = url
val screen_w = iv.resources.displayMetrics.widthPixels
val screen_h = iv.resources.displayMetrics.heightPixels
// 非同期処理を開始
val task = object : AsyncTask<Void, Void, Bitmap?>() {
override fun doInBackground(vararg params : Void) : Bitmap? {
return try {
createResizedBitmap(
activity, url.toUri(),
if(screen_w > screen_h)
screen_w
else
screen_h
)
} catch(ex : Throwable) {
log.trace(ex)
null
}
}
override fun onCancelled(bitmap : Bitmap?) {
onPostExecute(bitmap)
}
override fun onPostExecute(bitmap : Bitmap?) {
if(bitmap != null) {
if(isCancelled || url != last_image_uri) {
bitmap.recycle()
} else {
last_image_bitmap = bitmap
iv.setImageBitmap(last_image_bitmap)
iv.visibility = View.VISIBLE
}
}
}
}
last_image_task = task
task.executeOnExecutor(App1.task_executor)
} catch(ex : Throwable) {
log.trace(ex)
}
}
fun closeColumnSetting() {
llColumnSetting.visibility = View.GONE
}
fun onListListUpdated() {
etListName.setText("")
}
override fun onRefresh(direction : SwipyRefreshLayoutDirection) {
val column = this.column ?: return
// カラムを追加/削除したときに ColumnからColumnViewHolderへの参照が外れることがある
// リロードやリフレッシュ操作で直るようにする
column.addColumnViewHolder(this)
if(direction == SwipyRefreshLayoutDirection.TOP && column.canReloadWhenRefreshTop()) {
refreshLayout.isRefreshing = false
activity.handler.post {
this@ColumnViewHolder.column?.startLoading()
}
return
}
column.startRefresh(false, direction == SwipyRefreshLayoutDirection.BOTTOM)
}
override fun onCheckedChanged(view : CompoundButton, isChecked : Boolean) {
val column = this@ColumnViewHolder.column
if(loading_busy || column == null || status_adapter == null) return
// カラムを追加/削除したときに ColumnからColumnViewHolderへの参照が外れることがある
// リロードやリフレッシュ操作で直るようにする
column.addColumnViewHolder(this)
when(view.id) {
R.id.cbDontCloseColumn -> {
column.dont_close = isChecked
showColumnCloseButton()
activity.app_state.saveColumnList()
}
R.id.cbWithAttachment -> {
column.with_attachment = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.cbWithHighlight -> {
column.with_highlight = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.cbDontShowBoost -> {
column.dont_show_boost = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.cbDontShowReply -> {
column.dont_show_reply = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.cbDontShowReaction -> {
column.dont_show_reaction = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.cbDontShowVote -> {
column.dont_show_vote = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.cbDontShowNormalToot -> {
column.dont_show_normal_toot = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.cbDontShowFavourite -> {
column.dont_show_favourite = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.cbDontShowFollow -> {
column.dont_show_follow = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.cbInstanceLocal -> {
column.instance_local = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.cbDontStreaming -> {
column.dont_streaming = isChecked
activity.app_state.saveColumnList()
if(isChecked) {
column.stopStreaming()
} else {
column.onStart(activity)
}
}
R.id.cbDontAutoRefresh -> {
column.dont_auto_refresh = isChecked
activity.app_state.saveColumnList()
}
R.id.cbHideMediaDefault -> {
column.hide_media_default = isChecked
activity.app_state.saveColumnList()
column.fireShowContent(reason = "HideMediaDefault in ColumnSetting", reset = true)
}
R.id.cbSystemNotificationNotRelated -> {
column.system_notification_not_related = isChecked
activity.app_state.saveColumnList()
}
R.id.cbEnableSpeech -> {
column.enable_speech = isChecked
activity.app_state.saveColumnList()
}
R.id.cbOldApi -> {
column.use_old_api = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
}
}
override fun onClick(v : View) {
val column = this.column
val status_adapter = this.status_adapter
if(loading_busy || column == null || status_adapter == null) return
// カラムを追加/削除したときに ColumnからColumnViewHolderへの参照が外れることがある
// リロードやリフレッシュ操作で直るようにする
column.addColumnViewHolder(this)
when(v.id) {
R.id.btnColumnClose -> activity.closeColumn(column)
R.id.btnColumnReload -> {
App1.custom_emoji_cache.clearErrorCache()
if(column.isSearchColumn) {
etSearch.hideKeyboard()
etSearch.setText(column.search_query)
cbResolve.isChecked = column.search_resolve
}
refreshLayout.isRefreshing = false
column.startLoading()
}
R.id.btnSearch -> {
etSearch.hideKeyboard()
column.search_query = etSearch.text.toString().trim { it <= ' ' }
column.search_resolve = cbResolve.isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
R.id.llColumnHeader -> scrollToTop2()
R.id.btnColumnSetting -> llColumnSetting.visibility =
if(llColumnSetting.visibility == View.VISIBLE) View.GONE else View.VISIBLE
R.id.btnDeleteNotification -> Action_Notification.deleteAll(
activity,
column.access_info,
false
)
R.id.btnColor -> {
val idx = activity.app_state.column_list.indexOf(column)
ActColumnCustomize.open(activity, idx, ActMain.REQUEST_CODE_COLUMN_COLOR)
}
R.id.btnListAdd -> {
val tv = etListName.text.toString().trim { it <= ' ' }
if(tv.isEmpty()) {
showToast(activity, true, R.string.list_name_empty)
return
}
Action_List.create(activity, column.access_info, tv, null)
}
R.id.llRefreshError -> {
column.mRefreshLoadingErrorPopupState = 1 - column.mRefreshLoadingErrorPopupState
showRefreshError()
}
R.id.btnQuickFilterAll -> clickQuickFilter(Column.QUICK_FILTER_ALL)
R.id.btnQuickFilterMention -> clickQuickFilter(Column.QUICK_FILTER_MENTION)
R.id.btnQuickFilterFavourite -> clickQuickFilter(Column.QUICK_FILTER_FAVOURITE)
R.id.btnQuickFilterBoost -> clickQuickFilter(Column.QUICK_FILTER_BOOST)
R.id.btnQuickFilterFollow -> clickQuickFilter(Column.QUICK_FILTER_FOLLOW)
R.id.btnQuickFilterReaction -> clickQuickFilter(Column.QUICK_FILTER_REACTION)
R.id.btnQuickFilterVote -> clickQuickFilter(Column.QUICK_FILTER_VOTE)
}
}
override fun onLongClick(v : View) : Boolean {
return when(v.id) {
R.id.btnColumnClose -> {
val idx = activity.app_state.column_list.indexOf(column)
activity.closeColumnAll(idx)
true
}
else -> false
}
}
private fun showError(message : String) {
hideRefreshError()
tvLoading.visibility = View.VISIBLE
tvLoading.text = message
refreshLayout.isRefreshing = false
refreshLayout.visibility = View.GONE
}
private fun showColumnCloseButton() {
val dont_close = column?.dont_close ?: return
btnColumnClose.isEnabled = ! dont_close
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)
}
}
private val procShowColumnHeader : Runnable = Runnable {
val column = this.column
if(column == null || column.is_dispose.get()) return@Runnable
val acct = column.access_info.acct
val ac = AcctColor.load(acct)
val nickname = ac.nickname
tvColumnContext.text = if(nickname != null && nickname.isNotEmpty())
nickname
else
acct
var c : Int
c = ac.color_fg
tvColumnContext.setTextColor(
when {
c != 0 -> c
// column.header_fg_color != 0 -> column.header_fg_color
else -> getAttributeColor(activity, R.attr.colorTimeSmall)
}
)
c = ac.color_bg
if(c == 0) {
ViewCompat.setBackground(tvColumnContext, null)
} else {
tvColumnContext.setBackgroundColor(c)
}
tvColumnContext.setPaddingRelative(activity.acct_pad_lr, 0, activity.acct_pad_lr, 0)
tvColumnName.text = column.getColumnName(false)
showColumnCloseButton()
}
// カラムヘッダなど、負荷が低い部分の表示更新
fun showColumnHeader() {
activity.handler.removeCallbacks(procShowColumnHeader)
activity.handler.postDelayed(procShowColumnHeader, 50L)
}
internal fun showContent(
reason : String,
changeList : List<AdapterChange>? = null,
reset : Boolean = false
) {
// クラッシュレポートにadapterとリストデータの状態不整合が多かったので、
// とりあえずリストデータ変更の通知だけは最優先で行っておく
try {
status_adapter?.notifyChange(reason, changeList, reset)
} catch(ex : Throwable) {
log.trace(ex)
}
showColumnHeader()
showColumnStatus()
val column = this.column
if(column == null || column.is_dispose.get()) {
showError("column was disposed.")
return
}
if(! column.bFirstInitialized) {
showError("initializing")
return
}
if(column.bInitialLoading) {
var message : String? = column.task_progress
if(message == null) message = "loading?"
showError(message)
return
}
val initialLoadingError = column.mInitialLoadingError
if(initialLoadingError.isNotEmpty()) {
showError(initialLoadingError)
return
}
val status_adapter = this.status_adapter
if(status_adapter == null || status_adapter.itemCount == 0) {
showError(activity.getString(R.string.list_empty))
return
}
tvLoading.visibility = View.GONE
refreshLayout.visibility = View.VISIBLE
status_adapter.findHeaderViewHolder(listView)?.bindData(column)
if(column.bRefreshLoading) {
hideRefreshError()
} else {
refreshLayout.isRefreshing = false
showRefreshError()
}
proc_restoreScrollPosition.run()
}
private var bRefreshErrorWillShown = false
private fun hideRefreshError() {
if(! bRefreshErrorWillShown) return
bRefreshErrorWillShown = false
if(llRefreshError.visibility == View.GONE) return
val aa = AlphaAnimation(1f, 0f)
aa.duration = 666L
aa.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation : Animation?) {
}
override fun onAnimationStart(animation : Animation?) {
}
override fun onAnimationEnd(animation : Animation?) {
if(! bRefreshErrorWillShown) llRefreshError.visibility = View.GONE
}
})
llRefreshError.clearAnimation()
llRefreshError.startAnimation(aa)
}
private fun showRefreshError() {
val column = column
if(column == null) {
hideRefreshError()
return
}
val refreshError = column.mRefreshLoadingError
// val refreshErrorTime = column.mRefreshLoadingErrorTime
if(refreshError.isEmpty()) {
hideRefreshError()
return
}
tvRefreshError.text = refreshError
when(column.mRefreshLoadingErrorPopupState) {
// initially expanded
0 -> {
tvRefreshError.setSingleLine(false)
tvRefreshError.ellipsize = null
}
// tap to minimize
1 -> {
tvRefreshError.setSingleLine(true)
tvRefreshError.ellipsize = TextUtils.TruncateAt.END
}
}
if(! bRefreshErrorWillShown) {
bRefreshErrorWillShown = true
if(llRefreshError.visibility == View.GONE) {
llRefreshError.visibility = View.VISIBLE
val aa = AlphaAnimation(0f, 1f)
aa.duration = 666L
llRefreshError.clearAnimation()
llRefreshError.startAnimation(aa)
}
}
}
fun saveScrollPosition() : Boolean {
val column = this.column
when {
column == null -> log.d("saveScrollPosition [%d] , column==null", page_idx)
column.is_dispose.get() -> log.d(
"saveScrollPosition [%d] , column is disposed",
page_idx
)
listView.visibility != View.VISIBLE -> {
val scroll_save = ScrollPosition()
column.scroll_save = scroll_save
log.d(
"saveScrollPosition [%d] %s , listView is not visible, save %s,%s"
, page_idx
, column.getColumnName(true)
, scroll_save.adapterIndex
, scroll_save.offset
)
return true
}
else -> {
val scroll_save = ScrollPosition(this)
column.scroll_save = scroll_save
log.d(
"saveScrollPosition [%d] %s , listView is visible, save %s,%s"
, page_idx
, column.getColumnName(true)
, scroll_save.adapterIndex
, scroll_save.offset
)
return true
}
}
return false
}
fun setScrollPosition(sp : ScrollPosition, deltaDp : Float = 0f) {
val last_adapter = listView.adapter
if(column == null || last_adapter == null) return
sp.restore(this)
// 復元した後に意図的に少し上下にずらしたい
val dy = (deltaDp * activity.density + 0.5f).toInt()
if(dy != 0) listView.postDelayed(Runnable {
if(column == null || listView.adapter !== last_adapter) return@Runnable
try {
val recycler = fieldRecycler.get(listView) as RecyclerView.Recycler
val state = fieldState.get(listView) as RecyclerView.State
listLayoutManager.scrollVerticallyBy(dy, recycler, state)
} catch(ex : Throwable) {
log.trace(ex)
log.e("can't access field in class %s", RecyclerView::class.java.simpleName)
}
}, 20L)
}
inner class AdapterItemHeightWorkarea internal constructor(val adapter : ItemListAdapter) :
Closeable {
private val item_width : Int
private val widthSpec : Int
private var lastViewType : Int = - 1
private var lastViewHolder : RecyclerView.ViewHolder? = null
init {
this.item_width = listView.width - listView.paddingLeft - listView.paddingRight
this.widthSpec = View.MeasureSpec.makeMeasureSpec(item_width, View.MeasureSpec.EXACTLY)
}
override fun close() {
val childViewHolder = lastViewHolder
if(childViewHolder != null) {
adapter.onViewRecycled(childViewHolder)
lastViewHolder = null
}
}
// この関数はAdapterViewの項目の(marginを含む)高さを返す
fun getAdapterItemHeight(adapterIndex : Int) : Int {
fun View.getTotalHeight() : Int {
measure(widthSpec, heightSpec)
val lp = layoutParams as? ViewGroup.MarginLayoutParams
return measuredHeight + (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
}
listView.findViewHolderForAdapterPosition(adapterIndex)?.itemView?.let {
return it.getTotalHeight()
}
log.d("getAdapterItemHeight idx=$adapterIndex createView")
val viewType = adapter.getItemViewType(adapterIndex)
var childViewHolder = lastViewHolder
if(childViewHolder == null || lastViewType != viewType) {
if(childViewHolder != null) {
adapter.onViewRecycled(childViewHolder)
}
childViewHolder = adapter.onCreateViewHolder(listView, viewType)
lastViewHolder = childViewHolder
lastViewType = viewType
}
adapter.onBindViewHolder(childViewHolder, adapterIndex)
return childViewHolder.itemView.getTotalHeight()
}
}
// 特定の要素が特定の位置に来るようにスクロール位置を調整する
fun setListItemTop(listIndex : Int, yArg : Int) {
var adapterIndex = column?.toAdapterIndex(listIndex) ?: return
val adapter = status_adapter
if(adapter == null) {
log.e("setListItemTop: missing status adapter")
return
}
var y = yArg
AdapterItemHeightWorkarea(adapter).use { workarea ->
while(y > 0 && adapterIndex > 0) {
-- adapterIndex
y -= workarea.getAdapterItemHeight(adapterIndex)
y -= ListDivider.height
}
}
if(adapterIndex == 0 && y > 0) y = 0
listLayoutManager.scrollToPositionWithOffset(adapterIndex, y)
}
// この関数は scrollToPositionWithOffset 用のオフセットを返す
fun getListItemOffset(listIndex : Int) : Int {
val adapterIndex = column?.toAdapterIndex(listIndex)
?: return 0
val childView = listLayoutManager.findViewByPosition(adapterIndex)
?: throw IndexOutOfBoundsException("findViewByPosition($adapterIndex) returns null.")
// スクロールとともにtopは減少する
// しかしtopMarginがあるので最大値は4である
// この関数は scrollToPositionWithOffset 用のオフセットを返すので top - topMargin を返す
return childView.top - ((childView.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin
?: 0)
}
fun findFirstVisibleListItem() : Int {
val adapterIndex = listLayoutManager.findFirstVisibleItemPosition()
if(adapterIndex == RecyclerView.NO_POSITION)
throw IndexOutOfBoundsException()
return column?.toListIndex(adapterIndex)
?: throw IndexOutOfBoundsException()
}
fun scrollToTop() {
try {
listLayoutManager.scrollToPositionWithOffset(0, 0)
} catch(ignored : Throwable) {
}
}
fun scrollToTop2() {
val status_adapter = this.status_adapter
if(loading_busy || status_adapter == null) return
if(status_adapter.itemCount > 0) {
scrollToTop()
}
}
private fun clickQuickFilter(filter : Int) {
column?.quick_filter = filter
showQuickFilter()
activity.app_state.saveColumnList()
column?.startLoading()
}
private fun showQuickFilter() {
val column = this.column ?: return
val isNotificationColumn = column.column_type == Column.TYPE_NOTIFICATIONS
vg(svQuickFilter, isNotificationColumn)
if(! isNotificationColumn) return
vg(btnQuickFilterReaction, column.isMisskey)
vg(btnQuickFilterVote, column.isMisskey)
vg(btnQuickFilterFavourite, ! column.isMisskey)
val insideColumnSetting = Pref.bpMoveNotificationsQuickFilter(activity.pref)
val showQuickFilterButton : (btn : View, iconId : Int, selected : Boolean) -> Unit
if(insideColumnSetting) {
svQuickFilter.setBackgroundColor(0)
val colorFg = getAttributeColor(activity, R.attr.colorContentText)
val colorBgSelected = colorFg.applyAlphaMultiplier(0.25f)
showQuickFilterButton = { btn, iconId, selected ->
ViewCompat.setBackground(
btn,
if(selected) {
getAdaptiveRippleDrawable(
colorBgSelected,
colorFg
)
} else {
ContextCompat.getDrawable(activity, R.drawable.btn_bg_transparent)
}
)
when(btn) {
is ImageButton -> setIconDrawableId(activity, btn, iconId, colorFg)
is TextView -> btn.textColor = colorFg
}
}
} else {
val colorBg = column.getHeaderBackgroundColor()
val colorFg = column.getHeaderNameColor()
val colorBgSelected = Color.rgb(
(Color.red(colorBg) * 3 + Color.red(colorFg)) / 4,
(Color.green(colorBg) * 3 + Color.green(colorFg)) / 4,
(Color.blue(colorBg) * 3 + Color.blue(colorFg)) / 4
)
svQuickFilter.setBackgroundColor(colorBg)
showQuickFilterButton = { btn, iconId, selected ->
ViewCompat.setBackground(
btn,
getAdaptiveRippleDrawable(
if(selected) colorBgSelected else colorBg,
colorFg
)
)
when(btn) {
is ImageButton -> setIconDrawableId(activity, btn, iconId, colorFg)
is TextView -> btn.textColor = colorFg
}
}
}
showQuickFilterButton(
btnQuickFilterAll,
0,
column.quick_filter == Column.QUICK_FILTER_ALL
)
showQuickFilterButton(
btnQuickFilterMention,
R.drawable.ic_reply,
column.quick_filter == Column.QUICK_FILTER_MENTION
)
showQuickFilterButton(
btnQuickFilterFavourite,
R.drawable.ic_star,
column.quick_filter == Column.QUICK_FILTER_FAVOURITE
)
showQuickFilterButton(
btnQuickFilterBoost,
R.drawable.ic_repeat,
column.quick_filter == Column.QUICK_FILTER_BOOST
)
showQuickFilterButton(
btnQuickFilterFollow,
R.drawable.ic_follow_plus,
column.quick_filter == Column.QUICK_FILTER_FOLLOW
)
showQuickFilterButton(
btnQuickFilterReaction,
R.drawable.ic_add,
column.quick_filter == Column.QUICK_FILTER_REACTION
)
showQuickFilterButton(
btnQuickFilterVote,
R.drawable.ic_vote,
column.quick_filter == Column.QUICK_FILTER_VOTE
)
}
}