SwiftUI タンクをCanvasで作る

タンクをデザインする
このコードは、SwiftUIを使用して円筒タンクのアニメーションを作成します。主な特徴は以下の通りです:
Canvasを使用して、タンクの外観と内容物を描画しています。
タンクの充填レベルはfillPercentageという状態変数で制御され、0%から100%まで変更できます。
タンクの外観は灰色の線で描かれ、充填部分は青色で表示されます。
現在の充填パーセンテージがタンクの中央に白色のテキストで表示されます。
スライダーを使用して、充填レベルを動的に調整できます。
このコンポーネントを使用するには、CylinderTankView(fillPercentage: 50)のように初期充填パーセンテージを指定して呼び出します。
アニメーションをさらに滑らかにしたい場合は、withAnimationを使用してスライダーの値の変更をラップすることができます。また、タンクの色やサイズなどをカスタマイズするためのパラメータを追加することも可能です。
import SwiftUI
struct CylinderTankView: View {
@State private var fillPercentage: Double
init(fillPercentage: Double) {
_fillPercentage = State(initialValue: fillPercentage)
}
var body: some View {
VStack {
Canvas { context, size in
let width = size.width
let height = size.height
let centerX = width / 2
// Draw cylinder
context.stroke(
Path { path in
path.addLines([
CGPoint(x: centerX - width * 0.3, y: height * 0.1),
CGPoint(x: centerX - width * 0.3, y: height * 0.9),
CGPoint(x: centerX + width * 0.3, y: height * 0.9),
CGPoint(x: centerX + width * 0.3, y: height * 0.1)
])
},
with: .color(.gray),
lineWidth: 2
)
// Draw ellipses for top and bottom
context.stroke(
Ellipse().path(in: CGRect(x: centerX - width * 0.3, y: height * 0.1 - 10, width: width * 0.6, height: 20)),
with: .color(.gray),
lineWidth: 2
)
context.stroke(
Ellipse().path(in: CGRect(x: centerX - width * 0.3, y: height * 0.9 - 10, width: width * 0.6, height: 20)),
with: .color(.gray),
lineWidth: 2
)
// Draw fill
let fillHeight = height * 0.8 * CGFloat(fillPercentage / 100)
context.fill(
Path { path in
path.addRect(CGRect(x: centerX - width * 0.3, y: height * 0.9 - fillHeight, width: width * 0.6, height: fillHeight))
},
with: .color(.blue)
)
// Draw fill percentage text
let text = Text("\(Int(fillPercentage))%")
.font(.system(size: 24, weight: .bold))
.foregroundColor(.red)
context.draw(text, in: CGRect(x: centerX - 50, y: height * 0.5 - 15, width: 100, height: 30))
}
.frame(width: 200, height: 300)
Slider(value: $fillPercentage, in: 0...100, step: 1)
.padding()
}
}
}
struct ContentView: View {
var body: some View {
CylinderTankView(fillPercentage: 50)
}
}
#Preview {
ContentView()
}

Jetpack Composeでもできるのか?
このJetpack Composeのバージョンは、SwiftUIの実装と同様の機能を提供します:
Canvasを使用して円筒形のタンクを描画します。
タンクの輪郭、上下の楕円、充填部分を描画します。
充填パーセンテージをタンクの中央に表示します。
スライダーを使用して充填レベルを調整できます。
主な違いは以下の通りです:
Jetpack ComposeではCanvasの描画APIが若干異なります。例えば、drawLine、drawOval、drawRectなどの個別の関数を使用します。
テキスト描画にはAndroidのネイティブCanvasを使用しています。これは、Jetpack ComposeのCanvasが直接テキスト描画をサポートしていないためです。
状態管理にrememberとmutableStateOfを使用しています。
このコードを@Composable関数として定義しているので、Jetpack Composeを使用するAndroidアプリケーションの任意の場所でCylinderTankView(initialFillPercentage = 50f)のように呼び出すことができます。
なお、このコードはKotlinで書かれており、Androidプロジェクトで使用することを想定しています。必要に応じて、色やサイズ、レイアウトなどを調整できます。
package com.junichi.canvastemplate
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Slider
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun CylinderTankView(initialFillPercentage: Float) {
var fillPercentage by remember { mutableFloatStateOf(initialFillPercentage) }
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Canvas(modifier = Modifier.size(200.dp, 300.dp)) {
val width = size.width
val height = size.height
val centerX = width / 2f
// Draw cylinder
drawLine(
color = Color.Gray,
start = Offset(centerX - width * 0.3f, height * 0.1f),
end = Offset(centerX - width * 0.3f, height * 0.9f),
strokeWidth = 2f
)
drawLine(
color = Color.Gray,
start = Offset(centerX + width * 0.3f, height * 0.1f),
end = Offset(centerX + width * 0.3f, height * 0.9f),
strokeWidth = 2f
)
// Draw ellipses for top and bottom
drawOval(
color = Color.Gray,
topLeft = Offset(centerX - width * 0.3f, height * 0.1f - 10f),
size = Size(width * 0.6f, 20f),
style = Stroke(width = 2f)
)
drawOval(
color = Color.Gray,
topLeft = Offset(centerX - width * 0.3f, height * 0.9f - 10f),
size = Size(width * 0.6f, 20f),
style = Stroke(width = 2f)
)
// Draw fill
val fillHeight = height * 0.8f * (fillPercentage / 100f)
drawRect(
color = Color.Blue,
topLeft = Offset(centerX - width * 0.3f, height * 0.9f - fillHeight),
size = Size(width * 0.6f, fillHeight)
)
// Draw fill percentage text
drawContext.canvas.nativeCanvas.apply {
drawText(
"${fillPercentage.toInt()}%",
centerX,
height * 0.5f,
android.graphics.Paint().apply {
color = android.graphics.Color.RED
textSize = 24.sp.toPx()
textAlign = android.graphics.Paint.Align.CENTER
isFakeBoldText = true
}
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Slider(
value = fillPercentage,
onValueChange = { fillPercentage = it },
valueRange = 0f..100f,
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
fun CylinderTankScreen() {
CylinderTankView(initialFillPercentage = 50f)
}
Main
package com.junichi.canvastemplate
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.junichi.canvastemplate.ui.theme.CanvasTemplateTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
CanvasTemplateTheme {
Surface(
modifier = Modifier.fillMaxSize(),// fillMaxSizeは、画面いっぱいに表示するための関数
color = MaterialTheme.colorScheme.background// colorSchemeは、Material Designの色を設定するための関数
) {
// ここで、Textを表示する関数を呼び出す
CylinderTankScreen()
}
}
}
}
}