
Androidã¢ããªå¶äœ #14ïœã¿ã€ããŒã¢ããªæ¹é èšç»âŠ ã¯ã³ã¿ããã¿ã€ããŒãã€ãã«å®æïŒ
â ã¯ããã«
ãã€ãããããšãããããŸãïŒ
ãŸã£ããã§ãïŒ
仿¥ãAndroidã¢ããªã
æ¹é ããŠãããŸãïŒ
ãããŠä»åã¯âŠâŠ
ã¯ã³ã¿ããã¿ã€ããŒæ¹é èšç»ã
ã€ãã«æçµåã§ãïŒ
æåã¯ã«ããã©ãŒã¡ã³å°çšã®
3åã¿ã€ããŒã ã£ãã®ã§ããâŠâŠ
æ°ãã€ãã°ããããããªæ©èœã
远å ããŠããŸããïœ
ååãŸã§ã«ã
奜ããªæéãç»é²ã§ãã
ã«ã¹ã¿ã ãã¿ã³ãã¿ã³ã«å¥œããªç»åã
èšå®ããæ©èœã¿ã€ããŒã®å£çŽã
倿Žããæ©èœãã¿ã³ã®é 眮ãå ¥ã
æ¿ããæ©èœ
ãªã©ãå®è£ ããŠããŸããã
ä»åã¯æ°ããæ©èœã
ã©ãã©ã远å ããã®ã§ã¯ãªãã
èŠãç®ãšäœ¿ãããããäžå¿ã«
ãã©ãã·ã¥ã¢ããããŠãããŸãïŒ
ãªããä»åã¯ã³ãŒãã®è©³ãã
解説ã¯çç¥ããŸãã
宿ããã³ãŒãã¯ã
èšäºã®æåŸã«ä»é²ãšããŠ
æ²èŒããŸãã®ã§ã
æ°ã«ãªãæ¹ã¯èŠãŠãã ããïŒ

ãããªæãã§ã
â â ç»é¢ã®ã¬ã€ã¢ãŠããèŠçŽããŠã¿ãïŒ
ãŸãæ°ã«ãªã£ãã®ãã
ç»é¢å
šäœã®ãã©ã³ã¹
ã§ãã
ååãŸã§ã®ç»é¢ã§ã¯ã
ã¿ã€ããŒè¡šç€ºããã¿ã³ã
äžã®æ¹ã«éãŸã£ãŠããŸããã
ãã®äžæ¹ã§ã
ç»é¢ã®äžã«ã¯å€§ããªäœçœãâŠâŠ
ãã£ããå£çŽãèªç±ã«
倿Žã§ããããã«ããã®ã«ã
ãªãã ãå°ã
ãã£ãããªãæãã§ãïœ
ããã§ä»åã¯ã
æé衚瀺ãšããªã»ãããã¿ã³ã®
ééãåºããŠã¿ãŸããã
ãã¿ã³ã®é
眮ãå°ãäžããããšã§ã
ç»é¢å
šäœã®ãã©ã³ã¹ã調æŽïŒ
ããã ãã§ãã
ããªãå°è±¡ãå€ãããŸããã
æ©èœã¯åãã§ããé
眮ãå€ããã ãã§
䜿ãããããèŠãç®ãå€ããã®ã§ããïŒ


â â¡ ãã¿ã³ã®èŠããããæ¹åïŒ
æ¬¡ã«æ°ã«ãªã£ãã®ã¯ã
ãã¿ã³ã®èŠãããã§ãã
ä»åã®ã¢ããªã¯ã
奜ããªç»åãå£çŽã«ã§ããŸãã
ãããâŠâŠ
æããå£çŽãèšå®ãããšã
ãã¿ã³ãæåãèæ¯ã«
åãããŠããŸãããšããããŸãã
ç¹ã«ãèšå®å€æŽããã¿ã³ã¯ã
èæ¯ã«ãã£ãŠã¯
ããªãèŠãã«ããç¶æ
ã§ããã
ããã§ä»åã¯ã
èšå®å€æŽãã¿ã³ã®è²ã倿ŽïŒ
ããã«ãæäœãã¿ã³ã®åŸãã«
åéæã®èæ¯ã远å ããŸããã
ãããªãå£çŽã®é°å²æ°ã
æ®ããªããããã¿ã³ã
èŠããããªããŸãã
ãããŠã
ããªã»ãããã¿ã³ã«ã
é»ãæ ç·ã远å ïŒ
以åã®çœãæ ç·ããã
ãã¿ã³ã®èŒªéãã¯ã£ããããŸããã
ãããã现ãã調æŽãã
倧åãªã®ã§ããã
â ⢠ããªã»ãããã¿ã³ãå¢ããŠã倧äžå€«ïŒ
ããŠâŠâŠ
ã«ã¹ã¿ã ãã¿ã³ãèªç±ã«
远å ã§ããããã«ãªã£ãã®ã¯
è¯ãã®ã§ããã
ããã§äžã€åé¡ãã
ãã¿ã³ã远å ãç¶ãããã
ç»é¢ãã©ãã©ãé·ããªãã®ã§ã¯ïŒ
ãšããããšã§ãïœ
å®éããã¿ã³ãå¢ãããã³ã«
ç»é¢ã瞊ã«äŒžã³ãŠããŸãã®ã¯ã
ããŸã䜿ãããããããŸããã
ããã§ä»åã¯ã
ããªã»ããäžèЧã«
ã¹ã¯ããŒã«æ©èœã远å ããŸããïŒ
仿§ã¯ã·ã³ãã«ã§ãã
ãã¿ã³ã6åãŸã§ã¯é垞衚瀺ã
7å以äžã«ãªããšã
äžèЧã®äžãã¹ã¯ããŒã«ã§ããïŒ
ãšããä»çµã¿ã§ãã
ãããªããã¿ã³ãå¢ããŠãã
ã¿ã€ããŒã®æäœéšåã
ã©ãã©ãäžãžç§»åããã®ã
æããããŸãã
ã¡ãªã¿ã«ã
ãã¿ã³ãã¹ã¯ããŒã«ããŠããéã
ã¿ã€ããŒã®ã«ãŠã³ãããŠã³ã¯
ãã®ãŸãŸç¶ããŸãã
奜ããªã¿ã€ããŒã
ããã€ãç»é²ã§ããããã«ãªãã
䜿ãåæãè¯ããªããŸããïŒ

ã¹ã¯ããŒã«ã¢ãŒãã«åãæ¿ãã
ã¹ã¯ããŒã«ã远å ããã®ã§
ãã¿ã³ãå¢ããŠã倧äžå€«ïŒ
â ⣠誀æäœãé²ãããã®æ¹è¯ïŒ
ãããŠæåŸã«ã
ããäžã€æ°ã«ãªãåé¡ã
èŠã€ãããŸããã
ããã¯âŠâŠ
ã«ãŠã³ãããŠã³äžã«
å¥ã®ã¿ã€ããŒãã¿ã³ãæŒããšã
æéãåèšå®ãããŠããŸãããšïŒ
äŸãã°ã
ã©ãŒã¡ã³ã®3åã¿ã€ããŒã
䜿ã£ãŠããéäžã§ã
ééããŠãã¹ã¿ã®ãã¿ã³ã
æŒããŠããŸã£ããâŠâŠ
7åã«å€æŽãããŠããŸããŸãïœ
ããã¯å°ããŸããâŠâŠ
äžæåæ¢äžãåãåé¡ã
çºçããŠããŸããã
ããã§ä»åã¯ã
ã«ãŠã³ãããŠã³äžãšäžæåæ¢äžã¯
ããªã»ãããã¿ã³ã
æäœã§ããªãããã«ããŸããã
äžåºŠã¿ã€ããŒãéå§ãããã
ãªã»ãããããŸã§
å¥ã®æéãèšå®ã§ããŸããã
ãã¡ããã
äžæåæ¢ãåéã¯
ä»ãŸã§ã©ãã䜿ããŸãã
ãããªã誀ã£ãŠå¥ã®ãã¿ã³ã«
è§ŠããŠããŸã£ãŠãå®å¿ã§ãã
䟿å©ãªã¢ããªãäœãã«ã¯ã
誀æäœãé²ãããšã倧åãªã®ã§ããïŒ
â †ã¯ã³ã¿ããã¿ã€ããŒãã€ãã«å®æïŒ
ãšããããšã§âŠâŠ
ã¯ã³ã¿ããã¿ã€ããŒã
ã²ãšãŸã宿ã§ãïŒ
æåã¯ã«ããã©ãŒã¡ã³å°çšã®
3åã¿ã€ããŒã§ãããã
çŸåšã¯ãããªããšã
ã§ããããã«ãªããŸããïŒ
ã¯ã³ã¿ããã§ã¿ã€ããŒãéå§
éåžžã¿ã€ããŒãšã®åãæ¿ã
äžæåæ¢ã»åéã»ãªã»ãã
奜ããªæéã®ã«ã¹ã¿ã ã¿ã€ããŒã远å
ã«ã¹ã¿ã ãã¿ã³ã®ååãç»åã倿Ž
奜ããªç»åãå£çŽã«èšå®
ããªã»ãããã¿ã³ã®é çœ®å€æŽ
ãã¿ã³ã7å以äžã«ãªã£ãå Žåã®ã¹ã¯ããŒã«
ã«ãŠã³ãããŠã³äžã®èª€æäœé²æ¢
ãããâŠâŠ
ããã¶ãæ©èœãå¢ããŸãããïœ
æåã«äœã£ãã¿ã€ããŒãš
èŠæ¯ã¹ãŠã¿ããšã
åãã¢ããªãšã¯æããªãã»ã©
å€ãã£ãŠããŸããŸããïœ
ãŸã 现ããæ¹åç¹ã¯
ãããããããŸãããã
ä»åã¯ããã§å®æãšããŸãïŒ

â ⥠AIãšäžç·ã«ã¢ããªãäœã£ãŠã¿ãææ³
ããŠâŠâŠ
ä»åã®ã¿ã€ããŒå¶äœã
æ¯ãè¿ã£ãŠã¿ãããšæããŸãã
ããããã
æåã«èããŠããã®ã¯ã
ã䟿å©ãªã¢ããªã£ãŠäœã ããïŒã
ãšããããšã§ããã
äœãäœã£ãŠã¿ãããã©ã
äœãäœãã°ããã®ã
ãªããªãæãã€ããªãâŠâŠ
ãããªäžã§ã
ãšããããã«ããã©ãŒã¡ã³å°çšã®
3åã¿ã€ããŒã§ãäœã£ãŠã¿ããïŒ
ãšæã£ãã®ãå§ãŸãã§ãïœ
ãšãããâŠâŠ
å®éã«äœã£ãŠäœ¿ã£ãŠã¿ããšã
ãããæå€ã«äŸ¿å©ã ã£ãã®ã§ãã
ã ã£ããã
ããã©ãçšã®5åã¿ã€ããŒã
ãã£ãã䟿å©ãããªãïŒã
ããã¹ã¿çšã®7åãæ¬²ãããªâŠâŠã
ã奜ããªæéãç»é²ã§ãããïŒã
ãç»åã倿Žã§ãããé¢çœãããïŒã
ãšã次ã
ã«ããããããšã
å¢ããŠãããŸããïœ
ãããŠæ°ãã€ãã°ã
ä»ã®ãããªã¢ããªã«ãªã£ãŠããŸããã
ãã âŠâŠ
æ©èœã远å ããã°ããã»ã©ã
è¯ãã¢ããªã«ãªããšã¯éããŸããã
å®éãæ©èœã远å ãããã³ã«
ã³ãŒããè€éã«ãªã£ãŠããŸãã
éäžã§AIã«ã³ãŒãã
æŽçãçŽããŠãããããšã
ãããŸããã
ãŸããæ©èœã远å ããŠãã
ç»é¢ãèŠã¥ããã£ããã
æäœãã«ããã£ããâŠâŠ
æã£ãŠãããšããã«
ãããªãããšããããŸããã
ä»åã®å¶äœã§æããã®ã¯ã
ãåãã¢ããªããš
ã䜿ããããã¢ããªãã¯éã
ãšããããšã§ãã
ãããŠãããäžã€ã
AIã䜿ãã°ã
ããã°ã©ãã³ã°åå¿è
ã§ã
æ¯èŒççæéã§ã³ãŒãã
äœãããšãã§ããŸãã
ãããã
AIãäœã£ãã³ãŒãã
ãã®ãŸãŸäœ¿ãã°è¯ããšãã
ããã§ã¯ãããŸããã
å®éã«åãããŠã
åé¡ããªãã確èªããŠã
å¿
èŠãªãä¿®æ£ããã
ãã®ç¹°ãè¿ãã
ãšãŠã倧åãªã®ã ãšæããŸããã
ä»åããAIã«äœåºŠã
å©ããŠããããŸããïœ
ãšã¯ããâŠâŠ
äœãäœãããã®ãã
ã©ããŸã§äœãã®ããæ±ºããã®ã¯
èªåèªèº«ã§ãã
䟿å©ã ãããšãã£ãŠã
æ©èœã远å ãç¶ããŠããã
ãã€ãŸã§ãã£ãŠã宿ããŸããïœ
ãªã®ã§ä»åã¯ã
ãã®ãããã§äžåºåãã«
ããããšæããŸãïŒ
â âŠ å®æãããã©ãå ¬éã¯ãŸã å ïŒ
ããŠïŒ
ã¯ã³ã¿ããã¿ã€ããŒã¯
ã²ãšãŸã宿ããŸãããâŠâŠ
ããã§Google Playã«
å
¬éã§ããããã§ã¯ãããŸããã
宿©ã§ã®åäœç¢ºèªãã
Google Playãžã®éçºè
ç»é²ã
å¿
èŠãªãã¹ãã審æ»ãªã©âŠâŠ
å
¬éãããŸã§ã«ã¯ã
ãŸã ãŸã ä¹ãè¶ããªããš
ãããªãå£ããããŸãïœ
ç¹ã«ãã¢ããªãå
¬éãããšãªããšã
èªåã®ã¹ããã§åãã°è¯ã
ãšããããã«ã¯ãããŸããã
ãããããªç«¯æ«ã§ã®åäœãã
äžå
·åãžã®å¯Ÿå¿ãªã©ã
èããå¿
èŠããããŸãã
ãŸã ãŸã å匷ããããšã
ãããããããŸããâŠâŠ
ããã§ãã
ããããªãå ¬éããŠãããããªïŒã
ãšæããäœåã
äžã€äœãããšãã§ããã®ã¯ã
èªåã«ãšã£ãŠ
倧ããªäžæ©ã ãšæããŸãïŒ
å
¬éã«ã€ããŠã¯ã
ãŸãæ¹ããŠææŠããŠã¿ãããš
æã£ãŠããŸãã
â 仿¥ã®ãã³ã¬

â æ¬¡åããã¯ã²ãŒã ã¢ããªå¶äœã«ææŠïŒ
ãšããããšã§ã
ã¯ã³ã¿ããã¿ã€ããŒã®
æ¹é èšç»ã¯ä»åã§çµäºã§ãïŒ
ãããŸã§ãä»ãåãããã ãã
ããããšãããããŸããïŒ
ãããŠæ¬¡åããã¯âŠâŠ
AIãšäžç·ã«ã
ã²ãŒã ã¢ããªãäœã£ãŠãããŸãïŒ
ãšã¯ããã
ãããªã倧äœã²ãŒã ãäœã
ã€ããã¯ãããŸããïœ
ãŸãã¯ã
åçŽãªä»çµã¿ã§æ¯èŒççæéã«
äœããããªã²ãŒã ã«
ææŠããŠã¿ãããšæããŸãã
ã¿ã€ããŒã¢ããªã§åŠãã ããšã
ã©ããŸã§æŽ»ãããã®ãïŒ
ãããŠã
ã²ãŒã ã¢ããªã§ã¯ã©ããªåé¡ã
åŸ
ã¡åããŠããã®ãïŒ
ä»ããæ¥œãã¿ã§ããïœ
æåŸãŸã§èªãã§ããã ã
ããããšãããããŸãïŒ
ããã§ã¯ãŸãïŒ
â ä»é²ïœã¯ã³ã¿ããã¿ã€ããŒå®æçã³ãŒã
ä»åã®æ¹é ãåæ ãã
Kotlinã³ãŒããæ²èŒããŸãã
ã³ãŒããé·ããªã£ãŠããããã
ä»åã¯è©³ãã解説ãçç¥ããŸãã
ãããŸã§ã®å¶äœéçšã
ã³ãŒãã®å€æŽç¹ã«ã€ããŠã¯ã
éå»ã®èšäºãã芧ãã ããã
â»ä»åæ²èŒããã³ãŒãã¯ã
ã¯ã³ã¿ããã¿ã€ããŒã®
äžæå®æçã§ãã
Google Playãžã®å
¬éã«
å¿
èŠãªåäœç¢ºèªããã¹ãã
å®äºãããã®ã§ã¯ãããŸããã
âŒä»å宿ããã³ãŒãâŒ
package com.example.myapplication6
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.graphics.Matrix
import android.media.ExifInterface
import android.net.Uri
import android.os.Build
import android.media.AudioManager
import android.media.ToneGenerator
import android.os.Bundle
import android.os.SystemClock
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.font.FontWeight
import kotlinx.coroutines.delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import org.json.JSONArray
import org.json.JSONObject
import java.util.Locale
import java.util.UUID
// 1. ããŒã¿ïŒæšæºãšã«ã¹ã¿ã ãåãåã»åãäžèЧã§ç®¡çããã
// imageUriã«ã¯ã¢ããªå
ãžã³ããŒããç»åã®ãã¡ã€ã«åãä¿åããã
// bodyImageUriã«ã¯ã¯ã³ã¿ããã¿ã€ããŒã®å£çŽç»åã®ãã¡ã€ã«åãä¿åããã
data class TimerPreset(
val id: Long,
val name: String,
val durationSeconds: Int,
val imageUri: String? = null,
val builtInKey: String? = null
) {
val isBuiltIn: Boolean get() = builtInKey != null
}
data class TimerSettings(
val buttons: List<TimerPreset> = defaultButtons(),
val bodyImageUri: String? = null
)
private fun defaultButtons() = listOf(
TimerPreset(-1L, "ã©ãŒã¡ã³", 180, builtInKey = "ramen"),
TimerPreset(-2L, "ãã©ã", 300, builtInKey = "udon"),
TimerPreset(-3L, "ãã¹ã¿", 420, builtInKey = "pasta")
)
private fun newPresetId(existing: List<TimerPreset>): Long {
var id: Long
do { id = UUID.randomUUID().mostSignificantBits and Long.MAX_VALUE }
while (id == 0L || existing.any { it.id == id })
return id
}
private fun formatTime(seconds: Int): String =
String.format(Locale.ROOT, "%02d:%02d", seconds / 60, seconds % 60)
// 2. ä¿åïŒUIããJSONãSharedPreferencesã®è©³çްãåãé¢ãã
// æ°ããããŒã«ä¿åãããããæ§itemsãäžæžãããã«ç§»è¡ã§ããã
private class TimerRepository(context: Context) {
private val prefs = context.getSharedPreferences("timer_presets", Context.MODE_PRIVATE)
fun load(): TimerSettings {
val raw = prefs.all[SETTINGS_KEY] as? String
if (raw != null) {
runCatching {
val root = JSONObject(raw)
val array = root.getJSONArray("buttons")
val buttons = (0 until array.length()).map { index ->
decodePreset(array.getJSONObject(index))
}
require(buttons.map { it.id }.distinct().size == buttons.size)
require(defaultButtons().all { original ->
buttons.count { it.builtInKey == original.builtInKey } == 1
})
return TimerSettings(buttons, root.optionalString("bodyImageUri"))
}
}
// æ§çã®itemsã¯StringSetãšJSONæååã®äž¡æ¹ãèªã¿èŸŒãã
val legacy = when (val value = prefs.all["items"]) {
is Set<*> -> value.filterIsInstance<String>().sorted().mapNotNull { rawItem ->
val parts = rawItem.split(":", limit = 3)
if (parts.size != 3) null else {
val id = parts[0].toLongOrNull()
val seconds = parts[1].toIntOrNull()
if (id == null || seconds == null || seconds !in 1..5999 || parts[2].isBlank()) {
null
} else TimerPreset(id, parts[2], seconds)
}
}
is String -> runCatching {
val array = JSONArray(value)
(0 until array.length()).mapNotNull { index ->
runCatching { decodePreset(array.getJSONObject(index)).copy(builtInKey = null) }
.getOrNull()
}
}.getOrDefault(emptyList())
else -> emptyList()
}
val merged = defaultButtons().toMutableList()
legacy.forEach { preset ->
merged.add(if (merged.any { it.id == preset.id }) {
preset.copy(id = newPresetId(merged))
} else preset)
}
return TimerSettings(buttons = merged)
}
fun save(settings: TimerSettings) {
val array = JSONArray()
settings.buttons.forEach { preset ->
array.put(JSONObject().apply {
put("id", preset.id)
put("name", preset.name)
put("durationSeconds", preset.durationSeconds)
put("imageUri", preset.imageUri ?: JSONObject.NULL)
put("builtInKey", preset.builtInKey ?: JSONObject.NULL)
})
}
val root = JSONObject().apply {
put("version", 2)
put("buttons", array) // é
åã®é çªããã®ãŸãŸç»é¢ã®ãã¿ã³é ã«ãªãã
put("bodyImageUri", settings.bodyImageUri ?: JSONObject.NULL)
}
prefs.edit().putString(SETTINGS_KEY, root.toString()).apply()
}
private fun decodePreset(json: JSONObject): TimerPreset {
val seconds = if (json.has("durationSeconds")) json.getInt("durationSeconds")
else json.getInt("seconds")
val name = json.getString("name")
require(seconds in 1..5999 && name.isNotBlank())
val key = json.optionalString("builtInKey")
require(key == null || key in setOf("ramen", "udon", "pasta"))
return TimerPreset(json.getLong("id"), name, seconds,
json.optionalString("imageUri"), key)
}
private fun JSONObject.optionalString(key: String): String? =
if (!has(key) || isNull(key)) null else getString(key).takeIf { it.isNotBlank() }
companion object { private const val SETTINGS_KEY = "settings_v2" }
}
// 3. èšæž¬ïŒæ°åå
¥åã»éå§ã»åæ¢ã»ãªã»ãããããã«éçŽã
// ç»é¢ãå¢ãããŠããåãèšæž¬ã«ãŒã«ãå
±æã§ããã
@Stable
private class TimerController {
var remainingSeconds by mutableIntStateOf(0)
private set
var inputNumber by mutableIntStateOf(0)
private set
var isRunning by mutableStateOf(false)
private set
var hasStarted by mutableStateOf(false)
private set
var sessionId by mutableIntStateOf(0)
private set
private var deadlineMillis = 0L
val canInput: Boolean get() = !hasStarted
val displayText: String get() = if (hasStarted) formatTime(remainingSeconds)
else String.format(Locale.ROOT, "%02d:%02d", inputNumber / 100, inputNumber % 100)
fun inputDigit(digit: Int) {
if (canInput && digit in 0..9 && inputNumber < 1000) {
inputNumber = inputNumber * 10 + digit
}
}
fun selectPreset(seconds: Int) {
if (hasStarted) return // éå§ã»äžæåæ¢ã»çµäºåŸã¯å¥ã®ããªã»ããã§äžæžãããªãã
require(seconds > 0)
remainingSeconds = seconds
inputNumber = 0
hasStarted = true
start()
}
fun toggle() {
if (isRunning) {
updateRemaining()
isRunning = false
} else {
if (!hasStarted) {
// å
¥åäžã¯99:99ãŸã§è¡šç€ºãéå§æã ãå®éã®ç§æ°ãžå€æã
remainingSeconds = (inputNumber / 100) * 60 + inputNumber % 100
if (remainingSeconds == 0) return
inputNumber = 0
hasStarted = true
}
if (remainingSeconds > 0) start()
}
}
fun reset() {
isRunning = false
remainingSeconds = 0
inputNumber = 0
hasStarted = false
sessionId++
}
private fun start() {
deadlineMillis = SystemClock.elapsedRealtime() + remainingSeconds * 1000L
sessionId++ // èšæž¬äžã«å¥ã®ãã¿ã³ãæŒããå ŽåãåŸ
æ©åŠçãããçŽãã
isRunning = true
}
// æå»ãšã®å·®ããæ®ããç®åºããdelayã®é
ããç©ã¿éãªããªãããã«ããã
private fun updateRemaining() {
val millis = (deadlineMillis - SystemClock.elapsedRealtime()).coerceAtLeast(0L)
remainingSeconds = ((millis + 999L) / 1000L).toInt()
}
fun tick(): Boolean {
if (!isRunning) return false
updateRemaining()
if (remainingSeconds > 0) return false
isRunning = false
return true // å®äºé³ã鳎ããã®ã¯å®äºãã1åã ãã
}
}
// 4. ã¢ããªã®å
¥å£ã»ç»é¢å
±éåŠçã
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { MaterialTheme { CustomTimer() } }
}
}
@Composable
fun CustomTimer() {
val context = LocalContext.current
val repository = remember(context) { TimerRepository(context.applicationContext) }
var settings by remember { mutableStateOf(repository.load()) }
val imageStore = remember(context) { ButtonImageStore(context.applicationContext) }
val timer = remember { TimerController() }
// ç·šéãã£ã³ã»ã«ãå·®ãæ¿ãã§äžèŠã«ãªã£ãã³ããŒã¯ã次åèµ·åæã«çä»ããã
LaunchedEffect(imageStore) {
withContext(Dispatchers.IO) {
imageStore.deleteUnused(settings.buttons.mapNotNull { it.imageUri } + listOfNotNull(settings.bodyImageUri))
}
}
var normalMode by remember { mutableStateOf(false) }
var showEditor by remember { mutableStateOf(false) }
var editingPreset by remember { mutableStateOf<TimerPreset?>(null) }
var showDisplaySettings by remember { mutableStateOf(false) }
var rearrangeMode by remember { mutableStateOf(false) }
var selectedSwapId by remember { mutableStateOf<Long?>(null) }
val tone = remember { ToneGenerator(AudioManager.STREAM_ALARM, 100) }
DisposableEffect(tone) { onDispose { tone.release() } }
LaunchedEffect(timer.isRunning, timer.sessionId) {
while (timer.isRunning) {
delay(100L)
if (timer.tick()) {
tone.startTone(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 2000)
}
}
}
fun updateSettings(next: TimerSettings) {
settings = next
repository.save(next)
}
if (showEditor) {
PresetEditorDialog(
preset = editingPreset,
imageStore = imageStore,
onDismiss = { showEditor = false },
onSave = { name, seconds, imageFileName ->
val current = editingPreset
// ååã»æéã»ã³ããŒããç»åã®ãã¡ã€ã«åããŸãšããŠä¿åããã
val item = current?.copy(name = name, durationSeconds = seconds, imageUri = imageFileName)
?: TimerPreset(newPresetId(settings.buttons), name, seconds, imageUri = imageFileName)
val buttons = if (current == null) settings.buttons + item
else settings.buttons.map { if (it.id == current.id) item else it }
updateSettings(settings.copy(buttons = buttons))
showEditor = false
},
onDelete = {
val current = editingPreset
if (current != null && !current.isBuiltIn) {
updateSettings(settings.copy(buttons = settings.buttons.filterNot { it.id == current.id }))
}
showEditor = false
}
)
}
if (showDisplaySettings) {
DisplaySettingsDialog(
currentWallpaper = settings.bodyImageUri,
imageStore = imageStore,
rearrangeMode = rearrangeMode,
onDismiss = { showDisplaySettings = false },
onSaveWallpaper = { fileName ->
updateSettings(settings.copy(bodyImageUri = fileName))
showDisplaySettings = false
},
onToggleArrange = {
rearrangeMode = !rearrangeMode
selectedSwapId = null
showDisplaySettings = false
}
)
}
// èæ¯ãç»é¢å
šäœã«æ·ããŠããã®äžã«ã¹ã¯ããŒã«ã§ããUIãéããã
// ã¯ã³ã¿ããã¢ãŒãã ããä¿åããå£çŽã䜿ããéåžžã¢ãŒãã¯ã°ã¬ãŒã®ãŸãŸã
val fullScreenWallpaper = rememberButtonImage(
if (normalMode) null else settings.bodyImageUri
)
Box(modifier = Modifier.fillMaxSize().background(Color(0xFFD0D0D0))) {
if (!normalMode && fullScreenWallpaper != null) {
Image(
bitmap = fullScreenWallpaper,
contentDescription = null, // è£
食çšã®èæ¯
modifier = Modifier.matchParentSize(),
contentScale = ContentScale.Crop // æ¯çãä¿ã£ãŠç»é¢å
šé¢ãåãã
)
// èæ¯ãæŽŸæãªå Žåã«ãæåããã¿ã³ãèŠããããã«èãèŠãã
Box(Modifier.matchParentSize().background(Color.White.copy(alpha = 0.12f)))
}
Column(
modifier = Modifier.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing)
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, top = 36.dp, bottom = 28.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Button(onClick = {
timer.reset()
tone.stopTone()
normalMode = !normalMode
selectedSwapId = null
rearrangeMode = false
}) { Text(if (normalMode) "ã¯ã³ã¿ããã¿ã€ããŒãž" else "éåžžã¿ã€ããŒãž") }
if (normalMode) {
NormalTimerScreen(timer, onReset = { timer.reset(); tone.stopTone() })
} else {
OneTouchTimerScreen(
onFinishArrange = { rearrangeMode = false; selectedSwapId = null },
settings = settings,
timer = timer,
onAdd = { editingPreset = null; showEditor = true },
onEdit = { editingPreset = it; showEditor = true },
onReset = { timer.reset(); tone.stopTone() },
onOpenSettings = { showDisplaySettings = true },
rearrangeMode = rearrangeMode,
selectedSwapId = selectedSwapId,
onPresetTap = { preset ->
// ã«ãŠã³ãéå§åŸïŒäžæåæ¢ã»çµäºåŸãå«ãïŒã¯ããªã»ããã®æäœãçŠæ¢ã
if (timer.hasStarted) {
// ãªã»ããããããŸã§ã¯äœãããªãã
} else if (rearrangeMode) {
val fromId = selectedSwapId
if (fromId == null) {
selectedSwapId = preset.id
} else if (fromId == preset.id) {
selectedSwapId = null
} else {
val rearranged = settings.buttons.toMutableList()
val from = rearranged.indexOfFirst { it.id == fromId }
val to = rearranged.indexOfFirst { it.id == preset.id }
if (from >= 0 && to >= 0) {
val temp = rearranged[from]
rearranged[from] = rearranged[to]
rearranged[to] = temp
updateSettings(settings.copy(buttons = rearranged))
}
selectedSwapId = null
}
} else {
tone.stopTone()
timer.selectPreset(preset.durationSeconds)
}
}
)
}
}
}
}
// 5. ã¢ãŒãå¥ã®ç»é¢ãä¿ååŠçã¯èŠªç»é¢ãžã³ãŒã«ããã¯ããã
@Composable
private fun NormalTimerScreen(timer: TimerController, onReset: () -> Unit) {
Text("éåžžã¿ã€ããŒ", fontSize = 24.sp)
TimerDisplay(timer.displayText)
listOf(listOf(1, 2, 3), listOf(4, 5, 6), listOf(7, 8, 9), listOf(0)).forEach { digits ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
digits.forEach { digit ->
Button(onClick = { timer.inputDigit(digit) }, enabled = timer.canInput,
modifier = Modifier.size(72.dp)) {
Text(digit.toString(), fontSize = 28.sp)
}
}
}
}
TimerControls(timer, normalMode = true, onReset = onReset)
}
@Composable
private fun OneTouchTimerScreen(
settings: TimerSettings,
timer: TimerController,
onFinishArrange: () -> Unit,
onAdd: () -> Unit,
onEdit: (TimerPreset) -> Unit,
onReset: () -> Unit,
onOpenSettings: () -> Unit,
rearrangeMode: Boolean,
selectedSwapId: Long?,
onPresetTap: (TimerPreset) -> Unit
) {
// èæ¯ã¯èŠªã®CustomTimerã§å
šé¢è¡šç€ºãããã§ã¯æäœUIã ããæãã
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
TimerDisplay(formatTime(timer.remainingSeconds))
if (rearrangeMode) {
Column(
Modifier.fillMaxWidth()
.background(Color.White.copy(alpha = 0.92f), RoundedCornerShape(12.dp))
.padding(10.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
if (selectedSwapId == null) "é
çœ®å€æŽäžïŒäº€æããããã¿ã³ãéžãã§ãã ãã"
else "亀æå
ã®ãã¿ã³ãéžæïŒåããã¿ã³ã§è§£é€ïŒ",
color = Color(0xFF263238), fontSize = 13.sp
)
Button(onClick = onFinishArrange, modifier = Modifier.fillMaxWidth()) {
Text("é
çœ®å€æŽãå®äº")
}
}
}
Spacer(modifier = Modifier.height(80.dp))
// ããªã»ãããã¿ã³ã¯éå§åŸã«å
¥åçŠæ¢ããªã»ããã§åã³éžã¹ãã
// 6åãŸã§ã¯åŸæ¥ã©ããé垞衚瀺ã
// 7å以äžã§ã¯ãããªã»ããäžèЧã ãã2段åã®é«ãã«å¶éããŠã¹ã¯ããŒã«ã
if (settings.buttons.size > 6) {
Column(
modifier = Modifier.fillMaxWidth()
.height(310.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
PresetRows(settings.buttons, rearrangeMode, selectedSwapId, timer.hasStarted, onEdit, onPresetTap)
}
Text(
"â â ããªã»ããäžèЧãäžäžã«ã¹ã¯ããŒã«ã§ããŸã",
fontSize = 12.sp,
color = Color(0xFF263238),
modifier = Modifier
.background(Color.White.copy(alpha = 0.88f), RoundedCornerShape(8.dp))
.padding(horizontal = 8.dp, vertical = 4.dp)
)
} else {
// 1ã6åã®å Žåã¯ã¹ã¯ããŒã«é åãäœããªãã
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
PresetRows(settings.buttons, rearrangeMode, selectedSwapId, timer.hasStarted, onEdit, onPresetTap)
}
}
// å£çŽã«å·Šå³ãããæäœã§ãããããæäœãã¿ã³ãåéæã®ããã«ã§ãŸãšããã
Column(
modifier = Modifier.fillMaxWidth()
.background(Color.White.copy(alpha = 0.80f), RoundedCornerShape(18.dp))
.padding(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = onAdd,
modifier = Modifier.weight(1f).heightIn(min = 48.dp),
enabled = !rearrangeMode,
contentPadding = PaddingValues(horizontal = 4.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF6750A4), contentColor = Color.White
)
) { Text("ïŒ ããªã»ãã远å ", fontSize = 12.sp) }
Button(
onClick = onOpenSettings,
modifier = Modifier.weight(1f).heightIn(min = 48.dp),
contentPadding = PaddingValues(horizontal = 4.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF3F568E), contentColor = Color.White
)
) { Text("â èšå®å€æŽ", fontSize = 12.sp) }
}
TimerControls(timer, normalMode = false, onReset = onReset)
}
}
}
// ããªã»ããã®3å衚瀺ãå
±éåããé垞衚瀺ã»ã¹ã¯ããŒã«è¡šç€ºã§åãåŠçã䜿ãã
@Composable
private fun PresetRows(
buttons: List<TimerPreset>,
rearrangeMode: Boolean,
selectedSwapId: Long?,
timerHasStarted: Boolean,
onEdit: (TimerPreset) -> Unit,
onPresetTap: (TimerPreset) -> Unit
) {
buttons.chunked(3).forEach { items ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
items.forEach { preset ->
key(preset.id) {
Column(Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally) {
PresetButton(
preset = preset,
onClick = { onPresetTap(preset) },
swapSelected = rearrangeMode && selectedSwapId == preset.id,
enabled = !timerHasStarted
)
if (!rearrangeMode && !preset.isBuiltIn) {
TextButton(
onClick = { onEdit(preset) },
modifier = Modifier.padding(top = 2.dp).height(36.dp),
colors = ButtonDefaults.textButtonColors(
containerColor = Color.White.copy(alpha = 0.88f),
contentColor = Color(0xFF433283)
)
) { Text("ç·šé", fontSize = 13.sp, fontWeight = FontWeight.Bold) }
}
}
}
}
repeat(3 - items.size) { Spacer(Modifier.weight(1f)) }
}
}
}
// 6. å
±éããŒãïŒç»åã®è¡šç€ºæ¹æ³ã远å ããå Žæãããã«äžæ¬åããã
@Composable
private fun PresetButton(
preset: TimerPreset,
onClick: () -> Unit,
swapSelected: Boolean = false,
enabled: Boolean = true
) {
val drawable = when (preset.builtInKey) {
"ramen" -> R.drawable.ramen
"udon" -> R.drawable.udon
"pasta" -> R.drawable.pasta
else -> null
}
val color = when (preset.builtInKey) {
"ramen" -> Color.Yellow
"udon" -> Color(0xFF81D4FA)
"pasta" -> Color(0xFFFFAB91)
else -> Color(0xFFE1BEE7)
}
Button(onClick = onClick, enabled = enabled,
modifier = Modifier.fillMaxWidth().height(110.dp),
shape = RoundedCornerShape(16.dp),
border = BorderStroke(if (swapSelected) 5.dp else 2.dp,
if (swapSelected) Color(0xFF7B1FA2) else Color.Black),
contentPadding = PaddingValues(6.dp),
colors = ButtonDefaults.buttonColors(
containerColor = color,
contentColor = Color.Black,
disabledContainerColor = color,
disabledContentColor = Color.Black
)) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
// ã³ããŒç»åãåªå
ããŠè¡šç€ºãèªããªãç»åã¯æšæºç»å/æåãžæ»ãã
val copiedImage = rememberButtonImage(preset.imageUri)
if (copiedImage != null) {
Image(bitmap = copiedImage, contentDescription = preset.name,
modifier = Modifier.fillMaxWidth().height(55.dp), contentScale = ContentScale.Crop)
} else if (drawable != null) {
Image(painterResource(drawable), contentDescription = preset.name,
modifier = Modifier.fillMaxWidth().height(55.dp), contentScale = ContentScale.Crop)
}
Text(preset.name, fontSize = 12.sp, maxLines = 2, fontWeight = FontWeight.Bold)
Text(formatTime(preset.durationSeconds), fontSize = 12.sp)
}
}
}
@Composable
private fun TimerDisplay(text: String) {
Box(Modifier.fillMaxWidth().background(Color(0xFF202124), RoundedCornerShape(24.dp))
.padding(vertical = 24.dp), contentAlignment = Alignment.Center) {
Text(text, fontSize = 56.sp, color = Color(0xFF00E676), maxLines = 1)
}
}
@Composable
private fun TimerControls(timer: TimerController, normalMode: Boolean, onReset: () -> Unit) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { timer.toggle() }, modifier = Modifier.weight(1f).heightIn(min = 48.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF6750A4), contentColor = Color.White,
disabledContainerColor = Color(0xFFDDD7EA),
disabledContentColor = Color(0xFF615B6B)
),
enabled = timer.isRunning || timer.remainingSeconds > 0 || (normalMode && timer.inputNumber > 0)) {
Text(if (timer.isRunning) "ã¹ããã" else if (timer.hasStarted) "åé" else "ã¹ã¿ãŒã")
}
Button(onClick = onReset, modifier = Modifier.weight(1f).heightIn(min = 48.dp)) { Text("ãªã»ãã") }
}
}
// 7. ç·šéç»é¢ïŒéžæç»åã®ãã¬ãã¥ãŒã衚瀺ãããä¿åãã§ã¢ããªå
ãžã³ããŒã
@Composable
private fun PresetEditorDialog(
preset: TimerPreset?,
imageStore: ButtonImageStore,
onDismiss: () -> Unit,
onSave: (String, Int, String?) -> Unit,
onDelete: () -> Unit
) {
var name by remember(preset?.id) { mutableStateOf(preset?.name ?: "") }
var minutes by remember(preset?.id) { mutableStateOf(preset?.let { (it.durationSeconds / 60).toString() } ?: "") }
var seconds by remember(preset?.id) { mutableStateOf(preset?.let { (it.durationSeconds % 60).toString() } ?: "") }
var imageReference by remember(preset?.id) { mutableStateOf(preset?.imageUri) }
var selectedUri by remember(preset?.id) { mutableStateOf<Uri?>(null) }
var error by remember(preset?.id) { mutableStateOf("") }
var isSaving by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val imagePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
// éžæç»é¢ã§ãã£ã³ã»ã«ããå Žåã¯å
ã®ç»åãç¶æããã
if (uri != null) {
selectedUri = uri
imageReference = uri.toString()
error = ""
}
}
val preview = rememberButtonImage(imageReference)
AlertDialog(onDismissRequest = { if (!isSaving) onDismiss() },
title = { Text(if (preset == null) "ã¿ã€ããŒã远å " else "ã¿ã€ããŒãç·šé") },
text = {
Column(Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = name, onValueChange = { name = it }, enabled = !isSaving,
label = { Text("ãã¿ã³åïŒæ¥æ¬èªã䜿ããŸãïŒ") }, singleLine = true)
NumberInputField(minutes, "åïŒ0ã99ïŒ", enabled = !isSaving) { minutes = it }
NumberInputField(seconds, "ç§ïŒ0ã59ïŒ", enabled = !isSaving) { seconds = it }
Text("ãã¿ã³ã®ç»å")
if (preview != null) {
Image(bitmap = preview, contentDescription = "éžæç»åã®ãã¬ãã¥ãŒ",
modifier = Modifier.fillMaxWidth().height(120.dp), contentScale = ContentScale.Fit)
} else {
Text(if (imageReference == null) "ç»åãªãïŒæåã ãã§è¡šç€ºïŒ" else "ç»åãèªã¿èŸŒã¿äžããŸãã¯ãã¬ãã¥ãŒã§ããŸãã")
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = { imagePicker.launch("image/*") }, enabled = !isSaving) {
Text(if (imageReference == null) "ç»åãéžã¶" else "ç»åã倿Ž")
}
TextButton(onClick = {
selectedUri = null
imageReference = null
error = ""
}, enabled = !isSaving && imageReference != null) { Text("ç»åãå€ã") }
}
Text("ä¿åãããšãç»åãã¢ããªå°çšã®å Žæãžã³ããŒããŸãã", fontSize = 12.sp)
if (isSaving) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Text("ç»åãã³ããŒããŠããŸãâŠ")
}
if (error.isNotEmpty()) Text(error, color = MaterialTheme.colorScheme.error)
}
},
confirmButton = {
TextButton(enabled = !isSaving, onClick = {
val trimmed = name.trim()
val m = if (minutes.isEmpty()) 0 else minutes.toIntOrNull()
val s = if (seconds.isEmpty()) 0 else seconds.toIntOrNull()
if (trimmed.length !in 1..20 || m == null || s == null || m !in 0..99 || s !in 0..59 || m * 60 + s == 0) {
error = "ååã¯1ã20æåãæéã¯00:01ã99:59ã§å
¥åããŠãã ãã"
} else {
val uriToCopy = selectedUri
val existingImage = imageReference
isSaving = true
scope.launch {
try {
val savedImage = if (uriToCopy != null) {
imageStore.copyFrom(uriToCopy)
} else existingImage
onSave(trimmed, m * 60 + s, savedImage)
} catch (cancelled: CancellationException) {
throw cancelled
} catch (exception: Exception) {
error = exception.message ?: "ç»åãä¿åã§ããŸããã§ãããå¥ã®ç»åãéžãã§ãã ããã"
} finally {
isSaving = false
}
}
}
}) { Text("ä¿å") }
},
dismissButton = {
Row {
if (preset != null && !preset.isBuiltIn) {
TextButton(onClick = onDelete, enabled = !isSaving) {
Text("åé€", color = MaterialTheme.colorScheme.error)
}
}
TextButton(onClick = onDismiss, enabled = !isSaving) { Text("ãã£ã³ã»ã«") }
}
}
)
}
// 8. å£çŽã»ãã¿ã³é
眮ã®èšå®ãç»åä¿åã¯æ¢åã®ButtonImageStoreãåå©çšããã
@Composable
private fun DisplaySettingsDialog(
currentWallpaper: String?,
imageStore: ButtonImageStore,
rearrangeMode: Boolean,
onDismiss: () -> Unit,
onSaveWallpaper: (String?) -> Unit,
onToggleArrange: () -> Unit
) {
var imageReference by remember(currentWallpaper) { mutableStateOf(currentWallpaper) }
var selectedUri by remember(currentWallpaper) { mutableStateOf<Uri?>(null) }
var saving by remember { mutableStateOf(false) }
var error by remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
val imagePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null) {
selectedUri = uri
imageReference = uri.toString()
error = ""
}
}
val preview = rememberButtonImage(imageReference)
AlertDialog(
onDismissRequest = { if (!saving) onDismiss() },
title = { Text("ã¯ã³ã¿ããã¿ã€ããŒèšå®") },
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text("å£çŽã倿Ž", style = MaterialTheme.typography.titleMedium)
if (preview != null) {
Image(bitmap = preview, contentDescription = "å£çŽãã¬ãã¥ãŒ",
modifier = Modifier.fillMaxWidth().height(160.dp),
contentScale = ContentScale.Fit)
} else {
Text(if (imageReference == null) "çŸåšã®å£çŽïŒãªã" else "ãã¬ãã¥ãŒã§ããŸãã")
}
OutlinedButton(onClick = { imagePicker.launch("image/*") }, enabled = !saving) {
Text("ç»åãéžæã»å€æŽ")
}
Text(
text = "æšå¥šç»åãµã€ãºïŒ1080 à 2400 pxïŒçžŠé·ïŒ\n" +
"â»ç»é¢ãµã€ãºã«ãã£ãŠç»åã®ç«¯ãåããå ŽåããããŸãã",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
TextButton(onClick = {
imageReference = null
selectedUri = null
error = ""
}, enabled = !saving && imageReference != null) { Text("å£çŽãå€ã") }
Button(enabled = !saving, onClick = {
saving = true
scope.launch {
try {
val fileName = if (selectedUri != null) imageStore.copyFrom(selectedUri!!)
else imageReference
onSaveWallpaper(fileName)
} catch (cancelled: CancellationException) {
throw cancelled
} catch (exception: Exception) {
error = exception.message ?: "å£çŽãä¿åã§ããŸããã§ãã"
} finally {
saving = false
}
}
}) { Text("å£çŽãä¿å") }
if (saving) LinearProgressIndicator(Modifier.fillMaxWidth())
if (error.isNotEmpty()) Text(error, color = MaterialTheme.colorScheme.error)
HorizontalDivider()
Text("ãã¿ã³ã®é
çœ®å€æŽ", style = MaterialTheme.typography.titleMedium)
Text("2ã€ã®ãã¿ã³ãé ã«ã¿ãããããšäœçœ®ã亀æããŸããæ¢åã®ãã¿ã³ãšã«ã¹ã¿ã ãã¿ã³ã®äž¡æ¹ã«å¯Ÿå¿ããŠããŸãã")
Button(onClick = onToggleArrange, enabled = !saving) {
Text(if (rearrangeMode) "é
çœ®å€æŽã¢ãŒããçµäº" else "é
çœ®å€æŽã¢ãŒããéå§")
}
}
},
confirmButton = {
TextButton(enabled = !saving, onClick = onDismiss) { Text("éãã") }
}
)
}
// 9. ç»ååŠçïŒå€éšã®ç»åãžã®åç
§ã§ã¯ãªããå
ã®ç»åããŒã¿ãå
éšãžã³ããŒããã
// ç»åã®èªã¿èŸŒã¿ã»ã³ããŒã¯ããã¯ã°ã©ãŠã³ãã§è¡ããç»é¢æäœãæ¢ããªãã
private class ButtonImageStore(private val context: Context) {
private val directory get() = File(context.filesDir, "button_images")
suspend fun copyFrom(uri: Uri): String = withContext(Dispatchers.IO) {
if (!directory.exists() && !directory.mkdirs()) throw IOException("ç»åã®ä¿åå ŽæãäœããŸããã§ãã")
val file = File(directory, "button_${UUID.randomUUID()}.img")
var complete = false
try {
val input = context.contentResolver.openInputStream(uri)
?: throw IOException("ç»åãéããŸããã§ãã")
input.use { source ->
file.outputStream().use { destination ->
val buffer = ByteArray(8192)
var total = 0L
while (true) {
currentCoroutineContext().ensureActive()
val count = source.read(buffer)
if (count == -1) break
total += count
if (total > MAX_BYTES) throw IOException("ç»åã¯25MB以äžã®ãã®ãéžãã§ãã ãã")
destination.write(buffer, 0, count)
}
if (total == 0L) throw IOException("ç»åã空ã§ãããå¥ã®ç»åãéžãã§ãã ãã")
}
}
// ç»åãšããŠè¡šç€ºã§ããããšã確èªããŠããä¿åã確å®ããã
val bitmap = decode(file.name) ?: throw IOException("ãã®ç»å圢åŒã¯è¡šç€ºã§ããŸãããå¥ã®ç»åãéžãã§ãã ãã")
bitmap.recycle()
currentCoroutineContext().ensureActive()
complete = true
file.name // 絶察ãã¹ãä¿åãããã¢ããªå
ã®ãã¡ã€ã«åã ãèšé²ã
} finally {
if (!complete) file.delete()
}
}
fun decode(reference: String): Bitmap? {
return try {
val uri = if (reference.startsWith("content://")) Uri.parse(reference) else null
val file = if (uri == null) managedFile(reference) ?: return null else null
if (file != null && !file.isFile) return null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val source = if (uri != null) ImageDecoder.createSource(context.contentResolver, uri)
else ImageDecoder.createSource(file!!)
ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
val longest = maxOf(info.size.width, info.size.height)
if (longest > PREVIEW_SIZE) {
val ratio = PREVIEW_SIZE.toFloat() / longest
decoder.setTargetSize((info.size.width * ratio).toInt().coerceAtLeast(1),
(info.size.height * ratio).toInt().coerceAtLeast(1))
}
}
} else {
// Android 8以åïŒç»åå
šäœããã®ãŸãŸã¡ã¢ãªãžå±éããçž®å°ããŠèªã¿èŸŒãã
fun open() = if (uri != null) context.contentResolver.openInputStream(uri) else file!!.inputStream()
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
open()?.use { BitmapFactory.decodeStream(it, null, bounds) }
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null
var sample = 1
while (maxOf(bounds.outWidth, bounds.outHeight) / sample > PREVIEW_SIZE) sample *= 2
val options = BitmapFactory.Options().apply { inSampleSize = sample }
val bitmap = open()?.use { BitmapFactory.decodeStream(it, null, options) } ?: return null
// åçã®EXIFæ
å ±ã«åŸã£ãŠã瞊åãã»å転ãè£æ£ããã
val orientation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) runCatching {
open()?.use { ExifInterface(it).getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL) }
}.getOrNull() ?: ExifInterface.ORIENTATION_NORMAL else ExifInterface.ORIENTATION_NORMAL
orientBitmap(bitmap, orientation)
}
} catch (_: Exception) { null }
}
fun deleteUnused(references: List<String>) {
val keep = references.toSet()
// ã³ããŒéå§ãšèµ·åæã®çä»ããéãªã£ãŠããæ°ããç»åã¯æ¶ããªãã
val cutoff = System.currentTimeMillis() - 60L * 60 * 1000
directory.listFiles()?.filter { it.name.startsWith("button_") && it.name.endsWith(".img") }
?.forEach { if (it.name !in keep && it.lastModified() < cutoff) it.delete() }
}
private fun managedFile(name: String): File? {
// ãã®æ©èœã§äœã£ãç»å以å€ã®ãã¹ãèªã¿æžãããªãã
if (!name.startsWith("button_") || !name.endsWith(".img") || '/' in name || '\\' in name) return null
return File(directory, name)
}
private fun orientBitmap(bitmap: Bitmap, orientation: Int): Bitmap {
val matrix = Matrix()
when (orientation) {
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.setScale(1f, -1f)
ExifInterface.ORIENTATION_TRANSPOSE -> { matrix.setRotate(90f); matrix.postScale(-1f, 1f) }
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
ExifInterface.ORIENTATION_TRANSVERSE -> { matrix.setRotate(-90f); matrix.postScale(-1f, 1f) }
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
else -> return bitmap
}
val oriented = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
if (oriented !== bitmap) bitmap.recycle()
return oriented
}
companion object {
private const val MAX_BYTES = 25L * 1024 * 1024
private const val PREVIEW_SIZE = 512
}
}
@Composable
private fun rememberButtonImage(reference: String?): ImageBitmap? {
val context = LocalContext.current
val store = remember(context) { ButtonImageStore(context.applicationContext) }
val image by produceState<ImageBitmap?>(initialValue = null, key1 = reference) {
value = null
if (reference != null) {
value = withContext(Dispatchers.IO) { store.decode(reference)?.asImageBitmap() }
}
}
return image
}
@Composable
private fun NumberInputField(value: String, label: String, enabled: Boolean = true, onValueChange: (String) -> Unit) {
OutlinedTextField(value = value, enabled = enabled,
onValueChange = { if (it.length <= 2 && it.all { char -> char in '0'..'9' }) onValueChange(it) },
label = { Text(label) }, singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
}
â»ä»åã®ã³ãŒãã詊ãå Žåã¯ãAndroid Studioã®res/drawableãã©ã«ãã«ã次ã®ç»åãçšæããå¿ èŠããããŸãã
ramenïŒã«ããã©ãŒã¡ã³çš
udonïŒã«ãããã©ãçš
pastaïŒãã¹ã¿çš
ãŸããAndroid Studioã®ããŒãžã§ã³ããããžã§ã¯ãã®æ§æã«ãã£ãŠã¯ãã³ãŒãã®èª¿æŽãå¿ èŠã«ãªãå ŽåããããŸãã
ä»åã®ã³ãŒãã¯ãããã°ã©ãã³ã°åå¿è ã®ç§ãAIã®ãµããŒããåããªããå¶äœãããã®ã§ãã
ç¡é§ãªåŠçãæ¹åã§ããéšåãæ®ã£ãŠããå¯èœæ§ããããŸãããäžã€ã®åŠç¿èšé²ãšããŠå ¬éããŠããŸãã
åèã«ããŠããã ããã°å¹žãã§ãïŒ