Open2

SwiftUI タンクをCanvasで作る

JboyHashimotoJboyHashimoto

タンクをデザインする

このコードは、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()
}

JboyHashimotoJboyHashimoto

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()
                }
            }
        }
    }
}