1. ホーム
  2. Android

Androidのビューの描画処理を完全に説明し、ビュー(II)を理解するために、ステップバイステップであなたを取る

2022-02-17 18:12:33

以下のサイトから転載しています。 http://blog.csdn.net/guolin_blog/article/details/16330267


前回の記事では、LayoutInflaterの仕組みを分解して説明しましたが、これはViewをより深く理解するための第一歩とも言えます。この記事では、View の深堀りを続け、その描画処理が実際にどのようなものであるかを見ていきたいと思います。前回の記事をまだ読んでいない方は、まず、以下の記事を読んでみてください。 <スパン Android LayoutInflaterの原理解析、Viewを深く理解するために一歩一歩進む (I)  .


Androidプログラマーなら誰でも、日々の開発作業で常にViewを扱っていることを知っていると思います。Androidのレイアウトやコントロールは、TextView, Button, ImageView, ListViewなど、直接または間接的にViewから継承されています。これらのコントロールはAndroidのシステムから提供されているもので、私たちはそれらを受け取って使うだけですが、それらがどのように画面上に描画されるかご存知でしょうか?知っておいて損はないので、この記事の本題に入りましょう。


どのようなビューも突然画面に現れることはなく、表示されるまでに非常に科学的な描画プロセスを経る必要があることを理解することが重要です。各ビューは、onMeasure()、onLayout()、onDraw()の3つの主要なフェーズを通過する必要があり、それぞれを順番に見ていきます。


i. onMeasure()


measureは計測という意味なので、onMeasure()メソッドはその名の通り、ビューのサイズを計測するために使われます。Viewシステムの描画処理は、ViewRootのformTraversals()メソッドで始まり、内部的にViewのmeasure()メソッドが呼ばれます。measure()メソッドはwidthMeasureSpecとheightMeasureSpecという二つのパラメータをとり、それぞれ、ビューの幅と高さの仕様と大きさを決定するために使用されます。


MeasureSpec の値は、specSize と specMode を合わせたもので、specSize にはサイズが、specMode には仕様が 記録される。specMode には、次のように合計 3 種類がある。


1. EXACTLY

親ビューが子ビューのサイズをspecSizeの値で指定することを想定していることを示します。システムは、このルールに従って子ビューのサイズをデフォルトで設定しますが、開発者はもちろん好きなサイズに設定することができます。

2. AT_MOST

子ビューのサイズはシステムがデフォルトで設定しますが、開発者が好きなサイズに設定することができます。

3. 未確定

開発者が何の制限もなく、好きな大きさにビューを設定できることを示します。これは稀なケースで、使用される可能性は低くなります。


では、widthMeasureSpecとheightMeasureSpecの値はどこから来ているのか、気になるところです。通常、どちらの値も親ビューで計算されて子ビューに渡されます。これは、親ビューが子ビューのサイズをある程度決定していることを示しています。しかし、一番外側のビューであるルートビューは、そのwidthMeasureSpecとheightMeasureSpecをどこから取得しているのでしょうか?これは、ViewRootのソースコードを解析する必要があり、performTraversals()メソッドを見ると、以下のようなコードが見つかります。

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

ご覧の通り、getRootMeasureSpec()メソッドが呼び出されてwidthMeasureSpecとheightMeasureSpecの値が取得されていますが、このメソッドに渡されるパラメータに注目してください。lp.widthとlp.heightはViewGroupインスタンスが作成された際に割り当てられます。 次にgetRootMeasureSpec()メソッドのコードを見てください、以下のようになります。
private int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

ご覧のように、ここでは MeasureSpec.makeMeasureSpec() メソッドを使用して MeasureSpec を組み立て、rootDimension パラメータが MATCH_PARENT に等しい場合は MeasureSpec の specMode は EXACTLY に等しく、rootDimension が WRAP_CONTENT に等しい場合は MeasureSpec の specMode は AT_MOST に等しくなっていることがおわかりでしょう。で、MATCH_PARENT と WRAP_CONTENT の両方で specSize は windowSize と等しく、これは、ルートビューが常に全画面いっぱいになることを意味します。


ここまでMeasureSpec関連の内容を紹介しましたが、次にViewのMeasure()メソッド内のコードを以下のように見てみましょう。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
            widthMeasureSpec ! = mOldWidthMeasureSpec ||
            heightMeasureSpec ! = mOldHeightMeasureSpec) {
        mPrivateFlags &= ~MEASURED_DIMENSION_SET;
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
        }
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        if ((mPrivateFlags & MEASURED_DIMENSION_SET) ! = MEASURED_DIMENSION_SET) {
            throw new IllegalStateException("onMeasure() did not set the"
                    + " measured dimension by calling"
                    + " setMeasuredDimension()");
        }
        mPrivateFlags |= LAYOUT_REQUIRED;
    }
    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;
}

measure()メソッドはfinalなので、サブクラスでオーバーライドできないことに注意してください。そして、9行目でonMeasure()メソッドが呼び出され、ここで実際にViewのサイズを計測し、設定しています。デフォルトでは、以下のようにgetDefaultSize()メソッドが呼ばれ、Viewのサイズが取得されます。
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}


ここで渡されるmeasureSpecは、常にmeasure()メソッドから渡されるものです。次の判定は、specModeがAT_MOSTまたはEXACTLYに等しい場合、specSizeを返すというもので、これも システムのデフォルトの動作となります。そして、onMeasure()メソッド内でsetMeasuredDimension()メソッドを呼び出して計測したサイズを設定し、計測処理を終了しています。


もちろん、レイアウトには通常、複数の子ビューが含まれ、それぞれがメジャー処理を行う必要があるため、インタフェースのプレゼンテーションには多くのメジャーが含まれる場合があります。以下のように、子ビューのサイズを確認するために、ViewGroup に measureChildren() メソッドが定義されています。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) ! = GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

ここでは、まず現在のレイアウトの下にあるすべての子ビューを調べ、次のようにmeasureChild()メソッドを一つずつ呼び出して、対応する子ビューのサイズを計測します。
protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

4行目と6行目でgetChildMeasureSpec()メソッドが呼ばれ、レイアウトファイルで定義されたMATCH_PARENTやWRAP_CONTENTなどの値から子のMeasureSpecを計算していることがわかりますので、このメソッドの内部詳細については掲載を控えさせていただきます。そして、8行目で子ビューのmeasure()メソッドを呼び出し、算出されたMeasureSpecを渡し、その後の処理は上記と同じです。


もちろん、onMeasure() メソッドはオーバーライド可能です。つまり、システムのデフォルトの測定値を使用したくない場合は、例えば以下のように自由にカスタマイズすることができます。

public class MyView extends View {

	......
	
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		setMeasuredDimension(200, 200);
	}

}

これは、Viewのデフォルトの測定フローをオーバーライドし、レイアウトファイルで定義されたビューMyViewのサイズが何であれ、インターフェイスに表示される最終サイズは200*200になるようにします。


getMeasuredWidth() と getMeasuredHeight() を使ってビューの幅と高さの測定値を取得できるのは setMeasuredDimension() メソッドが呼ばれた後であり、その前に取得した値は 0 となることに注意してください。


このように、ビューサイズの制御は、親ビュー、レイアウトファイル、そしてビュー自体の組み合わせで行われます。親ビューは子ビューが参照するサイズを提供し、開発者はXMLファイルでビューのサイズを指定し、そしてビュー自体が最終的なサイズを決定します。


ここまでで、ビューの描画プロセスの第一段階の解析は終了です。


II. onLayout()


計測処理が終了し、ビューのサイズが計測されると、次はレイアウト処理になります。ViewRootのperformTraversals()メソッドは、計測が終わった後も継続して、Viewのlayout()メソッドを呼び出してこの処理を行うため、名前の通り、ビューのレイアウト、つまり、ビューの位置を決定するためのメソッドとなっています。

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

layout()メソッドは、左、上、右、下の座標を表す4つのパラメータを取りますが、これらはもちろん現在のビューの親ビューからの相対的なものです。見ての通り、先ほど計測した幅と高さもlayout()メソッドに渡されています。では、layout()メソッドのコードはどのようになっているのか、次のように見てみましょう。
public void layout(int l, int t, int r, int b) {
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    boolean changed = setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
        }
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~LAYOUT_REQUIRED;
        if (mOnLayoutChangeListeners ! = null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }
    mPrivateFlags &= ~FORCE_LAYOUT;
}

layout()メソッドでは、まずsetFrame()メソッドが呼ばれ、現在のビューの再描画が必要かどうかを判断するためにビューのサイズが変わったかどうかを判断し、ここで渡された4つのパラメータをmLeft、mTop、mRight、mBottom変数に代入しています。onLayout()メソッドは、onMeasure()メソッドのデフォルトの動作と同じように11行目で呼び出されています。onLayout()メソッドに入り、え?なんでこれが一行もコードがない空のメソッドなんだ?


そう、ViewのonLayout()メソッドは空のメソッドなのです。なぜなら、onLayout()手続きは、ビューがレイアウトのどこに位置するかを決定するもので、この操作はレイアウトによって行われるべき、つまり親ビューが子ビューの表示位置を決定するのだからです。そう考えると、ViewGroupのonLayout()メソッドがどのように書かれているか、次のコードで見てみましょう。

@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

このように、ViewGroup の onLayout() メソッドは抽象メソッドであることがわかり、ViewGroup のサブクラスはすべてこのメソッドをオーバーライドしなければならないことがわかります。そうです、LinearLayout や RelativeLayout などのレイアウトは、このメソッドをオーバーライドして、内部で独自のルールに従って子ビューをレイアウトしているのです。LinearLayoutとRelativeLayoutのレイアウトルールはより複雑なので、個別に分析することはしませんが、ここではonLayout()の処理をより理解するために、レイアウトをカスタマイズしてみましょう。


このカスタムレイアウトの目標は単純で、子ビューをインクルードして子ビューが適切に表示されるようにすることです。そこで、このレイアウトをSimpleLayoutと呼ぶことにし、そのコードを以下に示します。

public class SimpleLayout extends ViewGroup { (パブリッククラス シンプルレイアウト エクステンドビューグループ)

	public SimpleLayout(Context context, AttributeSet attrs) { (パブリックシンプルレイアウト(コンテキスト), 属性セット(アトリビュート))
		super(context, attrs);
	

	オーバーライド
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { .
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		if (getChildCount() > 0) { {...
			View childView = getChildAt(0);
			measureChild(childView, widthMeasureSpec, heightMeasureSpec);
		
	}

	オーバーライド
	protected void onLayout(boolean changed, int l, int t, int r, int b) {.
		
			View childView = getChildAt(0);
			childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()));
		
	}

}

コードは非常にシンプルなので、正確なロジックを見てみましょう。onMeasure()メソッドがonLayout()メソッドの前に呼ばれることは既にご存知でしょう。ここではonMeasure()メソッドの中で、SimpleLayoutが子ビューを含んでいるかどうかを判断し、もしそうなら子ビューの大きさを測るためにmeasureChild()を呼び出しています。


次に、onLayout()メソッドでもSimpleLayoutに子ビューが含まれているかどうかを判断し、子ビューの layout()メソッドを呼び出してSimpleLayoutレイアウト内での位置を決定します。ここで渡される4つのパラメータは0, 0, childView.getMeasuredWidth( ) and childView.getMeasuredHeight() で、それぞれ子ビューのSimpleLayout内の4点の座標、左上、右下、左端、を表しています。ここで、childView.getMeasuredWidth( ) と childView.getMeasuredHeight( ) メソッドを呼び出して得られる値は、onMeasure( )メソッドで計測した幅と高さを表します。


これでSimpleLayoutがレイアウトとして定義されましたので、次のステップではXMLファイルの中で次のように使用します。

<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
	
    <ImageView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher"
        />
    
</com.example.viewtest.SimpleLayout>

ご覧の通り、SimpleLayoutは通常のレイアウトファイルと同じように使用することができますが、子ビューを1つだけ含むことができ、余分な子ビューは破棄されることに注意してください。ここでは、SimpleLayoutはImageViewを含み、ImageViewの幅と高さはwrap_contentになっています。




OK! ImageViewが正常に表示され、希望通りの位置に表示されました。もしImageViewの表示位置を変更したい場合は、childView.layout()メソッドの4つのパラメータを変更するだけです。


onLayout()の処理が終わったら、getWidth()メソッドとgetHeight()メソッドを呼び出して、ビューの幅と高さを取得することができます。そういえば、以前から疑問を持っていた方も多いのではないでしょうか。getWidth()メソッドとgetMeasureWidth()メソッドの違いは何なのでしょうか?この2つのメソッドの値はいつも同じに見えます。実は、両者が同じなのは、レイアウトデザイナーが非常に優れたコーディング習慣を持っているからで、実は両者の間にはかなり大きな違いがあるのです。


まず、getMeasureWidth()メソッドはmeasure()処理の最後に利用でき、getWidth()メソッドはlayout()処理の後にのみ利用可能です。また、getMeasureWidth()メソッドの値はsetMeasuredDimension()メソッドで設定されますが、getWidth()メソッドの値はビューの右側の座標から左側の座標を引いた値になっています。


SimpleLayoutのonLayout()メソッドのコードを見ると、ここで子ビューのlayout()メソッドに渡されるパラメータは、0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight() の4つなのでgetWidth()メソッドは childView. getMeasuredWidth() - 0 = childView.getMeasuredWidth() なので、getWidth() と getMeasuredWidth() で得られる値は同じですが、 onLayout() メソッドを設定すると、以下のように変更されます。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
	if (getChildCount() > 0) {
		View childView = getChildAt(0);
		childView.layout(0, 0, 200, 200);
	}
}

この方法では、getWidth()メソッドは200 - 0 = 200という値を取得し、getMeasuredWidth()と同じではなくなります。もちろん、この方法はmeasure()処理で計算された結果を尊重しないので、一般的にはお勧めできません。 getHeight()とgetMeasureHeight()メソッドは上記と同じなので、解析は繰り返さないことにします。


この時点で、ビューの描画プロセスの第2段階も分析が終了しています。


III. onDraw()


計測とレイアウトの処理が終わったら、いよいよ描画の処理に移ります。ViewRoot のコードは引き続き実行され、Canvas オブジェクトを作成し、View の draw() メソッドを呼び出して実際の描画を実行します。2番目と5番目のステップは一般的にはほとんど使われないので、ここでは簡略化した描画処理のみを分析します。そのコードを以下に示します。

public void draw(Canvas キャンバス) {
	if (ViewDebug.TRACE_HIERARCHY) {...
	    ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW) を実行します。
	}
	final int privateFlags = mPrivateFlags;
	final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&.DIRTY_OPAQUE = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
	        (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
	mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
	// ステップ1、必要なら背景を描画する
	int saveCount;
	if (!dirtyOpaque) {
	    final Drawable background = mBGDrawable;
	    if (背景 ! = null) {
	        final int scrollX = mScrollX;
	        final int scrollY = mScrollY;
	        if (mBackgroundSizeChanged)の場合{。
	            background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
	            mBackgroundSizeChanged = falseです。
	        
	        if ((スクロールX | スクロールY) == 0) {
	            background.draw(canvas)を実行します。
	        } else {
	            canvas.translate(scrollX,scrollY)を実行します。
	            background.draw(canvas)を実行します。
	            canvas.translate(-scrollX, -scrollY)を実行します。
	        }
	    }
	}
	final int viewFlags = mViewFlags;
	boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) ! = 0;
	boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) ! = 0;
	if (!verticalEdges && !horizontalEdges) {。
	    // ステップ3、コンテンツを描画する
	    if (!dirtyOpaque) onDraw(canvas)。
	    // ステップ 4, 子を描画する
	    dispatchDraw(canvas)。
	    // ステップ 6、装飾(スクロールバー)の描画
	    onDrawScrollBars(canvas)。
	    // これで終わりです...
	    を返します。
	return; }.
}



ご覧のように、最初のステップは、コードの9行目から始まり、ビューの背景を描画することです。ここでは、mBGDrawable オブジェクトを取得し、レイアウト処理で決定されたビューの位置に基づいて背景の描画領域を設定し、Drawable の draw() メソッドを呼び出して背景の描画を終了しています。では、このmBGDrawableオブジェクトはどこから来るのでしょうか?実は、XMLのandroid:backgroundプロパティで設定した画像や色なのです。もちろん、setBackgroundColor() や setBackgroundResource() などのメソッドでコード内に代入することもできます。


次の第3ステップは34行目で実行され、このステップが行うことは、ビューの内容を描画することです。ここでonDraw()メソッドが呼び出されていることがわかりますが、onDraw()メソッドの中にはどんなコードが書かれているのでしょうか?中に入ってみると、これまた空のメソッドであることがわかります。実際、ビューの中身はそれぞれ確実に違うのですから、この部分をサブクラスに機能させて実現するのは当然といえば当然なのですが。


4番目のステップは、3番目のステップの直後に実行されます。このステップは、現在のビューのすべての子ビューを描画する役割を果たします。しかし、現在のビューに子ビューがない場合は、描画する必要もありません。そのため、View の dispatchDraw() メソッドは再び空のメソッドとなり、ViewGroup の dispatchDraw() メソッドに特定の描画コードが含まれることになりますね。


以上が終わると、6番目の最後のステップである、ビューのスクロールバーを描画するところまで行きます。では、現在のビューがListViewやScrollViewであるとは限らないのに、なぜスクロールバーが描画されるのか不思議に思われるかもしれません。実は、ButtonでもTextViewでも、どんなビューにもスクロールバーはあるのですが、通常は表示させないようにしています。スクロールバーを描画するコードのロジックもかなり複雑なので、ここでは3番目のステップにフォーカスしているため、掲載はしません。


以上のフロー分析で、Viewはコンテンツ部分を描画してくれないので、見せたいものに応じて各Viewが自ら描画する必要があることは既にお分かりかと思います。TextViewやImageViewなどのクラスのソースコードを見てみると、どれもonDraw()メソッドをオーバーライドして、そこで結構な量の描画ロジックを実行していることがわかると思います。Canvasは、何でも描画できるキャンバスとして非常にリッチなクラスなので、試してみましょう。


ここでは物事をシンプルにするために、非常にシンプルなビューを作成し、Canvas で少しランダムなものを描いただけで、コードは以下のようになります。

public class MyView extends View {

	private Paint mPaint;

	public MyView(Context context, AttributeSet attrs) {
		super(context, attrs);
		mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		mPaint.setColor(Color.YELLOW);
		canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
		mPaint.setColor(Color.BLUE);
		mPaint.setTextSize(20);
		String text = "Hello View";
		canvas.drawText(text, 0, getHeight() / 2, mPaint);
	}
}

ご覧の通り、Viewを継承したカスタムMyViewを作成し、MyViewのコンストラクタでPaintオブジェクトを作成しました。paintはCanvasと連携して描画するブラシのようなものです。ここでの描画ロジックは比較的シンプルです。onDraw()メソッドで、まずブラシを黄色に設定し、CanvasのdrawRect()メソッドを呼び出して矩形を描画しています。次に、ブラシを青に設定し、テキストのサイズを変更して、drawText() を呼び出してテキストを描画しています。


これだけで、カスタムビューが書かれ、次のようにXMLに追加できるようになりました。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.example.viewtest.MyView 
        android:layout_width="200dp"
        android:layout_height="100dp"
        />

</LinearLayout>

MyViewの幅を200dp、高さを100dpに設定し、プログラムを実行すると、以下のようになります。



画像に表示されているコンテンツは、ビューMyViewのコンテンツ部分でもあります。MyViewに背景を設定していないため、ここではViewが自動的に描画する背景効果は見えません。


もちろん、Canvasの使い方は他にもたくさんあるので、ここで全部を紹介することはできませんが。


この時点で、第3段階のビューの描画プロセスも分析が終了しています。これでビュー描画の全工程が終了しました。これで、ビューについての理解は深まりましたか? ご興味のある方は、お読みください。  Androidのビューの状態と再描画のフロー解析、View(3)に一歩踏み込んでみる  .


技術公開ページでは、質の高い技術記事を毎日更新しています。仕事や勉強に疲れた時の息抜きに、エンターテイメントサイトをフォローしてください。

以下のQRコードをWeChatで掃引してフォローしてください。