2018-12-01 00:02:18 +01:00
|
|
|
package jp.juggler.util
|
|
|
|
|
2021-05-22 00:03:16 +02:00
|
|
|
import android.content.*
|
2018-12-01 00:02:18 +01:00
|
|
|
import android.content.res.ColorStateList
|
2021-01-04 02:11:45 +01:00
|
|
|
import android.content.res.TypedArray
|
2018-12-01 00:02:18 +01:00
|
|
|
import android.graphics.Color
|
|
|
|
import android.graphics.ColorFilter
|
|
|
|
import android.graphics.PorterDuff
|
|
|
|
import android.graphics.PorterDuffColorFilter
|
2021-05-22 00:03:16 +02:00
|
|
|
import android.graphics.drawable.Drawable
|
|
|
|
import android.graphics.drawable.GradientDrawable
|
|
|
|
import android.graphics.drawable.RippleDrawable
|
|
|
|
import android.graphics.drawable.ShapeDrawable
|
2018-12-01 00:02:18 +01:00
|
|
|
import android.graphics.drawable.shapes.RectShape
|
|
|
|
import android.os.SystemClock
|
2019-02-03 05:29:50 +01:00
|
|
|
import android.text.Editable
|
|
|
|
import android.text.TextWatcher
|
2018-12-01 00:02:18 +01:00
|
|
|
import android.util.SparseArray
|
2019-09-12 19:16:07 +02:00
|
|
|
import android.widget.ImageButton
|
2018-12-01 00:02:18 +01:00
|
|
|
import android.widget.ImageView
|
2021-05-22 00:03:16 +02:00
|
|
|
import androidx.activity.ComponentActivity
|
|
|
|
import androidx.activity.result.ActivityResult
|
|
|
|
import androidx.activity.result.ActivityResultLauncher
|
|
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
2019-07-23 20:48:21 +02:00
|
|
|
import androidx.core.content.ContextCompat
|
2019-01-09 19:40:27 +01:00
|
|
|
import jp.juggler.subwaytooter.R
|
2018-12-01 00:02:18 +01:00
|
|
|
import java.util.*
|
2021-05-22 00:03:16 +02:00
|
|
|
import kotlin.reflect.full.isSubtypeOf
|
|
|
|
import kotlin.reflect.full.memberProperties
|
|
|
|
import kotlin.reflect.typeOf
|
2018-12-01 00:02:18 +01:00
|
|
|
|
2020-02-11 18:01:15 +01:00
|
|
|
object UiUtils {
|
2021-05-22 00:03:16 +02:00
|
|
|
|
|
|
|
val log = LogCategory("UiUtils")
|
2019-01-09 19:40:27 +01:00
|
|
|
}
|
|
|
|
|
2018-12-01 00:02:18 +01:00
|
|
|
// colorARGB.applyAlphaMultiplier(0.5f) でalpha値が半分になったARGB値を得る
|
2021-05-22 00:03:16 +02:00
|
|
|
fun Int.applyAlphaMultiplier(alphaMultiplier: Float? = null): Int {
|
|
|
|
return if (alphaMultiplier == null) {
|
|
|
|
this
|
|
|
|
} else {
|
|
|
|
val rgb = (this and 0xffffff)
|
|
|
|
val alpha = clipRange(0, 255, ((this ushr 24).toFloat() * alphaMultiplier + 0.5f).toInt())
|
|
|
|
return rgb or (alpha shl 24)
|
|
|
|
}
|
2018-12-01 00:02:18 +01:00
|
|
|
}
|
|
|
|
|
2021-05-22 00:03:16 +02:00
|
|
|
fun Context.attrColor(attrId: Int): Int {
|
|
|
|
val a = theme.obtainStyledAttributes(intArrayOf(attrId))
|
|
|
|
val color = a.getColor(0, Color.BLACK)
|
|
|
|
a.recycle()
|
|
|
|
return color
|
2018-12-01 00:02:18 +01:00
|
|
|
}
|
|
|
|
|
2021-05-22 00:03:16 +02:00
|
|
|
fun <T> TypedArray.use(block: (TypedArray) -> T): T =
|
|
|
|
try {
|
|
|
|
block(this)
|
|
|
|
} finally {
|
|
|
|
recycle()
|
|
|
|
}
|
2021-01-04 02:11:45 +01:00
|
|
|
|
2021-05-22 00:03:16 +02:00
|
|
|
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)}")
|
2021-01-04 02:11:45 +01:00
|
|
|
|
2021-05-22 00:03:16 +02:00
|
|
|
fun Context.attrDrawable(attrId: Int): Drawable {
|
|
|
|
val drawableId = getAttributeResourceId(attrId)
|
|
|
|
return ContextCompat.getDrawable(this, drawableId)
|
|
|
|
?: error("getDrawable failed. drawableId=0x${drawableId.toString(16)}")
|
2018-12-01 00:02:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
// 後方互換用にボタン背景Drawableを生成する
|
2020-09-09 21:46:50 +02:00
|
|
|
//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
|
|
|
|
//}
|
2018-12-01 00:02:18 +01:00
|
|
|
|
|
|
|
// 色を指定してRectShapeを生成する
|
2021-05-22 00:03:16 +02:00
|
|
|
private fun getRectShape(color: Int): Drawable {
|
|
|
|
val r = RectShape()
|
|
|
|
val shapeDrawable = ShapeDrawable(r)
|
|
|
|
shapeDrawable.paint.color = color
|
|
|
|
return shapeDrawable
|
2018-12-01 00:02:18 +01:00
|
|
|
}
|
|
|
|
|
2020-02-11 18:01:15 +01:00
|
|
|
// 色を指定して角丸Drawableを作成する
|
|
|
|
fun createRoundDrawable(
|
2021-05-22 00:03:16 +02:00
|
|
|
radius: Float,
|
|
|
|
fillColor: Int? = null,
|
|
|
|
strokeColor: Int? = null,
|
|
|
|
strokeWidth: Int = 4
|
2020-02-11 18:01:15 +01:00
|
|
|
) =
|
2021-05-22 00:03:16 +02:00
|
|
|
GradientDrawable().apply {
|
|
|
|
cornerRadius = radius
|
|
|
|
if (fillColor != null) setColor(fillColor)
|
|
|
|
if (strokeColor != null) setStroke(strokeWidth, strokeColor)
|
|
|
|
}
|
2020-02-11 18:01:15 +01:00
|
|
|
|
2018-12-01 00:02:18 +01:00
|
|
|
// 色を指定してRippleDrawableを生成する
|
2021-05-22 00:03:16 +02:00
|
|
|
fun getAdaptiveRippleDrawable(normalColor: Int, pressedColor: Int): Drawable {
|
|
|
|
return RippleDrawable(ColorStateList.valueOf(pressedColor), getRectShape(normalColor), null)
|
2020-02-11 18:01:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 色を指定してRippleDrawableを生成する
|
|
|
|
fun getAdaptiveRippleDrawableRound(
|
2021-05-22 00:03:16 +02:00
|
|
|
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)
|
|
|
|
)
|
|
|
|
}
|
2018-12-01 00:02:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
private class ColorFilterCacheValue(
|
2021-05-22 00:03:16 +02:00
|
|
|
val filter: ColorFilter,
|
|
|
|
var lastUsed: Long
|
2018-12-01 00:02:18 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
private val colorFilterCache = SparseArray<ColorFilterCacheValue>()
|
|
|
|
private var colorFilterCacheLastSweep = 0L
|
|
|
|
|
2021-05-22 00:03:16 +02:00
|
|
|
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
|
|
|
|
}
|
2018-12-01 00:02:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
private class ColoredDrawableCacheKey(
|
2021-05-22 00:03:16 +02:00
|
|
|
val drawableId: Int,
|
|
|
|
val rgb: Int,
|
|
|
|
val alpha: Int
|
2018-12-01 00:02:18 +01:00
|
|
|
) {
|
2021-05-22 00:03:16 +02:00
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
2018-12-01 00:02:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private class ColoredDrawableCacheValue(
|
2021-05-22 00:03:16 +02:00
|
|
|
val drawable: Drawable,
|
|
|
|
var lastUsed: Long
|
2018-12-01 00:02:18 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
private val coloredDrawableCache = HashMap<ColoredDrawableCacheKey, ColoredDrawableCacheValue>()
|
|
|
|
private var coloredDrawableCacheLastSweep = 0L
|
|
|
|
|
|
|
|
fun createColoredDrawable(
|
2021-05-22 00:03:16 +02:00
|
|
|
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 {
|
|
|
|
clipRange(0, 255, ((color ushr 24).toFloat() * alphaMultiplier + 0.5f).toInt())
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2018-12-01 00:02:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
fun setIconDrawableId(
|
2021-05-22 00:03:16 +02:00
|
|
|
context: Context,
|
|
|
|
imageView: ImageView,
|
|
|
|
drawableId: Int,
|
|
|
|
color: Int? = null,
|
|
|
|
alphaMultiplier: Float
|
2018-12-01 00:02:18 +01:00
|
|
|
) {
|
2021-05-22 00:03:16 +02:00
|
|
|
if (color == null) {
|
|
|
|
// ImageViewにアイコンを設定する。デフォルトの色
|
|
|
|
imageView.setImageDrawable(ContextCompat.getDrawable(context, drawableId))
|
|
|
|
} else {
|
|
|
|
imageView.setImageDrawable(
|
|
|
|
createColoredDrawable(
|
|
|
|
context,
|
|
|
|
drawableId,
|
|
|
|
color,
|
|
|
|
alphaMultiplier
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2018-12-01 00:02:18 +01:00
|
|
|
}
|
|
|
|
|
2019-01-16 13:33:07 +01:00
|
|
|
//fun setIconAttr(
|
|
|
|
// context : Context,
|
|
|
|
// imageView : ImageView,
|
|
|
|
// iconAttrId : Int,
|
|
|
|
// color : Int? = null,
|
|
|
|
// alphaMultiplier : Float? = null
|
|
|
|
//) {
|
|
|
|
// setIconDrawableId(
|
|
|
|
// context,
|
|
|
|
// imageView,
|
|
|
|
// getAttributeResourceId(context, iconAttrId),
|
|
|
|
// color,
|
|
|
|
// alphaMultiplier
|
|
|
|
// )
|
|
|
|
//}
|
2019-01-09 19:40:27 +01:00
|
|
|
|
2021-05-22 00:03:16 +02:00
|
|
|
fun CharSequence.copyToClipboard(context: Context) {
|
|
|
|
try {
|
|
|
|
// Gets a handle to the clipboard service.
|
|
|
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
|
|
?: throw NotImplementedError("missing ClipboardManager system service")
|
|
|
|
|
|
|
|
// Creates a new text clip to put on the clipboard
|
|
|
|
val clip = ClipData.newPlainText("text", this)
|
|
|
|
|
|
|
|
// Set the clipboard's primary clip.
|
|
|
|
|
|
|
|
clipboard.setPrimaryClip(clip)
|
|
|
|
|
|
|
|
context.showToast(false, R.string.copy_complete)
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
UiUtils.log.trace(ex)
|
|
|
|
context.showToast(ex, "copy failed.")
|
|
|
|
}
|
|
|
|
|
2019-01-17 20:40:51 +01:00
|
|
|
}
|
|
|
|
|
2020-02-11 18:01:15 +01:00
|
|
|
fun DialogInterface.dismissSafe() {
|
2021-05-22 00:03:16 +02:00
|
|
|
try {
|
|
|
|
dismiss()
|
|
|
|
} catch (ignored: Throwable) {
|
|
|
|
// 非同期処理の後などではDialogがWindowTokenを失っている場合があり、IllegalArgumentException がたまに出る
|
|
|
|
}
|
2019-01-19 03:36:40 +01:00
|
|
|
}
|
2019-01-20 22:04:57 +01:00
|
|
|
|
2019-02-03 05:29:50 +01:00
|
|
|
class CustomTextWatcher(
|
2021-05-22 00:03:16 +02:00
|
|
|
val callback: () -> Unit
|
2019-02-03 05:29:50 +01:00
|
|
|
) : TextWatcher {
|
2021-05-22 00:03:16 +02:00
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
2019-09-12 19:16:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// ImageButton のForeground colorで有効/無効を表現する
|
2021-05-22 00:03:16 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////
|
|
|
|
|
|
|
|
class ActivityResultHandler<A : ComponentActivity>(
|
|
|
|
val callback: A.(ActivityResult?) -> Unit
|
|
|
|
) {
|
|
|
|
private lateinit var log: LogCategory
|
|
|
|
private lateinit var context: Context
|
|
|
|
private lateinit var launcher: ActivityResultLauncher<Intent>
|
|
|
|
|
|
|
|
// startForActivityResultの代わりに呼び出す
|
|
|
|
fun launch(intent: Intent) = try {
|
|
|
|
launcher.launch(intent)
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
log.e(ex, "launch failed")
|
|
|
|
context.showToast(ex, "activity launch failed.")
|
|
|
|
}
|
|
|
|
|
|
|
|
// onCreate時に呼び出す
|
|
|
|
fun register(a: A, log: LogCategory) {
|
|
|
|
this.log = log
|
|
|
|
this.context = a.applicationContext
|
|
|
|
this.launcher = a.registerForActivityResult(
|
|
|
|
ActivityResultContracts.StartActivityForResult()
|
|
|
|
) {
|
|
|
|
callback(a, it)
|
|
|
|
}
|
|
|
|
}
|
2019-09-12 19:16:07 +02:00
|
|
|
}
|
2021-05-22 00:03:16 +02:00
|
|
|
|
|
|
|
@Suppress("unused")
|
|
|
|
fun <A : ComponentActivity> A.activityResultHandler(callback: A.(ActivityResult?) -> Unit) =
|
|
|
|
ActivityResultHandler(callback)
|