リリースしているアプリで今後も継続していくものについては いい加減 Jetpack Compose に移行しようと思っている。

Androidアプリ開発にまとまった時間がとれないので、 細切れ時間でも開発できる環境をつくろうと思って 試行錯誤しているうちに半年くらい経過してしまった。
Jetpack Compose を使った Android アプリの雛形は Android Studio で生成するとして、 そこで生成された MainActivity.kt を修正していきます。
setContent の部分で SketchTheme と出てきますが、これは 今回の AndroidStudio で雛形アプリ作成時に Sketch というアプリ名を指定したためです。 適宜読み替えてください。
// MainActivity.kt
class MainActivity : ComponentActivity() {
private companion object {
val bgColor = Color(238, 232, 213)
val modifier = Modifier.padding().fillMaxSize().background(bgColor).clipToBounds()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SketchTheme {
MyCanvas(modifier)
}
}
}
}
@Composable
fun MyCanvas(m: Modifier){
val stroke = Stroke(1f)
val brush = SolidColor(Color(88, 110, 117))
val path = Path()
path.moveTo(100f, 100f)
path.lineTo(200f, 100f)
path.lineTo(200f, 200f)
path.lineTo(100f, 200f)
path.close()
Canvas(modifier = m){
drawPath(
path = path,
brush = brush,
style = stroke)
}
}
MyCanvas という Composable が定義されていて、そこに Path で四角形を 描画しただけです。
このポストにあるような環境で 開発しているので、次のようにして apk をビルドしてインストールします。
$ ./gradlew assembleDebug
$ adb install app/build/outputs/apk/debug/app-debug.apk
起動するとこうなりました。

線(Stroke)が細い。 とりあえず Stroke(1f) として太さを 1f として安易に指定してあるのがまずい。
いつも使っているペンは 0.35mm 程度なので、ストロークがその太さになるように修正する。
計算方法ですが、 densityDpi を使います。 この値は実行するデバイスごとに異なる値になるのですが、 意味するところは(実行しているデバイスにおける) 1インチあたりのピクセル数 とのこと。
1インチを 25.4mm として計算すれば Stroke に渡すべき太さの値は次のようになる:
val densityDpi = LocalContext.current.resources.displayMetrics.densityDpi
val strokeWidth = 0.35f * densityDpi.toFloat() / 25.4f
val stroke = Stroke(strokeWidth)
つまり、 densityDpi値 : 25.4mm = 求めるピクセル数 : 0.35mm なので 「内項の積=外項の積」によりこれでよいはず。
実行してみる:

さっきより太くなった。たぶん、これで 0.35mm なのであろう。
もっと簡単に解決する方法がありそう。そのうち claude さんとかにきいてみる。
この四角領域をドラッグできるようにしてみる。
MyCanvas を修正する:
// MainActivity.kt
@Composable
fun MyCanvas(m: Modifier){
...
var underDragging by remember { mutableStateOf(false) }
var dragShadow by remember { mutableStateOf(Rect(0f,0f, 0f,0f)) }
val mDnd = m.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset->
if (Rect(100f,100f, 200f,200f).contains(offset)) {
underDragging = true
dragShadow = Rect(100f,100f, 200f,200f)
} else {
underDragging = false
}
},
onDrag = { change, dragAmount->
if (underDragging) {
change.consume()
dragShadow = Rect(
dragShadow.left + dragAmount.x,
dragShadow.top + dragAmount.y,
dragShadow.right + dragAmount.x,
dragShadow.bottom + dragAmount.y)
}
},
onDragEnd = {
underDragging = false
}
)
}
Canvas(modifier = mDnd){
drawPath(
path = path,
brush = brush,
style = stroke)
if (underDragging) {
drawRect(
color = Color.Gray,
topLeft = Offset(dragShadow.left, dragShadow.top),
size = Size(
dragShadow.right - dragShadow.left,
dragShadow.bottom - dragShadow.top)
)
}
}
}
ドラッグ中かどうか underDragging と ドラッグシャドー用の領域 dragShadow を remember しておき、 modifier にドラッグスタート他のイベントが発生したときに実行するファンクションを指す感じで実装します。 (だいたい claude にきいただけです)
行き当たりばったりで実装したので、冗長なコードですが、それはあとでリファクタリングすることにしてまずは実行してみます。

できた!意外に Jetpack Compose いいね。
ドラッグ可能な四角をパスで定義していましたが、 これを Rect で表現することにします。 あとで Drop したときに位置を移動できるようにしたいので、 remeber しておきます。
このように変更:
/*
val path = Path()
path.moveTo(100f, 100f)
path.lineTo(200f, 100f)
path.lineTo(200f, 200f)
path.lineTo(100f, 200f)
path.close()
*/
var box by remember { mutableStateOf(Rect(100f, 100f, 200f, 200f)) }
描画時はそのまま Path を使いたいので、Rect から Path へ変換する補助関数を定義:
val rectToPath: (Rect)->Path = { rect->
Path().apply {
moveTo(rect.left, rect.top)
lineTo(rect.right, rect.top)
lineTo(rect.right, rect.bottom)
lineTo(rect.left, rect.bottom)
close()
}
}
これを使い描画時のコードはこのように変更:
/*
drawPath(
path = path,
brush = brush,
style = stroke)
*/
drawPath(
path = rectToPath(box),
brush = brush,
style = stroke)
あとは onDragStart に指していた関数を変更:
/*
onDragStart = { offset->
if (Rect(100f,100f, 200f,200f).contains(offset)) {
underDragging = true
dragShadow = Rect(100f,100f, 200f,200f)
} else {
underDragging = false
}
},
*/
onDragStart = { offset->
if (box.contains(offset)) {
underDragging = true
dragShadow = box.copy()
} else {
underDragging = false
}
},
完成したコードはこのようになりました。
// MainActivity.kt
...
val rectToPath: (Rect)->Path = { rect->
Path().apply {
moveTo(rect.left, rect.top)
lineTo(rect.right, rect.top)
lineTo(rect.right, rect.bottom)
lineTo(rect.left, rect.bottom)
close()
}
}
@Composable
fun MyCanvas(m: Modifier){
val densityDpi = LocalContext.current.resources.displayMetrics.densityDpi
val strokeWidth = 0.35f * densityDpi.toFloat() / 25.4f
val stroke = Stroke(strokeWidth)
val brush = SolidColor(Color(88, 110, 117))
var box by remember { mutableStateOf(Rect(100f, 100f, 200f, 200f)) }
var underDragging by remember { mutableStateOf(false) }
var dragShadow by remember { mutableStateOf(Rect(0f,0f, 0f,0f)) }
val mDnd = m.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset->
if (box.contains(offset)) {
underDragging = true
dragShadow = box.copy()
} else {
underDragging = false
}
},
onDrag = { change, dragAmount->
if (underDragging) {
change.consume()
dragShadow = Rect(
dragShadow.left + dragAmount.x,
dragShadow.top + dragAmount.y,
dragShadow.right + dragAmount.x,
dragShadow.bottom + dragAmount.y)
}
},
onDragEnd = {
underDragging = false
}
)
}
Canvas(modifier = mDnd){
drawPath(
path = rectToPath(box),
brush = brush,
style = stroke)
if (underDragging) {
drawRect(
color = Color.Gray,
topLeft = Offset(dragShadow.left, dragShadow.top),
size = Size(
dragShadow.right - dragShadow.left,
dragShadow.bottom - dragShadow.top)
)
}
}
}
onDragEnd に指していた関数を次のように変更します:
/*
onDragEnd = {
underDragging = false
}
*/
onDragEnd = {
underDragging = false
box = dragShadow.copy()
}
つまり Drop したときに box の新しい位置(移動先の位置)に変更する処理を追加しただけです。

これでドラッグ対象の四角を自在に移動させることができるようになりました。