📱

Android:Canvasで線をひく(drawLineする)ときにハマったポイント

2022/10/27に公開

はじめに

Androidアプリを作っている際に、Canvasをつかって線をひくときにハマってしまったポイントについて解説します。

https://developer.android.com/reference/android/graphics/Canvas

ハマってしまったポイントとは、複数本の線を順番に引くときの座標系の指定方法です。
結論から述べると、中心線を意識して座標系を考える必要ありました。

サンプルコードでは、下記のような複数本の線を画面の幅いっぱいに、順々に、ひくことを考えてみます。

解説

サンプルコード

全体のサンプルコード全体は下記のとおりです。

MainActivity.kt
MainActivity.kt
private lateinit var binding: ActivityMainBinding

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        requestFullScreen()

        val canvas = drawMultipleLines(this)
        binding.mainActivityLayout.addView(canvas)

    }

    private fun requestFullScreen() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
            window.decorView.windowInsetsController?.show(
                WindowInsets.Type.systemBars()
                // WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars()
            )
        }else{
            window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
            window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
            window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
            window.decorView.systemUiVisibility = (
                    View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                            or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                            or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                            or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                            or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                            or View.SYSTEM_UI_FLAG_FULLSCREEN)
        }

    }
}

class drawMultipleLines(context: Context) : View(context){
    private val paint: Paint = Paint()
    private val paint2: Paint = Paint()
    private val paint3: Paint = Paint()

    private val baseLineWidth = 100F
    private val LineWidth1 = 3F * baseLineWidth
    private val LineWidth2 = 1F * baseLineWidth
    private val LineWidth3 = 2F * baseLineWidth

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        paint.setStrokeWidth(LineWidth1)
        paint.setColor(ContextCompat.getColor(context, R.color.purple_200))
        paint.setStyle(Paint.Style.STROKE)
        canvas?.drawLine(0F, LineWidth1/2,
            width.toFloat(), LineWidth1/2, paint);

        paint2.setStrokeWidth(LineWidth2)
        paint2.setColor(ContextCompat.getColor(context, R.color.purple_500))
        paint2.setStyle(Paint.Style.STROKE)
        canvas?.drawLine(0F, LineWidth1 + LineWidth2/2,
            width.toFloat(), LineWidth1 + LineWidth2/2, paint2);

        paint3.setStrokeWidth(LineWidth3)
        paint3.setColor(ContextCompat.getColor(context, R.color.purple_700))
        paint3.setStyle(Paint.Style.STROKE)
        canvas?.drawLine(0F, LineWidth1 + LineWidth2 + LineWidth3/2,
            width.toFloat(), LineWidth1 + LineWidth2 + LineWidth3/2, paint3);
    }
}
activity_main.xml
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/mainActivityLayout"
    tools:context=".MainActivity"/>

本サプルコードではviewBindingを使用しているので、build.gradle(Module)内でviewBindingを有効にしています。

build.gradle
build.gradle

android {
//...省略...
    viewBinding {
        enabled = true
    }
//...省略...
}

viewBindingをつかってlayoutを表示

まずbindingクラスをlateinitで宣言しています。

MainActivity.kt
private lateinit var binding: ActivityMainBinding

そして、onCreate内でActivityで使用するbindingクラスのインスタンスを作成します。
こちらの詳細な説明は、Android developesの「バインディングオブジェクトを作成する」に記載があります。
その後、binding.rootでルートビューを取得し、setContentViewにセットすることでlayoutをActivityに表示させます。

MainActivity.kt
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)

画面のフルスクリーン化

わかりやすくするため、ステータスバーやナビゲーションバーを非表示(フルスクリーン化)にします。
Android OSのバージョンによって、フルスクリーン化の処理を分けています。

MainActivity.kt
    private fun requestFullScreen() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
            window.decorView.windowInsetsController?.show(
                WindowInsets.Type.systemBars()
                // WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars()
            )
        }else{
            window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
            window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
            window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
            window.decorView.systemUiVisibility = (
                    View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                            or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                            or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                            or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                            or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                            or View.SYSTEM_UI_FLAG_FULLSCREEN)
        }

    }

Android 11以上ではWindowInsetsControllerを使い、Android 4.1 ~ 10ではsetSystemUiVisibilityをつかって実現しています。

Canvasで線をひく

Canvasで線をひくdrawMultipleLines関数について説明します。
線をひく際の流れは下記のとおりです。

  1. Paintクラスのインスタンスを作成
  2. 線の幅、カラー、描画スタイルをセット
  3. drawLineで描画
MainActivity.kt
// Paintクラスのインスタンスを作成
private val paint: Paint = Paint()

// 線幅を設定
private val baseLineWidth = 100F
private val LineWidth1 = 3F * baseLineWidth

//...省略...
override fun onDraw(canvas: Canvas?){
        super.onDraw(canvas)
	// 線幅をセット
        paint.setStrokeWidth(LineWidth1)
	// 線の色をセット
        paint.setColor(ContextCompat.getColor(context, R.color.purple_200))
	/*
	描画スタイルの定数を指定
	Paint.Style.STROKE : ライン
	Paint.Style.FILL_AND_STROKE : 塗りつぶしとライン
	Paint.Style.FILL : 塗りつぶし
	*/
        paint.setStyle(Paint.Style.STROKE)
	// 線をひく
	// drawLine(float startX(X座標の開始点), float startY(Y座標の開始点), float stopX(X座標の終了点), float stopY(Y座標の終了点), Paint paint(Paintクラスのインスタンス))
        canvas.drawLine(0F, LineWidth1/2,
            width.toFloat(), LineWidth1/2, paint);
//...省略...

ここまで1本の線をひく手順です。
ここで注意が必要なのは、canvas.drawLineの座標設定です。
下記の図のように、線の中心線を意識して座標を設定します。
それぞれの線の開始点は図中の緑点になります。

それを表現すると、サンプルコードでは下記のコードに相当します。

MainActivity.xml
canvas.drawLine(0F, LineWidth1/2,width.toFloat(), LineWidth1/2, paint)
canvas.drawLine(0F, LineWidth1 + LineWidth2/2,width.toFloat(), LineWidth1 + LineWidth2/2, paint2)
canvas.drawLine(0F, LineWidth1 + LineWidth2 + LineWidth3/2,width.toFloat(), LineWidth1 + LineWidth2 + LineWidth3/2, paint3)

これで、複数線を順々にひくことができます。

おわりに

最初、Canvasを扱うとき座標系に戸惑いましたが、一度理解してしまえば簡単です。

Discussion