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

499 lines
17 KiB
Kotlin

package jp.juggler.subwaytooter
import android.app.Activity
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.ImageButton
import android.widget.ImageView
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.core.content.ContextCompat
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.emoji.EmojiMap
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.pref.lazyContext
import jp.juggler.subwaytooter.span.EmojiImageSpan
import jp.juggler.subwaytooter.span.createSpan
import jp.juggler.subwaytooter.table.UserRelation
import jp.juggler.util.*
import jp.juggler.util.data.notZero
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.*
import org.xmlpull.v1.XmlPullParser
import kotlin.math.max
import kotlin.math.min
private val log = LogCategory("Styler")
fun defaultColorIcon(context: Context, iconId: Int): Drawable? =
ContextCompat.getDrawable(context, iconId)?.also {
it.setTint(context.attrColor(R.attr.colorTextContent))
it.setTintMode(PorterDuff.Mode.SRC_IN)
}
fun TootVisibility.getVisibilityIconId(isMisskeyData: Boolean): Int {
val isMisskey = when (PrefI.ipVisibilityStyle.value) {
PrefI.VS_MASTODON -> false
PrefI.VS_MISSKEY -> true
else -> isMisskeyData
}
return when {
isMisskey -> when (this) {
TootVisibility.Public -> R.drawable.ic_public
TootVisibility.UnlistedHome -> R.drawable.ic_home
TootVisibility.PrivateFollowers -> R.drawable.ic_lock_open
TootVisibility.DirectSpecified -> R.drawable.ic_mail
TootVisibility.DirectPrivate -> R.drawable.ic_lock
TootVisibility.WebSetting -> R.drawable.ic_question
TootVisibility.AccountSetting -> R.drawable.ic_question
TootVisibility.LocalPublic -> R.drawable.ic_local_ltl
TootVisibility.LocalHome -> R.drawable.ic_local_home
TootVisibility.LocalFollowers -> R.drawable.ic_local_lock_open
TootVisibility.Unknown -> R.drawable.ic_question
TootVisibility.Limited -> R.drawable.ic_account_circle
TootVisibility.Mutual -> R.drawable.ic_bidirectional
}
else -> when (this) {
TootVisibility.Public -> R.drawable.ic_public
TootVisibility.UnlistedHome -> R.drawable.ic_lock_open
TootVisibility.PrivateFollowers -> R.drawable.ic_lock
TootVisibility.DirectSpecified -> R.drawable.ic_mail
TootVisibility.DirectPrivate -> R.drawable.ic_mail
TootVisibility.WebSetting -> R.drawable.ic_question
TootVisibility.AccountSetting -> R.drawable.ic_question
TootVisibility.LocalPublic -> R.drawable.ic_local_ltl
TootVisibility.LocalHome -> R.drawable.ic_local_lock_open
TootVisibility.LocalFollowers -> R.drawable.ic_local_lock
TootVisibility.Unknown -> R.drawable.ic_question
TootVisibility.Limited -> R.drawable.ic_account_circle
TootVisibility.Mutual -> R.drawable.ic_bidirectional
}
}
}
fun TootVisibility.getVisibilityString(isMisskeyData: Boolean): String {
val isMisskey = when (PrefI.ipVisibilityStyle.value) {
PrefI.VS_MASTODON -> false
PrefI.VS_MISSKEY -> true
else -> isMisskeyData
}
return lazyContext.getString(
when {
isMisskey -> when (this) {
TootVisibility.Public -> R.string.visibility_public
TootVisibility.UnlistedHome -> R.string.visibility_home
TootVisibility.PrivateFollowers -> R.string.visibility_followers
TootVisibility.DirectSpecified -> R.string.visibility_direct
TootVisibility.DirectPrivate -> R.string.visibility_private
TootVisibility.WebSetting -> R.string.visibility_web_setting
TootVisibility.AccountSetting -> R.string.visibility_account_setting
TootVisibility.LocalPublic -> R.string.visibility_local_public
TootVisibility.LocalHome -> R.string.visibility_local_home
TootVisibility.LocalFollowers -> R.string.visibility_local_followers
TootVisibility.Unknown -> R.string.visibility_unknown
TootVisibility.Limited -> R.string.visibility_limited
TootVisibility.Mutual -> R.string.visibility_mutual
}
else -> when (this) {
TootVisibility.Public -> R.string.visibility_public
TootVisibility.UnlistedHome -> R.string.visibility_unlisted
TootVisibility.PrivateFollowers -> R.string.visibility_followers
TootVisibility.DirectSpecified -> R.string.visibility_direct
TootVisibility.DirectPrivate -> R.string.visibility_direct
TootVisibility.WebSetting -> R.string.visibility_web_setting
TootVisibility.AccountSetting -> R.string.visibility_account_setting
TootVisibility.LocalPublic -> R.string.visibility_local_public
TootVisibility.LocalHome -> R.string.visibility_local_unlisted
TootVisibility.LocalFollowers -> R.string.visibility_local_followers
TootVisibility.Unknown -> R.string.visibility_unknown
TootVisibility.Limited -> R.string.visibility_limited
TootVisibility.Mutual -> R.string.visibility_mutual
}
}
)
}
// アイコン付きの装飾テキストを返す
fun getVisibilityCaption(
context: Context,
isMisskeyData: Boolean,
visibility: TootVisibility,
): CharSequence {
val iconId = visibility.getVisibilityIconId(isMisskeyData)
val sv = visibility.getVisibilityString(isMisskeyData)
val color = context.attrColor(R.attr.colorTextContent)
val sb = SpannableStringBuilder()
// アイコン部分
val start = sb.length
sb.append(" ")
val end = sb.length
sb.setSpan(
EmojiImageSpan(
context,
iconId,
useColorShader = true,
color = color
),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
// 文字列部分
sb.append(' ')
sb.append(sv)
return sb
}
fun setFollowIcon(
context: Context,
ibFollow: ImageButton,
ivDot: ImageView,
relation: UserRelation,
who: TootAccount,
defaultColor: Int,
alphaMultiplier: Float,
) {
val colorFollowed =
PrefI.ipButtonFollowingColor.value.notZero()
?: context.attrColor(R.attr.colorButtonAccentFollow)
val colorFollowRequest =
PrefI.ipButtonFollowRequestColor.value.notZero()
?: context.attrColor(R.attr.colorButtonAccentFollowRequest)
val colorError = context.attrColor(R.attr.colorRegexFilterError)
// 被フォロー状態
when {
relation.blocked_by -> {
ivDot.visibility = View.VISIBLE
setIconDrawableId(
context,
ivDot,
R.drawable.ic_blocked_by,
color = colorError,
alphaMultiplier = alphaMultiplier
)
}
relation.requested_by -> {
ivDot.visibility = View.VISIBLE
setIconDrawableId(
context,
ivDot,
R.drawable.ic_requested_by,
color = colorFollowRequest,
alphaMultiplier = alphaMultiplier
)
}
relation.followed_by -> {
ivDot.visibility = View.VISIBLE
setIconDrawableId(
context,
ivDot,
R.drawable.ic_follow_dot,
color = colorFollowed,
alphaMultiplier = alphaMultiplier
)
// 被フォローリクエスト状態の時に followed_by が 真と偽の両方がありえるようなので
// Relationshipだけを見ても被フォローリクエスト状態は分からないっぽい
// 仕方ないので馬鹿正直に「 followed_byが真ならバッジをつける」しかできない
}
else -> {
ivDot.visibility = View.GONE
}
}
// フォローボタン
// follow button
val color: Int
val iconId: Int
val contentDescription: String
when {
relation.blocking -> {
iconId = R.drawable.ic_block
color = defaultColor
contentDescription = context.getString(R.string.follow)
}
relation.muting -> {
iconId = R.drawable.ic_volume_off
color = defaultColor
contentDescription = context.getString(R.string.follow)
}
relation.getFollowing(who) -> {
iconId = R.drawable.ic_follow_cross
color = colorFollowed
contentDescription = context.getString(R.string.unfollow)
}
relation.getRequested(who) -> {
iconId = R.drawable.ic_follow_wait
color = colorFollowRequest
contentDescription = context.getString(R.string.unfollow)
}
else -> {
iconId = R.drawable.ic_follow_plus
color = defaultColor
contentDescription = context.getString(R.string.follow)
}
}
setIconDrawableId(
context,
ibFollow,
iconId,
color = color,
alphaMultiplier = alphaMultiplier
)
ibFollow.contentDescription = contentDescription
}
private fun getHorizontalPadding(v: View, dpDelta: Float): Int {
// Essential Phone PH-1は 短辺439dp
val formWidthMax = 460f
val dm = v.resources.displayMetrics
val screenW = dm.widthPixels
val contentW = (0.5f + formWidthMax * dm.density).toInt()
val padW = max(0, (screenW - contentW) / 2)
return padW + (0.5f + dpDelta * dm.density).toInt()
}
private fun getOrientationString(orientation: Int?) = when (orientation) {
null -> "null"
Configuration.ORIENTATION_LANDSCAPE -> "landscape"
Configuration.ORIENTATION_PORTRAIT -> "portrait"
Configuration.ORIENTATION_UNDEFINED -> "undefined"
else -> orientation.toString()
}
fun fixHorizontalPadding(v: View, dpDelta: Float = 12f) {
val padT = v.paddingTop
val padB = v.paddingBottom
val dm = v.resources.displayMetrics
val widthDp = dm.widthPixels / dm.density
if (widthDp >= 640f && v.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
val padLr = (0.5f + dpDelta * dm.density).toInt()
when (PrefI.ipJustifyWindowContentPortrait.value) {
PrefI.JWCP_START -> {
v.setPaddingRelative(padLr, padT, padLr + dm.widthPixels / 2, padB)
return
}
PrefI.JWCP_END -> {
v.setPaddingRelative(padLr + dm.widthPixels / 2, padT, padLr, padB)
return
}
else -> Unit
}
}
val padLr = getHorizontalPadding(v, dpDelta)
v.setPaddingRelative(padLr, padT, padLr, padB)
}
fun fixHorizontalMargin(v: View) {
val lp = v.layoutParams
if (lp is ViewGroup.MarginLayoutParams) {
val dm = v.resources.displayMetrics
val orientationString = getOrientationString(v.resources?.configuration?.orientation)
val widthDp = dm.widthPixels / dm.density
log.d("fixHorizontalMargin: orientation=$orientationString, w=${widthDp}dp, h=${dm.heightPixels / dm.density}")
if (widthDp >= 640f && v.resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) {
when (PrefI.ipJustifyWindowContentPortrait.value) {
PrefI.JWCP_START -> {
lp.marginStart = 0
lp.marginEnd = dm.widthPixels / 2
return
}
PrefI.JWCP_END -> {
lp.marginStart = dm.widthPixels / 2
lp.marginEnd = 0
return
}
}
}
val padLr = getHorizontalPadding(v, 0f)
lp.leftMargin = padLr
lp.rightMargin = padLr
}
}
// ActMainの初期化時に更新される
var stylerRoundRatio: Float = 0.33f * 0.5f
var stylerBoostAlpha: Float = 1f
fun calcIconRound(wh: Int) = wh.toFloat() * stylerRoundRatio
fun calcIconRound(lp: ViewGroup.LayoutParams) =
min(lp.width, lp.height).toFloat() * stylerRoundRatio
fun SpannableStringBuilder.appendColorShadeIcon(
context: Context,
@DrawableRes drawableId: Int,
text: String,
color: Int? = null,
): SpannableStringBuilder {
val start = this.length
this.append(text)
val end = this.length
this.setSpan(
EmojiImageSpan(context, drawableId, useColorShader = true, color = color),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
return this
}
fun SpannableStringBuilder.appendMisskeyReaction(
context: Context,
emojiUtf16: String,
text: String,
): SpannableStringBuilder {
val emoji = EmojiMap.unicodeMap[emojiUtf16]
when {
emoji == null ->
append("text")
PrefB.bpUseTwemoji.value -> {
val start = this.length
append(text)
val end = this.length
this.setSpan(
emoji.createSpan(context),
start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
else ->
this.append(emoji.unifiedCode)
}
return this
}
fun Context.setSwitchColor(root: View?) {
root ?: return
val colorBg = attrColor(R.attr.colorWindowBackground)
val colorOff = attrColor(R.attr.colorSwitchOff)
val colorOn = PrefI.ipSwitchOnColor.value
val colorDisabled = mixColor(colorBg, colorOff)
val colorTrackDisabled = mixColor(colorBg, colorDisabled)
val colorTrackOn = mixColor(colorBg, colorOn)
val colorTrackOff = mixColor(colorBg, colorOff)
// https://stackoverflow.com/a/25635526/9134243
val thumbStates = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_enabled),
intArrayOf(android.R.attr.state_checked),
intArrayOf()
),
intArrayOf(
colorDisabled,
colorOn,
colorOff
)
)
val trackStates = ColorStateList(
arrayOf(
intArrayOf(-android.R.attr.state_enabled),
intArrayOf(android.R.attr.state_checked),
intArrayOf()
),
intArrayOf(
colorTrackDisabled,
colorTrackOn,
colorTrackOff
)
)
root.scan {
(it as? SwitchCompat)?.apply {
thumbTintList = thumbStates
trackTintList = trackStates
}
}
}
fun ViewGroup.generateLayoutParamsEx(): ViewGroup.LayoutParams? =
try {
val parser = resources.getLayout(R.layout.generate_params)
// Skip everything until the view tag.
while (true) {
val token = parser.nextToken()
if (token == XmlPullParser.START_TAG) break
}
generateLayoutParams(parser)
} catch (ex: Throwable) {
log.e(ex, "generateLayoutParamsEx failed")
null
}
fun Activity.setStatusBarColor(forceDark: Boolean = false) {
window?.apply {
if (Build.VERSION.SDK_INT < 30) {
@Suppress("DEPRECATION")
clearFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
)
}
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
var c = when {
forceDark -> Color.BLACK
else -> PrefI.ipStatusBarColor.value.notZero()
?: attrColor(android.R.attr.colorPrimaryDark)
}
setStatusBarColorCompat(c)
c = when {
forceDark -> Color.BLACK
else -> PrefI.ipNavigationBarColor.value
}
setNavigationBarColorCompat(c)
}
}