［Android］カスタムビューの作り方 (フルカスタムコンポーネント)

# はじめに

Android では[公式](https://developer.android.com/guide/topics/ui/custom-components)にあるとおりカスタムビューを作成する方法が何パターンかあります。
今回はフルカスタムコンポーネントを作成してみたので作成方法をまとめたいと思います。

| 名称 | 説明 |
| --- | --- |
| フルカスタムコンポーネント | `View`を継承していちから新たなコンポーネントを作成するパターン |
| 複合コンポーネント | 既存のコンポーネントを複数組み合わせて新たなコンポーネントを作成するパターン |

![mermaid.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/ad67be1d-78a4-4abf-75ec-a915df29d736.png)

今回は`number`と`displayMode`を設定すると、モードごとに数字を表示する次のようなビューを作ろうと思います。

| ![Screenshot_1577611733.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/6de6ed67-7aa2-bdc7-7e21-13cecb3ad2ff.png) | ![Screenshot_1577612085.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/2d67bfb3-f978-37d4-577a-f8cee2274b75.png)| 
| --- | --- |

# 1. 独自の`View`クラスを継承する
フルカスタムコンポーネントを作成するために、まず`View`を継承したクラスを実装します。

```NumberView.kt
class NumberView(context : Context, attributeSet: AttributeSet) : View(context, attributeSet)
```

このように`View`を継承したクラスを定義するとレイアウトで`CustomView`を記述できるようになります。

```main_activity.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

    <kaleidot725.customviewsample.NumberView
        android:id="@+id/number_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:number="1234567890"
        app:displayMode="japanese"/>

</FrameLayout>
```

# 2. 独自の`Attributes`を宣言する

フルカスタムコンポーネントで独自の`Attriubtes`を利用するには、
`values`に`attrs.xml`を作成し、独自の`Attributes`を宣言する必要があります。
次のように`attrs.xml`を記述すると、レイアウトで`Attributes`で利用できるようになります。

```attrs.xml
<resources>
    <declare-styleable name="NumberView">
        <attr name="number" format="integer"/>
        <attr name="displayMode" format="enum">
            <enum name="number" value="0"/>
            <enum name="english" value="1"/>
            <enum name="japanese" value="2"/>
        </attr>
    </declare-styleable>
</resources>
```

```main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

    <kaleidot725.customviewsample.NumberView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:number="1" 
        app:displayMode="number"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```

# 3. 独自の`Attributes`を取得する
`obtainStyledAttributes `を利用すれば、独自に宣言した`attributes`を取得できます。
`attributes`を宣言すると`R.styleable.<CustomView名称>_<attributes名称>`を指定できるようになっているので、
次のようにその`ID`を`obtainStyledAttributes`に指定して`attributes`を取得します。

```NumberView.kt
    private var number : Int = 0
    private var displayMode : Int = 0

    init {
        context.theme.obtainStyledAttributes(attributeSet, R.styleable.NumberView, 0, 0).apply {
            try {
                number = getInteger(R.styleable.NumberView_number, 0)
                displayMode = getInteger(R.styleable.NumberView_displayMode, 0)
            } finally {
                recycle()
            }
        }
    }
```

# 4. `onDraw`に描画処理を記述する

`View.onDraw`をオーバーライドすることで`View`の描画内容を変更できます。
`View`の描画には`Canvas`を利用します、`Canvas`でどのような描画ができるかは次に詳しくまとまっています。

- [Canvas | Android Developers](https://developer.android.com/reference/android/graphics/Canvas)

今回は`Canvas`の`drawText`で文字の描画、`drawRect`で背景の描画を実装してみます。
また文字の描画では`number`と`displayMode`に応じて描画する文字列を変換するようにしてみます。

```NumberView.kt
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        val backgroundPaint = Paint().apply {
            setColor(Color.GREEN)
        }
        canvas?.drawRect(0f, 0f, this.width.toFloat(), this.height.toFloat(), backgroundPaint)

        val textPaint = Paint().apply {
            setColor(Color.BLACK)
            textSize = 50f
        }
        val text = getDisplayNumber(number, displayMode)
        val textWidth = getTextWidth(text, textPaint)
        canvas?.drawText(text, (this.width / 2f) - (textWidth / 2), this.height / 2f, textPaint)
    }

    private fun getTextWidth(text : String, paint : Paint) : Float {
        val bounds = Rect()
        paint.getTextBounds(text, 0, text.length, bounds)
        return bounds.width().toFloat() + bounds.left.toFloat()
    }

    private fun getDisplayNumber(number : Int, displayMode : Int) : String {
        fun String.replace(vararg pairs: Pair<String, String>): String =
            pairs.fold(this) { acc, (old, new) -> acc.replace(old, new, ignoreCase = true) }

        return when(displayMode) {
            0 -> { number.toString() }
            1 -> { number.toString().replace(
                    "0" to "〇", "1" to "一", "2" to "二", "3" to "三", "4" to "四",
                    "5" to "五", "6" to "六", "7" to "七", "8" to "八", "9" to "九"
                )
            }
            else -> { "error" }
        }
    }
```

すると指定した`Number`が`Japanse`に変換されて表示されるようになります。

![Screenshot_1577611733.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/6de6ed67-7aa2-bdc7-7e21-13cecb3ad2ff.png)

# 5. 独自の`Attributes`を変更できるようにする

最後に独自の`Attributes`を変更できるように`setNumber`と`setDisplayMode`メソッドを用意します。
`Attributes`の変更を反映させるために変更後は`invalidate`と`requestLayout`を呼ぶ必要があります。

```NumberView.kt
    private var number : Int = 0
    private var displayMode : Int = 0

    fun setNumber(number : Int) {
        this.number = number
        this.invalidate()
        this.requestLayout()
    }

    fun setDisplayMode(displayMode: Int) {
        this.displayMode = displayMode
        this.invalidate()
        this.requestLayout()
    }
```

試しに`NumberView`を取得して、`setNumber`と`setDisplayMode`を呼び出してみます。

```
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val numberView : NumberView = findViewById(R.id.number_view)
        numberView.setNumber(999999)
        numberView.setDisplayMode(0)
    }
}

```

こんな感じで設定変更が適用され表示も変化するようになります。

![Screenshot_1577612085.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/2d67bfb3-f978-37d4-577a-f8cee2274b75.png)

# おわりに

今回作成したサンプルは次にまとめています。
なので必要に応じて閲覧していただければと思います。

<a href="https://github.com/kaleidot725/android-mylab/tree/master/CustomViewSample"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/180757ac-284d-78e8-4a7e-2eff07903da1.png" height=125/></a>

# 参考文献

- [Custom View Component | Android Developers](https://developer.android.com/guide/topics/ui/custom-components)
- [Canvas | Android Developers](https://developer.android.com/reference/android/graphics/Canvas)