【Kotlin/Android】カスタムViewの実装方法!onDrawや属性の使い方
この記事からわかること
- Android Studio/KotlinでカスタムViewを実装する方法
- onDrawメソッドとは?
- カスタム属性の定義
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Android Studio:Koala
- Kotlin:1.8.20
公式リファレンス:Create custom view components
カスタムビューを実装する
Androidアプリでは標準のビューでは対応できない特別なユーザーインターフェース要素や動作を実現するためにカスタムビューを実装することができるようになっています。
カスタムビューで定義することで独自のデザインやユーザーインタラクション(タッチやドラッグなど)の処理を細かくコントロールすることができるようになるのが大きなメリットになります。
カスタムビュー管理クラスを定義する
プロジェクト内でカスタムビューを活用するためには「New」>「 UiComponent」>「Custom View」からカスタムビュークラス名を自動生成することが可能です。
この手順でカスタムビューを追加するといろいろなデモコードが追加されてややこしくなってしまうので一旦必要最低限のコードまで省略したのが以下になります。カスタムビュークラスはView
を継承し、コードやXMLからビュー生成ができるようにするためのconstructor
を定義します。
class MyCustomView : View {
/** ①コードからのビュー作成用 */
constructor(context: Context) : super(context) {
init(null, 0)
}
/**
* ②XMLからのビュー作成用(属性)
* XML <com.example.MyCustomView ... /> と指定できるようにする
*/
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(attrs, 0)
}
/**
* ③XMLからのビュー作成用(属性 + スタイル)
* XMLで <com.example.MyCustomView style="@style/MyStyle" ... /> と指定できるようにする
*/
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
context,
attrs,
defStyle
) {
init(attrs, defStyle)
}
/** 基本の初期化メソッド */
private fun init(attrs: AttributeSet?, defStyle: Int) { }
/** ビューが画面に描画される際に呼び出されるメソッド */
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
}
}
ついでに自動生成するとカスタムビューのプレビュー用のレイアウトファイルsample_my_custom_view
も自動生成してくれます。<com.example.MyCustomView ... />
でビューを参照できていることを確認できます。
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.XXXXXX.MyCustomView
android:background="@color/black"
android:layout_width="300dp"
android:layout_height="300dp" />
</FrameLayout>
onDrawメソッド
onDraw
はビューが画面に描画される際に呼び出されるメソッドです。引数のCanvas
オブジェクトを使用してビュー上に図形やテキスト、画像などを描画することが可能です。例えばテキストを追加で描画したい場合はandroid.graphics.Paint
で描画する際のスタイルやフォーマットを指定し、drawText
で描画します。
private val paint = Paint()
override fun onDraw(canvas: Canvas) {
paint.color = Color.DKGRAY
paint.textSize = 100f
canvas.drawText("Hello World" , 10f, 100f, paint)
super.onDraw(canvas)
}
- テキストの描画:Canvas.drawText
- 図形の描画:Canvas.drawRect, Canvas.drawCircle, Canvas.drawPath
- 画像の描画:Canvas.drawBitmap
onTouchEventメソッド
onTouchEvent
はビューがタッチされた際に呼び出されるメソッドです。引数のMotionEvent
オブジェクトからイベントの種類を識別できます。
/** ビューがタッチされた際に呼び出されるメソッド */
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN) {
Toast.makeText(context, "タッチしたよ", Toast.LENGTH_LONG).show()
return true
}
return super.onTouchEvent(event)
}
onMeasureメソッド
onMeasure
はビューのサイズを定義するためのメソッドです。setMeasuredDimension
を使用してビューのサイズを設定します。
/** ビューのサイズを定義するメソッド */
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 例:固定で200 * 100サイズにする
val width = 200
val height = 100
// サイズを設定
setMeasuredDimension(width, height)
}
widthMeasureSpec/heightMeasureSpec
引数から受け取れるwidthMeasureSpec
/heightMeasureSpec
ではモード情報とサイズ情報が取得することができます。モードとサイズの取得にはgetMode
/getSize
を使用します。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
}
取得できるモードは以下の3種類です。
- MeasureSpec.AT_MOST・・・親ビューから最大サイズを指定された場合(子ビューのサイズは最大サイズ以下なら自由)
- MeasureSpec.EXACTLY・・・親ビューから正確なサイズを指定された場合(子ビューのサイズは親から指定されたサイズ)
- MeasureSpec. UNSPECIFIED・・・親ビューからサイズの制約を指定されない場合(子ビューのサイズは任意)
実装例
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = 200
val desiredHeight = 100
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
// 幅の測定
val width: Int = when (widthMode) {
// 親からのサイズに従う
MeasureSpec.EXACTLY -> widthSize
// 最大サイズに制限
MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
// 制限なし
MeasureSpec.UNSPECIFIED -> desiredWidth
else -> desiredWidth
}
// 高さの測定
val height: Int = when (heightMode) {
// 親からのサイズに従う
MeasureSpec.EXACTLY -> heightSize
// 最大サイズに制限
MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)
// 制限なし
MeasureSpec.UNSPECIFIED -> desiredHeight
else -> desiredHeight
}
// 測定されたサイズを設定
setMeasuredDimension(width, height)
}
invalidateメソッド
ビューを明示的に再描画させる(onDrawを再度呼び出す)にはinvalidate
メソッドを使用します。invalidate
自体は現在のビューを無効にするメソッドで、UIスレッドで呼び出す必要があります。
例えばテキストを入れ替える機能を実装する場合は以下のようにchangeText
の最後でinvalidate
を呼び出します。記述し忘れるとビューが更新されません。
private val paint = Paint()
private var text = "Hello World"
override fun onDraw(canvas: Canvas) {
paint.color = Color.DKGRAY
paint.textSize = 100f
canvas.drawText(text , 10f, 100f, paint)
super.onDraw(canvas)
}
public fun changeText(text: String) {
this.text = text
invalidate() // 再描画が始まる
}
カスタム属性を定義する
カスタムビューではXML側から属性を指定することでビューの外観や動作をコントロールできるようにカスタム属性を定義することが可能です。カスタム属性を定義するにはリソースに<declare-styleable>
を追加します。
attr
タグを追加してname
に属性名をformat
にデータ型を指定します。
<resources>
<declare-styleable name="MyCustomView">
<attr name="exampleString" format="string" />
<attr name="exampleDimension" format="dimension" />
<attr name="exampleColor" format="color" />
<attr name="exampleDrawable" format="color|reference" />
</declare-styleable>
</resources>
カスタム属性をXML側で利用できるようにするために名前空間(name space)を明示的に指定する必要があるので注意してください。名前空間xmlns:app="http://schemas.android.com/apk/res-auto"
を追加してください。xmlns:XXX
のXXX
は別になんでも良いです。
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.XXXXXX.MyCustomView
android:layout_width="300dp"
android:layout_height="300dp"
app:exampleDimension="24sp"
app:exampleDrawable="@android:drawable/ic_menu_add"
app:exampleString="Hello, MyCustomView" />
コードからカスタム属性の値を取得する
XMLで指定したカスタム属性の値をコードから参照してみたいと思います。参照するためにはobtainStyledAttributes
メソッドでまず属性リストを取得します。そこからデータ型に応じたメソッドで取得したい属性名を指定することで各値を取得することができます。
// 属性を読み込む
val a: TypedArray = context.obtainStyledAttributes(
attrs, R.styleable.MyCustomView, defStyle, 0
)
// XMLから指定した値
val text = a.getString(
R.styleable.MyCustomView_exampleString
)
// XMLから指定した値
val color = a.getColor(
R.styleable.MyCustomView_exampleColor,
Color.RED
)
// XMLから指定した値
val dimension = a.getDimension(
R.styleable.MyCustomView_exampleDimension,
0f
)
// hasValue:値があるかどうか
if (a.hasValue(R.styleable.MyCustomView_exampleDrawable)) {
val exampleDrawable = a.getDrawable(
R.styleable.MyCustomView_exampleDrawable
)
exampleDrawable?.callback = this
}
// 最後に必ずリサイクルする
a.recycle()
最後に必ずrecycle
メソッドを呼び出してリサイクルします。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。