Android view绘制

阅读他人的,自己进行总结的。原文章:

Android LayoutInflater原理分析,带你一步步深入了解View(一)_郭霖的专栏-CSDN博客_layoutinflater

Android视图绘制流程完全解析,带你一步步深入了解View(二)_郭霖的专栏-CSDN博客_android 视图绘制

该作者的文章,非常只好,我是想要方便记忆,所以自己总结一下。

以上4篇,原作者分为LayoutInflater的原理分析、视图的绘制流程、视图的状态及重绘和自定义view。

我们先介绍前两篇

目录

一:LayoutInflater原理分析

1、如何使用LayoutInflater

2、例子

3、源码分析

4、问题引出

二、视图绘制流程 

1、onMeasure()

2、onLayout()

3、onDraw


一:LayoutInflater原理分析

1、如何使用LayoutInflater

LayoutInflater.from(context).inflate(R.layout.activityMain, null);

2、例子

activity_main.xml

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

button_Layout.xml:


<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button" >
</Button>

MainActivity:


public class MainActivity extends Activity {
	private LinearLayout mainLayout;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mainLayout = (LinearLayout) findViewById(R.id.main_layout);
		LayoutInflater layoutInflater = LayoutInflater.from(this);
		View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null);
		mainLayout.addView(buttonLayout);
	}
}

效果图:

3、源码分析

 我们调用的方法是LayoutInflater的inflate方法,无论调用哪个inflate最后调用的都是下面这个inflate方法。

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        mConstructorArgs[0] = mContext;
        View result = root;
        try {
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
            }
            ......
            final String name = parser.getName();
            if (TAG_MERGE.equals(name)) {
                ......
                rInflate(parser, root, attrs);
            } else {
                View temp = createViewFromTag(name, attrs);
                ViewGroup.LayoutParams params = null;
                if (root != null) {
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        temp.setLayoutParams(params);
                    }
                }
                rInflateChildren(parser, temp, attrs, true);
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                }
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }
        } catch (XmlPullParserException e) {
            ......
        }
        return result;
    }
}

首先LayoutInflater使用pull解析方式来解析布局文件的,然后在方法中第22行,调用了createViewFromTag()方法。然后在其后续会调用rInflateChildren()

createViewFromTag:用于根据节点名来创建View对象,在其内部有调用createView方法(createView方法中通过反射的方式创建出View的实例并返回)

rInflateChildren:会调用rInflate方法。

rInflate:查找这个View下的子元素,每次递归完成后则将这个View添加到父布局当中。

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
                boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String name = parser.getName();
            if (TAG_REQUEST_FOCUS.equals(name)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }
        ......
    }

同样在第25行是createViewFromTag()创建实例,然后在第28行递归调用rInflateChildren()。

inflate方法中第三个参数,我这边不介绍了,大概是会给加载的文件制定一个父布局,即root

4、问题引出

我要改变button的大小,我改成如下所示。

<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="80dp"
    android:text="Button" >
</Button>

300宽和80高,无任何变化。什么原因导致的,由于我们button没有父容器,他的宽高属性就会失去作用,我们改成如下所示,效果就可以更改了:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <Button
        android:layout_width="300dp"
        android:layout_height="80dp"
        android:text="Button" >
    </Button>
</RelativeLayout>

那activity中setContentView我们的宽高怎么会有效呢,其实在setContentView方法中,Android会自动在最外层再嵌套一个FrameLayout。证实:

public class MainActivity extends Activity {
	private LinearLayout mainLayout;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mainLayout = (LinearLayout) findViewById(R.id.main_layout);
		ViewParent viewParent = mainLayout.getParent();
		Log.d("TAG", "the parent of mainLayout is " + viewParent);
	}
 
}

二、视图绘制流程 

1、onMeasure()

measure:测量。

原文中说的onMeasure从ViewRoot开始,我的sdk是android-29,我这边就置换过来,是从ViewRootImpl.performTraversals()开始的;

根据原文MeasureSpec总结得出,其MeasureSpec的specMode有三种类型:

我们来看看performTraversals()方法:

......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
private static 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;
    }

 我们只看关键代码,其中getRootMeasureSpec()方法可以证实我们上图所述。lp.width和lp.height创建ViewGroup实例时就被赋值了,接着执行了performMeasure();

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

我们可以看到执行了View.onMeasure()了;那我们继续后面的追踪。

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        if (forceLayout || needsLayout) {
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            resolveRtlPropertiesIfNeeded();
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
        ......
    }

我们看到其中在第8行执行了onMeasure方法。measure方法参数接受了ViewRootImpl确认之后的高度、宽度的规格和大小.onMeasure才是我们真正去测量并且设置View的地方。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
    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;
    }

onMeasure调用了setMeasuredDimension、getDefaultSize、getSuggestedMinimumWidth、getSuggestedMinimumHeight

getSuggestedMinimumWidth():返回视图应使用的建议最小宽度。

getSuggestedMinimumHeight():返回视图应使用的建议最小高度。

getDefaultSize():获取视图的大小

setMeasuredDimension():设定测量出的大小

这样measure的一次过程就结束了。一个界面会设计到多次measure,因为一个布局中包含多个子视图,每个视图都要经历一次measure过程。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);
            }
        }
    }
    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);
    }

我们可以看到在measureChildren中第6行的判断,如果我们没有设置这个控件的Visibility为GONE那么我们就会走到measureChild()中,在measureChild中我们也会去获取控件的MeasureSpec,之后就跟前面的measure后的流程一样了。

来一个小例子吧。onMeasure可以重写,不用系统默认的测量方式,按照我们自己的意愿定制,如:

public class MyView extends View {
	......
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		setMeasuredDimension(200, 200);
	}
}

通过上述流程中,我们知道measure最后是到setMeasuredDimension方法中去的,这样我们定义这个视图的无论多大,都将显示200*200。需要注意的是在setMeasuredDimension调用之前,我们getMeasuredWidth、getMeasuredHeight得到的返回值会是0,调用之后才能获取视图测量出的宽高。

测量这一步我们就差不多分析完了。父视图、布局文件和视图本身共同完成,父视图提供子视图参考大小,开发人员在布局文件中指定视图大小,视图本身对最终大小进行拍板。

总结流程:

ViewRootImpl.performTraversals()
  --ViewRootImpl.performMeasure()
    (子视图的话  ViewGroup.measureChildren->ViewGroup.measureChild)
    --View.onMeasure()
      --View.Measure()

2、onLayout()

layout:布局、安排、设计

measure之后就应该layout了,我们通过观察下面ViewRootImpl中的performTraversals()方法看看:

   ......
    if (!mStopped || mReportNextDraw) {...}
}else {....}
if (surfaceSizeChanged) {
    updateBoundsSurface();
}
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
    performLayout(lp, mWidth, mHeight);
    ......

其中,我们刚才说的onMeasure就发生在第二行的if判断体中。和之前一样我的版本是android-29跟原文不太一样。这边会调用的方法是performLayout方法();就像performMeasure调用View.measure()一样,performLayout调用了View.Layout();

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        ......
        final View host = mView;
        if (host == null) {
            return;
        }
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            mInLayout = false;
            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {
                ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
                if (validLayoutRequesters != null) {
                    ......
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
         ......
    }

layout()方法接受四个参数,代表左上右下的坐标。我们可以看到上面传的宽高给了我们刚才measure测量出的宽高。坐标是相对于当前视图而言。

    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            ......
        }

    }

layout方法中,把坐标信息赋值给几个变量。change这个布尔值代表是否发生过变化,来确认它是否需要重新绘制。在13行时走进了onLayout中。我们来看看onLayout:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}

我们看到该方法没有实现。onLayout为了确定视图在布局中所在位置,所以应该由布局来完成。因此我们看看ViewGroup中的onLayout:

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

可以看到这是一个抽象方法,那么所有子类必须重写次方法。LinearLayout、RelativeLayout等布局都实现了次方法,所以完成了那些强大布局的功能。

我们来一个例子:

SimpleLayout继承自ViewGroup:

public class SimpleLayout extends ViewGroup {
	public SimpleLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		if (getChildCount() > 0) {
			View childView = getChildAt(0);
			measureChild(childView, widthMeasureSpec, heightMeasureSpec);
		}
	}
	@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, childView.getMeasuredWidth(), childView.getMeasuredHeight());
		}
	}
}

以上的方法都做过介绍了,就不再赘述。

SimpleLayout的布局文件:

<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>

我java代码中只想要第一个子视图,所以只拿第一个,多余的就会被舍弃掉。这边SimpleLayout包含一个ImageView。运行效果如图:

 如果想要改变ImageView显示的位置,改变childView,Layout()的四个参数就好了。onLayout结束后就可以拿到getWidth和getHeight的值了。getWidth和getMeasureWidth的值好像一直相同。getWidth和getMeasureWidth有什么区别:

1.getMeasure在measure过程结束后可以拿到,getWidth要到layout过程结束拿到

2.getMeasureWidth通过setMeasureDimension()设置,getWdith通过右边坐标减左边坐标计算出的

他们的值相同的原因相信大家知道了,当然我们可以让它不相同。还是上面的代码我们改变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。不会和getMeasureWidth相同。通常情况不推荐这么写,getHeight()与getMeasureHeight()方法之间关系同上,不再赘述。

总结流程:

ViewRootImpl.performTraversals()
  --ViewRootImpl.performLayout()
    --View.Layout()
      --(? extends ViewGroup).onLayout() 

3、onDraw

测量完(measure)和布局结束(layout)就该将视图描绘(draw)出来了。这边才是真正开始对视图进行绘制。根据上面的经验我们可以很快得出在ViewRootImpl中开始。在2625行走出了onLayout的if判断体。2747行的if判断体进入performDraw()我们直接只贴相应代码:

if (!cancelDraw) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }

            performDraw();
        } else {

继续跟踪performDraw:

private void performDraw() {
    ......
    try {
      boolean canUseAsync = draw(fullRedrawNeeded);
    ....
  .....
}
private boolean draw(boolean fullRedrawNeeded) {
    ......
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
                }
    ......
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
    .......
    try{
        ....
        mView.draw(canvas);
        ......
    }
    .......
}

原文没有这部分没有详细出来,我这边把流向给大家贴出来。主要意思原文讲清楚了:“执行并创建一个Canvas对象,然后调用View.draw()方法来执行具体的绘制”。上面的方法就不详细叙述了,都在ViewRootImpl中,有兴趣可以自己翻阅源码看看。我们来看看View.draw(),源码比较多,我把相应步骤代码贴出:

@CallSuper
public void draw(Canvas canvas) {
    ......
    /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *	    1. 画背景
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      2. 如有必要,保存画布的层以准备褪色
         *      3. Draw view's content
         * 	    3. 绘制视图的内容
         *      4. Draw children
         * 	    4. 画孩子
         *      5. If necessary, draw the fading edges and restore layers
         * 	    5. 如有必要,绘制淡入淡出的边并恢复图层
         *      6. Draw decorations (scrollbars for instance)
         * 	    6. 绘制装饰(例如滚动条)
         */
    // Step 1, draw the background, if needed
    drawBackground(canvas);
    // Step 3, draw the content
    onDraw(canvas);
    // Step 4, draw the children
    dispatchDraw(canvas);
    drawAutofilledHighlight(canvas);
    // Overlay is part of the content and draws beneath Foreground
    if (mOverlay != null && !mOverlay.isEmpty()) {
        mOverlay.getOverlayView().dispatchDraw(canvas);
    }
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
    // Step 7, draw the default focus highlight
    drawDefaultFocusHighlight(canvas);
.......
}

    

第一步:对视图进行绘制,这里会先得到一个mDrawable对象,然后根据layout过程确定的视图位置来进行背景的绘制,之后调用mDrawable的draw()方法。那这个drawable对象哪里来的呢,通过设置android:background属性设置的图片或颜色setBackgroundColor,setBackgroundResource等方法。

    private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        setBackgroundBounds();
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mThreadedRenderer != null) {
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
            final RenderNode renderNode = mBackgroundRenderNode;
            if (renderNode != null && renderNode.hasDisplayList()) {
                setBackgroundRenderNodeProperties(renderNode);
                ((RecordingCanvas) canvas).drawRenderNode(renderNode);
                return;
            }
        }
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

第三步:对视图的内容进行绘制,可以看到调用了onDraw()。onDraw方法是空实现,因为每个视图的内容各不相同,交给子类实现。

第四步:对当前视图的所有子视图进行绘制。调用了dispatchDraw(),它也是空实现,而ViewGroup的dispatchDraw()方法中有具体的绘制代码,代码过多,有兴趣可以翻看源码,这边不贴了。

第六步:对视图的滚动条进行绘制,大家之前以为只有ListView、ScrollView这类的有滚动条吧,其实button、textView也好,任何一个视图都有滚动条。只是我们没让他显示出来。

我们来一个例子:

MyView.java:

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);
	}
}

布局文件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>

效果如下图所示:

总结流程:

ViewRootImpl.performTraversals()
  --ViewRootImpl.performDraw()
    --ViewRootImpl.draw(boolean)
      --ViewRootImpl.drawSoftware()
        --View.draw(Canvas)
          --(第3步中)onDraw()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值