Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ComposeでTimeRangePickerを作る_YUMEMI.grow Mobile #2
Search
kako351
April 12, 2023
Technology
920
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
ComposeでTimeRangePickerを作る_YUMEMI.grow Mobile #2
kako351
April 12, 2023
More Decks by kako351
See All by kako351
Play Billing Library 7.0.0 変更点まとめ@potatotips#88
kako351
0
1.6k
Paging3のSeparatorsを使って LazyColumnにヘッダーや 別のアイテムを挿入する
kako351
0
810
CircleCIでFlakyなテストを再実行する_potatotips#83
kako351
0
240
Composeの座標を取得する ~コーチマークにおける活用事例~_DroidKaigi.collect#1
kako351
2
3.2k
チームで導入する Jetpack Compose あの素晴らしいLTをもう一度.ver
kako351
1
1.4k
【DevFest & ADS JP 22】チームで導入するJetpackCompose@おいしい健康
kako351
0
2.6k
Other Decks in Technology
See All in Technology
KPIだけでは評価できないプロダクトが考えるべき Evalsという第二の評価系 / Beyond KPIs: Evals as a Second Evaluation Framework for Products #PdEConf
aki_iinuma
3
3.5k
GuardDuty 検知対応を DevOps Agent で効率化しようとしている話 / GuardDuty Investigations with DevOps Agent
masahirokawahara
1
280
こんなアーキテクチャ図は嫌だ BEYOND THE TIME: 半年後の自分へ贈る15のメッセージ / 15 of Anti-pattern in AWS Architecture Diagrams
naospon
3
220
Genie Code ワークショップ 応用編 / Genie-Code-Workshop-advanced
databricksjapan
PRO
0
310
Beyond the Hype: Practical AI for Your Oracle Database with MCP
thatjeffsmith
1
230
Sony-DroidKaigi2026
sony
1
310
AIで実装は速くなった。なのにプロダクトは速くならない。職能の壁を越えて価値のフローを設計する
nwiizo
6
6.7k
いま、生成AIにKaggleをどこまで 任せられるか — ROGIIコンペでの進め方とTips
k951286
2
1.3k
PdMをやめて、 "プロダクトビルダー"という 働き方に変えました / PdM to Product Builder
shikichee
2
680
Bet AI Day 2026丨AIを「使う」から、AIが「働く」へ ― LayerXが進める「組織AI」の社会実装
layerx
PRO
2
2.1k
全員がプロダクトへ向き合う組織を持続成長させるために——組織づくりのフライホイールと4象限 / The Flywheel Model and Four Quadrants for Organizational Design
hiro_torii
3
760
全社に広がるMCPサーバーを、 どう安全に管理するか MCPass開発の舞台裏
mtpooh
3
280
Featured
See All Featured
The Spectacular Lies of Maps
axbom
PRO
1
970
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
3.5k
Exploring the Power of Turbo Streams & Action Cable | RailsConf2023
kevinliebholz
37
6.6k
Hiding What from Whom? A Critical Review of the History of Programming languages for Music
tomoyanonymous
3
1.2k
How STYLIGHT went responsive
nonsquared
100
6.3k
The Cult of Friendly URLs
andyhume
79
7k
A designer walks into a library…
pauljervisheath
211
25k
Code Reviewing Like a Champion
maltzj
528
40k
Rebuilding a faster, lazier Slack
samanthasiow
85
9.6k
Fireside Chat
paigeccino
42
4k
What the history of the web can teach us about the future of AI
inesmontani
PRO
1
680
Deep Space Network (abreviated)
tonyrice
0
290
Transcript
Composeで TimeRangePickerを作る YUMEMI.grow Mobile #2 kako351@おいしい健康
自己紹介 kako351 / @kako_351 おいしい健康 Androidエンジニア Androidエンジニア募集中! 仕事 • バイク(ハンターカブ)
• ギター • コーヒー自宅焙煎 趣味
What’s new おいしい健康
食事記録機能をリリースしました! 🎉 時計部分はJetpack ComposeのCanvasで書かれていま す。(私ではなくて同僚が実装してくれました...!) これをみてCanvas面白そうだなと思ったのが今回の話す内容の きっかけです。 What’s new おいしい健康
Jetpack ComposeでTimeRangePickerを 作れるようになる TimeRangePickerとは TimeRangePickerは、ユーザーが時間範囲を選 択できるようにするUIコンポーネントです。開 始時間と終了時間の間で選択された時間範囲を 表します。 本日のゴール
本日話す内容 • 弧を描く • 時計盤を作る • ドラッグ操作を制御する • ドラッグ操作で時刻を設定する •
2つの時刻を繋ぐ弧を描く • https://developer.android.com/ • https://github.com/Droppers/TimeRangePicker • https://ja.wikipedia.org/wiki/%E4%B8%89%E8%A7%9 2%E9%96%A2%E6%95%B0 アジェンダ 参考
弧を描く
Canvas( //... ) { drawArc( color = Color.LightGray, startAngle =
0f, sweepAngle = 360f, useCenter = false, style = Stroke(width = 50f), )
Canvas( //... ) { drawArc( color = Color.LightGray, startAngle =
0f, sweepAngle = 360f, useCenter = false, style = Stroke(width = 50f), ) 弧を描くには drawArcを利用 します
• drawArc • drawCircle • drawOval • drawLine • drawRect
draw〇〇 • drawRoundRect • drawPath • drawPoint • drawImage
時計盤を作る
for (hour in 0..23) { val centerX = size.width /
2 val centerY = size.height / 2 val radius = (size.width / 2) - 50f val lineLength = 20f val angle = hour * (360 / 24) // 360度を24分割 val radian = Math.toRadians(angle.toDouble()) val startX = (centerX + radius * Math.cos(radian)).toFloat() val startY = (centerY + radius * Math.sin(radian)).toFloat() val endX = (centerX + (radius - lineLength) * Math.cos(radian)).toFloat() val endY = (centerY + (radius - lineLength) * Math.sin(radian)).toFloat() drawLine( color = Color.Black, start = Offset(x = startX, y = startY), end = Offset(x = endX, y = endY), cap = StrokeCap.Round, strokeWidth = 3f ) }
for (hour in 0..23) { val centerX = size.width /
2 val centerY = size.height / 2 val radius = (size.width / 2) - 50f val lineLength = 20f val angle = hour * (360 / 24) // 360度を24分割 24時間計の時計盤にするため 0時から23時まで表します 毎時刻の角度を求めます
val radian = Math.toRadians(angle.toDouble()) val startX = (centerX + radius
* Math.cos(radian)).toFloat() val startY = (centerY + radius * Math.sin(radian)).toFloat() val endX = (centerX + (radius - lineLength) * Math.cos(radian)).toFloat() val endY = (centerY + (radius - lineLength) * Math.sin(radian)).toFloat() 角度からX Y座標を求めます 孤の中心を囲むように配置するため にcanterX, centerYを足しています
drawLine( color = Color.Black, start = Offset(x = startX, y
= startY), end = Offset(x = endX, y = endY), cap = StrokeCap.Round, strokeWidth = 3f ) 線を引く開始位置の座標と終了位置 の座標を渡して線を描画します。
if (hour % 6 == 0) { drawIntoCanvas { it.nativeCanvas.drawText(
"$hour", endX, endY + lineLength, Paint().apply { color = Color.Black.toArgb() textSize = 45f textAlign = Paint.Align.CENTER } ) } } else { 時計盤ぽくするために、6の倍数時 刻の時はその時間をテキストで表示 してみます。
if (hour % 6 == 0) { drawIntoCanvas { it.nativeCanvas.drawText(
"$hour", endX, endY + lineLength, Paint().apply { color = Color.Black.toArgb() textSize = 45f textAlign = Paint.Align.CENTER } ) } } else { Canvas内でテキストを表示するには drawIntoCanvasの中で nativeCanvas.drawText() を呼び出す 必要があります。
if (hour % 6 == 0) { drawIntoCanvas { it.nativeCanvas.drawText(
"$hour", endX, endY + lineLength, Paint().apply { color = Color.Black.toArgb() textSize = 45f textAlign = Paint.Align.CENTER } ) } } else { 時刻が右中央から始まってしまいま す。 中央上に配置したい。
val angle = hour * (360 / 24) // 360度を24分割
val radian = Math.toRadians(angle.toDouble()) val startX = (centerX + radius * Math.cos(radian)).toFloat() val startY = (centerY + radius * Math.sin(radian)).toFloat() 中央 (x:0 y:0) cosθ = 1 sinθ = 0 0時 = 0度(angle = 0)と 考えると... 中央座標からの0度の座 標を指定しているので 右中央に配置されます が右中
val angle = hour * (360 / 24)// 360度を24分割 val
radian = Math.toRadians(angle.toDouble()) - (PI / 2) val startX = (centerX + radius * Math.cos(radian)).toFloat() val startY = (centerY + radius * Math.sin(radian)).toFloat() 0時を中央上に配置する 場合、反時計周りに90 度回転すればよい angleから90度を引けば いい PI = 180度 / 2 を引きま す
ドラッグ操作を 制御する
var dragOffsetX by remember { mutableStateOf(0f) } var dragOffsetY by
remember { mutableStateOf(0f) } Canvas( modifier = Modifier .pointerInput(Unit) { detectDragGestures( // … onDrag = { change, dragAmount -> change.consume() dragOffsetX = change.position.x dragOffsetY = change.position.y } ) }, ) { drawCircle( color = Color.Blue, radius = 100f, center = Offset(x = dragOffsetX, y = dragOffsetY) )
Canvas( modifier = Modifier .pointerInput(Unit) { detectDragGestures( // … onDrag
= { change, dragAmount -> change.consume() dragOffsetX = change.position.x dragOffsetY = change.position.y } ) }, Canvasでジェスチャーを検知したい 時は.pointerInputを呼び出します
Canvas( modifier = Modifier .pointerInput(Unit) { detectDragGestures( // … onDrag
= { change, dragAmount -> change.consume() dragOffsetX = change.position.x dragOffsetY = change.position.y } ) }, 双方向のドラッグ操作を制御したい 時はdetectDragGesturesを呼び出し ます
・draggable修飾子 縦方向のみのドラッグや横方向のみのドラッグなど単一方向のドラッグ操作を制 御したい時にはdraggable修飾子で十分です。 ・pointerInput修飾子 ( + detectDragGestures) 縦横斜めなど、ドラッグ操作全体を制御したい時はpointerInput修飾子の中で detectDragGesturesを呼び出します。 単一方向ドラッグと双方向ドラッグ
ドラッグ操作で 時刻を設定する
var centerX by remember { mutableStateOf(0f) } var centerY by
remember { mutableStateOf(0f) } var dragOffsetX by remember { mutableStateOf(0f) } var dragOffsetY by remember { mutableStateOf(0f) } // xy座標からAngleを決める val dragAngle = remember(key1 = dragOffsetX, key2 = dragOffsetY) { derivedStateOf { Math.toDegrees( Math.atan2( (dragOffsetY - centerY).toDouble(), (dragOffsetX - centerX).toDouble() ) ).toFloat() } } ドラッグ中の座標から角度を求める
var centerX by remember { mutableStateOf(0f) } var centerY by
remember { mutableStateOf(0f) } Canvas( modifier = Modifier .onSizeChanged { centerX = it.width / 2f centerY = it.height / 2f } Canvasのサイズを取得する onSizeChangedでComposable外に Comsableのサイズを渡します。 サイズ取得の方法はいくつかありますが、今回 はonSizeChangedがニーズに合っていました
val timeX = remember(key1 = dragAngle.value, key2 = centerX) {
derivedStateOf { val radius = centerX val angle = Math.toRadians(dragAngle.value.toDouble()) (radius * Math.cos(angle)).toFloat() + centerX } } val timeY = remember(key1 = dragAngle.value, key2 = centerY) { derivedStateOf { val radius = centerY val angle = Math.toRadians(dragAngle.value.toDouble()) (radius * Math.sin(angle)).toFloat() + centerY } } ドラッグ中の角度から弧上の座標を求める
drawCircle( color = Color.Blue, radius = 25f, center = Offset(x
= timeX.value, y = timeY.value), ) 座標に合わせて円を描画する
val time = remember(key1 = dragAngle.value) { derivedStateOf { var
angle = dragAngle.value + Math.toDegrees(PI / 2) if (angle < 0) angle += 360f if (angle >= 360f) angle -= 360f val hour = (angle / 15).toInt() val minute = ((angle % 15) / 15 * 60).toInt() "%02d:%02d".format(hour, minute) } } ドラッグ中の角度から時刻を求める
2つの時刻を繋ぐ 弧を描く
var endTimeDragOffsetX by remember { mutableStateOf(0f) } var endTimeDragOffsetY by
remember { mutableStateOf(0f) } // xy座標からAngleを決める val endTimeDragAngle = remember(key1 = endTimeDragOffsetX, key2 = endTimeDragOffsetY) { derivedStateOf { Math.toDegrees( Math.atan2( (endTimeDragOffsetY - centerY).toDouble(), (endTimeDragOffsetX - centerX).toDouble() ) ).toFloat() } } 終了時刻分の角度を求める
val startAngle = startTimeDragAngle.value val sweepAngle = when(endTimeDragAngle.value < startTimeDragAngle.value)
{ true -> endTimeDragAngle.value - startTimeDragAngle.value + 360f false -> endTimeDragAngle.value - startTimeDragAngle.value } drawArc( color = Color.Blue, startAngle = startAngle, sweepAngle = sweepAngle, useCenter = false, style = Stroke( width = 50f, cap = StrokeCap.Round ), ) ドラッグ操作した角度に合わせて弧を描画する
val startAngle = startTimeDragAngle.value val sweepAngle = when(endTimeDragAngle.value < startTimeDragAngle.value)
{ true -> endTimeDragAngle.value - startTimeDragAngle.value + 360f false -> endTimeDragAngle.value - startTimeDragAngle.value } drawArc( color = Color.Blue, startAngle = startAngle, sweepAngle = sweepAngle, useCenter = false, style = Stroke( width = 50f, cap = StrokeCap.Round ), ) ドラッグ操作した角度に合わせて弧を描画する drawArcはstartAngleとsweepAngleの 間を時計回りに描画します。 数字の小さい方から大きい方へ
val startAngle = startTimeDragAngle.value val sweepAngle = when(endTimeDragAngle.value < startTimeDragAngle.value)
{ true -> endTimeDragAngle.value - startTimeDragAngle.value + 360f false -> endTimeDragAngle.value - startTimeDragAngle.value } drawArc( color = Color.Blue, startAngle = startAngle, sweepAngle = sweepAngle, useCenter = false, style = Stroke( width = 50f, cap = StrokeCap.Round ), ) ドラッグ操作した角度に合わせて弧を描画する 開始位置が終了位置より大きくなる ときは360度足します
https://github.com/kako351/Compose-TimeRangePicker Githubに公開しています
• Canvasコンポーザブル内ではdrawArc, drawCircle,drawLineなどで図形や線を 描画できます • Canvas内にテキストを表示したい場合は、drawIntoCanvas内でdrawTextを使 います • Canvas上のドラッグ操作を検知するならpointerInput修飾子内で detectDragGesturesを呼び出します
まとめ
ご静聴 ありがとう ございました 39 YUMEMI.grow Mobile #2 2023/04/12 kako351@おいしい健康