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
910
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
790
CircleCIでFlakyなテストを再実行する_potatotips#83
kako351
0
230
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
BigQuery を検索ソースとした AI Agent の作り方って 〇〇 通りあんねん
satohjohn
0
120
Claude Mythos、Fable...フロンティアAIの最新動向と企業のセキュリティ対策
flatt_security
0
160
現場をAIで動かす「フィジカル AI」の組み込み設計の考え方【SORACOM Discovery 2026】
soracom
PRO
0
120
Vポイント分析基盤におけるデータモデリング20年史
taromatsui_cccmkhd
4
780
データと地図で読む 大井町の「かわるもの、かわらないもの」
yoshiyama_hana
0
490
AI研修(Day1)【MIXI 26新卒技術研修】
mixi_engineers
PRO
1
1.9k
ソフトウェアアーキテクチャ研修【MIXI 26新卒技術研修】
mixi_engineers
PRO
2
740
AWS環境のセキュリティ不安を解消した企業事例 ~よくある課題と対策を一挙公開~
asanoharuki
0
220
システム監視入門
grimoh
4
690
オートマトンと字句解析でRoslynを読む
tomokusaba
0
110
データベース研修【MIXI 26新卒技術研修】
mixi_engineers
PRO
1
500
AI エージェント時代のデジタルアイデンティティ
fujie
0
280
Featured
See All Featured
Designing for Performance
lara
611
70k
Bash Introduction
62gerente
615
220k
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
370
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
What's in a price? How to price your products and services
michaelherold
247
13k
BBQ
matthewcrist
89
10k
Unsuck your backbone
ammeep
672
58k
Claude Code のすすめ
schroneko
67
230k
KATA
mclloyd
PRO
35
15k
The untapped power of vector embeddings
frankvandijk
2
1.8k
My Coaching Mixtape
mlcsv
0
180
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
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@おいしい健康