App de cronômetros, focado em minimalismo e design inspirado pelo meu pai :)
Feito com Kotlin + Jetpack Compose, Material 3, nenhuma dependência além do AndroidX.
O APK está em Releases. Baixe no próprio celular e toque no arquivo para instalar — o Android vai pedir para autorizar instalação de fonte desconhecida, o que é esperado para um app fora da Play Store.
Precisa de Android 8.0 (API 26) ou superior.
- Vários cronômetros ao mesmo tempo, cada um com seu título (toque no título para renomear).
- Contagem crescente com décimos de segundo até 1 hora; depois disso,
h:mm:ss, com os dias num prefixo miúdo (2d+5:07:31) para não estourar a largura da coluna. - Retorno tátil no play/pause, no zerar e ao começar a rega.
- Notificação opcional por cronômetro (o ícone de folha no card liga e desliga), com botões de Pausar/Retomar e Zerar direto na barra de status.
- Um bonsai que cresce junto com o cronômetro, de semente a ancião ao longo de anos, com uma espécie própria (e uma cor própria) que se revela depois de semanas.
- Segure o dedo no bonsai para regar — gotas caindo e respingo na terra. É puro afago: não altera estado nenhum.
- Tema claro, escuro ou seguir o sistema, no botão do canto superior direito. A escolha fica em SharedPreferences.
Cada cronômetro tem sua própria árvore, desenhada em Canvas a partir de um fator de
crescimento 0..1 derivado do tempo decorrido. A escala é logarítmica: em escala linear
um cronômetro de três meses ficaria idêntico a um de três dias durante quase toda a vida
dele. Com log, cada ordem de grandeza avança um tanto parecido no desenho.
| decorrido | fator | estágio |
|---|---|---|
| 1 min | 0.01 | semente |
| 1 hora | 0.16 | broto |
| 1 dia | 0.42 | muda |
| 2 semanas | 0.62 | formação |
| 1 mês | 0.70 | adulto |
| 3 meses | 0.79 | robusto |
| 6 meses | 0.85 | maduro |
| 1 ano | 0.91 | ancião |
| 3 anos | 1.00 | ancião |
O vaso e a linha da terra não mudam de tamanho — são a régua contra a qual se percebe que a árvore cresceu. O que responde ao fator, em ordem de entrada:
| elemento | entra em | ≈ tempo |
|---|---|---|
| cotilédones (o V de duas folhinhas) | 0.004 – 0.03 | primeiros minutos |
| copa principal | 0.04 – 0.22 | primeira hora |
| galho direito + copa | 0.30 – 0.52 | ~1 dia |
| galho esquerdo + copa | 0.50 – 0.72 | ~2 semanas |
| copa alta (4º pad) | 0.70 – 0.84 | 1 a 3 meses |
| musgo na base | 0.62 – 0.90 | semanas a 6 meses |
| nebari (raízes aflorando) | 0.76 – 0.94 | 3 meses a 1 ano |
Os três últimos são o que impede a faixa dos meses de ser um planalto: sem eles, 1 mês e 1 ano desenhavam praticamente a mesma árvore. Altura, espessura e curvatura do tronco crescem continuamente ao longo de tudo.
Cada cronômetro tem uma espécie própria, derivada do hash do id — estável para sempre, sem
precisar guardar nada. A cor dela não vem com a muda: aparece entre ~2 semanas e ~8
meses (expressionOf), então é recompensa de bonsai tardio. O nome da espécie só se anuncia
no card quando a cor já está visível na copa.
| espécie | como se manifesta |
|---|---|
| ácer | folhagem vira carmim |
| ginkgo | folhagem vira dourado |
| pinheiro | folhagem vira verde-azulado escuro |
| cerejeira | flores rosa pontilhadas nas copas |
| azaleia | flores magenta pontilhadas nas copas |
| romãzeira | poucos frutos alaranjados na parte de baixo das copas |
Cada espécie tem duas versões da cor. Não é capricho: no tema claro as copas são
verde-escuras, então flores precisam ser claras para aparecer; no escuro as copas são
verde-sálvia claras e as flores precisam ser saturadas. As espécies TINT fazem o
caminho oposto — a cor tinge a copa inteira, então precisa contrastar com o fundo da tela,
não com o verde. A escuridão é lida de MaterialTheme.colorScheme.background.luminance(),
e não do tema do sistema, para acertar mesmo quando o usuário força um tema contra ele.
O musgo da base fica sempre verde em qualquer espécie — musgo não é a árvore.
As transições usam smoothstep entre limiares para que galho e copa surjam crescendo em vez
de pipocar na tela. O fator é quantizado em 300 passos, senão a árvore se redesenharia 15
vezes por segundo sem nenhuma mudança visível.
Não existe loop contando segundos, nem serviço em primeiro plano. Cada cronômetro guarda apenas dois números: quanto tempo já acumulou e o instante (epoch) em que a sessão atual começou. O tempo decorrido é sempre derivado do relógio do sistema, então nada se perde se o Android matar o processo.
Na barra de status, quem desenha o tempo subindo é o próprio Android — a notificação usa
setUsesChronometer com o when no passado. O app só reposta a notificação quando o
estado muda (play, pause, zerar, renomear, boot).
O estado vive num JSON em filesDir/stopwatches.json, reescrito de forma atômica a cada
mudança.
A máquina já está preparada:
| ferramenta | versão | onde |
|---|---|---|
| Microsoft OpenJDK | 17.0.20 | C:\Program Files\Microsoft\jdk-17.0.20.8-hotspot |
| Android SDK (platform 35, build-tools 35.0.0, platform-tools) | — | %LOCALAPPDATA%\Android\Sdk |
| Gradle | 8.10.2 | wrapper no projeto + cópia em %LOCALAPPDATA%\Gradle |
Para recompilar:
/Cronometro && ./gradlew assembleDebugO APK sai em app/build/outputs/apk/debug/app-debug.apk.
Ative Opções do desenvolvedor (Ajustes → Sobre o telefone → toque 7× em "Número da versão") e ligue a Depuração USB. Com o cabo conectado:
"$LOCALAPPDATA/Android/Sdk/platform-tools/adb.exe" install -r app/build/outputs/apk/debug/app-debug.apkO build de release é assinado com um keystore que não vive no repositório. O Gradle lê o
caminho e as senhas de ~/.gradle/gradle.properties; sem essas chaves o assembleRelease
ainda roda, só sai sem assinatura.
Uma vez na vida, gere o keystore (escolha uma senha e guarde-a junto do arquivo — perder qualquer um dos dois significa nunca mais conseguir atualizar o app por cima):
keytool -genkeypair -v -keystore ~/keys/bonsai.jks -alias bonsai -keyalg RSA -keysize 4096 -validity 10950Depois acrescente ao ~/.gradle/gradle.properties (nunca ao do projeto):
BONSAI_KEYSTORE=C:/Users/<voce>/keys/bonsai.jks
BONSAI_KEYSTORE_PASSWORD=<sua senha>
BONSAI_KEY_ALIAS=bonsai
BONSAI_KEY_PASSWORD=<sua senha>A partir daí, publicar uma versão é:
./gradlew assembleRelease && gh release create v1.0 app/build/outputs/apk/release/app-release.apk --title "Bonsai 1.0" --notes "Primeira versão."Guarde o
bonsai.jkscomo você guarda senhas. O Android identifica um app pela assinatura: se um dia você compilar com outro keystore, o celular vai recusar a atualização e a única saída será desinstalar — o que apaga todos os cronômetros acumulados.
O projeto abre normalmente no Android Studio via Open nesta pasta — útil para o preview do Compose e o inspetor de layout. Não é necessário para compilar.
local.propertiesaponta osdk.dirdesta máquina e está no.gitignore, como deve ser.
app/src/main/java/com/bonsai/cronometro/
BonsaiApp.kt observa o repositório e mantém a barra de status em dia
MainActivity.kt
data/
Stopwatch.kt modelo: acumulado + instante de início
StopwatchRepository.kt singleton de processo, persistência em JSON
ThemePreference.kt claro / escuro / seguir o sistema
notify/
Notifications.kt montagem e sincronização das notificações
StopwatchActionReceiver.kt botões Pausar/Zerar da notificação
BootReceiver.kt reposta as notificações depois de reiniciar
ui/
HomeScreen.kt lista, diálogos, permissão de notificação, tick da tela
StopwatchCard.kt o card de cada cronômetro
BonsaiArt.kt o bonsai desenhado em Canvas, em função do crescimento
Growth.kt a curva logarítmica e os nove estágios
Species.kt as seis espécies e como a cor delas emerge
Watering.kt a rega ao segurar o dedo
Glyphs.kt play, pause, folha, sol, lua, + e × desenhados à mão
Theme.kt paleta e tipografia
util/TimeFormat.kt
Apache 2.0. Use, modifique e redistribua à vontade, inclusive comercialmente, mantendo o aviso de copyright e declarando o que mudou.
- Android 8.0 (API 26) ou superior.
- No Android 13+ o app pede permissão de notificação na primeira vez que você liga a folha. Negar não quebra nada — o cronômetro continua correndo, só não aparece na barra.