View Lifecycle — 概要、onMeasure onLayout onDrawのプロセス

著者: IT Sectr 公開日: 2026-03-05 読了時間: 10 分

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は3つのフェーズで構成されます:onMeasure(サイズ)、onLayout(位置)、onDraw(描画) — そしてinvalidate()またはrequestLayout()によってトリガーされます。
  • onMeasureはMeasureSpec(AT_MOST、EXACTLY、UNSPECIFIED)に基づいてViewの幅と高さを計算します。
  • onLayoutはViewGroup内の子Viewを配置し、left、top、right、bottomの座標を決定します。
  • onDrawはViewのコンテンツをCanvasにレンダリングします:背景、テキスト、図形、画像。
  • 不適切なView Lifecycleは、UIパフォーマンス問題(ジャンク、ドロップフレーム)や階層問題の主な原因です。

View Lifecycle — Androidにおける概要

View Lifecycleは、AndroidのView(およびViewGroup)が画面上に自身を表示するために経るプロセスです。ActivityやFragmentとは異なり、ViewにはonStart/onStop/onDestroyがありません — その“ライフ”は測定、配置、描画の周期的なプロセスで構成されています。このサイクルは、Viewを表示または再描画する必要があるたびにトリガーされます。

View Lifecycleの3つのフェーズ:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — Viewの希望する寸法を決定します。システムはMeasureSpecを渡します — 許可される寸法(正確な値、最大値、または制限なし)に関する指示です。
  • onLayout(boolean changed, int left, int top, int right, int bottom) — Viewとその子を画面上に配置します。Viewの場合は自身の境界を定義し、ViewGroupの場合は子要素の位置を定義します。
  • onDraw(Canvas canvas) — 提供されたCanvasにViewのコンテンツを描画します。システムはCanvasを提供し、コマンドをビットマップまたはGPUに変換します。

完全なView Lifecycleサイクルには、Viewをウィンドウにアタッチする関連メソッドも含まれます:onAttachedToWindow(Viewがウィンドウにアタッチされ、HWアクセラレーションを持つ)とonDetachedFromWindow(Viewがデタッチされ、リソースが解放される)。これらのメソッドはViewのライフタイムに一度呼び出され、アニメーションやセンサーの登録/解除に重要です。

Android Performance Blogによると、UIパフォーマンス問題(ジャンク、フレームドロップ)の65%は、onMeasureとonDrawの不適切な実装に関連しています:過剰なオーバーライド、不必要なrequestLayout()の呼び出し、onDraw内でのオブジェクト作成。

onMeasure:Viewの寸法測定

onMeasure — View Lifecycleの中で最も重要で最も複雑なフェーズ。この段階で、AndroidはViewが画面上でどのくらいのスペースを占めるかを決定します。システムはMeasureSpecを渡します — モードとサイズで構成されるintにパックされた命令です。

3つのMeasureSpecモード:

モード定数意味
EXACTLYMeasureSpec.EXACTLY親によって設定された正確なサイズ(match_parentまたは固定幅)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTViewは指定された最大サイズまで可能(wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIED制限なし — Viewは任意のサイズ可能(ScrollView、RecyclerView)幅無制限 → MeasureSpec(0, UNSPECIFIED)

onMeasureの実装は以下を行うべきです:

  • 測定された寸法を保存するためにsetMeasuredDimension(int width, int height)を呼び出す。
  • パディングを考慮する — 利用可能な幅からgetPaddingLeft() + getPaddingRight()を引く。
  • ViewGroupの場合 — measureChild()またはmeasureChildWithMargins()を介してすべての子を測定する。
  • wrap_contentの場合 — コンテンツ(テキスト、画像)に基づいてサイズを計算する。
  • onMeasure内でrequestLayout()を呼び出さない — 無限ループを引き起こします。

典型的な間違い:wrap_content使用時にMeasureSpecを考慮しないこと。Viewがwrap_contentに設定されているが、onMeasureがAT_MOSTを処理せず固定サイズを返す場合、Viewはクリップされるか、必要以上にスペースを取ります。

onLayout:画面上へのViewの配置

onLayout — ViewまたはViewGroupがその子を自身の境界内に配置するフェーズ。通常のView(ViewGroupではない)の場合、onLayoutは不要です — システムは親から渡されたパラメータでlayout()を呼び出します。ViewGroupの場合、onLayoutは必須です — これなしでは子Viewは配置されません。

onLayoutのシグネチャ:

java
@Override
protected void onLayout(boolean changed,
        int left, int top,
        int right, int bottom) {
    // 子Viewの配置
}

changedパラメータは、前回のレイアウトと比較してViewの位置やサイズが変更されたかどうかを示します。falseの場合、Viewは最適化のために子の位置再計算をスキップできます。

ViewGroupの場合、onLayoutは以下を行うべきです:

  • getChildCount()getChildAt(i)を介してすべての子を反復処理する。
  • 各子について、left、top、right、bottomを決定する — ViewGroup内の座標(パディングを考慮)。
  • 各子に対してchild.layout(l, t, r, b)を呼び出す。
  • グラビティ、マージン、アライメントを考慮する。

onMeasureの後にonLayoutが呼び出されます — 測定された寸法はgetMeasuredWidth()/getMeasuredHeight()を介して利用可能です。layout()の後に子Viewの実際の寸法が異なる場合、再測定のためにrequestLayout()が呼び出されます。これは“layoutパス”と呼ばれ、再計算の連鎖反応を引き起こす可能性があります。

onDraw:Canvasコンテンツの描画

onDraw — ViewがCanvas上に自身を描画するフェーズ。これはonMeasureとonLayoutなしで複数回呼び出すことができる唯一のフェーズです — Viewがinvalidate()としてマークされている場合。Canvasは描画APIを提供します:drawLine、drawRect、drawCircle、drawText、drawBitmap、drawPath。

onDrawのルール:

  • onDraw内でオブジェクトを作成しない — 各onDraw呼び出しは事前に作成されたオブジェクト(Path、Paint、Rect)を使用するべきです。onDraw内でのオブジェクト作成はGCポーズとドロップフレームを引き起こします。
  • onDraw内でrequestLayout()やinvalidate()を呼び出さない — 無限の再描画ループを引き起こします。
  • 長時間の計算を行わない — onDrawはUIスレッドで実行されます。複雑な計算はバックグラウンドスレッドに移動するか、事前に計算するべきです。
  • ハードウェアアクセラレーションを使用する — API 14+以降、CanvasはGPUを介して動作できます。複雑なグラフィックス(グラデーション、影、回転)には、HWアクセラレーションが最大300%のパフォーマンス向上を提供します。
  • 表示領域のみを描画する — 不可視部分をクリップするにはcanvas.clipRect()を使用します。

ViewGroupでの描画順序:背景(setBackgroundDrawable)→ onDraw(コンテンツ)→ dispatchDraw(子View)→ onDrawForeground(フォアグラウンド)。dispatchDrawは各子のonDrawを呼び出します。dispatchDrawのオーバーライドは、子要素の上にエフェクトを適用するために使用されます。

Android Vitalsの統計によると、onDrawでのフレームドロップの最も一般的な原因は、メソッド内でのオブジェクト作成(48%)、decodeResourceの呼び出し(22%)、キャッシュなしの複雑なPath操作(15%)です。

無効化:Viewが再描画されるタイミング

無効化 — 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の最適化:ベストプラクティス

カスタムViewはユニークなUIを作成するための強力なツールですが、パフォーマンスルールの厳守が必要です。View Lifecycleの最適化に関するGoogleの主要な推奨事項は以下の通りです。

  • 計算可能なものはすべて事前計算する — サイズ、座標、パス、グラデーション色。onDrawでは描画のみ実行します。
  • 測定結果をキャッシュする — Viewに固定寸法がある場合、MeasureSpecを保存し、追加計算なしでsetMeasuredDimensionを返します。
  • ViewConfigurationを使用する — getScaledTouchSlop、getScaledMinimumFlingVelocity — タッチ処理用。
  • 階層内のView数を最小限に — 複数の要素を組み合わせるカスタムViewは、3–5のネストされたViewを持つViewGroupよりも常に高速です。Googleは1画面あたり10個以下のネストされたViewを推奨しています。
  • フラットな階層にはConstraintLayoutを使用する — RelativeLayoutに近いパフォーマンスで単一のViewGroupを構築しますが、ネストはありません。
  • 再描画後にハードウェアレイヤーを無効にする — アニメーション付きViewにはsetLayerType(LAYER_TYPE_HARDWARE)を、完了後はsetLayerType(LAYER_TYPE_NONE)を使用します。
  • Rectを指定してinvalidate()を使用する — View全体ではなく、変更された領域のみを再描画します。
  • オーバードローを避ける — Android StudioのProfile GPU Renderingを使用して不要な再描画を特定します。Googleアプリの平均オーバードローは1.5倍、推奨最大値は2.5倍です。

KotlinでのViewコード例

例1:カスタムView — 進行状況インジケーター

onMeasure、onDraw、invalidateの正しい実装を備えたシンプルな円形進行状況インジケーター。

kotlin
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内では作成されません。

例2:ViewGroup — シンプルなFlowLayout

子Viewを行に配置するカスタムViewGroup(Flexbox wrapのような)。

kotlin
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を返します。

例3:Pathキャッシュを使用したonDraw

スムーズなベジェ曲線を描画し、Pathを事前計算してキャッシュするカスタムView。

kotlin
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 Lifecycleの違いは?

View LifecycleはActivityの作成/破棄から独立した周期的な描画プロセス(onMeasure → onLayout → onDraw)です。ViewにはonStart/onStopがありません — 表示可能(ウィンドウにアタッチ)かそうでないかのどちらかです。Activity Lifecycleはアプリケーションコンポーネントの状態を管理し、View LifecycleはUIの描画を管理します。

requestLayout()がパフォーマンスに与える影響は?

requestLayout()はルートからViewツリー全体に対して完全なonMeasure → onLayout → onDrawサイクルをトリガーします。requestLayout()を頻繁に呼び出すと(例:アニメーションフレームごと)、ジャンクやドロップフレームの原因になります。Googleによると、10要素のViewGroupでの1回のrequestLayoutは平均2–5ミリ秒かかります。アニメーションにはinvalidate()を使用してください。

onAttachedToWindowはいつ呼び出される?

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に背景がある場合。super.onDraw()はViewの背景を描画します。カスタムViewに背景がないか、独自の背景を描画する場合、super.onDraw()は省略できます — これにより1回の描画パスを節約できます。ViewGroupの場合、super.dispatchDraw()は必須です — 子Viewを描画します。

まとめ

  • View Lifecycle — 3つの描画フェーズ:onMeasure(寸法)、onLayout(位置)、onDraw(レンダリング)。
  • onMeasureはMeasureSpec(EXACTLY、AT_MOST、UNSPECIFIED)を処理し、setMeasuredDimensionを呼び出します。
  • ViewGroupのonLayoutは子Viewをleft/top/right/bottom座標で配置します。
  • onDrawはCanvasにコンテンツをレンダリングします — このメソッド内でオブジェクトを作成しないでください。
  • invalidate()はonDrawのみをトリガーし、requestLayout()は完全なonMeasure → onLayout → onDrawサイクルをトリガーします。
  • カスタムViewはUIを15–40%高速化しますが、正しいonMeasure実装とonDrawでのオブジェクトキャッシュが必要です。
  • 複雑なグラフィックスには、ハードウェアアクセラレーションを使用し、Path/Bitmapをキャッシュしてください。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください