Custom Viewは、Viewクラスまたはそのサブクラス(Button、TextView、ImageView)を継承し、主要なメソッド(描画用のonDraw、サイズ測定用のonMeasure、タッチ処理用のonTouchEvent)をオーバーライドして作成される、Androidのカスタムインターフェースコンポーネントです。Google Android Developer Guide(2024)によると、標準のAndroid SDKコンポーネントが必要な動作や外観を提供しない場合(カスタムアニメーション、非標準形状、特殊なグラフ、ゲーム要素など)にCustom Viewが使用されます。
重要なポイント
Custom Viewは、android.view.View(またはそのサブクラス)を継承し、システムメソッドをオーバーライドしてカスタムの描画、測定、イベント処理ロジックを実装するクラスです。Custom Viewは、独自のUIコンポーネントを作成するためのAndroidの基本的なメカニズムです。
AndroidはCustom Viewを作成する2つのアプローチを提供します:Viewを継承する方法(onDrawによる完全カスタム描画)と、既存のViewサブクラスを継承する方法(例えば、基本動作を維持しながら機能を追加するためにButtonやTextViewをオーバーライド)。
基本コンストラクタはContextとAttributeSetを受け入れる必要があります — これによりシステムがXMLマークアップからViewを作成できます。Viewがコードからのみ使用される場合は、Contextのみのコンストラクタで十分です。スタイル付きの第3コンストラクタはAndroidテーマのサポートに必要です。
class CustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
strokeWidth = 4f
style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(width / 2f,
height / 2f,
50f, paint)
}
}
最小限のCustom ViewはコンストラクタとonDrawで構成されます。ただし、AndroidのLayoutシステムで正しく動作するには、onMeasureもオーバーライドする必要があります — そうしないと、Viewがゼロサイズで表示されたり、wrap_contentレイアウトパラメータに誤って反応したりする可能性があります。
Google I/O 2023によると、Google Playのトップ100アプリの65%でカスタムViewが使用されています。Custom Viewを作成する最も一般的な理由:カスタムアニメーション(35%)、カスタムチャートとダイアグラム(25%)、特殊な制御要素(20%)、ブランドコンポーネント(20%)。
onMeasureは、受け取ったMeasureSpec(親の制約)に基づいてViewが希望する寸法を報告するメソッドです。適切なonMeasureの実装がないと、wrap_contentでViewの高さがゼロになったり、match_parentで画面全体を占めたりする可能性があります。
MeasureSpecはモード(EXACTLY、AT_MOST、UNSPECIFIED)と値で構成されます。EXACTLY — 親が正確なサイズを設定(match_parentまたは固定値)。AT_MOST — 親が最大値を設定(wrap_content)。UNSPECIFIED — 制限なし(ScrollView、ListView)。
override fun onMeasure(
widthMeasureSpec: Int,
heightMeasureSpec: Int
) {
val desiredWidth = paddingLeft + paddingRight
+ DEFAULT_WIDTH
val desiredHeight = paddingTop + paddingBottom
+ DEFAULT_HEIGHT
val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
val modeWidth = MeasureSpec.getMode(widthMeasureSpec)
val resultWidth = when (modeWidth) {
MeasureSpec.EXACTLY -> measuredWidth
MeasureSpec.AT_MOST ->
desiredWidth.coerceAtMost(measuredWidth)
else -> desiredWidth
}
val resultHeight = resolveSize(desiredHeight,
heightMeasureSpec)
setMeasuredDimension(resultWidth, resultHeight)
}
resolveSizeは、onMeasureの実装を簡素化するAndroidのユーティリティメソッドです。希望サイズとMeasureSpecを受け取り、正しい値を返します:EXACTLYの場合は正確なサイズ、AT_MOSTの場合は希望と最大の最小値、UNSPECIFIEDの場合は希望サイズ。
onLayoutはView(ViewGroupではない)の場合、通常はオーバーライドされません — 子要素の配置のために親から呼び出されます。ViewGroupの場合、onLayoutは必須です — 各子Viewに対してlayout()を呼び出す必要があります。
onDrawはCustom Viewの心臓部です。このメソッドはViewの再描画のたびにAndroidから呼び出されます。Canvasオブジェクトが渡され、それを通じてすべての描画(幾何学図形、テキスト、パス、Bitmap、アニメーション)が実行されます。
Canvas APIはすべての基本操作のメソッドを提供します:drawCircle、drawRect、drawLine、drawPath、drawText、drawBitmap、drawArc、drawOval。スタイルを設定するには、色、太さ、塗りつぶしスタイル、フォント、影、効果を定義するオブジェクトであるPaintが使用されます。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 背景
canvas.drawRect(0f, 0f,
width.toFloat(), height.toFloat(), bgPaint)
// テキスト
canvas.drawText("Custom View",
paddingLeft.toFloat(),
paddingTop.toFloat() + textPaint.textSize,
textPaint)
// 円
canvas.drawCircle(
width / 2f,
height / 2f,
radius, circlePaint)
// パス(ベジェ曲線)
val path = Path().apply {
moveTo(0f, height.toFloat())
quadTo(width / 2f, 0f,
width.toFloat(), height.toFloat())
}
canvas.drawPath(path, pathPaint)
}
Paintは描画スタイル設定器です。アンチエイリアシング(Paint.ANTI_ALIAS_FLAG)は滑らかなエッジに必須です。サブピクセルテキスト(SUBPIXEL_TEXT_FLAG)はテキスト品質を向上させます。Style.FILL、STROKE、FILL_AND_STROKEは、図形を塗りつぶすか、輪郭を描くか、その両方を決定します。
重要:onDraw内でPaint、Path、Rectオブジェクトを作成しないでください — 描画ループでの割り当てを引き起こし、ガベージコレクションを誘発してFPSを低下させます。すべての描画オブジェクトはコンストラクタまたはinitメソッドで作成してください。アニメーションには、再描画をトリガーするためにValueAnimatorまたはObjectAnimatorをinvalidate()とともに使用します。
カスタム属性を使用すると、標準のandroid:layout_widthやandroid:backgroundと同様に、XMLマークアップからCustom Viewを設定できます。属性はres/values/attrs.xmlファイルで名前、タイプ、オプションのデフォルト値とともに宣言されます。
属性のタイプ:string、integer、float、boolean、color、dimension、enum、flag、fraction、reference(リソース参照)。各タイプに対して、Androidは自動的にXMLから値を解析してTypedArrayに渡します。
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CustomView">
<attr name="circleColor"
format="color" />
<attr name="circleRadius"
format="dimension" />
<attr name="labelText"
format="string" />
<attr name="showAnimation"
format="ブール値" />
</declare-styleable>
</resources>
Custom Viewのコンストラクタでは、属性はcontext.obtainStyledAttributesを介して読み取られ、TypedArrayが返されます。TypedArrayは型付きアクセスメソッドを提供します:getColor、getDimension、getString、getBoolean、getInt。読み取り後、リソースを解放するためにTypedArrayでrecycle()を呼び出す必要があります。
init {
val typedArray = context.obtainStyledAttributes(
attrs,
R.styleable.CustomView
)
circleColor = typedArray.getColor(
R.styleable.CustomView_circleColor,
Color.BLUE
)
circleRadius = typedArray.getDimension(
R.styleable.CustomView_circleRadius,
50f
)
labelText = typedArray.getString(
R.styleable.CustomView_labelText
) ?: "Default"
typedArray.recycle()
}
XMLでの使用:app名前空間を追加し(xmlns:app="http://schemas.android.com/apk/res-auto")、app:circleColor="@color/red"のようにカスタム属性を使用します。attrs.xmlの宣言が正しければ、Android Studioが自動補完し、属性タイプを検証します。
onTouchEventは、Viewがタッチされるたびに呼び出されるメソッドです。イベントタイプ(ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL)、座標、圧力、指の数(マルチタッチ)に関する情報を含むMotionEventオブジェクトを受け取ります。
複雑なジェスチャ(スワイプ、ピンチ、長押し)を処理するには、onTouchEventと組み合わせてGestureDetectorまたはScaleGestureDetectorを使用します。GestureDetectorはonSingleTapUp、onFling、onLongPress、onDoubleTapなどの標準ジェスチャの認識を簡素化します。
private val gestureDetector = GestureDetector(
context, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean {
handleTap(e.x, e.y)
return true
}
})
override fun onTouchEvent(event: MotionEvent): Boolean {
val handled = gestureDetector.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_MOVE -> {
currentX = event.x
currentY = event.y
invalidate()
return true
}
}
return handled || super.onTouchEvent(event)
}
ヒットテスト — タッチがViewの特定の領域に当たったかどうかを判断します。長方形領域の場合はRect.contains(x, y)を使用します。円の場合は中心からの距離を確認:sqrt(dx^2 + dy^2) < radius。任意の形状の場合はPath.op()またはRegion.contains()。
マルチタッチはMotionEvent.getPointerCount()とgetPointerId(index)を介して処理されます。各指はACTION_DOWNからACTION_POINTER_UPまで保持される一意のIDを取得します。ACTION_MOVEはすべてのアクティブな指のデータを含む場合があります — 移動の補間にはgetHistoricalX/Yを使用します。
Custom Viewは、onDrawが頻繁に呼び出される場合(アニメーション、スクロール)や重い操作を含む場合に、パフォーマンスのボトルネックになる可能性があります。Androidはカスタムコンポーネントのレンダリングを最適化するためのいくつかのメカニズムを提供しています。
View.setLayerTypeを使用すると、Viewのレンダリングをソフトウェアレイヤー(LAYER_TYPE_SOFTWARE)またはハードウェアレイヤー(LAYER_TYPE_HARDWARE)に切り替えられます。ソフトウェアレイヤーは、ハードウェアアクセラレーションでサポートされていない複雑なCanvasグラフィック(drawTextOnPathや複雑なPath効果など)に役立ちます。
invalidate(Rect)は、コンポーネント全体ではなく、Viewの指定された領域のみを再描画します。これは、大きなCustom View(グラフィック、マップ、描画キャンバス)にとって非常に重要であり、毎フレームの完全な再描画がFPSの低下を引き起こします。vsync同期にはpostInvalidateOnAnimation()を使用します。
class EfficientCustomView
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
// コンストラクタで一度作成されるオブジェクト
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val rect = Rect()
private val position = PointF()
fun updatePosition(x: Float, y: Float) {
position.set(x, y)
// ポイント周辺の領域のみ再描画
rect.set(
(x - 10).toInt(),
(y - 10).toInt(),
(x + 10).toInt(),
(y + 10).toInt()
)
invalidate(rect)
}
}
ハードウェアアクセラレーションはAndroid 3.0+(API 11+)でデフォルトで有効です。Canvas操作のdrawCircle、drawRect、drawBitmapはハードウェアアクセラレーションされ、GPUで実行されます。ただし、drawTextOnPath、drawVertices、複雑なクリッピング操作はアクセラレーションされません — これらにはLAYER_TYPE_SOFTWAREを使用します。
Android Performance Patterns(Google、2023)によると、Custom Viewの遅延の主な原因 — onDrawでの割り当て(毎フレームのオブジェクト作成)、1つの要素変更時のView全体の再描画、静的コンテンツに対するLayerType.HARDWAREの欠如。これら3つの問題を修正すると、ほとんどのシナリオでFPSが30から60に向上します。
| メソッド | 目的 | 呼び出し頻度 |
|---|---|---|
| onDraw | Viewの内容を描画 | invalidate()ごと |
| onMeasure | Viewのサイズを決定 | レイアウト変更時 |
| onLayout | 子Viewの配置 | onMeasure後 |
| onTouchEvent | タッチ処理 | タッチごと |
| onSizeChanged | サイズ変更への反応 | 最初のレイアウトと変更時 |
よくある質問
Custom View(Canvas API)は、最大のカスタム描画パフォーマンス(グラフィック、ビデオエディタ、マップ)が必要な場合、既存のViewベースのコードとの統合、またはAPI 21未満のAndroidバージョンのサポートが必要な場合に適しています。Jetpack Composeはほとんどの新しいプロジェクトのための最新のアプローチであり、宣言的UIを使用し、動的インターフェースにより適しています。
はい、ViewがXMLレイアウトでwrap_contentを使用する場合。onMeasureがないと、デフォルトのView.onMeasure実装がデフォルトサイズを設定しないため、wrap_contentはmatch_parentのように動作します。Viewが常に固定サイズまたはmatch_parentを持つ場合は、onMeasureをオーバーライドしなくても問題ありません。
ValueAnimatorまたはObjectAnimatorを使用してViewのプロパティ(色、半径、位置)を変更し、アニメーションコールバックでinvalidate()を呼び出して再描画します。ValueAnimatorはメインスレッドで実行され、vsyncと同期しています。複雑なアニメーション(物理、パーティクル)の場合は、Choreographer.FrameCallbackまたはAndroid Animation Frameworkを使用します。
onTouchEventで、MotionEvent.getPointerCount()を使用して指の数を判断し、getPointerId(i)を使用して各指を一意のIDで追跡します。ACTION_POINTER_DOWN / ACTION_POINTER_UP — 指の追加/削除イベント。ピンチズームには、onTouchEventと組み合わせてScaleGestureDetectorを使用します — ピンチ検出を簡素化します。
はい、AndroidViewを介して使用できます — Viewベースの要素をCompose階層に埋め込むコンポーザブル関数です。AndroidViewはViewを作成するためのファクトリと状態同期のためのupdateコールバックを受け入れます。これにより、完全な書き換えなしで既存のCustom Viewを新しいComposeプロジェクトで再利用できます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。