ColumnViewHolderクラスのメソッドを拡張関数として別ファイルに移動する

This commit is contained in:
tateisu 2021-05-19 16:51:34 +09:00
parent aa29a1e5ec
commit eaed84d8a4
7 changed files with 1910 additions and 1814 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,299 @@
package jp.juggler.subwaytooter
import android.view.View
import android.widget.CompoundButton
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.entity.TootAnnouncement
import jp.juggler.util.hideKeyboard
import jp.juggler.util.isCheckedNoAnime
import jp.juggler.util.showToast
import jp.juggler.util.withCaption
import java.util.regex.Pattern
fun ColumnViewHolder.onListListUpdated() {
etListName.setText("")
}
fun ColumnViewHolder.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)
}
}
}
fun ColumnViewHolder.isRegexValid(): Boolean {
val s = etRegexFilter.text.toString()
val error = checkRegexFilterError(s)
tvRegexFilterError.text = error ?: ""
return error == null
}
fun ColumnViewHolder.onCheckedChangedImpl(view: CompoundButton?, isChecked: Boolean) {
view ?: return
val column = this.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()
}
}
}
fun ColumnViewHolder.onClickImpl(v: View?) {
v?: return
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)
}
}
}
fun ColumnViewHolder.onLongClickImpl(v: View?): Boolean {
v?: return false
return when (v) {
btnColumnClose ->
activity.app_state.columnIndex(column)?.let {
activity.closeColumnAll(it)
true
} ?: false
else -> false
}
}

View File

@ -0,0 +1,451 @@
package jp.juggler.subwaytooter
import android.os.SystemClock
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import jp.juggler.emoji.UnicodeEmoji
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.TootReaction
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.dialog.EmojiPicker
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.util.*
import jp.juggler.util.*
import org.jetbrains.anko.allCaps
import org.jetbrains.anko.padding
import org.jetbrains.anko.textColor
fun ColumnViewHolder.hideAnnouncements() {
val column = column ?: return
if (column.announcementHideTime <= 0L)
column.announcementHideTime = System.currentTimeMillis()
activity.app_state.saveColumnList()
showAnnouncements()
}
fun ColumnViewHolder.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()
}
fun ColumnViewHolder.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@showAnnouncements
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) {
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) {
removeReaction(item, reaction.name)
} else {
addReaction(item, TootReaction.parseFedibird(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)
}
fun ColumnViewHolder.addReaction(item: TootAnnouncement, sample: TootReaction?) {
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")
}
ColumnViewHolder.log.d("addReaction: $code ${result.emoji.javaClass.simpleName}")
addReaction(item, TootReaction.parseFedibird(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()
}
}
})
}
fun ColumnViewHolder.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()
}
}
})
}

View File

@ -0,0 +1,280 @@
package jp.juggler.subwaytooter
import android.view.View
import android.widget.ImageView
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection
import jp.juggler.subwaytooter.streaming.canSpeech
import jp.juggler.subwaytooter.streaming.canStreaming
import jp.juggler.subwaytooter.util.endPadding
import jp.juggler.subwaytooter.util.startPadding
import jp.juggler.subwaytooter.view.ListDivider
import jp.juggler.util.*
import kotlinx.coroutines.*
import org.jetbrains.anko.backgroundColor
import org.jetbrains.anko.bottomPadding
import org.jetbrains.anko.topPadding
fun ColumnViewHolder.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) {
ColumnViewHolder.log.trace(ex)
}
}
fun ColumnViewHolder.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) {
ColumnViewHolder.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) {
ColumnViewHolder.log.trace(ex)
}
}
fun ColumnViewHolder.onPageDestroy(page_idx: Int) {
// タブレットモードの場合、onPageCreateより前に呼ばれる
val column = this.column
if (column != null) {
ColumnViewHolder.log.d("onPageDestroy [%d] %s", page_idx, tvColumnName.text)
saveScrollPosition()
listView.adapter = null
column.removeColumnViewHolder(this)
this.column = null
}
closeBitmaps()
activity.closeListItemPopup()
}
fun ColumnViewHolder.onPageCreate(column: Column, page_idx: Int, page_count: Int) {
binding_busy = true
try {
this.column = column
this.page_idx = page_idx
ColumnViewHolder.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
}
}

View File

@ -0,0 +1,280 @@
package jp.juggler.subwaytooter
import android.annotation.SuppressLint
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import jp.juggler.subwaytooter.util.ScrollPosition
import jp.juggler.subwaytooter.view.ListDivider
import jp.juggler.util.abs
import java.io.Closeable
private class ErrorFlickListener(
private val cvh: ColumnViewHolder,
) : View.OnTouchListener, GestureDetector.OnGestureListener {
private val gd = GestureDetector(cvh.activity, this)
val density = cvh.activity.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) {
// フリック方向が上下ではない
ColumnViewHolder.log.d("fling? not vertical view. $vx $vy")
} else {
val vyDp = vy / density
val limit = 1024f
ColumnViewHolder.log.d("fling? $vyDp/$limit")
if (vyDp >= limit) {
val column = cvh.column
if (column != null && column.lastTask == null) {
column.startLoading()
}
}
}
return true
}
}
private class AdapterItemHeightWorkarea(
val listView: RecyclerView,
val adapter: ItemListAdapter
) : Closeable {
private val item_width: Int
private val widthSpec: Int
var lastViewType: Int = -1
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, ColumnViewHolder.heightSpec)
val lp = layoutParams as? ViewGroup.MarginLayoutParams
return measuredHeight + (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
}
listView.findViewHolderForAdapterPosition(adapterIndex)?.itemView?.let {
return it.getTotalHeight()
}
ColumnViewHolder.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()
}
}
@SuppressLint("ClickableViewAccessibility")
fun ColumnViewHolder.initLoadingTextView() {
llLoading.setOnTouchListener(ErrorFlickListener(this))
}
// 特定の要素が特定の位置に来るようにスクロール位置を調整する
fun ColumnViewHolder.setListItemTop(listIndex: Int, yArg: Int) {
var adapterIndex = column?.toAdapterIndex(listIndex) ?: return
val adapter = status_adapter
if (adapter == null) {
ColumnViewHolder.log.e("setListItemTop: missing status adapter")
return
}
var y = yArg
AdapterItemHeightWorkarea(listView, 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 ColumnViewHolder.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 ColumnViewHolder.findFirstVisibleListItem(): Int {
val adapterIndex = listLayoutManager.findFirstVisibleItemPosition()
if (adapterIndex == RecyclerView.NO_POSITION)
throw IndexOutOfBoundsException()
return column?.toListIndex(adapterIndex)
?: throw IndexOutOfBoundsException()
}
fun ColumnViewHolder.scrollToTop() {
try {
listView.stopScroll()
} catch (ex: Throwable) {
ColumnViewHolder.log.e(ex, "stopScroll failed.")
}
try {
listLayoutManager.scrollToPositionWithOffset(0, 0)
} catch (ex: Throwable) {
ColumnViewHolder.log.e(ex, "scrollToPositionWithOffset failed.")
}
}
fun ColumnViewHolder.scrollToTop2() {
val status_adapter = this.status_adapter
if (binding_busy || status_adapter == null) return
if (status_adapter.itemCount > 0) {
scrollToTop()
}
}
fun ColumnViewHolder.saveScrollPosition(): Boolean {
val column = this.column
when {
column == null -> ColumnViewHolder.log.d("saveScrollPosition [%d] , column==null", page_idx)
column.is_dispose.get() -> ColumnViewHolder.log.d(
"saveScrollPosition [%d] , column is disposed",
page_idx
)
listView.visibility != View.VISIBLE -> {
val scroll_save = ScrollPosition()
column.scroll_save = scroll_save
ColumnViewHolder.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
ColumnViewHolder.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 ColumnViewHolder.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 = ColumnViewHolder.fieldRecycler.get(listView) as RecyclerView.Recycler
val state = ColumnViewHolder.fieldState.get(listView) as RecyclerView.State
listLayoutManager.scrollVerticallyBy(dy, recycler, state)
} catch (ex: Throwable) {
ColumnViewHolder.log.trace(ex)
ColumnViewHolder.log.e("can't access field in class %s", RecyclerView::class.java.simpleName)
}
}, 20L)
}
// 相対時刻を更新する
fun ColumnViewHolder.updateRelativeTime() = rebindAdapterItems()
fun ColumnViewHolder.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)
}
}

View File

@ -0,0 +1,136 @@
package jp.juggler.subwaytooter
import android.content.res.ColorStateList
import android.graphics.Color
import android.view.View
import android.widget.ImageButton
import android.widget.TextView
import jp.juggler.util.applyAlphaMultiplier
import jp.juggler.util.attrColor
import jp.juggler.util.getAdaptiveRippleDrawableRound
import jp.juggler.util.vg
import org.jetbrains.anko.backgroundDrawable
import org.jetbrains.anko.textColor
fun ColumnViewHolder.clickQuickFilter(filter: Int) {
column?.quick_filter = filter
showQuickFilter()
activity.app_state.saveColumnList()
column?.startLoading()
}
fun ColumnViewHolder.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
)
}

View File

@ -0,0 +1,215 @@
package jp.juggler.subwaytooter
import android.content.res.ColorStateList
import android.text.TextUtils
import android.view.View
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import jp.juggler.util.notZero
import jp.juggler.util.vg
import org.jetbrains.anko.textColor
// カラムヘッダなど、負荷が低い部分の表示更新
fun ColumnViewHolder.showColumnHeader() {
activity.handler.removeCallbacks(procShowColumnHeader)
activity.handler.postDelayed(procShowColumnHeader, 50L)
}
fun ColumnViewHolder.showColumnStatus() {
activity.handler.removeCallbacks(procShowColumnStatus)
activity.handler.postDelayed(procShowColumnStatus, 50L)
}
fun ColumnViewHolder.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)
}
fun ColumnViewHolder.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)
}
fun ColumnViewHolder.showColumnCloseButton() {
val dont_close = column?.dont_close ?: return
btnColumnClose.isEnabled = !dont_close
btnColumnClose.alpha = if (dont_close) 0.3f else 1f
}
internal fun ColumnViewHolder.showContent(
reason: String,
changeList: List<AdapterChange>? = null,
reset: Boolean = false
) {
// クラッシュレポートにadapterとリストデータの状態不整合が多かったので、
// とりあえずリストデータ変更の通知だけは最優先で行っておく
try {
status_adapter?.notifyChange(reason, changeList, reset)
} catch (ex: Throwable) {
ColumnViewHolder.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()
}
fun ColumnViewHolder.showColumnSetting(show: Boolean): Boolean {
llColumnSetting.vg(show)
llColumnHeader.invalidate()
return show
}
fun ColumnViewHolder.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 ColumnViewHolder.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)
}