【Android】View的工作流程

View的工作流程

开始了解一下View的工作流程,就是measure、layout和draw。measure用来测量View的宽高,layout用来确定View的位置,draw则用来绘制View。这一讲我们来看看measure流程,measure流程分为View的measure流程和ViewGroup的measure流程,只不过ViewGroup的measure流程除了要完成自己的测量还要遍历去调用子元素的measure()方法。

以下是按照原文格式提取内容与代码,并在代码中添加注释:

View的工作流程入口

这篇是新版本的东西

https://blog.youkuaiyun.com/Tai_Monster/article/details/130915515

一、DecorView被加载到Window中

当 DecorView 创建完毕,要加载到 Window 中时,我们需要先了解一下 Activity 的创建过程。当我们调用 Activity 的 startActivity方法时,最终是调用 ActivityThread 的 handleLaunchActivity方法来创建 Activity的,代码如下所示:

  1. Activity的创建过程
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // 创建Activity
    Activity a = performLaunchActivity(r, customIntent);//1
    if (a!= null) {
        // 配置Configuration
        r.createdConfig = new Configuration(mConfiguration);
        Bundle oldState = r.state;
        // 处理Activity的状态
        handleResumeActivity(r.token, false, r.isForward,
              !r.activity.mFinished &&!r.startsNotResumed);//2
    }
}

在上面代码注释1处调用 performLaunchActivity方法来创建 Activity,在这里面会调用到Activity的 onCreate 方法,从而完成DecorView的创建。接着在上面代码注释2处调用handleResumeActivity方法,代码如下所示:

  1. DecorView的创建与加载到Window中
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;
    ActivityClientRecord r = performResumeActivity(token, clearHide);//1
    if (r!= null) {
        final Activity a = r.activity;
        if (r.window == null &&!a.mFinished && willBeVisible) {
            // 获取Activity的Window
            r.window = r.activity.getWindow();
            // 获取Window的DecorView
            View decor = r.window.getDecorView();//2
            decor.setVisiblity(View.INVISIBLE);
            WindowManager wm = a.getWindowManager();//3
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                wm.addView(decor, l);//4
                //。。。
            }
        }
    }
}

在上面代码注释1处的 performResumeActivity 方法中会调用 Activity 的onResume 方法。接着往下看,注释2处得到了DecorView。注释3处得到了WindowManager,WindowManager是一个接口并且继承了接口 ViewManager。在注释4处调用 WindowManager 的 addView 方法,WindowManager 的实现类是 WindowManagerlmpl,所以实际调用的是 WindowManagerlmpl 的addView 方法。具体代码如下所示:

  1. WindowManager添加View
public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        // 应用默认Token
        applyDefaultToken(params);
        // 通过WindowManagerGlobal添加视图
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
}

在 WindowManagerlmpl 的 addView 方法中,又调用了 WindowManagerGlobal 的 addView

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
   ...
    ViewRootImpl root;
    ViewPanelParent = null;
    synchronized (mLock) {
       ...
        root = new ViewRootImpl(view.getContext(), display); //1
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
       ...
    }
    try {
        root.setView(view, wparams, panelParent); //2
    } catch (RuntimeException e) {
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

在上面代码注释1处创建了ViewRootImpl实例,在注释2处调用了ViewRootImplsetView方法并将DecorView作为参数传进去,这样就把DecorView加载到了Window中。当然界面仍不会显示出什么来,因为View的工作流程还没有执行完,还需要经过measurelayout以及draw才会把View绘制出来。

onCreate方法完成了DecorView的创建。在onResume方法中将DecorView加载进入Window。

二、ViewRootImpl的PerformTraversals方法

前面讲到了将DecorView加载到Window中,是通过ViewRootImplsetView方法。ViewRootImpl还有一个方法PerformTraversals,这个方法使得ViewTree开始View的工作流程,代码如下所示:

在这里插入图片描述

  1. PerformTraversals方法代码
private void performTraversals() {
    if (!mStopped) {
        // 获取根视图的宽度测量规格
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        // 获取根视图的高度测量规格
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        // 执行测量操作
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        // 执行布局操作
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        if (!mCancelDraw &&!newSurface) {
            if (!skipDraw || mReportNextDraw) {
                if (mPendingTransitions!= null && mPendingTransitions.size() > 0) {
                    // 启动过渡动画
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).startChangingAnimations();
                    }
                    // 清除过渡动画列表
                    mPendingTransitions.clear();
                }
                // 执行绘制操作
                performDraw();
            }
        }
    }
}

这里面主要执行了3个方法,分别是performMeasure、performLayout和performDraw,在其方法的内部又会分别调用View的measure、layout和draw方法。需要注意的是,performMeasure方法中需要传入两个参数,分别是childWidthMeasureSpec和childHeightMeasureSpec。要了解这两个参数,需要了解MeasureSpec。

理解MeasureSpec

MeasureSpecView的内部类,封装了一个View的规格尺寸,包括View的宽和高的信息。它的作用是在Measure流程中,系统将ViewLayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的宽和高。

在这里插入图片描述

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0x3 << MODE_SHIFT;

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY = 1 << MODE_SHIFT;
    public static final int AT_MOST = 2 << MODE_SHIFT;

    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

    public static int adjust(int measureSpec, int delta) {
        final int mode = getMode(measureSpec);
        int size = getSize(measureSpec);
        size += delta;
        if (size < 0) {
            Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta);
            size = 0;
        }
        return makeMeasureSpec(size, mode);
    }

    public static String toString(int measureSpec) {
        int mode = getMode(measureSpec);
        int size = getSize(measureSpec);
        StringBuilder sb = new StringBuilder("MeasureSpec: ");
        if (mode == UNSPECIFIED) {
            sb.append("UNSPECIFIED ");
        } else if (mode == EXACTLY) {
            sb.append("EXACTLY ");
        } else if (mode == AT_MOST) {
            sb.append("AT_MOST ");
        }
        sb.append("size=").append(size);
        return sb.toString();
    }
}
  • MeasureSpec是一个32位的int值,高2位为SpecMode(测量的模式),低30位为SpecSize(测量的大小)。
  • SpecMode有3种模式:
    • UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。
    • AT_MOST:最大模式,对应于wrap_content属性,只要尺寸不超过父控件允许的最大尺寸就行。
    • EXACTLY:精确模式,对应于match_parent属性和具体的数值,父容器测量出View所需要的大小,也就是SpecSize的值。

对于每一个View,都有一个MeasureSpec,而这个MeasureSpec保存了该View的尺寸规格。在View的测量流程中,通过makeMeasureSpec来保存宽和高的信息。通过getModegetSize得到模式和宽、高。MeasureSpec是受自身LayoutParams和父容器的MeasureSpec共同影响的。

作为顶层 View的 DecorView来说,其并没有父容器,那么它的 MeasureSpec 是如何得来的呢?为了解决这个疑问,我们再回到 ViewRootlmpl的 PerformTraveals 方法,如下所示:

private void performTraversals() {
    if (!mStopped) {
        // 获取根视图的宽度测量规格
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        // 获取根视图的高度测量规格
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        // 执行测量操作
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        // 执行布局操作
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        if (!mCancelDraw &&!newSurface) {
            if (!skipDraw || mReportNextDraw) {
                if (mPendingTransitions!= null && mPendingTransitions.size() > 0) {
                    // 启动过渡动画
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).startChangingAnimations();
                    }
                    // 清除过渡动画列表
                    mPendingTransitions.clear();
                }
                // 执行绘制操作
                performDraw();
            }
        }
    }
}

在这里插入图片描述

  1. **getRootMeasureSpec**方法分析
    1. 该方法用于根据窗口大小和根布局参数来确定测量规格(MeasureSpec)。

    2. 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;
      }
      
    3. getRootMeasureSpec方法的第一个参数windowSize指的是窗口的尺寸,所以对于DecorView来说,它的 MeasureSpec 由自身的 LayoutParams 和窗口的尺寸决定,这一点和普通 View 是不同的。接着往下看,就会看到根据自身的LayoutParams来得到不同的MeasureSpec。

    4. performMeasure方法中需要传入两个参数,即childWidthMeasureSpec和 childHcightMcasureSpcc,这代表什么我们也应该明白了。接着回到PcrformTraveals 方法,查看在注释2处的performMeasure方法内部做了什么,代码如下所示:

  2. **performMeasure**方法分析
    1. 该方法用于执行视图的测量操作。
    2. private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
          Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
          try {
              mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
          } finally {
              Trace.traceEnd(Trace.TRACE_TAG_VIEW);
          }
      }
      

getRootMeasureSpec方法用于根据根视图的布局参数和窗口大小确定根视图的测量规格,

performMeasure方法则是实际执行视图测量操作的方法。

  • 该方法首先开始跟踪视图测量操作,然后调用视图的measure方法进行测量,最后结束跟踪操作。

measure流程概述

measure流程分为Viewmeasure流程和ViewGroupmeasure流程。其中,ViewGroupmeasure流程除完成自身测量外,还需遍历调用子元素的measure()方法。

一、View的measure流程

(一)onMeasure()方法(测量尺寸)
// onMeasure方法是View测量过程的入口,它接收宽度和高度的测量规格参数,用于确定View最终的测量宽高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 通过调用getDefaultSize方法,结合建议的最小宽度/高度以及传入的测量规格,来设置View的测量宽高
    // getSuggestedMinimumWidth和getSuggestedMinimumHeight方法分别用于获取建议的最小宽度和高度值
    // widthMeasureSpec和heightMeasureSpec是父容器传递过来的测量规格,包含了模式和大小信息
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

此方法通过调用setMeasuredDimension方法,并传入基于getDefaultSize以及相关获取建议最小宽度、高度方法得到的值,来开启View的测量过程。

(二)setMeasuredDimension()方法
// 此方法用于最终设置View的宽高,会考虑光学边界等因素进行调整后再设置
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    // 判断当前View与父容器的布局模式是否为光学模式(用于处理一些特殊的布局边界情况,比如在某些特定布局下考虑视图的视觉边界效果)
    boolean optical = isLayoutModeOptical(this);
    if (optical!= isLayoutModeOptical(mParent)) {
        // 获取光学边界的Insets信息(Insets包含了视图在上下左右方向上与其他元素的边界间隔信息,比如内边距等)
        Insets insets = getOpticalInsets();
        int opticalWidth = insets.left + insets.right;
        int opticalHeight = insets.top + insets.bottom;

        // 根据是否为光学模式,对测量的宽度和高度进行相应的调整(加上或减去光学边界宽度/高度)
        // 如果是光学模式,就加上光学宽度/高度,否则减去,以此来准确设置符合光学布局要求的宽高
        measuredWidth += optical? opticalWidth : -opticalWidth;
        measuredHeight += optical? opticalHeight : -opticalHeight;
    }
    // 调用底层方法设置最终的测量宽高,该方法一般是在系统内部进一步处理设置宽高的具体操作
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

该方法主要用于设置View的宽高,会考虑当前View与父容器的光学布局模式情况,对宽高进行相应调整后,通过setMeasuredDimensionRaw方法完成最终设置。

(三)getDefaultSize()方法
// 根据测量规格和给定的默认大小,返回View最终的测量大小
// 参数size通常是默认的大小值,measureSpec则包含了父容器传递过来的测量规格信息(模式和大小)
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    // 获取测量规格中的模式(UNSPECIFIED、AT_MOST、EXACTLY这三种模式,用于确定View尺寸的限制情况)
    int specMode = MeasureSpec.getMode(measureSpec);
    // 获取测量规格中的大小值(去除模式位后的数值,代表了父容器提供的一个尺寸参考值)
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        // 如果是未指定模式,意味着父容器对View没有尺寸限制,View可以自行决定大小,所以直接返回传入的默认大小size
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        // 如果是最大模式或精确模式,说明父容器对View有一定的尺寸要求,此时返回测量规格中的大小值specSize
        // 对于AT_MOST模式,View的大小不能超过这个specSize;对于EXACTLY模式,View应按此大小进行布局
        result = specSize;
        break;
    }
    return result;
}

getDefaultSize方法依据传入的测量规格中的模式(specMode)来确定返回的View测量大小。在AT_MOSTEXACTLY模式下,返回测量规格中的大小值(specSize);在UNSPECIFIED模式下,则返回传入的第一个参数值。

(四)MeasureSpec类
// MeasureSpec类用于辅助测量View,它是一个32位的int值,通过位运算来分离和获取其中的测量模式以及测量大小信息
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0x3 << MODE_SHIFT;

    /**
     * 未指定模式,父容器对View没有尺寸限制,View可自行决定大小,常用于系统内部测量场景,比如某些自动调整大小的情况
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * 精确模式,对应match_parent属性或具体的数值,父容器确定了View的精确大小,View会按此大小进行布局,比如在固定布局中某个View占据固定的空间大小
     */
    public static final int EXACTLY = 1 << MODE_SHIFT;

    /**
     * 最大模式,对应wrap_content属性,View的大小不能超过父容器允许的最大尺寸,常用于希望View根据自身内容自适应大小,但又不能超出父容器范围的情况
     */
    public static final int AT_MOST = 2 << MODE_SHIFT;

    // 获取测量规格中的模式(通过与模式掩码进行按位与操作,提取出高两位表示的模式信息)
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    // 获取测量规格中的大小值(通过与模式掩码取反后进行按位与操作,去除高两位的模式位,获取低30位表示的大小信息)
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

MeasureSpec类用于辅助测量View,它是一个32位的int值,高两位表示测量模式(分为UNSPECIFIEDEXACTLYAT_MOST三种),低30位表示测量大小。

  • UNSPECIFIED模式:父容器对View无尺寸限制,View可自行决定大小,常用于系统内部测量。
  • AT_MOST模式:对应wrap_content属性,View大小不能超过父控件允许的最大尺寸。
  • EXACTLY模式:对应match_parent属性或具体数值,父容器确定了View的精确大小。

SpecMode 是 View 的测量模式,而 SpecSize 是 View 的测量大小。

很显然,需要依据不同的 SpecMode 值来返回不同的 result 值,也就是 SpecSize。在 AT_MOST 和 EXACTLY 这两种模式下,都会返回 SpecSize 这个值,这意味着 View 在这两种模式下的测量宽和高直接取决于 SpecSize。由此可知,对于一个直接继承自 View 的自定义 View 来说,它的 wrap_content 和 match_parent 属性的效果是等同的。

所以,如果要实现自定义 View 的 wrap_content 功能,就需要重写 onMeasure 方法,并针对自定义 View 的 wrap_content 属性进行相应处理。

而在 UNSPECIFIED 模式下,返回的是 getDefaultSize 方法的第一个参数 size 的值。从 onMeasure 方法来看,size 的值是通过 getSuggestedMinimumWidth 方法或者 getSuggestedMinimumHeight 方法得到的。

接下来,我们查看一下 getSuggestedMinimumWidth 方法具体做了什么。实际上,弄懂了getSuggestedMinimumWidth 方法就可以了,因为 getSuggestedMinimumHeight 方法与其原理是一样的。

(五)getSuggestedMinimumWidth()方法(以宽度为例,高度同理)
// 该方法用于获取建议的最小宽度值,根据View是否设置背景等情况来确定返回值
// 这个值会作为getDefaultSize方法的一个参数参与到View宽度测量大小的计算中
protected int getSuggestedMinimumWidth() {
    // 如果View没有设置背景,返回mMinWidth(mMinWidth可通过属性或方法设置,默认值为0,代表View的最小宽度限制)
    return (mBackground == null)? mMinWidth : Math.max(mMinWidth, mBackground.getMinimumWidth());
}

此方法用于获取建议的最小宽度值。

View未设置背景,则取值为mMinWidth(可通过android:minWidth属性或setMinimumWidth方法设置,默认值为0);

若设置了背景,则取mMinWidth和背景(Drawable类型)最小宽度两者中的最大值。

// 用于设置View的最小宽度值,设置后会触发重新布局请求,以便系统根据新的最小宽度重新计算和调整布局
public void setMinimumWidth(int minWidth) {
    mMinWidth = minWidth;
    requestLayout();
}

setMinimumWidth方法用于设置View的最小宽度值,设置后会触发重新布局请求。

如果 View 设置了背景,则取值为 max(mMinWidth,mBackground.getMinimumWidth()),也就是取 mMinWidth 和 mBackground.getMinimumWidth()之间的最大值。

此前讲了 mMinWidth,下面看看mBackground.getMinimumWidth()。

这个mBackground是Drawable 类型的,Drawable类的 getMinimumWidth 方法如下所示:

// 获取Drawable类型的背景的最小宽度值,如果其固有宽度(比如图片资源本身的宽度等)大于0则返回固有宽度,否则返回0
// 这个方法主要用于在有背景的情况下,结合mMinWidth来确定View的建议最小宽度值
public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0? intrinsicWidth : 0;
}

getMinimumWidth方法用于获取Drawable类型背景的最小宽度值,若其固有宽度大于0,则返回固有宽度,否则返回0。

getSuggestedMinimumWidth 方法就是:如果 View 没有设置背景,则返回 mMinWidth;如果设置了背景,就返回mMinWidth 和 Drawable 的最小宽度之间的最大值。

所以在我们结论就是:如果是UNSPECIFIED模式,那么view的大小就会被设置为有背景下的mMinWidth 和 Drawable 的最小宽度之间的最大值

其他情况就是:都返回specSize这个值,也就是View测量后的大小

二、ViewGroup的measure流程

讲完了 View的 measure 流程,接下来看看 ViewGroup的 measure 流程。对于 ViewGroup,它不只要测量自身,还要遍历地调用子元素的 measure()方法。ViewGroup中没有定义onMeasure()方法,但却定义了measureChildren()方法:

(一)measureChildren()方法(ViewGroup.java
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);
        }
    }
}

该方法会遍历ViewGroup的所有子元素,对非GONE状态的子元素调用measureChild方法进行测量。

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

此方法先获取子元素的布局参数,然后依据父容器的测量规格以及子元素自身布局参数,获取子元素的宽度和高度测量规格,最后调用子元素的measure方法进行测量。

调用 child.getLayoutParams()方法来获得子元素的LayoutParams 属性,获取子元素的MeasureSpec 并调用子元素的 measure()方法进行测量。getChildMeasureSpec()方法里写了什么呢?其代码如下:

(三)getChildMeasureSpec()方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = 0;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = 0;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

getChildMeasureSpec方法根据父容器的测量规格以及子元素的LayoutParams属性来确定子元素的测量规格。需注意,当父容器的测量规格属性为AT_MOST,子元素的LayoutParams属性为WRAP_CONTENT时,子元素的测量规格属性也为AT_MOST,其specSize值为父容器的specSize减去padding的值,这种情况下,为避免与设置MATCH_PARENT效果相同,可在LayoutParams属性为WRAP_CONTENT时指定默认宽高。

三、LinearLayout的measure流程

(一)onMeasure()方法(LinearLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

LinearLayout根据自身的方向(垂直或水平),调用相应的测量方法(measureVerticalmeasureHorizontal)来进行测量。

(二)measureVertical()方法(垂直方向测量部分源码)
v// 用于测量垂直方向布局相关尺寸的方法,接收宽度和高度的测量规格参数
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // 初始化总长度为0,这个总长度会随着子元素的测量等操作不断更新,用于计算最终布局在垂直方向占用的空间大小
    mTotalLength = 0;

    // 遍历所有子元素(通过索引i),这里假设count表示子元素的总数
    for (int i = 0; i < count; ++i) {
        // 获取当前索引对应的虚拟子视图(具体获取逻辑由getVirtualChildAt方法决定)
        final View child = getVirtualChildAt(i);

        // 如果子视图为空,调用measureNullChild方法计算该空子视图对应的长度,并累加到总长度mTotalLength上,然后跳过当前循环后续部分,进入下一次循环
        if (child == null) {
            mTotalLength += measureNullChild(i);
            continue;
        }

        // 如果子视图的可见性为GONE,调用getChildrenSkipCount方法获取需要跳过的子元素数量,更新索引i,跳过这些不可见的子元素,进入下一次循环
        if (child.getVisibility() == View.GONE) {
            i += getChildrenSkipCount(child, i);
            continue;
        }

        // 如果在当前子元素之前存在分割线,将分割线的高度累加到总长度mTotalLength上
        if (hasDividerBeforeChildAt(i)) {
            mTotalLength += mDividerHeight;
        }

        // 获取当前子视图的布局参数,用于后续判断和尺寸计算等操作
        LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

        // 累加当前子视图布局参数中的权重值(weight),用于后续根据权重计算尺寸等情况
        totalWeight += lp.weight;

        // 如果高度测量模式是EXACTLY(精确模式,即父容器已经明确指定了子视图的精确高度),并且当前子视图高度为0且权重大于0,
        // 进行以下操作:
        // 1. 记录当前总长度为totalLength。
        // 2. 更新总长度mTotalLength,取当前总长度和当前总长度加上子视图上下外边距后的较大值,目的可能是在权重布局下确保总长度满足一定要求。
        // 3. 设置skippedMeasure为true,表示可能有测量被跳过(具体含义根据上下文而定)
        if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            skippedMeasure = true;
        } else {
            // 初始化一个变量oldHeight为Integer.MIN_VALUE,用于记录子视图原始高度(后续会根据情况更新和恢复这个值)
            int oldHeight = Integer.MIN_VALUE;

            // 如果子视图高度为0且权重大于0,将oldHeight设为0,并将子视图的高度设置为WRAP_CONTENT(包裹内容模式),
            // 可能是为了先按照包裹内容模式测量子视图尺寸
            if (lp.height == 0 && lp.weight > 0) {
                oldHeight = 0;
                lp.height = LayoutParams.WRAP_CONTENT;
            }

            // 在布局前测量子视图尺寸,传入宽度和高度的测量规格以及相关参数,
            // 如果totalWeight为0(可能表示没有权重布局相关情况),传入当前总长度mTotalLength作为垂直方向的偏移量,否则传入0
            measureChildBeforeLayout(
                    child, i, widthMeasureSpec, 0, heightMeasureSpec,
                    totalWeight == 0? mTotalLength : 0);

            // 如果之前记录的oldHeight不是初始值Integer.MIN_VALUE,说明之前对子视图高度进行了临时修改,这里将子视图高度恢复为原始值
            if (oldHeight!= Integer.MIN_VALUE) {
                lp.height = oldHeight;
            }

            // 获取子视图测量后的高度
            final int childHeight = child.getMeasuredHeight();
            // 记录当前总长度为totalLength
            final int totalLength = mTotalLength;
            // 更新总长度mTotalLength,取当前总长度和当前总长度加上子视图高度、上下外边距以及获取下一个位置偏移量(getNextLocationOffset方法获取)后的较大值,
            // 目的是准确计算布局在垂直方向占用的总空间
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                    lp.bottomMargin + getNextLocationOffset(child));
        }
    }

    // 如果使用最大子元素相关策略(useLargestChild为true),并且高度测量模式是AT_MOST(最大模式,子视图高度不能超过某个指定值)
    // 或者UNSPECIFIED(未指定模式,父容器对子视图高度没有限制,子视图可以根据自身内容决定高度),执行以下操作:
    if (useLargestChild &&
            (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
        // 重置总长度为0,准备重新计算
        mTotalLength = 0;

        // 再次遍历所有子元素
        for (int i = 0; i < count; ++i) {
            // 获取当前索引对应的虚拟子视图
            final View child = getVirtualChildAt(i);

            // 如果子视图为空,调用measureNullChild方法计算该空子视图对应的长度,并累加到总长度mTotalLength上,然后跳过当前循环后续部分,进入下一次循环
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            // 如果子视图的可见性为GONE,调用getChildrenSkipCount方法获取需要跳过的子元素数量,更新索引i,跳过这些不可见的子元素,进入下一次循环
            if (child.getVisibility() == GONE) {
                i += getChildrenSkipCount(child, i);
                continue;
            }

            // 获取当前子视图的布局参数
            final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                    child.getLayoutParams();
            // 记录当前总长度为totalLength
            final int totalLength = mTotalLength;
            // 更新总长度mTotalLength,取当前总长度和当前总长度加上最大子元素高度(largestChildHeight,应该是之前记录好的某个最大子元素高度值)、
            // 子视图上下外边距以及获取下一个位置偏移量后的较大值,目的可能是基于最大子元素情况来确定布局在垂直方向占用的总空间
            mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                    lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
        }
    }

    // 将顶部和底部内边距累加到总长度上,这样总长度就包含了内边距的空间占用情况
    mTotalLength += mPaddingTop + mPaddingBottom;

    // 将最终计算得到的总长度赋值给heightSize变量,这个变量可能用于后续向父容器汇报布局在垂直方向的尺寸情况等操作
    int heightSize = mTotalLength;
}

measureVertical方法中,首先定义mTotalLength用于存储LinearLayout在垂直方向的高度。然后遍历子元素,根据子元素的MeasureSpec模式分别计算每个子元素的高度。若子元素为wrap_content,则将每个子元素的高度、垂直方向的margin等相关值相加并赋值给mTotalLength,以此得出整个LinearLayout的高度;若布局高度设置为match_parent或具体数值,则与View的测量方法一样进行处理。

这是一段关于View布局流程的代码文档。以下是提取内容并整理格式后的结果:

View的layout布局流程

layout方法的作用是确定元素的位置。ViewGroup中的layout方法用来确定子元素的位置,View中的layout方法则用来确定自身的位置。首先我们看看View的layout方法:

// layout方法用于确定View自身在父容器中的位置
// 参数l、t、r、b分别代表View的左、上、右、下边界坐标
public void layout(int l, int t, int r, int b) {
    // 如果mPrivateFlags3的PFLAG_3_MEASURE_NEEDED_BEFORE_LAYOUT标志位被设置
    if ((mPrivateFlags3 & PFLAG_3_MEASURE_NEEDED_BEFORE_LAYOUT)!= 0) {
        // 调用onMeasure方法进行测量,传入旧的宽度和高度测量规格
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        // 清除PFLAG_3_MEASURE_NEEDED_BEFORE_LAYOUT标志位
        mPrivateFlags3 &= ~PFLAG_3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    // 记录View的旧的左、上、右、下边界坐标
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    // 判断是否基于光学边界布局(与父视图相关)
    boolean changed = isLayoutModeOptical(mParent);

    // 设置光学边界帧,内部会根据changed的值进行不同操作
    setOpticalFrame(l, t, r, b);

    // 如果布局发生了改变或者PFLAG_LAYOUT_REQUIRED标志位被设置
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // 调用onLayout方法进行子视图布局(对于ViewGroup有实际操作,View中通常为空实现)
        onLayout(changed, l, t, r, b);
        // 清除PFLAG_LAYOUT_REQUIRED标志位
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        // 获取ListenerInfo对象,用于处理布局改变的监听器相关操作
        ListenerInfo li = mListenerInfo;
        // 如果监听器信息不为空且布局改变监听器列表不为空
        if (li!= null && li.mOnLayoutChangeListeners!= null) {
            // 克隆布局改变监听器列表
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>) li.mOnLayoutChangeListeners.clone();
            // 获取监听器数量
            int numListeners = listenersCopy.size();
            // 遍历每个监听器并调用onLayoutChange方法通知布局改变
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

    // 计算View的旧宽度和旧高度
    int oldWidth = mRight - mLeft;
    int oldHeight = mBottom - mTop;
    // 计算View的新宽度和新高度
    int newWidth = right - left;
    int newHeight = bottom - top;
    // 判断View的大小是否发生改变
    boolean sizeChanged = (newWidth!= oldWidth) || (newHeight!= oldHeight);
    // 如果大小发生改变,设置RenderNode的边界
    if (sizeChanged) {
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    }
    // 返回布局是否发生改变
    return changed;
}

layout 方法的 4 个参数 l、t、r、b 分别是 View 从左、上、右、下相对于其父容器的距离。

传进来里面的四个参数分别是View的四个点的坐标,它的坐标不是相对屏幕的原点,而且相对于它的父布局来说的。

接着来查看 setFrame 方法里做了什么,代码如下所示:

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    if (DBG) {
        Log.d("View", this + " View.setFrame(" + left + "," + top + "," + right + "," + bottom + ")");
    }
    if (mLeft!= left || mRight!= right || mTop!= top || mBottom!= bottom) {
        changed = true;
        int drawn = mPrivateFlags & PFLAG_DRAWN;
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth!= oldWidth) || (newHeight!= oldHeight);
        invalidate(sizeChanged);
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    }
    return changed;
}

setFrame方法用传进来的l、t、r、b这4个参数分别初始化mLeft、mTop、mRight、mBottom这4个值,这样就确定了该View在父容器中的位置。在调用setFrame方法后,会调用onLayout方法:

// onLayout方法在View中是一个空实现
// 它在布局过程中用于确定子视图的位置,不同的View或ViewGroup会有不同的实现
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

onLayout方法是一个空方法,这和onMeasure方法类似。确定位置时根据不同的控件有不同的实现,所以在View和ViewGroup中均没有实现onLayout方法。既然这样,我们下面就来查看LinearLayout的onLayout方法:

// 在LinearLayout中重写了onLayout方法
// 根据方向(垂直或水平)调用不同的布局方法
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // 如果方向是垂直的,调用layoutVertical方法进行布局
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        // 如果方向是水平的,调用layoutHorizontal方法进行布局(此处未列出layoutHorizontal方法)
        layoutHorizontal(l, t, r, b);
    }
}

与onMeasure方法类似,根据方向来调用不同的方法。这里仍旧查看垂直方向的layoutVertical方法,如下所示:

// layoutVertical方法用于垂直方向上排列LinearLayout的子视图
void layoutVertical(int left, int top, int right, int bottom) {
    // 获取LinearLayout的虚拟子视图数量(可能包括GONE状态的子视图)
    final int count = getVirtualChildCount();
    // 遍历每个子视图
    for (int i = 0; i < count; ++i) {
        // 获取当前索引的子视图
        final View child = getVirtualChildAt(i);
        // 如果子视图为空
        if (child == null) {
            // 累加测量空子视图的高度到childTop(此处measureNullChild方法未列出)
            childTop += measureNullChild(i);
        } else if (child.getVisibility()!= GONE) {
            // 如果子视图可见
            // 获取子视图测量后的宽度和高度
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            // 获取子视图的布局参数(LinearLayout.LayoutParams类型)
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();
            // 如果在当前子视图之前有分割线,累加分割线高度到childTop
            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }
            // 累加子视图的上外边距到childTop
            childTop += lp.topMargin;
            // 设置子视图的位置和大小(调用setChildFrame方法)
            setChildFrame(child, childLeft, childTop, childWidth, childHeight);
            // 累加子视图的高度和下外边距到childTop
            childTop += childHeight + lp.bottomMargin;
            // 根据子视图情况可能跳过一些索引(此处getChildrenSkipCount方法未列出)
            i += getChildrenSkipCount(child, i);
        }
    }
}

这个方法会遍历子元素并调用setChildFrame方法。其中childTop值是不断累加的,这样子元素才会依次按照垂直方向一个接一个排列下去而不会是重叠的。接着看setChildFrame方法:

// setChildFrame方法用于设置子视图的位置和大小
private void setChildFrame(View child, int left, int top, int width, int height) {
    // 调用子视图的layout方法设置其位置和大小
    child.layout(left, top, left + width, top + height);
}

以下是对上述代码的注释:

View的draw流程

View的draw流程很简单,下面先来看看View的draw方法。官方注释清楚地说明了每一步的做法,它们分别是:

  1. 如果需要,则绘制背景。
  2. 保存当前canvas层。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果需要,则绘制View的褪色边缘,这类似于阴影效果。
  6. 绘制装饰,比如滚动条。

其中第2步和第5步可以跳过,所以这里不做分析,重点分析其他步骤。

步骤1:绘制背景(drawBackground)

// 绘制背景的方法
private void drawBackground(Canvas canvas) {
    // 获取View的背景Drawable对象
    final Drawable background = mBackground;
    // 如果背景为空,直接返回,不进行绘制
    if (background == null) {
        return;
    }
    // 设置背景的边界(这里可能涉及到一些布局相关的边界设置,具体方法未展示)
    setBackgroundBounds();
    // 获取View在X轴方向的滚动偏移量
    final int scrollX = mScrollX;
    // 获取View在Y轴方向的滚动偏移量
    final int scrollY = mScrollY;
    // 如果滚动偏移量在X和Y方向都为0,直接在当前canvas上绘制背景
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        // 如果有滚动偏移量,先将canvas按照滚动偏移量进行平移
        canvas.translate(scrollX, scrollY);
        // 在平移后的canvas上绘制背景
        background.draw(canvas);
        // 绘制完成后,将canvas平移回原来的位置
        canvas.translate(-scrollX, -scrollY);
    }
}

从上面代码注释1处可看出绘制背景考虑了偏移参数scrollX和scrollY。如果有偏移值不为0,则会在偏移后的canvas绘制背景。

步骤3:绘制View的内容

// onDraw方法是一个空实现,用于在自定义View时重写来绘制View自身的内容
protected void onDraw(Canvas canvas) {
}

步骤4:绘制子View

调用了 dispatchDraw方法,这个方法也是一个空实现,如下所示:

protected void dispatchDraw(Canvas canvas){
}

ViewGroup重写了这个方法,紧接着看看ViewGroup的 dispatchDraw方法:

// dispatchDraw方法用于分发绘制操作给子View
protected void dispatchDraw(Canvas canvas) {
    // 这里可能是一些初始化或准备工作(省略号表示未展示的代码)
  ...
    // 遍历子View
    for (int i = 0; i < childrenCount; i++) {
        // 处理一些可能的临时子View情况(具体逻辑根据mTransientIndices和mTransients相关操作确定)
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransients.get(transientIndex);
            // 如果子View可见或者有动画,调用drawChild方法绘制子View
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation()!= null) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
        }
        // 获取当前索引的子View
        final View child = mChildren.get(i);
        // 如果子View可见或者有动画,调用drawChild方法绘制子View
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                child.getAnimation()!= null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    // 这里可能是一些后续处理或清理工作(省略号表示未展示的代码)
  ...
}

源码****很长,这里截取了关键的部分,在dispatchDraw方法中对子类View进行遍历,并调用drawChild方法:

// drawChild方法用于绘制单个子View,它调用子View的draw方法
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

这里主要调用了View的draw方法。代码如下所示:

// View的draw方法,用于实际绘制操作
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    // 这里可能是一些绘制相关的准备工作或判断(省略号表示未展示的代码)
  ...
    // 如果不使用绘图缓存进行绘制
    if (!drawWithDrawingCache) {
        // 如果使用渲染节点进行绘制
        if (drawWithRenderNode) {
            // 清除一些标记
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            // 使用DisplayListCanvas绘制渲染节点
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // 清除一些标记
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            // 调用dispatchDraw方法进行绘制(可能会递归调用到子View的绘制)
            dispatchDraw(canvas);
        }
    } else {
        // 清除一些脏标记
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        // 直接调用draw方法进行绘制(可能会有不同的绘制逻辑,取决于是否使用缓存)
        draw(canvas);
    }
    // 这里可能是一些绘制完成后的处理工作(省略号表示未展示的代码)
  ...
}

源码****很长,我们挑重点的看。在上面代码注释1处判断是否有缓存,如果没有则正常绘制,如果有则利用缓存显示。

步骤6:绘制装饰

// onDrawForeground方法用于绘制View的前景装饰
public void onDrawForeground(Canvas canvas) {
    // 绘制滚动指示器
    drawScrollIndicators(canvas);
    // 绘制滚动条
    drawScrollBars(canvas);
    // 获取前景装饰的Drawable对象
    final Drawable foregroundInfo = mForegroundInfo;
    // 如果前景装饰不为空,绘制前景装饰
    if (foregroundInfo!= null) {
        mForegroundInfo.draw(canvas);
    }
}
       // 清除一些标记
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        // 使用DisplayListCanvas绘制渲染节点
        ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
    } else {
        // 清除一些标记
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        // 调用dispatchDraw方法进行绘制(可能会递归调用到子View的绘制)
        dispatchDraw(canvas);
    }
} else {
    // 清除一些脏标记
    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
    // 直接调用draw方法进行绘制(可能会有不同的绘制逻辑,取决于是否使用缓存)
    draw(canvas);
}
// 这里可能是一些绘制完成后的处理工作(省略号表示未展示的代码)


}


**源码****很长,我们挑重点的看。在上面代码注释1处判断是否有缓存,如果没有则正常绘制,如果有则利用缓存显示。**

**步骤6:绘制装饰**

```Java
// onDrawForeground方法用于绘制View的前景装饰
public void onDrawForeground(Canvas canvas) {
    // 绘制滚动指示器
    drawScrollIndicators(canvas);
    // 绘制滚动条
    drawScrollBars(canvas);
    // 获取前景装饰的Drawable对象
    final Drawable foregroundInfo = mForegroundInfo;
    // 如果前景装饰不为空,绘制前景装饰
    if (foregroundInfo!= null) {
        mForegroundInfo.draw(canvas);
    }
}

这些代码详细描述了Android中View在绘制过程中的各个步骤和相关方法的实现逻辑,包括背景绘制、自身内容绘制、子View绘制和前景装饰绘制等操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值