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

2868 lines
109 KiB
Kotlin

package jp.juggler.subwaytooter
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.os.SystemClock
import android.text.InputType
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.view.*
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo
import android.widget.*
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayout
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection
import jp.juggler.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.action.Action_Account
import jp.juggler.subwaytooter.action.Action_List
import jp.juggler.subwaytooter.action.Action_Notification
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootTask
import jp.juggler.subwaytooter.api.TootTaskRunner
import jp.juggler.subwaytooter.api.entity.CustomEmoji
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.dialog.EmojiPicker
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.streaming.*
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.view.ListDivider
import jp.juggler.subwaytooter.view.MyLinkMovementMethod
import jp.juggler.subwaytooter.view.MyTextView
import jp.juggler.subwaytooter.view.OutsideDrawerLayout
import jp.juggler.util.*
import kotlinx.coroutines.*
import org.jetbrains.anko.*
import org.jetbrains.anko.custom.customView
import java.io.Closeable
import java.lang.Runnable
import java.lang.reflect.Field
import java.util.regex.Pattern
@SuppressLint("ClickableViewAccessibility")
class ColumnViewHolder(
val activity: ActMain,
parent: ViewGroup
) : 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 lateinit var llLoading: View
private lateinit var btnConfirmMail: Button
private lateinit var tvLoading: TextView
lateinit var listView: RecyclerView
lateinit var refreshLayout: SwipyRefreshLayout
lateinit var listLayoutManager: LinearLayoutManager
private lateinit var llColumnHeader: View
private lateinit var tvColumnIndex: TextView
private lateinit var tvColumnStatus: TextView
private lateinit var tvColumnContext: TextView
private lateinit var ivColumnIcon: ImageView
private lateinit var tvColumnName: TextView
private lateinit var llColumnSetting: View
private lateinit var llColumnSettingInside: LinearLayout
private lateinit var btnSearch: ImageButton
private lateinit var btnSearchClear: ImageButton
private lateinit var etSearch: EditText
private lateinit var cbResolve: CheckBox
private lateinit var etRegexFilter: EditText
private lateinit var tvRegexFilterError: TextView
private lateinit var btnAnnouncementsBadge: ImageView
private lateinit var btnAnnouncements: ImageButton
private lateinit var btnAnnouncementsCutout: Paint
private lateinit var btnColumnSetting: ImageButton
private lateinit var btnColumnReload: ImageButton
private lateinit var btnColumnClose: ImageButton
private lateinit var flColumnBackground: View
private lateinit var ivColumnBackgroundImage: ImageView
private lateinit var llSearch: View
private lateinit var cbDontCloseColumn: CheckBox
private lateinit var cbRemoteOnly: CheckBox
private lateinit var cbWithAttachment: CheckBox
private lateinit var cbWithHighlight: CheckBox
private lateinit var cbDontShowBoost: CheckBox
private lateinit var cbDontShowFollow: CheckBox
private lateinit var cbDontShowFavourite: CheckBox
private lateinit var cbDontShowReply: CheckBox
private lateinit var cbDontShowReaction: CheckBox
private lateinit var cbDontShowVote: CheckBox
private lateinit var cbDontShowNormalToot: CheckBox
private lateinit var cbDontShowNonPublicToot: CheckBox
private lateinit var cbInstanceLocal: CheckBox
private lateinit var cbDontStreaming: CheckBox
private lateinit var cbDontAutoRefresh: CheckBox
private lateinit var cbHideMediaDefault: CheckBox
private lateinit var cbSystemNotificationNotRelated: CheckBox
private lateinit var cbEnableSpeech: CheckBox
private lateinit var cbOldApi: CheckBox
private lateinit var llRegexFilter: View
private lateinit var btnDeleteNotification: Button
private lateinit var btnColor: Button
private lateinit var btnLanguageFilter: Button
private lateinit var svQuickFilter: HorizontalScrollView
private lateinit var btnQuickFilterAll: Button
private lateinit var btnQuickFilterMention: ImageButton
private lateinit var btnQuickFilterFavourite: ImageButton
private lateinit var btnQuickFilterBoost: ImageButton
private lateinit var btnQuickFilterFollow: ImageButton
private lateinit var btnQuickFilterPost: ImageButton
private lateinit var btnQuickFilterReaction: ImageButton
private lateinit var btnQuickFilterVote: ImageButton
private lateinit var llRefreshError: FrameLayout
private lateinit var ivRefreshError: ImageView
private lateinit var tvRefreshError: TextView
private lateinit var llListList: View
private lateinit var etListName: EditText
private lateinit var btnListAdd: View
private lateinit var llHashtagExtra: LinearLayout
private lateinit var etHashtagExtraAny: EditText
private lateinit var etHashtagExtraAll: EditText
private lateinit var etHashtagExtraNone: EditText
private lateinit var llAnnouncementsBox: View
private lateinit var tvAnnouncementsCaption: TextView
private lateinit var tvAnnouncementsIndex: TextView
private lateinit var btnAnnouncementsPrev: ImageButton
private lateinit var btnAnnouncementsNext: ImageButton
private lateinit var llAnnouncements: View
private lateinit var tvAnnouncementPeriod: TextView
private lateinit var tvAnnouncementContent: MyTextView
private lateinit var llAnnouncementExtra: LinearLayout
private val announcementContentInvalidator: NetworkEmojiInvalidator
var lastAnnouncementShown = 0L
private val extra_invalidator_list = ArrayList<NetworkEmojiInvalidator>()
private val isPageDestroyed: Boolean
get() = column == null || activity.isFinishing
private var binding_busy: Boolean = false
private var last_image_uri: String? = null
private var last_image_bitmap: Bitmap? = null
private var last_image_task: Job? = 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() {
llLoading.setOnTouchListener(ErrorFlickListener(activity))
}
val viewRoot: View = inflate(activity, parent)
init {
viewRoot.scan { v ->
try {
// ボタンではないTextViewのフォントを変更する
if (v is TextView && v !is Button) {
v.typeface = ActMain.timeline_font
}
} catch (ex: Throwable) {
log.trace(ex)
}
}
if (Pref.bpShareViewPool(activity.pref)) {
listView.setRecycledViewPool(activity.viewPool)
}
listView.itemAnimator = null
//
// val animator = listView.itemAnimator
// if( animator is DefaultItemAnimator){
// animator.supportsChangeAnimations = false
// }
btnListAdd.setOnClickListener(this)
etListName.setOnEditorActionListener { _, actionId, _ ->
var handled = false
if (actionId == EditorInfo.IME_ACTION_SEND) {
btnListAdd.performClick()
handled = true
}
handled
}
btnQuickFilterAll.setOnClickListener(this)
btnQuickFilterMention.setOnClickListener(this)
btnQuickFilterFavourite.setOnClickListener(this)
btnQuickFilterBoost.setOnClickListener(this)
btnQuickFilterFollow.setOnClickListener(this)
btnQuickFilterPost.setOnClickListener(this)
btnQuickFilterReaction.setOnClickListener(this)
btnQuickFilterVote.setOnClickListener(this)
llColumnHeader.setOnClickListener(this)
btnAnnouncements.setOnClickListener(this)
btnColumnSetting.setOnClickListener(this)
btnColumnReload.setOnClickListener(this)
btnColumnClose.setOnClickListener(this)
btnColumnClose.setOnLongClickListener(this)
btnDeleteNotification.setOnClickListener(this)
btnConfirmMail.setOnClickListener(this)
btnColor.setOnClickListener(this)
btnLanguageFilter.setOnClickListener(this)
refreshLayout.setOnRefreshListener(this)
refreshLayout.setDistanceToTriggerSync((0.5f + 20f * activity.density).toInt())
llRefreshError.setOnClickListener(this)
btnAnnouncementsPrev.setOnClickListener(this)
btnAnnouncementsNext.setOnClickListener(this)
cbDontCloseColumn.setOnCheckedChangeListener(this)
cbRemoteOnly.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)
cbDontShowNonPublicToot.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
btnAnnouncements.layoutParams.width = wh
btnAnnouncements.layoutParams.height = wh
btnAnnouncements.setPaddingRelative(pad, pad, pad, pad)
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)
btnSearch.setOnClickListener(this)
btnSearchClear.setOnClickListener(this)
etSearch.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ ->
if (!binding_busy) {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
btnSearch.performClick()
return@OnEditorActionListener true
}
}
false
})
// 入力の追跡
etRegexFilter.addTextChangedListener(CustomTextWatcher {
if (binding_busy || isPageDestroyed) return@CustomTextWatcher
if (!isRegexValid()) return@CustomTextWatcher
column?.regex_text = etRegexFilter.text.toString()
activity.app_state.saveColumnList()
activity.handler.removeCallbacks(proc_start_filter)
activity.handler.postDelayed(proc_start_filter, 666L)
})
etHashtagExtraAny.addTextChangedListener(CustomTextWatcher {
if (binding_busy || isPageDestroyed) return@CustomTextWatcher
column?.hashtag_any = etHashtagExtraAny.text.toString()
activity.app_state.saveColumnList()
activity.handler.removeCallbacks(proc_start_filter)
activity.handler.postDelayed(proc_start_filter, 666L)
})
etHashtagExtraAll.addTextChangedListener(CustomTextWatcher {
if (binding_busy || isPageDestroyed) return@CustomTextWatcher
column?.hashtag_all = etHashtagExtraAll.text.toString()
activity.app_state.saveColumnList()
activity.handler.removeCallbacks(proc_start_filter)
activity.handler.postDelayed(proc_start_filter, 666L)
})
etHashtagExtraNone.addTextChangedListener(CustomTextWatcher {
if (binding_busy || isPageDestroyed) return@CustomTextWatcher
column?.hashtag_none = etHashtagExtraNone.text.toString()
activity.app_state.saveColumnList()
activity.handler.removeCallbacks(proc_start_filter)
activity.handler.postDelayed(proc_start_filter, 666L)
})
announcementContentInvalidator =
NetworkEmojiInvalidator(activity.handler, tvAnnouncementContent)
tvAnnouncementContent.movementMethod = MyLinkMovementMethod
}
private val proc_start_filter: Runnable = Runnable {
if (binding_busy || isPageDestroyed) return@Runnable
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) {
binding_busy = true
try {
this.column = column
this.page_idx = page_idx
log.d("onPageCreate [%d] %s", page_idx, column.getColumnName(true))
val bSimpleList =
column.type != ColumnType.CONVERSATION && Pref.bpSimpleList(activity.pref)
tvColumnIndex.text = activity.getString(R.string.column_index, page_idx + 1, page_count)
tvColumnStatus.text = "?"
ivColumnIcon.setImageResource(column.getIconId())
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.isNotificationColumn
// 添付メディアや正規表現のフィルタ
val bAllowFilter = column.canStatusFilter()
showColumnSetting(false)
cbDontCloseColumn.isCheckedNoAnime = column.dont_close
cbRemoteOnly.isCheckedNoAnime = column.remote_only
cbWithAttachment.isCheckedNoAnime = column.with_attachment
cbWithHighlight.isCheckedNoAnime = column.with_highlight
cbDontShowBoost.isCheckedNoAnime = column.dont_show_boost
cbDontShowFollow.isCheckedNoAnime = column.dont_show_follow
cbDontShowFavourite.isCheckedNoAnime = column.dont_show_favourite
cbDontShowReply.isCheckedNoAnime = column.dont_show_reply
cbDontShowReaction.isCheckedNoAnime = column.dont_show_reaction
cbDontShowVote.isCheckedNoAnime = column.dont_show_vote
cbDontShowNormalToot.isCheckedNoAnime = column.dont_show_normal_toot
cbDontShowNonPublicToot.isCheckedNoAnime = column.dont_show_non_public_toot
cbInstanceLocal.isCheckedNoAnime = column.instance_local
cbDontStreaming.isCheckedNoAnime = column.dont_streaming
cbDontAutoRefresh.isCheckedNoAnime = column.dont_auto_refresh
cbHideMediaDefault.isCheckedNoAnime = column.hide_media_default
cbSystemNotificationNotRelated.isCheckedNoAnime = column.system_notification_not_related
cbEnableSpeech.isCheckedNoAnime = column.enable_speech
cbOldApi.isCheckedNoAnime = column.use_old_api
etRegexFilter.setText(column.regex_text)
etSearch.setText(column.search_query)
cbResolve.isCheckedNoAnime = column.search_resolve
cbRemoteOnly.vg(column.canRemoteOnly())
cbWithAttachment.vg(bAllowFilter)
cbWithHighlight.vg(bAllowFilter)
etRegexFilter.vg(bAllowFilter)
llRegexFilter.vg(bAllowFilter)
btnLanguageFilter.vg(bAllowFilter)
cbDontShowBoost.vg(column.canFilterBoost())
cbDontShowReply.vg(column.canFilterReply())
cbDontShowNormalToot.vg(column.canFilterNormalToot())
cbDontShowNonPublicToot.vg(column.canFilterNonPublicToot())
cbDontShowReaction.vg(isNotificationColumn && column.isMisskey)
cbDontShowVote.vg(isNotificationColumn)
cbDontShowFavourite.vg(isNotificationColumn && !column.isMisskey)
cbDontShowFollow.vg(isNotificationColumn)
cbInstanceLocal.vg(column.type == ColumnType.HASHTAG)
cbDontStreaming.vg(column.canStreaming())
cbDontAutoRefresh.vg(column.canAutoRefresh())
cbHideMediaDefault.vg(column.canNSFWDefault())
cbSystemNotificationNotRelated.vg(column.isNotificationColumn)
cbEnableSpeech.vg(column.canSpeech())
cbOldApi.vg(column.type == ColumnType.DIRECT_MESSAGES)
btnDeleteNotification.vg(column.isNotificationColumn)
llSearch.vg(column.isSearchColumn)?.let {
btnSearchClear.vg(Pref.bpShowSearchClear(activity.pref))
}
llListList.vg(column.type == ColumnType.LIST_LIST)
cbResolve.vg(column.type == ColumnType.SEARCH)
llHashtagExtra.vg(column.hasHashtagExtra())
etHashtagExtraAny.setText(column.hashtag_any)
etHashtagExtraAll.setText(column.hashtag_all)
etHashtagExtraNone.setText(column.hashtag_none)
// 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
//
listView.adapter = status_adapter
//XXX FastScrollerのサポートを諦める。ライブラリはいくつかあるんだけど、設定でON/OFFできなかったり頭文字バブルを無効にできなかったり
// listView.isFastScrollEnabled = ! Pref.bpDisableFastScroller(Pref.pref(activity))
column.addColumnViewHolder(this)
lastAnnouncementShown = -1L
fun dip(dp: Int): Int = (activity.density * dp + 0.5f).toInt()
val context = activity
val announcementsBgColor = Pref.ipAnnouncementsBgColor(App1.pref).notZero()
?: context.attrColor(R.attr.colorSearchFormBackground)
btnAnnouncementsCutout.apply {
color = announcementsBgColor
}
llAnnouncementsBox.apply {
background = createRoundDrawable(dip(6).toFloat(), announcementsBgColor)
val pad_tb = dip(2)
setPadding(0, pad_tb, 0, pad_tb)
}
val searchBgColor = Pref.ipSearchBgColor(App1.pref).notZero()
?: context.attrColor(R.attr.colorSearchFormBackground)
llSearch.apply {
backgroundColor = searchBgColor
startPadding = dip(12)
endPadding = dip(12)
topPadding = dip(3)
bottomPadding = dip(3)
}
llListList.apply {
backgroundColor = searchBgColor
startPadding = dip(12)
endPadding = dip(12)
topPadding = dip(3)
bottomPadding = dip(3)
}
showColumnColor()
showContent(reason = "onPageCreate", reset = true)
} finally {
binding_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(task.ctType.marker) // L,T,B,G
sb.append(
when {
task.isCancelled -> "~"
task.ctClosed.get() -> "!"
task.ctStarted.get() -> ""
else -> "?"
}
)
}
val streamStatus = column.getStreamingStatus()
log.d(
"procShowColumnStatus: streamStatus=${streamStatus}, column=${column.access_info.acct}/${
column.getColumnName(
true
)
}"
)
when (streamStatus) {
StreamStatus.Missing, StreamStatus.Closed -> {
}
StreamStatus.Connecting, StreamStatus.Open -> {
sb.appendColorShadeIcon(activity, R.drawable.ic_pulse, "Streaming")
sb.append("?")
}
StreamStatus.Subscribed -> {
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()
val csl = ColorStateList.valueOf(c)
tvColumnName.textColor = c
ivColumnIcon.imageTintList = csl
btnAnnouncements.imageTintList = csl
btnColumnSetting.imageTintList = csl
btnColumnReload.imageTintList = csl
btnColumnClose.imageTintList = csl
// カラムヘッダ文字色(B)
c = column.getHeaderPageNumberColor()
tvColumnIndex.textColor = c
tvColumnStatus.textColor = c
// カラム内部の背景色
flColumnBackground.setBackgroundColor(
column.column_bg_color.notZero()
?: Column.defaultColorContentBg
)
// カラム内部の背景画像
ivColumnBackgroundImage.alpha = column.column_bg_image_alpha
loadBackgroundImage(ivColumnBackgroundImage, column.column_bg_image)
// エラー表示
tvLoading.textColor = column.getContentColor()
status_adapter?.findHeaderViewHolder(listView)?.showColor()
// カラム色を変更したらクイックフィルタの色も変わる場合がある
showQuickFilter()
showAnnouncements(force = false)
}
private fun closeBitmaps() {
try {
ivColumnBackgroundImage.visibility = View.GONE
ivColumnBackgroundImage.setImageDrawable(null)
last_image_bitmap?.recycle()
last_image_bitmap = null
last_image_task?.cancel()
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() || Pref.bpDontShowColumnBackgroundImage(activity.pref)) {
// 指定がないなら閉じる
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
// 非同期処理を開始
last_image_task = GlobalScope.launch(Dispatchers.Main) {
val bitmap = try {
withContext(Dispatchers.IO) {
try {
createResizedBitmap(
activity, url.toUri(),
if (screen_w > screen_h)
screen_w
else
screen_h
)
} catch (ex: Throwable) {
log.trace(ex)
null
}
}
} catch (ex: Throwable) {
null
}
if (bitmap != null) {
if (!coroutineContext.isActive || url != last_image_uri) {
bitmap.recycle()
} else {
last_image_bitmap = bitmap
iv.setImageBitmap(last_image_bitmap)
iv.visibility = View.VISIBLE
}
}
}
} catch (ex: Throwable) {
log.trace(ex)
}
}
fun showColumnSetting(show: Boolean): Boolean {
llColumnSetting.vg(show)
llColumnHeader.invalidate()
return show
}
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 (binding_busy || column == null || status_adapter == null) return
// カラムを追加/削除したときに ColumnからColumnViewHolderへの参照が外れることがある
// リロードやリフレッシュ操作で直るようにする
column.addColumnViewHolder(this)
when (view) {
cbDontCloseColumn -> {
column.dont_close = isChecked
showColumnCloseButton()
activity.app_state.saveColumnList()
}
cbWithAttachment -> {
column.with_attachment = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbRemoteOnly -> {
column.remote_only = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbWithHighlight -> {
column.with_highlight = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbDontShowBoost -> {
column.dont_show_boost = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbDontShowReply -> {
column.dont_show_reply = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbDontShowReaction -> {
column.dont_show_reaction = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbDontShowVote -> {
column.dont_show_vote = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbDontShowNormalToot -> {
column.dont_show_normal_toot = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbDontShowNonPublicToot -> {
column.dont_show_non_public_toot = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbDontShowFavourite -> {
column.dont_show_favourite = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbDontShowFollow -> {
column.dont_show_follow = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbInstanceLocal -> {
column.instance_local = isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
cbDontStreaming -> {
column.dont_streaming = isChecked
activity.app_state.saveColumnList()
activity.app_state.streamManager.updateStreamingColumns()
}
cbDontAutoRefresh -> {
column.dont_auto_refresh = isChecked
activity.app_state.saveColumnList()
}
cbHideMediaDefault -> {
column.hide_media_default = isChecked
activity.app_state.saveColumnList()
column.fireShowContent(reason = "HideMediaDefault in ColumnSetting", reset = true)
}
cbSystemNotificationNotRelated -> {
column.system_notification_not_related = isChecked
activity.app_state.saveColumnList()
}
cbEnableSpeech -> {
column.enable_speech = isChecked
activity.app_state.saveColumnList()
}
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 (binding_busy || column == null || status_adapter == null) return
// カラムを追加/削除したときに ColumnからColumnViewHolderへの参照が外れることがある
// リロードやリフレッシュ操作で直るようにする
column.addColumnViewHolder(this)
when (v) {
btnColumnClose -> activity.closeColumn(column)
btnColumnReload -> {
App1.custom_emoji_cache.clearErrorCache()
if (column.isSearchColumn) {
etSearch.hideKeyboard()
etSearch.setText(column.search_query)
cbResolve.isCheckedNoAnime = column.search_resolve
}
refreshLayout.isRefreshing = false
column.startLoading()
}
btnSearch -> {
etSearch.hideKeyboard()
column.search_query = etSearch.text.toString().trim { it <= ' ' }
column.search_resolve = cbResolve.isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
btnSearchClear -> {
etSearch.setText("")
column.search_query = ""
column.search_resolve = cbResolve.isChecked
activity.app_state.saveColumnList()
column.startLoading()
}
llColumnHeader -> scrollToTop2()
btnColumnSetting -> {
if (showColumnSetting(!isColumnSettingShown)) {
hideAnnouncements()
}
}
btnDeleteNotification -> Action_Notification.deleteAll(
activity,
column.access_info,
false
)
btnColor ->
activity.app_state.columnIndex(column)?.let {
ActColumnCustomize.open(activity, it, ActMain.REQUEST_CODE_COLUMN_COLOR)
}
btnLanguageFilter ->
activity.app_state.columnIndex(column)?.let {
ActLanguageFilter.open(activity, it, ActMain.REQUEST_CODE_LANGUAGE_FILTER)
}
btnListAdd -> {
val tv = etListName.text.toString().trim { it <= ' ' }
if (tv.isEmpty()) {
activity.showToast(true, R.string.list_name_empty)
return
}
Action_List.create(activity, column.access_info, tv, null)
}
llRefreshError -> {
column.mRefreshLoadingErrorPopupState = 1 - column.mRefreshLoadingErrorPopupState
showRefreshError()
}
btnQuickFilterAll -> clickQuickFilter(Column.QUICK_FILTER_ALL)
btnQuickFilterMention -> clickQuickFilter(Column.QUICK_FILTER_MENTION)
btnQuickFilterFavourite -> clickQuickFilter(Column.QUICK_FILTER_FAVOURITE)
btnQuickFilterBoost -> clickQuickFilter(Column.QUICK_FILTER_BOOST)
btnQuickFilterFollow -> clickQuickFilter(Column.QUICK_FILTER_FOLLOW)
btnQuickFilterPost -> clickQuickFilter(Column.QUICK_FILTER_POST)
btnQuickFilterReaction -> clickQuickFilter(Column.QUICK_FILTER_REACTION)
btnQuickFilterVote -> clickQuickFilter(Column.QUICK_FILTER_VOTE)
btnAnnouncements -> toggleAnnouncements()
btnAnnouncementsPrev -> {
column.announcementId =
TootAnnouncement.move(column.announcements, column.announcementId, -1)
activity.app_state.saveColumnList()
showAnnouncements()
}
btnAnnouncementsNext -> {
column.announcementId =
TootAnnouncement.move(column.announcements, column.announcementId, +1)
activity.app_state.saveColumnList()
showAnnouncements()
}
btnConfirmMail -> {
Action_Account.resendConfirmMail(activity, column.access_info)
}
}
}
override fun onLongClick(v: View): Boolean {
return when (v) {
btnColumnClose ->
activity.app_state.columnIndex(column)?.let {
activity.closeColumnAll(it)
true
} ?: false
else -> false
}
}
private fun showError(message: String) {
hideRefreshError()
refreshLayout.isRefreshing = false
refreshLayout.visibility = View.GONE
llLoading.visibility = View.VISIBLE
tvLoading.text = message
btnConfirmMail.vg(column?.access_info?.isConfirmed == false)
}
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 ac = AcctColor.load(column.access_info)
tvColumnContext.text = ac.nickname
tvColumnContext.setTextColor(
ac.color_fg.notZero()
?: activity.attrColor(R.attr.colorTimeSmall)
)
tvColumnContext.setBackgroundColor(ac.color_bg)
tvColumnContext.setPaddingRelative(activity.acct_pad_lr, 0, activity.acct_pad_lr, 0)
tvColumnName.text = column.getColumnName(false)
showColumnCloseButton()
showAnnouncements(force = false)
}
// カラムヘッダなど、負荷が低い部分の表示更新
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
}
llLoading.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.isSingleLine = false
tvRefreshError.ellipsize = null
}
// tap to minimize
1 -> {
tvRefreshError.isSingleLine = 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)
}
internal 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 {
listView.stopScroll()
} catch (ex: Throwable) {
log.e(ex, "stopScroll failed.")
}
try {
listLayoutManager.scrollToPositionWithOffset(0, 0)
} catch (ex: Throwable) {
log.e(ex, "scrollToPositionWithOffset failed.")
}
}
fun scrollToTop2() {
val status_adapter = this.status_adapter
if (binding_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
svQuickFilter.vg(column.isNotificationColumn) ?: return
btnQuickFilterReaction.vg(column.isMisskey)
btnQuickFilterFavourite.vg(!column.isMisskey)
val insideColumnSetting = Pref.bpMoveNotificationsQuickFilter(activity.pref)
val showQuickFilterButton: (btn: View, iconId: Int, selected: Boolean) -> Unit
if (insideColumnSetting) {
svQuickFilter.setBackgroundColor(0)
val colorFg = activity.attrColor(R.attr.colorContentText)
val colorBgSelected = colorFg.applyAlphaMultiplier(0.25f)
val colorFgList = ColorStateList.valueOf(colorFg)
val colorBg = activity.attrColor(R.attr.colorColumnSettingBackground)
showQuickFilterButton = { btn, iconId, selected ->
btn.backgroundDrawable =
getAdaptiveRippleDrawableRound(
activity,
if (selected) colorBgSelected else colorBg,
colorFg,
roundNormal = true
)
when (btn) {
is TextView -> btn.textColor = colorFg
is ImageButton -> {
btn.setImageResource(iconId)
btn.imageTintList = colorFgList
}
}
}
} else {
val colorBg = column.getHeaderBackgroundColor()
val colorFg = column.getHeaderNameColor()
val colorFgList = ColorStateList.valueOf(colorFg)
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 ->
btn.backgroundDrawable = getAdaptiveRippleDrawableRound(
activity,
if (selected) colorBgSelected else colorBg,
colorFg
)
when (btn) {
is TextView -> btn.textColor = colorFg
is ImageButton -> {
btn.setImageResource(iconId)
btn.imageTintList = colorFgList
}
}
}
}
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(
btnQuickFilterPost,
R.drawable.ic_send,
column.quick_filter == Column.QUICK_FILTER_POST
)
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
)
}
private fun inflate(activity: ActMain, parent: ViewGroup) = with(activity.UI {}) {
val b = Benchmark(log, "Item-Inflate", 40L)
var label: TextView? = null
val rv = verticalLayout {
// トップレベルのViewGroupのlparamsはイニシャライザ内部に置くしかないみたい
val lp = parent.generateLayoutParamsEx()
if (lp != null) {
lp.width = matchParent
lp.height = matchParent
if (lp is ViewGroup.MarginLayoutParams) {
lp.setMargins(0, 0, 0, 0)
}
layoutParams = lp
}
llColumnHeader = customView<OutsideDrawerLayout> {
lparams(matchParent, wrapContent)
orientation = LinearLayout.VERTICAL
background = ContextCompat.getDrawable(context, R.drawable.bg_column_header)
startPadding = dip(12)
endPadding = dip(12)
topPadding = dip(3)
bottomPadding = dip(3)
linearLayout {
lparams(matchParent, wrapContent)
gravity = Gravity.BOTTOM
tvColumnContext = textView {
gravity = Gravity.END
startPadding = dip(4)
endPadding = dip(4)
textColor = context.attrColor(R.attr.colorColumnHeaderAcct)
textSize = 12f
}.lparams(0, wrapContent) {
weight = 1f
}
tvColumnStatus = textView {
gravity = Gravity.END
textColor = context.attrColor(R.attr.colorColumnHeaderPageNumber)
textSize = 12f
}.lparams(wrapContent, wrapContent) {
marginStart = dip(12)
}
tvColumnIndex = textView {
gravity = Gravity.END
textColor = context.attrColor(R.attr.colorColumnHeaderPageNumber)
textSize = 12f
}.lparams(wrapContent, wrapContent) {
marginStart = dip(4)
}
}
linearLayout {
lparams(matchParent, wrapContent) {
topMargin = dip(0)
}
gravity = Gravity.CENTER_VERTICAL
isBaselineAligned = false
ivColumnIcon = imageView {
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
scaleType = ImageView.ScaleType.FIT_CENTER
}.lparams(dip(32), dip(32)) {
endMargin = dip(4)
}
tvColumnName = textView {
// Kannada語の "ಸ್ಥಳೀಯ ಟೈಮ್ ಲೈನ್" の上下が途切れることがあるらしい
// GS10+では再現しなかった
}.lparams(dip(0), wrapContent) {
weight = 1f
}
frameLayout {
lparams(wrapContent, wrapContent) {
gravity = Gravity.CENTER_VERTICAL
startMargin = dip(2)
}
clipChildren = false
btnAnnouncements = imageButton {
background =
ContextCompat.getDrawable(
context,
R.drawable.btn_bg_transparent_round6dp
)
contentDescription = context.getString(R.string.announcements)
setImageResource(R.drawable.ic_info_outline)
padding = dip(8)
scaleType = ImageView.ScaleType.FIT_CENTER
btnAnnouncementsCutout = Paint().apply {
isAntiAlias = true
}
val path = Path()
addOutsideDrawer(this) { canvas, parent, view, left, top ->
if (llAnnouncementsBox.visibility == View.VISIBLE) {
val viewW = view.width
val viewH = view.height
val triTopX = (left + viewW / 2).toFloat()
val triTopY = top.toFloat() + viewH * 0.75f
val triBottomLeft = left.toFloat()
val triBottomRight = (left + viewW).toFloat()
val triBottom = parent.height.toFloat()
path.reset()
path.moveTo(triTopX, triTopY)
path.lineTo(triBottomRight, triBottom)
path.lineTo(triBottomLeft, triBottom)
path.lineTo(triTopX, triTopY)
canvas.drawPath(path, btnAnnouncementsCutout)
}
}
}.lparams(dip(40), dip(40))
btnAnnouncementsBadge = imageView {
setImageResource(R.drawable.announcements_dot)
scaleType = ImageView.ScaleType.FIT_CENTER
}.lparams(dip(7), dip(7)) {
gravity = Gravity.END or Gravity.TOP
endMargin = dip(4)
topMargin = dip(4)
}
}
frameLayout {
lparams(wrapContent, wrapContent) {
gravity = Gravity.CENTER_VERTICAL
startMargin = dip(2)
}
clipChildren = false
btnColumnSetting = imageButton {
background =
ContextCompat.getDrawable(
context,
R.drawable.btn_bg_transparent_round6dp
)
contentDescription = context.getString(R.string.setting)
setImageResource(R.drawable.ic_tune)
padding = dip(8)
scaleType = ImageView.ScaleType.FIT_CENTER
val paint = Paint().apply {
isAntiAlias = true
color =
context.attrColor(R.attr.colorColumnSettingBackground)
}
val path = Path()
addOutsideDrawer(this) { canvas, parent, view, left, top ->
if (llColumnSetting.visibility == View.VISIBLE) {
val viewW = view.width
val viewH = view.height
val triTopX = (left + viewW / 2).toFloat()
val triTopY = top.toFloat() + viewH * 0.75f
val triBottomLeft = left.toFloat()
val triBottomRight = (left + viewW).toFloat()
val triBottom = parent.height.toFloat()
path.reset()
path.moveTo(triTopX, triTopY)
path.lineTo(triBottomRight, triBottom)
path.lineTo(triBottomLeft, triBottom)
path.lineTo(triTopX, triTopY)
canvas.drawPath(path, paint)
}
}
}.lparams(dip(40), dip(40))
}
btnColumnReload = imageButton {
background =
ContextCompat.getDrawable(
context,
R.drawable.btn_bg_transparent_round6dp
)
contentDescription = context.getString(R.string.reload)
setImageResource(R.drawable.ic_refresh)
padding = dip(8)
scaleType = ImageView.ScaleType.FIT_CENTER
}.lparams(dip(40), dip(40)) {
gravity = Gravity.CENTER_VERTICAL
startMargin = dip(2)
}
btnColumnClose = imageButton {
background =
ContextCompat.getDrawable(
context,
R.drawable.btn_bg_transparent_round6dp
)
contentDescription = context.getString(R.string.close_column)
setImageResource(R.drawable.ic_close)
padding = dip(8)
scaleType = ImageView.ScaleType.FIT_CENTER
}.lparams(dip(40), dip(40)) {
gravity = Gravity.CENTER_VERTICAL
startMargin = dip(2)
}
}
} // end of column header
llColumnSetting = maxHeightScrollView {
lparams(matchParent, wrapContent)
isScrollbarFadingEnabled = false
maxHeight = dip(240)
backgroundColor =
context.attrColor(R.attr.colorColumnSettingBackground)
llColumnSettingInside = verticalLayout {
lparams(matchParent, wrapContent)
startPadding = dip(12)
endPadding = dip(12)
topPadding = dip(3)
bottomPadding = dip(3)
llHashtagExtra = verticalLayout {
lparams(matchParent, wrapContent)
label = textView {
textColor =
context.attrColor(R.attr.colorColumnHeaderPageNumber)
text = context.getString(R.string.hashtag_extra_any)
}.lparams(matchParent, wrapContent)
etHashtagExtraAny = editText {
id = View.generateViewId()
inputType = InputType.TYPE_CLASS_TEXT
maxLines = 1
setHorizontallyScrolling(true)
isHorizontalScrollBarEnabled = true
}.lparams(matchParent, wrapContent)
label?.labelFor = etHashtagExtraAny.id
label = textView {
textColor =
context.attrColor(R.attr.colorColumnHeaderPageNumber)
text = context.getString(R.string.hashtag_extra_all)
}.lparams(matchParent, wrapContent)
etHashtagExtraAll = editText {
id = View.generateViewId()
inputType = InputType.TYPE_CLASS_TEXT
maxLines = 1
setHorizontallyScrolling(true)
isHorizontalScrollBarEnabled = true
}.lparams(matchParent, wrapContent)
label?.labelFor = etHashtagExtraAll.id
label = textView {
textColor =
context.attrColor(R.attr.colorColumnHeaderPageNumber)
text = context.getString(R.string.hashtag_extra_none)
}.lparams(matchParent, wrapContent)
etHashtagExtraNone = editText {
id = View.generateViewId()
inputType = InputType.TYPE_CLASS_TEXT
maxLines = 1
setHorizontallyScrolling(true)
isHorizontalScrollBarEnabled = true
}.lparams(matchParent, wrapContent)
label?.labelFor = etHashtagExtraNone.id
} // end of hashtag extra
cbDontCloseColumn = checkBox {
text = context.getString(R.string.dont_close_column)
}.lparams(matchParent, wrapContent)
cbRemoteOnly = checkBox {
text = context.getString(R.string.remote_only)
}.lparams(matchParent, wrapContent)
cbWithAttachment = checkBox {
text = context.getString(R.string.with_attachment)
}.lparams(matchParent, wrapContent)
cbWithHighlight = checkBox {
text = context.getString(R.string.with_highlight)
}.lparams(matchParent, wrapContent)
cbDontShowBoost = checkBox {
text = context.getString(R.string.dont_show_boost)
}.lparams(matchParent, wrapContent)
cbDontShowFavourite = checkBox {
text = context.getString(R.string.dont_show_favourite)
}.lparams(matchParent, wrapContent)
cbDontShowFollow = checkBox {
text = context.getString(R.string.dont_show_follow)
}.lparams(matchParent, wrapContent)
cbDontShowReply = checkBox {
text = context.getString(R.string.dont_show_reply)
}.lparams(matchParent, wrapContent)
cbDontShowReaction = checkBox {
text = context.getString(R.string.dont_show_reaction)
}.lparams(matchParent, wrapContent)
cbDontShowVote = checkBox {
text = context.getString(R.string.dont_show_vote)
}.lparams(matchParent, wrapContent)
cbDontShowNormalToot = checkBox {
text = context.getString(R.string.dont_show_normal_toot)
}.lparams(matchParent, wrapContent)
cbDontShowNonPublicToot = checkBox {
text = context.getString(R.string.dont_show_non_public_toot)
}.lparams(matchParent, wrapContent)
cbInstanceLocal = checkBox {
text = context.getString(R.string.instance_local)
}.lparams(matchParent, wrapContent)
cbDontStreaming = checkBox {
text = context.getString(R.string.dont_use_streaming_api)
}.lparams(matchParent, wrapContent)
cbDontAutoRefresh = checkBox {
text = context.getString(R.string.dont_refresh_on_activity_resume)
}.lparams(matchParent, wrapContent)
cbHideMediaDefault = checkBox {
text = context.getString(R.string.hide_media_default)
}.lparams(matchParent, wrapContent)
cbSystemNotificationNotRelated = checkBox {
text = context.getString(R.string.system_notification_not_related)
}.lparams(matchParent, wrapContent)
cbEnableSpeech = checkBox {
text = context.getString(R.string.enable_speech)
}.lparams(matchParent, wrapContent)
cbOldApi = checkBox {
text = context.getString(R.string.use_old_api)
}.lparams(matchParent, wrapContent)
llRegexFilter = linearLayout {
lparams(matchParent, wrapContent)
label = textView {
textColor =
context.attrColor(R.attr.colorColumnHeaderPageNumber)
text = context.getString(R.string.regex_filter)
}.lparams(wrapContent, wrapContent)
tvRegexFilterError = textView {
textColor = context.attrColor(R.attr.colorRegexFilterError)
}.lparams(0, wrapContent) {
weight = 1f
startMargin = dip(4)
}
}
etRegexFilter = editText {
id = View.generateViewId()
inputType = InputType.TYPE_CLASS_TEXT
maxLines = 1
setHorizontallyScrolling(true)
isHorizontalScrollBarEnabled = true
}.lparams(matchParent, wrapContent)
label?.labelFor = etRegexFilter.id
btnDeleteNotification = button {
isAllCaps = false
text = context.getString(R.string.notification_delete)
}.lparams(matchParent, wrapContent)
btnColor = button {
isAllCaps = false
text = context.getString(R.string.color_and_background)
}.lparams(matchParent, wrapContent)
btnLanguageFilter = button {
isAllCaps = false
text = context.getString(R.string.language_filter)
}.lparams(matchParent, wrapContent)
}
} // end of column setting scroll view
llAnnouncementsBox = verticalLayout {
lparams(matchParent, wrapContent) {
startMargin = dip(6)
endMargin = dip(6)
bottomMargin = dip(2)
}
val buttonHeight = ActMain.boostButtonSize
val paddingH = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
val paddingV = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
linearLayout {
lparams(matchParent, wrapContent)
val pad_lr = dip(6)
setPadding(pad_lr, 0, pad_lr, 0)
background = ContextCompat.getDrawable(
context,
R.drawable.btn_bg_transparent_round6dp
)
gravity = Gravity.CENTER_VERTICAL or Gravity.END
tvAnnouncementsCaption = textView {
gravity = Gravity.END or Gravity.CENTER_VERTICAL
text = context.getString(R.string.announcements)
}.lparams(0, wrapContent) {
weight = 1f
}
btnAnnouncementsPrev = imageButton {
background = ContextCompat.getDrawable(
context,
R.drawable.btn_bg_transparent_round6dp
)
contentDescription = context.getString(R.string.previous)
imageResource = R.drawable.ic_arrow_start
setPadding(paddingH, paddingV, paddingH, paddingV)
scaleType = ImageView.ScaleType.FIT_CENTER
}.lparams(buttonHeight, buttonHeight) {
marginStart = dip(4)
}
tvAnnouncementsIndex = textView {
}.lparams(wrapContent, wrapContent) {
marginStart = dip(4)
}
btnAnnouncementsNext = imageButton {
background = ContextCompat.getDrawable(
context,
R.drawable.btn_bg_transparent_round6dp
)
contentDescription = context.getString(R.string.next)
imageResource = R.drawable.ic_arrow_end
setPadding(paddingH, paddingV, paddingH, paddingV)
scaleType = ImageView.ScaleType.FIT_CENTER
}.lparams(buttonHeight, buttonHeight) {
marginStart = dip(4)
}
}
llAnnouncements = maxHeightScrollView {
lparams(matchParent, wrapContent) {
topMargin = dip(1)
}
val pad_lr = dip(6)
val pad_tb = dip(2)
setPadding(pad_lr, pad_tb, pad_lr, pad_tb)
scrollBarStyle = View.SCROLLBARS_OUTSIDE_OVERLAY
isScrollbarFadingEnabled = false
maxHeight = dip(240)
verticalLayout {
lparams(matchParent, wrapContent)
// 期間があれば表示する
tvAnnouncementPeriod = textView {
gravity = Gravity.END
}.lparams(matchParent, wrapContent) {
bottomMargin = dip(3)
}
tvAnnouncementContent = myTextView {
setLineSpacing(lineSpacingExtra, 1.1f)
// tools:text="Contents\nContents"
}.lparams(matchParent, wrapContent) {
topMargin = dip(3)
}
llAnnouncementExtra = verticalLayout {
lparams(matchParent, wrapContent) {
topMargin = dip(3)
}
}
}
}
}
llSearch = verticalLayout {
lparams(matchParent, wrapContent)
linearLayout {
lparams(matchParent, wrapContent)
isBaselineAligned = false
gravity = Gravity.CENTER
etSearch = editText {
id = View.generateViewId()
imeOptions = EditorInfo.IME_ACTION_SEARCH
inputType = InputType.TYPE_CLASS_TEXT
maxLines = 1
}.lparams(0, wrapContent) {
weight = 1f
}
btnSearchClear = imageButton {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
contentDescription = context.getString(R.string.clear)
imageResource = R.drawable.ic_close
imageTintList = ColorStateList.valueOf(
context.attrColor(R.attr.colorVectorDrawable)
)
}.lparams(dip(40), dip(40)) {
startMargin = dip(4)
}
btnSearch = imageButton {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
contentDescription = context.getString(R.string.search)
imageResource = R.drawable.ic_search
imageTintList = ColorStateList.valueOf(
context.attrColor(R.attr.colorVectorDrawable)
)
}.lparams(dip(40), dip(40)) {
startMargin = dip(4)
}
}
cbResolve = checkBox {
text = context.getString(R.string.resolve_non_local_account)
}.lparams(wrapContent, wrapContent) // チェックボックスの余白はタッチ判定外
} // end of search bar
llListList = linearLayout {
lparams(matchParent, wrapContent)
isBaselineAligned = false
gravity = Gravity.CENTER
etListName = editText {
hint = context.getString(R.string.list_create_hint)
imeOptions = EditorInfo.IME_ACTION_SEND
inputType = InputType.TYPE_CLASS_TEXT
}.lparams(0, wrapContent) {
weight = 1f
}
btnListAdd = imageButton {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
contentDescription = context.getString(R.string.add)
imageResource = R.drawable.ic_add
imageTintList = ColorStateList.valueOf(
context.attrColor(
R.attr.colorVectorDrawable
)
)
}.lparams(dip(40), dip(40)) {
startMargin = dip(4)
}
} // end of list list header
svQuickFilter = horizontalScrollView {
lparams(matchParent, wrapContent)
isFillViewport = true
linearLayout {
lparams(matchParent, dip(40))
btnQuickFilterAll = button {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
minWidthCompat = dip(40)
startPadding = dip(4)
endPadding = dip(4)
isAllCaps = false
stateListAnimator = null
text = context.getString(R.string.all)
}.lparams(wrapContent, matchParent) {
margin = 0
}
btnQuickFilterMention = imageButton {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
contentDescription = context.getString(R.string.mention2)
}.lparams(dip(40), matchParent) {
margin = 0
}
btnQuickFilterFavourite = imageButton {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
contentDescription = context.getString(R.string.favourite)
}.lparams(dip(40), matchParent) {
margin = 0
}
btnQuickFilterBoost = imageButton {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
contentDescription = context.getString(R.string.boost)
}.lparams(dip(40), matchParent) {
margin = 0
}
btnQuickFilterFollow = imageButton {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
contentDescription = context.getString(R.string.follow)
}.lparams(dip(40), matchParent) {
margin = 0
}
btnQuickFilterPost = imageButton {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
contentDescription = context.getString(R.string.notification_type_post)
}.lparams(dip(40), matchParent) {
margin = 0
}
btnQuickFilterReaction = imageButton {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
contentDescription = context.getString(R.string.reaction)
}.lparams(dip(40), matchParent) {
margin = 0
}
btnQuickFilterVote = imageButton {
backgroundResource = R.drawable.btn_bg_transparent_round6dp
contentDescription = context.getString(R.string.vote_polls)
}.lparams(dip(40), matchParent) {
margin = 0
}
}
} // end of notification quick filter bar
flColumnBackground = frameLayout {
ivColumnBackgroundImage = imageView {
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
scaleType = ImageView.ScaleType.CENTER_CROP
visibility = View.GONE
}.lparams(matchParent, matchParent)
llLoading = verticalLayout {
lparams(matchParent, matchParent)
isBaselineAligned = false
gravity = Gravity.CENTER
tvLoading = textView {
gravity = Gravity.CENTER
}.lparams(matchParent, wrapContent)
btnConfirmMail = button {
text = activity.getString(R.string.resend_confirm_mail)
background = ContextCompat.getDrawable(
activity,
R.drawable.btn_bg_transparent_round6dp
)
}.lparams(matchParent, wrapContent) {
topMargin = dip(8)
}
}
refreshLayout = swipyRefreshLayout {
lparams(matchParent, matchParent)
direction = SwipyRefreshLayoutDirection.BOTH
// スタイルで指定しないとAndroid 6 で落ちる…
listView = recyclerView {
listLayoutManager = LinearLayoutManager(activity)
layoutManager = listLayoutManager
}.lparams(matchParent, matchParent) {
}
}
llRefreshError = frameLayout {
foregroundGravity = Gravity.BOTTOM
backgroundResource = R.drawable.bg_refresh_error
startPadding = dip(6)
endPadding = dip(6)
topPadding = dip(3)
bottomPadding = dip(3)
ivRefreshError = imageView {
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
scaleType = ImageView.ScaleType.FIT_CENTER
imageResource = R.drawable.ic_error
imageTintList = ColorStateList.valueOf(Color.RED)
}.lparams(dip(24), dip(24)) {
gravity = Gravity.START or Gravity.CENTER_VERTICAL
startMargin = dip(4)
}
tvRefreshError = textView {
textColor = Color.WHITE
}.lparams(matchParent, wrapContent) {
gravity = Gravity.TOP or Gravity.START
startMargin = dip(32)
}
}.lparams(matchParent, wrapContent) {
margin = dip(6)
}
}.lparams(matchParent, 0) {
weight = 1f
}
}
b.report()
rv
}
private fun hideAnnouncements() {
val column = column ?: return
if (column.announcementHideTime <= 0L)
column.announcementHideTime = System.currentTimeMillis()
activity.app_state.saveColumnList()
showAnnouncements()
}
private fun toggleAnnouncements() {
val column = column ?: return
if (llAnnouncementsBox.visibility == View.VISIBLE) {
if (column.announcementHideTime <= 0L)
column.announcementHideTime = System.currentTimeMillis()
} else {
showColumnSetting(false)
column.announcementHideTime = 0L
}
activity.app_state.saveColumnList()
showAnnouncements()
}
private fun showAnnouncements(force: Boolean = true) {
val column = column ?: return
if (!force && lastAnnouncementShown >= column.announcementUpdated) {
return
}
lastAnnouncementShown = SystemClock.elapsedRealtime()
fun clearExtras() {
for (invalidator in extra_invalidator_list) {
invalidator.register(null)
}
extra_invalidator_list.clear()
}
llAnnouncementExtra.removeAllViews()
clearExtras()
val listShown = TootAnnouncement.filterShown(column.announcements)
if (listShown?.isEmpty() != false) {
btnAnnouncements.vg(false)
llAnnouncementsBox.vg(false)
btnAnnouncementsBadge.vg(false)
llColumnHeader.invalidate()
return
}
btnAnnouncements.vg(true)
val expand = column.announcementHideTime <= 0L
llAnnouncementsBox.vg(expand)
llColumnHeader.invalidate()
btnAnnouncementsBadge.vg(false)
if (!expand) {
val newer = listShown.find { it.updated_at > column.announcementHideTime }
if (newer != null) {
column.announcementId = newer.id
btnAnnouncementsBadge.vg(true)
}
return
}
val content_color = column.getContentColor()
val item = listShown.find { it.id == column.announcementId }
?: listShown[0]
val itemIndex = listShown.indexOf(item)
val enablePaging = listShown.size > 1
val alphaPrevNext = if (enablePaging) 1f else 0.3f
setIconDrawableId(
activity,
btnAnnouncementsPrev,
R.drawable.ic_arrow_start,
color = content_color,
alphaMultiplier = alphaPrevNext
)
setIconDrawableId(
activity,
btnAnnouncementsNext,
R.drawable.ic_arrow_end,
color = content_color,
alphaMultiplier = alphaPrevNext
)
btnAnnouncementsPrev.vg(expand)?.run {
isEnabled = enablePaging
}
btnAnnouncementsNext.vg(expand)?.run {
isEnabled = enablePaging
}
tvAnnouncementsCaption.textColor = content_color
tvAnnouncementsIndex.textColor = content_color
tvAnnouncementPeriod.textColor = content_color
val f = activity.timeline_font_size_sp
if (!f.isNaN()) {
tvAnnouncementsCaption.textSize = f
tvAnnouncementsIndex.textSize = f
tvAnnouncementPeriod.textSize = f
tvAnnouncementContent.textSize = f
}
val spacing = activity.timeline_spacing
if (spacing != null) {
tvAnnouncementPeriod.setLineSpacing(0f, spacing)
tvAnnouncementContent.setLineSpacing(0f, spacing)
}
tvAnnouncementsCaption.typeface = ActMain.timeline_font_bold
val font_normal = ActMain.timeline_font
tvAnnouncementsIndex.typeface = font_normal
tvAnnouncementPeriod.typeface = font_normal
tvAnnouncementContent.typeface = font_normal
tvAnnouncementsIndex.vg(expand)?.text =
activity.getString(R.string.announcements_index, itemIndex + 1, listShown.size)
llAnnouncements.vg(expand)
var periods: StringBuilder? = null
fun String.appendPeriod() {
val sb = periods
if (sb == null) {
periods = StringBuilder(this)
} else {
sb.append("\n")
sb.append(this)
}
}
val (strStart, strEnd) = TootStatus.formatTimeRange(
item.starts_at,
item.ends_at,
item.all_day
)
when {
// no periods.
strStart == "" && strEnd == "" -> {
}
// single date
strStart == strEnd -> {
activity.getString(R.string.announcements_period1, strStart)
.appendPeriod()
}
else -> {
activity.getString(R.string.announcements_period2, strStart, strEnd)
.appendPeriod()
}
}
if (item.updated_at > item.published_at) {
val strUpdateAt = TootStatus.formatTime(activity, item.updated_at, false)
activity.getString(R.string.edited_at, strUpdateAt).appendPeriod()
}
val sb = periods
tvAnnouncementPeriod.vg(sb != null)?.text = sb
tvAnnouncementContent.textColor = content_color
tvAnnouncementContent.text = item.decoded_content
tvAnnouncementContent.tag = this@ColumnViewHolder
announcementContentInvalidator.register(item.decoded_content)
// リアクションの表示
val density = activity.density
val buttonHeight = ActMain.boostButtonSize
val marginBetween = (buttonHeight.toFloat() * 0.2f + 0.5f).toInt()
val marginBottom = (buttonHeight.toFloat() * 0.2f + 0.5f).toInt()
val paddingH = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
val paddingV = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
val box = FlexboxLayout(activity).apply {
flexWrap = FlexWrap.WRAP
justifyContent = JustifyContent.FLEX_START
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
topMargin = (0.5f + density * 3f).toInt()
}
}
// +ボタン
run {
val b = ImageButton(activity)
val blp = FlexboxLayout.LayoutParams(
buttonHeight,
buttonHeight
).apply {
bottomMargin = marginBottom
endMargin = marginBetween
}
b.layoutParams = blp
b.background = ContextCompat.getDrawable(
activity,
R.drawable.btn_bg_transparent_round6dp
)
b.contentDescription = activity.getString(R.string.reaction_add)
b.scaleType = ImageView.ScaleType.FIT_CENTER
b.padding = paddingV
b.setOnClickListener {
addReaction(item, null)
}
setIconDrawableId(
activity,
b,
R.drawable.ic_add,
color = content_color,
alphaMultiplier = 1f
)
box.addView(b)
}
val reactions = item.reactions?.filter { it.count > 0L }?.notEmpty()
if (reactions != null) {
var lastButton: View? = null
val options = DecodeOptions(
activity,
column.access_info,
decodeEmoji = true,
enlargeEmoji = 1.5f,
mentionDefaultHostDomain = column.access_info
)
val actMain = activity
val disableEmojiAnimation = Pref.bpDisableEmojiAnimation(actMain.pref)
for (reaction in reactions) {
val url = if (disableEmojiAnimation) {
reaction.static_url.notEmpty() ?: reaction.url.notEmpty()
} else {
reaction.url.notEmpty() ?: reaction.static_url.notEmpty()
}
val b = Button(activity).also { btn ->
btn.layoutParams = FlexboxLayout.LayoutParams(
FlexboxLayout.LayoutParams.WRAP_CONTENT,
buttonHeight
).apply {
endMargin = marginBetween
bottomMargin = marginBottom
}
btn.minWidthCompat = buttonHeight
btn.allCaps = false
btn.tag = reaction
btn.background = if (reaction.me == true) {
getAdaptiveRippleDrawableRound(
actMain,
actMain.attrColor(R.attr.colorButtonBgCw),
actMain.attrColor(R.attr.colorRippleEffect)
)
} else {
ContextCompat.getDrawable(actMain, R.drawable.btn_bg_transparent_round6dp)
}
btn.setTextColor(content_color)
btn.setPadding(paddingH, paddingV, paddingH, paddingV)
btn.text = if (url == null) {
EmojiDecoder.decodeEmoji(options, "${reaction.name} ${reaction.count}")
} else {
SpannableStringBuilder("${reaction.name} ${reaction.count}").also { sb ->
sb.setSpan(
NetworkEmojiSpan(url, scale = 1.5f),
0,
reaction.name.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
val invalidator =
NetworkEmojiInvalidator(actMain.handler, btn)
invalidator.register(sb)
extra_invalidator_list.add(invalidator)
}
}
btn.setOnClickListener {
if (reaction.me == true) {
removeReaction(item, reaction.name)
} else {
addReaction(item, TootAnnouncement.Reaction(jsonObject {
put("name", reaction.name)
put("count", 1)
put("me", true)
putNotNull("url", reaction.url)
putNotNull("static_url", reaction.static_url)
}))
}
}
}
box.addView(b)
lastButton = b
}
lastButton
?.layoutParams
?.cast<ViewGroup.MarginLayoutParams>()
?.endMargin = 0
}
llAnnouncementExtra.addView(box)
}
private fun addReaction(item: TootAnnouncement, sample: TootAnnouncement.Reaction?) {
val column = column ?: return
if (sample == null) {
EmojiPicker(activity, column.access_info, closeOnSelected = true) { result ->
val emoji = result.emoji
val code = when (emoji) {
is UnicodeEmoji -> emoji.unifiedCode
is CustomEmoji -> emoji.shortcode
else -> error("unknown emoji type")
}
log.d("addReaction: $code ${result.emoji.javaClass.simpleName}")
addReaction(item, TootAnnouncement.Reaction(jsonObject {
put("name", code)
put("count", 1)
put("me", true)
// 以下はカスタム絵文字のみ
if (emoji is CustomEmoji) {
putNotNull("url", emoji.url)
putNotNull("static_url", emoji.static_url)
}
}))
}.show()
return
}
TootTaskRunner(activity).run(column.access_info, object : TootTask {
override suspend fun background(client: TootApiClient): TootApiResult? {
return client.request(
"/api/v1/announcements/${item.id}/reactions/${sample.name.encodePercent()}",
JsonObject().toPutRequestBuilder()
)
// 200 {}
}
override suspend fun handleResult(result: TootApiResult?) {
result ?: return
if (result.jsonObject == null) {
activity.showToast(true, result.error)
} else {
sample.count = 0
val list = item.reactions
if (list == null) {
item.reactions = mutableListOf(sample)
} else {
val reaction = list.find { it.name == sample.name }
if (reaction == null) {
list.add(sample)
} else {
reaction.me = true
++reaction.count
}
}
column.announcementUpdated = SystemClock.elapsedRealtime()
showAnnouncements()
}
}
})
}
private fun removeReaction(item: TootAnnouncement, name: String) {
val column = column ?: return
TootTaskRunner(activity).run(column.access_info, object : TootTask {
override suspend fun background(client: TootApiClient): TootApiResult? {
return client.request(
"/api/v1/announcements/${item.id}/reactions/${name.encodePercent()}",
JsonObject().toDeleteRequestBuilder()
)
// 200 {}
}
override suspend fun handleResult(result: TootApiResult?) {
result ?: return
if (result.jsonObject == null) {
activity.showToast(true, result.error)
} else {
val it = item.reactions?.iterator() ?: return
while (it.hasNext()) {
val reaction = it.next()
if (reaction.name == name) {
reaction.me = false
if (--reaction.count <= 0) it.remove()
break
}
}
column.announcementUpdated = SystemClock.elapsedRealtime()
showAnnouncements()
}
}
})
}
}