View Lifecycle — Androidが画面上のユーザーインターフェース要素(View)を描画および再描画するために呼び出すメソッドのシーケンスです。ActivityやFragmentとは異なり、Viewは拡張されたライフサイクルを持たない軽量コンポーネントですが、厳格な3段階のプロセス(onMeasure(測定)、onLayout(配置)、onDraw(描画))を経ます。View Lifecycleの理解は、カスタムViewの作成、パフォーマンスの最適化、描画問題の解決に必要です。Googleによると、カスタムViewは適切に実装された場合、標準のネストされたViewGroupの組み合わせと比較してUIを15–40%高速化します。カスタムViewに関するAndroidドキュメントは、onMeasure、onLayout、onDrawをView Lifecycleの3つの柱として説明しています。
重要ポイント
View Lifecycleは、AndroidのView(およびViewGroup)が画面上に自身を表示するために経るプロセスです。ActivityやFragmentとは異なり、ViewにはonStart/onStop/onDestroyがありません — その“ライフ”は測定、配置、描画の周期的なプロセスで構成されています。このサイクルは、Viewを表示または再描画する必要があるたびにトリガーされます。
View Lifecycleの3つのフェーズ:
完全なView Lifecycleサイクルには、Viewをウィンドウにアタッチする関連メソッドも含まれます:onAttachedToWindow(Viewがウィンドウにアタッチされ、HWアクセラレーションを持つ)とonDetachedFromWindow(Viewがデタッチされ、リソースが解放される)。これらのメソッドはViewのライフタイムに一度呼び出され、アニメーションやセンサーの登録/解除に重要です。
Android Performance Blogによると、UIパフォーマンス問題(ジャンク、フレームドロップ)の65%は、onMeasureとonDrawの不適切な実装に関連しています:過剰なオーバーライド、不必要なrequestLayout()の呼び出し、onDraw内でのオブジェクト作成。
onMeasure — View Lifecycleの中で最も重要で最も複雑なフェーズ。この段階で、AndroidはViewが画面上でどのくらいのスペースを占めるかを決定します。システムはMeasureSpecを渡します — モードとサイズで構成されるintにパックされた命令です。
3つのMeasureSpecモード:
| モード | 定数 | 意味 | 例 |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | 親によって設定された正確なサイズ(match_parentまたは固定幅) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | Viewは指定された最大サイズまで可能(wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | 制限なし — Viewは任意のサイズ可能(ScrollView、RecyclerView) | 幅無制限 → MeasureSpec(0, UNSPECIFIED) |
onMeasureの実装は以下を行うべきです:
setMeasuredDimension(int width, int height)を呼び出す。getPaddingLeft() + getPaddingRight()を引く。measureChild()またはmeasureChildWithMargins()を介してすべての子を測定する。典型的な間違い:wrap_content使用時にMeasureSpecを考慮しないこと。Viewがwrap_contentに設定されているが、onMeasureがAT_MOSTを処理せず固定サイズを返す場合、Viewはクリップされるか、必要以上にスペースを取ります。
onLayout — ViewまたはViewGroupがその子を自身の境界内に配置するフェーズ。通常のView(ViewGroupではない)の場合、onLayoutは不要です — システムは親から渡されたパラメータでlayout()を呼び出します。ViewGroupの場合、onLayoutは必須です — これなしでは子Viewは配置されません。
onLayoutのシグネチャ:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// 子Viewの配置
}
changedパラメータは、前回のレイアウトと比較してViewの位置やサイズが変更されたかどうかを示します。falseの場合、Viewは最適化のために子の位置再計算をスキップできます。
ViewGroupの場合、onLayoutは以下を行うべきです:
getChildCount()とgetChildAt(i)を介してすべての子を反復処理する。child.layout(l, t, r, b)を呼び出す。onMeasureの後にonLayoutが呼び出されます — 測定された寸法はgetMeasuredWidth()/getMeasuredHeight()を介して利用可能です。layout()の後に子Viewの実際の寸法が異なる場合、再測定のためにrequestLayout()が呼び出されます。これは“layoutパス”と呼ばれ、再計算の連鎖反応を引き起こす可能性があります。
onDraw — ViewがCanvas上に自身を描画するフェーズ。これはonMeasureとonLayoutなしで複数回呼び出すことができる唯一のフェーズです — Viewがinvalidate()としてマークされている場合。Canvasは描画APIを提供します:drawLine、drawRect、drawCircle、drawText、drawBitmap、drawPath。
onDrawのルール:
canvas.clipRect()を使用します。ViewGroupでの描画順序:背景(setBackgroundDrawable)→ onDraw(コンテンツ)→ dispatchDraw(子View)→ onDrawForeground(フォアグラウンド)。dispatchDrawは各子のonDrawを呼び出します。dispatchDrawのオーバーライドは、子要素の上にエフェクトを適用するために使用されます。
Android Vitalsの統計によると、onDrawでのフレームドロップの最も一般的な原因は、メソッド内でのオブジェクト作成(48%)、decodeResourceの呼び出し(22%)、キャッシュなしの複雑なPath操作(15%)です。
無効化 — Viewの再描画をトリガーするメカニズム。invalidate()を呼び出すと、Viewが“ダーティ”としてマークされ、次の描画サイクルでonDrawの呼び出しがスケジュールされます。requestLayout()の呼び出しはより“重い”操作で、完全なサイクルをトリガーします:onMeasure → onLayout → onDraw。
| メソッド | 機能 | 使用タイミング |
|---|---|---|
| invalidate() | onMeasure/onLayoutなしでonDrawをトリガー | 外観のみ変更(色、テキスト、進行状況) |
| invalidate(Rect) | 指定された領域のみ再描画 | Viewの一部が変更 — アニメーション、選択 |
| postInvalidate() | 非UIスレッドからinvalidateを呼び出す | バックグラウンドスレッドが描画データを更新 |
| requestLayout() | onMeasure → onLayout → onDrawをトリガー | コンテンツサイズが変更(テキスト、画像) |
| forceLayout() | 強制再測定のためにViewをマーク | 内部状態が変更、サイズが変わる可能性 |
アニメーションとView Lifecycle:ViewPropertyAnimatorとValueAnimatorはアニメーションフレームごとにinvalidate()を呼び出します。ObjectAnimatorはViewのセッターを呼び出し、セッターがサイズ(幅/高さ)を変更する場合、自動的にrequestLayout()を呼び出します。これは複雑なViewGroupではコストがかかる可能性があります:各requestLayoutがルートビューまでの完全な階層をトリガーします。
最適化ルール:外観のみが変更される場所(色、透明度、サイズ変更なしの回転)では、requestLayout()の代わりにinvalidate()を使用します。requestLayoutは、サイズやサイズに影響するコンテンツを変更する場合にのみ使用します。
カスタムViewはユニークなUIを作成するための強力なツールですが、パフォーマンスルールの厳守が必要です。View Lifecycleの最適化に関するGoogleの主要な推奨事項は以下の通りです。
setLayerType(LAYER_TYPE_HARDWARE)を、完了後はsetLayerType(LAYER_TYPE_NONE)を使用します。onMeasure、onDraw、invalidateの正しい実装を備えたシンプルな円形進行状況インジケーター。
class CircularProgressView constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
style = Paint.Style.STROKE
strokeWidth = 8f
strokeCap = Paint.Cap.ROUND
}
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.LTGRAY
style = Paint.Style.STROKE
strokeWidth = 8f
}
private var progress = 0f
private var viewWidth = 0
private var viewHeight = 0
fun setProgress(value: Float) {
progress = value.coerceIn(0f, 100f)
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 100 * resources.displayMetrics.density.toInt()
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
val size = minOf(width, height).coerceAtLeast(desiredSize)
setMeasuredDimension(size, size)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val padding = progressPaint.strokeWidth / 2
val radius = (minOf(viewWidth, viewHeight) - padding) / 2
val cx = viewWidth / 2f
val cy = viewHeight / 2f
canvas.drawCircle(cx, cy, radius, backgroundPaint)
val sweepAngle = (progress / 100f) * 360f
canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius,
-90f, sweepAngle, false, progressPaint)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
}
}
円形プログレスバー:onMeasureはMeasureSpecに基づいて正方形サイズを返し、onSizeChangedは寸法を記憶し、onDrawは背景と進行状況の円弧を描画します。進行状況が変更されるとInvalidateが呼び出されます — onMeasure/onLayoutは影響を受けません。Paintはコンストラクタで一度作成され、onDraw内では作成されません。
子Viewを行に配置するカスタムViewGroup(Flexbox wrapのような)。
class FlowLayout constructor(
context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {
private val horizontalSpacing = 8.dpToPx(resources)
private val verticalSpacing = 8.dpToPx(resources)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = MeasureSpec.getSize(widthMeasureSpec)
var totalHeight = paddingTop + paddingBottom
var rowWidth = paddingLeft
var rowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
if (rowWidth + child.measuredWidth > width - paddingRight) {
totalHeight += rowHeight + verticalSpacing
rowWidth = paddingLeft
rowHeight = 0
}
rowWidth += child.measuredWidth + horizontalSpacing
rowHeight = maxOf(rowHeight, child.measuredHeight)
}
totalHeight += rowHeight
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean,
l: Int, t: Int, r: Int, b: Int) {
var rowTop = paddingTop
var rowLeft = paddingLeft
var rowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
if (rowLeft + child.measuredWidth > r - paddingRight) {
rowTop += rowHeight + verticalSpacing
rowLeft = paddingLeft
rowHeight = 0
}
child.layout(rowLeft, rowTop, rowLeft + child.measuredWidth, rowTop + child.measuredHeight)
rowLeft += child.measuredWidth + horizontalSpacing
rowHeight = maxOf(rowHeight, child.measuredHeight)
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
}
FlowLayoutはonMeasureをオーバーライドします:各子を測定し、幅を超えると新しい行に折り返し、全体の高さを計算します。onLayoutは改行を考慮して座標で子を配置します。generateLayoutParamsは子ViewのマージンをサポートするためにMarginLayoutParamsを返します。
スムーズなベジェ曲線を描画し、Pathを事前計算してキャッシュするカスタムView。
class WaveView constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#4A90D9")
style = Paint.Style.FILL
}
private val wavePath = Path()
private var isPathDirty = true
private var viewWidth = 0
private var viewHeight = 0
fun refreshWave() {
isPathDirty = true
invalidate()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
isPathDirty = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (isPathDirty) {
wavePath.reset()
val amplitude = viewHeight * 0.1f
wavePath.moveTo(0f, viewHeight * 0.5f)
for (x in 0..viewWidth step 4) {
val y = viewHeight * 0.5f + amplitude * Math.sin(x * 2 * Math.PI / viewWidth).toFloat()
wavePath.lineTo(x.toFloat(), y)
}
wavePath.lineTo(viewWidth.toFloat(), viewHeight.toFloat())
wavePath.lineTo(0f, viewHeight.toFloat())
wavePath.close()
isPathDirty = false
}
canvas.drawPath(wavePath, wavePaint)
}
}
Pathキャッシュ:isPathDirty = trueはViewの寸法が変更されるかrefreshWave()が呼び出された場合のみ。onDrawでは、Pathが“ダーティ”の場合のみ再計算されます。これにより、アニメーションフレームごとのベジェ曲線の再計算を防ぎ、CPUを節約します。
よくある質問
View LifecycleはActivityの作成/破棄から独立した周期的な描画プロセス(onMeasure → onLayout → onDraw)です。ViewにはonStart/onStopがありません — 表示可能(ウィンドウにアタッチ)かそうでないかのどちらかです。Activity Lifecycleはアプリケーションコンポーネントの状態を管理し、View LifecycleはUIの描画を管理します。
requestLayout()はルートからViewツリー全体に対して完全なonMeasure → onLayout → onDrawサイクルをトリガーします。requestLayout()を頻繁に呼び出すと(例:アニメーションフレームごと)、ジャンクやドロップフレームの原因になります。Googleによると、10要素のViewGroupでの1回のrequestLayoutは平均2–5ミリ秒かかります。アニメーションにはinvalidate()を使用してください。
onAttachedToWindowはViewがWindowにアタッチされたときに呼び出されます — 可視階層の一部になります。この時点で、ViewはHWアクセラレーションとWindowリソース(WindowManager、Display)へのアクセスを取得します。onAttachedToWindowは、Viewが表示されている間存続するアニメーションリスナーやBroadcastReceiverを登録する適切な場所です。
オーバードローは、1つのフレームでピクセルが複数回描画される状況です。余分なパスごとにGPU時間が無駄になります。削減方法:テーマにwindowBackgroundを設定する(レイアウトで背景を描画しない)、canvas.clipRect()を使用する、ネストされた背景のマージを避ける、ネストされたLinearLayoutの代わりにConstraintLayoutを使用する。Android Studio → Profile GPU Rendering → Overdrawでオーバードローの色マップが表示されます(青=1倍、赤=3倍+)。
はい、Viewに背景がある場合。super.onDraw()はViewの背景を描画します。カスタムViewに背景がないか、独自の背景を描画する場合、super.onDraw()は省略できます — これにより1回の描画パスを節約できます。ViewGroupの場合、super.dispatchDraw()は必須です — 子Viewを描画します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。