SubwayTooter-Android-App/base/src/main/java/jp/juggler/util/ui/UiUtils.kt

374 lines
12 KiB
Kotlin

package jp.juggler.util.ui
import android.content.Context
import android.content.DialogInterface
import android.content.res.ColorStateList
import android.content.res.Resources
import android.content.res.TypedArray
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import android.media.RingtoneManager
import android.os.SystemClock
import android.text.Editable
import android.text.TextWatcher
import android.util.DisplayMetrics
import android.util.SparseArray
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import jp.juggler.util.data.clip
import jp.juggler.util.data.notZero
import jp.juggler.util.getUriExtra
import jp.juggler.util.log.LogCategory
private val log = LogCategory("UiUtils")
// colorARGB.applyAlphaMultiplier(0.5f) でalpha値が半分になったARGB値を得る
fun Int.applyAlphaMultiplier(alphaMultiplier: Float? = null): Int {
return if (alphaMultiplier == null) {
this
} else {
val rgb = (this and 0xffffff)
val alpha = ((this ushr 24).toFloat() * alphaMultiplier + 0.5f).toInt().clip(0, 255)
return rgb or (alpha shl 24)
}
}
fun Context.attrColor(attrId: Int): Int {
val a = theme.obtainStyledAttributes(intArrayOf(attrId))
val color = a.getColor(0, Color.BLACK)
a.recycle()
return color
}
fun <T> TypedArray.use(block: (TypedArray) -> T): T =
try {
block(this)
} finally {
recycle()
}
fun Context.getAttributeResourceId(attrId: Int) =
theme.obtainStyledAttributes(intArrayOf(attrId))
.use { it.getResourceId(0, 0) }
.notZero() ?: error("missing resource id. attr_id=0x${attrId.toString(16)}")
fun Context.attrDrawable(attrId: Int): Drawable {
val drawableId = getAttributeResourceId(attrId)
return ContextCompat.getDrawable(this, drawableId)
?: error("getDrawable failed. drawableId=0x${drawableId.toString(16)}")
}
/////////////////////////////////////////////////////////
// 後方互換用にボタン背景Drawableを生成する
//private fun getStateListDrawable(normalColor : Int, pressedColor : Int) : StateListDrawable {
// val states = StateListDrawable()
// states.addState(intArrayOf(android.R.attr.state_pressed), ColorDrawable(pressedColor))
// states.addState(intArrayOf(android.R.attr.state_focused), ColorDrawable(pressedColor))
// states.addState(intArrayOf(android.R.attr.state_activated), ColorDrawable(pressedColor))
// states.addState(intArrayOf(), ColorDrawable(normalColor))
// return states
//}
// 色を指定してRectShapeを生成する
private fun getRectShape(color: Int): Drawable {
val r = RectShape()
val shapeDrawable = ShapeDrawable(r)
shapeDrawable.paint.color = color
return shapeDrawable
}
// 色を指定して角丸Drawableを作成する
fun createRoundDrawable(
radius: Float,
fillColor: Int? = null,
strokeColor: Int? = null,
strokeWidth: Int = 4,
) = GradientDrawable().apply {
cornerRadius = radius
if (fillColor != null) setColor(fillColor)
if (strokeColor != null) setStroke(strokeWidth, strokeColor)
}
// 色を指定してRippleDrawableを生成する
fun getAdaptiveRippleDrawable(normalColor: Int, pressedColor: Int): Drawable {
return RippleDrawable(ColorStateList.valueOf(pressedColor), getRectShape(normalColor), null)
}
// 色を指定してRippleDrawableを生成する
fun getAdaptiveRippleDrawableRound(
context: Context,
normalColor: Int,
pressedColor: Int,
roundNormal: Boolean = false,
): Drawable {
val dp6 = context.resources.displayMetrics.density * 6f
return if (roundNormal) {
// 押してない時に通常色を塗る範囲も角丸にする
RippleDrawable(
ColorStateList.valueOf(pressedColor),
createRoundDrawable(dp6, fillColor = normalColor),
null
)
} else {
// 押してない時に通常色を塗る範囲は四角だが、リップルエフェクトは角丸
return RippleDrawable(
ColorStateList.valueOf(pressedColor),
getRectShape(normalColor),
createRoundDrawable(dp6, Color.WHITE)
)
}
}
/////////////////////////////////////////////////////////
private class ColorFilterCacheValue(
val filter: ColorFilter,
var lastUsed: Long,
)
private val colorFilterCache = SparseArray<ColorFilterCacheValue>()
private var colorFilterCacheLastSweep = 0L
private fun createColorFilter(rgb: Int): ColorFilter {
synchronized(colorFilterCache) {
val now = SystemClock.elapsedRealtime()
val cacheValue = colorFilterCache[rgb]
if (cacheValue != null) {
cacheValue.lastUsed = now
return cacheValue.filter
}
val size = colorFilterCache.size()
if (now - colorFilterCacheLastSweep >= 10000L && size >= 128) {
colorFilterCacheLastSweep = now
for (i in size - 1 downTo 0) {
val v = colorFilterCache.valueAt(i)
if (now - v.lastUsed >= 10000L) {
colorFilterCache.removeAt(i)
}
}
}
val f = PorterDuffColorFilter(rgb, PorterDuff.Mode.SRC_ATOP)
colorFilterCache.put(rgb, ColorFilterCacheValue(f, now))
return f
}
}
/////////////////////////////////////////////////////////
private class ColoredDrawableCacheKey(
val drawableId: Int,
val rgb: Int,
val alpha: Int,
) {
override fun equals(other: Any?): Boolean {
return this === other || (
other is ColoredDrawableCacheKey &&
drawableId == other.drawableId &&
rgb == other.rgb &&
alpha == other.alpha
)
}
override fun hashCode(): Int {
return drawableId xor (rgb or (alpha shl 24))
}
}
private class ColoredDrawableCacheValue(
val drawable: Drawable,
var lastUsed: Long,
)
private val coloredDrawableCache = HashMap<ColoredDrawableCacheKey, ColoredDrawableCacheValue>()
private var coloredDrawableCacheLastSweep = 0L
fun createColoredDrawable(
context: Context,
drawableId: Int,
color: Int,
alphaMultiplier: Float,
): Drawable {
val rgb = (color and 0xffffff) or Color.BLACK
val alpha = if (alphaMultiplier >= 1f) {
(color ushr 24)
} else {
((color ushr 24).toFloat() * alphaMultiplier + 0.5f).toInt().clip(0, 255)
}
val cacheKey = ColoredDrawableCacheKey(drawableId, rgb, alpha)
synchronized(coloredDrawableCache) {
val now = SystemClock.elapsedRealtime()
val cacheValue = coloredDrawableCache[cacheKey]
if (cacheValue != null) {
cacheValue.lastUsed = now
return cacheValue.drawable
}
if (now - coloredDrawableCacheLastSweep >= 10000L && coloredDrawableCache.size >= 128) {
coloredDrawableCacheLastSweep = now
val list = coloredDrawableCache.entries.sortedBy { it.value.lastUsed }
for (i in 0 until list.size - 64) {
val (k, v) = list[i]
if (now - v.lastUsed <= 10000L) break
coloredDrawableCache.remove(k)
}
}
// 色指定が他のアイコンに影響しないようにする
// カラーフィルターとアルファ値を設定する
val d = ContextCompat.getDrawable(context, drawableId)!!.mutate()
d.colorFilter = createColorFilter(rgb)
d.alpha = alpha
coloredDrawableCache[cacheKey] = ColoredDrawableCacheValue(d, now)
return d
}
}
//////////////////////////////////////////////////////////////////
fun setIconDrawableId(
context: Context,
imageView: ImageView,
drawableId: Int,
color: Int? = null,
alphaMultiplier: Float = 1f,
) {
if (color == null) {
// ImageViewにアイコンを設定する。デフォルトの色
imageView.setImageDrawable(ContextCompat.getDrawable(context, drawableId))
} else {
imageView.setImageDrawable(
createColoredDrawable(
context,
drawableId,
color,
alphaMultiplier
)
)
}
}
//fun setIconAttr(
// context : Context,
// imageView : ImageView,
// iconAttrId : Int,
// color : Int? = null,
// alphaMultiplier : Float? = null
//) {
// setIconDrawableId(
// context,
// imageView,
// getAttributeResourceId(context, iconAttrId),
// color,
// alphaMultiplier
// )
//}
fun DialogInterface.dismissSafe() {
try {
dismiss()
} catch (ignored: Throwable) {
// 非同期処理の後などではDialogがWindowTokenを失っている場合があり、IllegalArgumentException がたまに出る
}
}
class CustomTextWatcher(
val callback: () -> Unit,
) : TextWatcher {
override fun beforeTextChanged(
s: CharSequence,
start: Int,
count: Int,
after: Int,
) {
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
callback()
}
}
// ImageButton のForeground colorで有効/無効を表現する
fun ImageButton.setEnabledColor(context: Context, iconId: Int, color: Int, enabled: Boolean) {
isEnabled = enabled
setImageDrawable(
createColoredDrawable(
context = context,
drawableId = iconId,
color = color,
alphaMultiplier = when (enabled) {
true -> 1f
else -> 0.5f
}
)
)
}
var View.isEnabledAlpha: Boolean
get() = isEnabled
set(enabled) {
this.isEnabled = enabled
this.alpha = when (enabled) {
true -> 1f
else -> 0.3f
}
}
/////////////////////////////////////////////////
val AppCompatActivity.isLiveActivity: Boolean
get() = !(isFinishing || isDestroyed)
/**
* Ringtone pickerの処理結果のUriまたはnull
*/
val ActivityResult.decodeRingtonePickerResult
get() = when {
isNotOk -> null
else -> data?.getUriExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
}
fun AppCompatActivity.setNavigationBack(toolbar: Toolbar) =
toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
fun ComponentActivity.setNavigationBack(toolbar: Toolbar) =
toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
val Float.roundPixels get() = (this + 0.5f).toInt()
fun DisplayMetrics.dpFloat(src: Float) = (density * src)
fun DisplayMetrics.dpFloat(src: Int) = (density * src.toFloat())
fun Resources.dpFloat(src: Float) = displayMetrics.dpFloat(src)
fun Resources.dpFloat(src: Int) = displayMetrics.dpFloat(src)
fun Context.dpFloat(src: Float) = resources.dpFloat(src)
fun Context.dpFloat(src: Int) = resources.dpFloat(src)
fun DisplayMetrics.dp(src: Float) = (density * src).roundPixels
fun DisplayMetrics.dp(src: Int) = (density * src.toFloat()).roundPixels
fun Resources.dp(src: Float) = displayMetrics.dp(src)
fun Resources.dp(src: Int) = displayMetrics.dp(src)
fun Context.dp(src: Float) = resources.dp(src)
fun Context.dp(src: Int) = resources.dp(src)