• 请加作者QQ或者微信联系。作者QQ:524100248,微信号:sendtion。

Android View绘制13问13答

安卓教程 sendtion 2年前 (2016-03-23) 780次浏览 已收录 0个评论 扫描二维码

来源:AndroidChina

1.view 的绘制流程分几步,从哪开始?哪个过程结束以后能看到 view?

答:从 ViewRoot 的 performTraversals 开始,经过 measure,layout,draw 三个流程。draw 流程结束以后就可以在屏幕上看到 view 了。

2.view 的测量宽高和实际宽高有区别吗?

答:基本上百分之 99 的情况下都是可以认为没有区别的。有两种情况,有区别。第一种 就是有的时候会因为某些原因 view 会多次测量,那第一次测量的宽高 肯定和最后实际的宽高 是不一定相等的,但是在这种情况下

最后一次测量的宽高和实际宽高是一致的。此外,实际宽高是在 layout 流程里确定的,我们可以在 layout 流程里 将实际宽高写死 写成硬编码,这样测量的宽高和实际宽高就肯定不一样了,虽然这么做没有意义 而且也不好。

3.view 的 measureSpec 由谁决定?顶级 view 呢?

答:由 view 自己的 layoutparams 和父容器  一起决定自己的 measureSpec。一旦确定了 spec,onMeasure 中就可以确定 view 的宽高了。

顶级 view 就稍微特殊一点,对于 decorView 的测量在 ViewRootImpl 的源码里。

//desire 的这 2 个参数就代表屏幕的宽高,
  childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
  childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  //decorView 的 measureSpec 就是在这里确定的,其实比普通 view 的 measurespec 要简单的多
  //代码就不分析了 一目了然的东西
  private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
}

4.对于普通 view 来说,他的 measure 过程中,与父 view 有关吗?如果有关,这个父 view 也就是 viewgroup 扮演了什么角色?

答:看源码:

//对于普通 view 的 measure 来说 是由这个 view 的 父 view ,也就是 viewgroup 来触发的。
//也就是下面这个 measureChildWithMargins 方法
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
         //第一步 先取得子 view 的 layoutParams 参数值
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //然后开始计算子 view 的 spec 的值,注意这里看到 计算的时候除了要用子 view 的 layoutparams 参数以外
        //还用到了父 view 也就是 viewgroup 自己的 spec 的值
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//这个算 view 的 spec 的方法 看上去一大串 但是真的逻辑非常简单 就是根据父亲 viewgroup
//的 meaurespec 同时还有 view 自己的 params 来确定 view 自己的 measureSpec。
//注意这里的参数是 padding,这个值的含义是 父容器已占用的控件的大小 所以 view 的 Specsize
//的值 你们可以看到 是要减去这个 padding 的值的。总大小-已经用的 =可用的。 很好理解。
//然后就是下面的 switch 逻辑 要自己梳理清楚。其实也不难,主要是下面几条原则
//如果 view 采用固定宽高,也就是写死的数值那种。那就不管父亲的 spec 的值了,view 的 spec 就肯定是 exactly 并且大小遵循 layout 参数里设置的大小。
//如果 view 的宽高是 match_parent ,那么就要看父容器 viewgroup 的 spec 的值了,如果父 view 的 spec 是 exactly 模式,
//那 view 也肯定是 exactly,并且大小就是父容器剩下的空间。如果父容器是 at_most 模式,那 view 也是 at_most 并且不会超过剩余空间大小
//如果 view 的宽高是 wrap_content, 那就不管父容器的 spec 了,view 的 spec 一定是 at_most 并且不会超过父 view 剩余空间的大小。
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) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

5.view 的 meaure 和 onMeasure 有什么关系?

答:看源码:

//view 的 measure 是 final 方法 我们子类无法修改的。
 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            resolveRtlPropertiesIfNeeded();
            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                    mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
//不过可以看到的是在 measure 方法里调用了 onMeasure 方法
//所以就能知道 我们在自定义 view 的时候一定是重写这个方法!
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

6.简要分析 view 的 measure 流程?

答:先回顾问题 4,viewgroup 算出子 view 的 spec 以后 会调用子 view 的 measure 方法,而子 view 的 measure 方法 我们问题 5 也看过了实际上是调用的 onMeasure 方法

所以我们只要分析好 onMeasure 方法即可,注意 onMeasure 方法的参数 正是他的父 view 算出来的那 2 个 spec 的值(这里 view 的 measure 方法会把这个 spec 里的 specSize 值做略微的修改 这个部分 不做分析 因为 measure 方法修改 specSize 的部分很简单)。

//可以看出来这个就是 setMeasuredDimension 方法的调用 这个方法看名字就知道就是确定 view 的测量宽高的
//所以我们分析的重点就是看这个 getDefaultSize 方法 是怎么确定 view 的测量宽高的
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
//这个方法特别简单 基本可以认为就是近似的返回 spec 中的 specSize,除非你的 specMode 是 UNSPECIFIED
//UNSPECIFIED 这个一般都是系统内部测量才用的到,这种时候返回 size 也就是 getSuggestedMinimumWidth 的返回值
 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;
}
//跟 view 的背景相关 这里不多做分析了
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

7.自定义 view 中 如果 onMeasure 方法 没有对 wrap_content 做处理 会发生什么?为什么?怎么解决?

答:如果没有对 wrap_content 做处理 ,那即使你在 xml 里设置为 wrap_content.其效果也和 match_parent 相同。看问题 4 的分析。我们可以知道 view 自己的 layout 为 wrap,那 mode 就是 at_most(不管父亲 view 是什么 specmode).

这种模式下宽高就是等于 specSize(getDefaultSize 函数分析可知),而这里的 specSize 显然就是 parentSize 的大小。也就是父容器剩余的大小。那不就和我们直接设置成 match_parent 是一样的效果了么?

解决方式就是在 onMeasure 里 针对 wrap 来做特殊处理 比如指定一个默认的宽高,当发现是 wrap_content 就设置这个默认宽高即可。

8.ViewGroup 有 onMeasure 方法吗?为什么?

答:没有,这个方法是交给子类自己实现的。不同的 viewgroup 子类 肯定布局都不一样,那 onMeasure 索性就全部交给他们自己实现好了。

9.为什么在 activity 的生命周期里无法获得测量宽高?有什么方法可以解决这个问题吗?

答:因为 measure 的过程和 activity 的生命周期  没有任何关系。你无法确定在哪个生命周期执行完毕以后 view 的 measure 过程一定走完。可以尝试如下几种方法 获取 view 的测量宽高。

//重写 activity 的这个方法
public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            int width = tv.getMeasuredWidth();
            int height = tv.getMeasuredHeight();
            Log.v("burning", "width==" + width);
            Log.v("burning", "height==" + height);
        }
    }

或者重写这个方法

@Override
    protected void onStart() {
        super.onStart();
        tv.post(new Runnable() {
            @Override
            public void run() {
                int width = tv.getMeasuredWidth();
                int height = tv.getMeasuredHeight();
            }
        });
    }

再或者:

@Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = tv.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                int width = tv.getMeasuredWidth();
                int height = tv.getMeasuredHeight();
                tv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
    }

10.layout 和 onLayout 方法有什么区别?

答:layout 是确定本身 view 的位置 而 onLayout 是确定所有子元素的位置。layout 里面 就是通过 serFrame 方法设设定本身 view 的 四个顶点的位置。这 4 个位置以确定 自己 view 的位置就固定了

然后就调用 onLayout 来确定子元素的位置。view 和 viewgroup 的 onlayout 方法都没有写。都留给我们自己给子元素布局

11.draw 方法 大概有几个步骤?

答: 一共是 4 个步骤, 绘制背景———绘制自己——–绘制 chrildren—-绘制装饰。

12.setWillNotDraw 方法有什么用?

答:这个方法在 view 里。

/**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

用于设置标志位的 也就是说 如果你的自定义 view 不需要 draw 的话,就可以设置这个方法为 true。这样系统知道你这个 view 不需要 draw 可以优化执行速度。viewgroup 一般都默认设置这个为 true,因为 viewgroup 多数都是只负责布局

不负责 draw 的。而 view 这个标志位 默认一般都是关闭的。

13.自定义 view 有哪些需要注意的点?

答:主要是要处理 wrap_content 和 padding。否则 xml 那边设置这 2 个属性就根本没用了。还有不要在 view 中使用 handler 因为人家已经提供了 post 方法。如果是继承自 viewGroup,那在 onMeasure 和 onLayout 里面 也要考虑

padding 和 layout 的影响。也就是说 specSize 要算一下 。最后就是如果 view 的动画或者线程需要停止,可以考虑在 onDetachedFromWindow 里面来做。

针对上述的几点,给出几个简单的自定义 view 供大家理解。

给出一个圆形的 view 范例:

package com.example.administrator.motioneventtest;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
/**
 * Created by Administrator on 2016/2/4.
 */
public class CircleView extends View {
    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private void init() {
        mPaint.setColor(mColor);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        //处理为 wrap_content 时的情况
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //处理 padding 的情况
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
    }
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    public CircleView(Context context) {
        super(context);
        init();
    }
    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
}

然后下面再给出一个范例,稍微复杂一点是自定义 viewgroup 了(主要是加强对 onMeasure 和 onLayout 的理解), 需求如下:

一个水平的 viewgroup,内部的子元素 为了简单 我们假定他们的宽高都是一样的。来写一个这样的简单的 viewgroup。

packagecom.example.administrator.motioneventtest;
importandroid.content.Context;
importandroid.util.AttributeSet;
importandroid.util.Log;
importandroid.view.View;
importandroid.view.ViewGroup;
/**
 * Created by Administrator on 2016/2/4.
 */
//这里我们只处理了 padding 的状态 没有处理 margin 的状态,子 view 的 margin 对 measure 和 layout 的影响
//就留给读者自己完成了
publicclassCustomHorizontalLayout extendsViewGroup {
    //设置默认的控件最小是多少 这里不提供自定义属性了 写死在代码里 你们可以自行拓展
    finalintminHeight = 0;
    finalintminWidth = 0;
    publicCustomHorizontalLayout(Context context) {
        super(context);
    }
    publicCustomHorizontalLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    publicCustomHorizontalLayout(Context context, AttributeSet attrs, intdefStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    protectedvoidonMeasure(intwidthMeasureSpec, intheightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        intmeasureWidth = 0;
        intmeasureHeight = 0;
        finalintchildCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        intwidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        intwidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        intheightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        intheightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        finalView childView = getChildAt(0);
        finalintpaddingLeft = getPaddingLeft();
        finalintpaddingRight = getPaddingRight();
        finalintpaddingTop = getPaddingTop();
        finalintpaddingBottom = getPaddingBottom();
        //没有子控件 时 我们的宽高要作特殊处理
        if(childCount == 0) {
            //当没有子控件时,如果长宽有一个为 wrap 那么就让这个控件以最小的形式展现
            //这里我们最小设置为 0
            if(widthSpecMode == MeasureSpec.AT_MOST || heightSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(minWidth, minHeight);
            } else{
                //否则根据我们的 layout 属性来
                setMeasuredDimension(getLayoutParams().width, getLayoutParams().height);
            }
        } elseif(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            measureWidth = childView.getMeasuredWidth() * childCount;
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(paddingLeft + measureWidth + paddingRight, paddingTop + measureHeight + paddingBottom);
        } elseif(heightSpecMode == MeasureSpec.AT_MOST) {
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(paddingLeft + paddingRight + widthSpecSize, paddingTop + paddingBottom + measureHeight);
        } elseif(widthSpecMode == MeasureSpec.AT_MOST) {
            measureWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(paddingLeft + paddingRight + measureWidth, paddingTop + paddingBottom + heightSpecSize);
        }
    }
    @Override
    protectedvoidonLayout(booleanchanged, intl, intt, intr, intb) {
        finalintpaddingLeft = getPaddingLeft();
        finalintpaddingRight = getPaddingRight();
        finalintpaddingTop = getPaddingTop();
        finalintpaddingBottom = getPaddingBottom();
        //左边初始位置为 0
        intchildLeft = 0+ paddingLeft;
        finalintchildCount = getChildCount();
        for(inti = 0; i < childCount; i++) {
            finalView childView = getChildAt(i);
            if(childView.getVisibility() != View.GONE) {
                finalintchildWidth = childView.getMeasuredWidth();
                childView.layout(childLeft, 0+ paddingTop, childLeft + childWidth, paddingTop + childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }
}

乐趣公园 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:Android View 绘制 13 问 13 答
喜欢 (0)
[sendtion@163.com]
分享 (0)
sendtion
关于作者:
一个不断奋斗追逐梦想的少年~
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址