赞
踩
在开发过程中我们经常要进行view的自定义。如果熟练掌握自定义技巧的话就能做出很多控件出来。这篇文章来讲讲view绘制背后发生的那些事。
首先先说说view绘制的整体过程。
View绘制的源码分析 ,它的三大流程都是在ViewRootImpl中完成的,从ViewRootImpl中的performTraversals开始,有三个方法performMeasure, performLayout, prformDraw分别对measure,layout,draw三个方法。在onMeasure对所有子元素进行measure过程 ,这时measure就从父容器传递到子元素。子元素重复父元素的过程。
layout与draw类似,只是draw通过diapatchDraw来实现。measure完成后可以通过getMeasureWidth,getMeasureHeight分别获取View测量后的宽高。在实际情况下几乎所有情况它都等于最终宽高。layout过程决定view的四个顶点的坐标和实际view的宽高,完成之后可以通过getTop,getBottom,getLeft,getRight来拿 到view的四个顶点位置。并通过getWidth()和getHeight()来拿到最终宽高。draw决定了view的显示,只有完成才能显示在屏幕上。
所以,view的生命周期,从构造方法开始,依次执行
onAttachToWindow——>onMeasure——>onSizeChanged——>onLayout——> onMeasure——>onLayout——>onDraw——>onDetachedFromWindow
View与ViewGroup里的方法调用过程总结如下:
1.首先,Activity onCreate并初始化view
2.然后,Activity onResume后调用view的onAttachedToWindow,因此常常在onAttachedToWindow方法中做初始化工作,比如注册一些广播、开始动画等等……
3.接下来,如果设置了背景则调用onDraw,之后倘若是viewgroup则调用dispatchDraw。
说明:dispatchDraw()主要是分发给子组件进行绘制,我们通常定制组件的时候重写的是onDraw()方法。值得注意的是ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法, 而绕过了draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法,或者自定制一个Drawable,重写它的draw(Canvas c)和 getIntrinsicWidth(),getIntrinsicHeight()方法,然后设为背景。
4.最后,当点击回退键时,Activity onDestroy后才调用onDetachedFromWindow,这时我们就在这个方法做一些收尾工作,如:取消广播注册、停止动画等等。
说明:onDraw与dispatchDraw将可能由于setVisible或onresume调用多次,而onAttachedToWindow与onDetachedFromWindow在创建与销毁view的过程中只会调用一次。
5.此外在创建期间还会多次调用onMeasure和onLayout
注:onFinishInflate 的调用,当View中所有的子控件均被映射成xml后触发,也就是onFinishInflate ——>onAttachToWindow......
在测量过程中系统会将View的LayoutParams根据容器所施加的规则转换成对应的MeasureSpec,然后再根据这个测量出view。
Measure是一个32位的int,高2位代表SpecMode,低30位代表SpecSize。SpecMode表示测量模式,SpecSize指在某种测量模式下规格的大小。其代码如下:
- 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) ;
- }
- }
其实MeasureSpec中源码很值得我们学习。他用一个32位的int来表示模式和大小,节省了空间,也更直观。MeasureSpec通过将specMode和specSize打包成一个int来避免过多的对象内存分配。以上是MeasureSpec的打包和解包过程。
specMode有三种状态:UNSPECIFIED,EXACTLY(相当于matchparent和精确值这两种模式),ATMOST(wrap_content)。
对于一般容器,它的MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams共同决定的。PhoneWindow包了一层DecorView,DecorView里才是title和我们的content view。所以行分析DecorView。
先来看下DecorView的产生源码:
- childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
- childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
- performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
- 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;
- }
这里很清楚,分别分MatchPraent和wrap_content和自定义来计算宽高。再来看下普通的view,在ViewGroup的measureChildWIthMargins中:
- protected void measureChildWithMargins (View child,
- int parentWidthMeasureSpec , int widthUsed,
- int parentHeightMeasureSpec , int heightUsed) {
- final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams() ;
-
- 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);
- }
再看下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) {
- // 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 = 0;
- resultMode = MeasureSpec. UNSPECIFIED;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
- // Child wants to determine its own size.... find out how
- // big it should be
- resultSize = 0;
- resultMode = MeasureSpec. UNSPECIFIED;
- }
- break;
- }
- return MeasureSpec.makeMeasureSpec(resultSize, resultMode) ;
- }
以上表明,如果父是EXACTLY,parentSize,那么子如果是EXACTLY,
具体的值size:那子的MeasureSpec就是EXACTLY,size;
MATCH_PARENT:那子的MeasureSpec就是EXACTLY,parentSize;
WRAPCONTENT:那子的MeasureSpec就是ATMOST,parentSize;
如果父是ATMOST,parentSize,那么子如果是EXACTLY,
具体的值size:那子的MeasureSpec就是EXACTLY,size;
MATCHPARENT:那子的MeasureSpec就是ATMOST,parentSize;
WRAPCONTENT:那子的MeasureSpec就是ATMOST,parentSize;
总结:对于普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。
measure。如果是view,measure绘制其自身。如果是VIewGroup,measure绘制自身外,还要绘制其子元素。先看View的measure方法,measure是一个final方法,不能重写:
- if (cacheIndex < 0 |if (cacheIndex < 0 || sIgnoreMeasureCache ) {
- // measure ourselves, this should set the measured dimension flag back
- onMeasure(widthMeasureSpec , heightMeasureSpec);
- mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- }| sIgnoreMeasureCache ) {
- // measure ourselves, this should set the measured dimension flag back
- onMeasure(widthMeasureSpec , heightMeasureSpec);
- mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- }
调用了onMeasure(),来看下它的源码:
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth() , widthMeasureSpec) ,
- getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)) ;
- }
看下getSuggestedMinimumWidth(),它就是获取背景大小和mMinWidth的较大值:
- protected int getSuggestedMinimumWidth () {
- return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground .getMinimumWidth());
- }
那么mMinWidth是什么呢,mMinWidth就是设置的android:minWidth的属性,没设置就等于0。不信,看如下代码:
- case R.styleable.View_minWidth:
- mMinWidth = a.getDimensionPixelSize(attr , 0) ;
- break;
getMinimumWidth()表示的是获取背景图大小,它位于Drawable下:
- public int getMinimumHeight() {
- final int intrinsicHeight = getIntrinsicHeight() ;
- return intrinsicHeight > 0 ? intrinsicHeight : 0 ;
- }
看下getDefaultSize方法:
- 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;
- }
它返回了specSize,它是测量后的大小。由上面的分析可知,view的宽高由specSize决定,而如果直接继承View的控件需要重写onMeasure方法并设置wrapcontent的自身大小。否则wrapcontent就相当 于Match_parent了。一般一重写方法如下:
- 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);
- 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);
- }
- }
上面的200是指定的一个默认宽高。
ViewGroup的measure过程,它没有重写onMeasure,它会调用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);
- }
- }
- }
分别绘制child,进入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);
- }
获取LayoutParams,通过getChildMeasureSpec来创建子无素的MeasureSpec,调用child.measure,因为ViewGroup有不同的特性,所以无法实现统一的onMeasure。
viewGroup会遍历所有子元素并调用 其layout方法,layout方法来确定子元素的位置。ViewGroup如下:
- protected abstract void onLayout(boolean changed,
- int l , int t, int r, int b) ;
AbsoluteLayout重载的onLayout方法:
- @Override
- protected void onLayout(boolean changed, int l, int t,
- int r, int b) {
- int count = getChildCount();
-
- for (int i = 0; i < count; i++) {
- View child = getChildAt(i);
- if (child.getVisibility() != GONE) {
- AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) child.getLayoutParams();
- int childLeft = mPaddingLeft + lp.x;
- int childTop = mPaddingTop + lp.y;
- child.layout(childLeft, childTop,
- childLeft + child.getMeasuredWidth(),
- childTop + child.getMeasuredHeight());
- }
- }
- }
需要子类自己实现。看下view的layout:
- 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);
- mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
-
- ListenerInfo li = mListenerInfo;
- if (li != null && li.mOnLayoutChangeListeners != null) {
- ArrayList<OnLayoutChangeListener> listenersCopy =
- (ArrayList<OnLayoutChangeListener>)li. 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 &= ~PFLAG_FORCE_LAYOUT;
- mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
- }
在setFrame中确定了view的四个顶点坐标。mleft等。onLayout view也没有具体实现,要看具体的。以LinearLayout为例:
- protected void onLayout(boolean changed, int l , int t, int r, int b) {
- if (mOrientation == VERTICAL) {
- layoutVertical(l, t, r , b);
- } else {
- layoutHorizontal(l, t, r , b);
- }
- }
以layoutVertical为例:
- void layoutVertical(int left, int top , int right, int bottom) {
- final int paddingLeft = mPaddingLeft ;
-
- int childTop ;
- int childLeft ;
-
- // Where right end of child should go
- final int width = right - left;
- int childRight = width - mPaddingRight ;
-
- // Space available for child
- int childSpace = width - paddingLeft - mPaddingRight ;
-
- final int count = getVirtualChildCount() ;
-
- final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
- final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
-
- switch (majorGravity) {
- case Gravity.BOTTOM:
- // mTotalLength contains the padding already
- childTop = mPaddingTop + bottom - top - mTotalLength;
- break;
-
- // mTotalLength contains the padding already
- case Gravity.CENTER_VERTICAL:
- childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
- break;
-
- case Gravity.TOP:
- default :
- childTop = mPaddingTop;
- break;
- }
-
- for (int i = 0; i < count ; i++) {
- final View child = getVirtualChildAt(i) ;
- if (child == null) {
- childTop += measureNullChild(i);
- } else if (child.getVisibility() != GONE) {
- final int childWidth = child.getMeasuredWidth() ;
- final int childHeight = child.getMeasuredHeight();
-
- final LinearLayout.LayoutParams lp =
- (LinearLayout.LayoutParams) child.getLayoutParams() ;
-
- int gravity = lp. gravity;
- if (gravity < 0) {
- gravity = minorGravity;
- }
- final int layoutDirection = getLayoutDirection() ;
- final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection) ;
- switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
- case Gravity. CENTER_HORIZONTAL :
- childLeft = paddingLeft + ((childSpace - childWidth) / 2)
- + lp. leftMargin - lp.rightMargin ;
- break;
-
- case Gravity.RIGHT:
- childLeft = childRight - childWidth - lp. rightMargin;
- break;
-
- case Gravity.LEFT:
- default:
- childLeft = paddingLeft + lp. leftMargin;
- break;
- }
-
- if (hasDividerBeforeChildAt(i)) {
- childTop += mDividerHeight;
- }
-
- childTop += lp.topMargin;
- setChildFrame(child , childLeft, childTop + getLocationOffset(child) ,
- childWidth, childHeight);
- childTop += childHeight + lp. bottomMargin + getNextLocationOffset(child);
-
- i += getChildrenSkipCount(child , i);
- }
- }
- }
主要看以下代码:
- final int childWidth = child.getMeasuredWidth() ;
- final int childHeight = child.getMeasuredHeight() ;
- setChildFrame(child , childLeft, childTop + getLocationOffset(child) ,
- childWidth , childHeight);
- childTop += childHeight + lp. bottomMargin + getNextLocationOffset(child);
top会逐渐增大,所以会往下排。setChildFrame仅仅是调用子元素的layout方法
- private void setChildFrame(View child, int left, int top , int width, int height) {
- child.layout(left, top, left + width , top + height);
- }
通过子元素的layout来确定自身
它有以下几步:
绘制背景,(canvas)
绘制自己。(onDraw)
绘制children(dispatchDraw)
绘制装饰(onDrawScrollBars)
看下view的draw源码:
- public void draw(Canvas canvas) {
- final int privateFlags = mPrivateFlags;
- final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK ) == PFLAG_DIRTY_OPAQUE &&
- (mAttachInfo == null || !mAttachInfo .mIgnoreDirtyState );
- mPrivateFlags = (privateFlags & ~ PFLAG_DIRTY_MASK ) | PFLAG_DRAWN;
-
- /*
- * Draw traversal performs several drawing steps which must be executed
- * in the appropriate order:
- *
- * 1. Draw the background
- * 2. If necessary, save the canvas' layers to prepare for fading
- * 3. Draw view's content
- * 4. Draw children
- * 5. If necessary, draw the fading edges and restore layers
- * 6. Draw decorations (scrollbars for instance)
- */
- // Step 1, draw the background, if needed
- int saveCount;
-
- if (!dirtyOpaque) {
- drawBackground(canvas);
- }
-
- // skip step 2 & 5 if possible (common case)
- final int viewFlags = mViewFlags;
- boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL ) != 0;
- boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL ) != 0;
- if (!verticalEdges && !horizontalEdges) {
- // Step 3, draw the content
- if (!dirtyOpaque) onDraw(canvas) ;
-
- // Step 4, draw the children
- dispatchDraw(canvas) ;
-
- // Step 6, draw decorations (scrollbars)
- onDrawScrollBars(canvas) ;
-
- if ( mOverlay != null && !mOverlay.isEmpty()) {
- mOverlay .getOverlayView().dispatchDraw(canvas) ;
- }
-
- // we're done...
- return;
- }
viewgroup中的dispatchDraw用于遍历子view并调用子view的draw方法。这样就一层层的传下去。到此源码分析就结束了。在绘制view的时候经常会在activity中获得view的宽高,因为activity的生命周期和view不同步,在oncreate中无法获取到view的宽高,接下来讲讲activity中如何获取view。
onWindowFocusChanged:view己经初始化完毕,宽高己经准备好。当Activity得到焦点和失去焦点均会被调用,所以它会调用多次。
通过view.post,将一个runnable投弟到消息队列尾部,等待looper调用时,view己经初始化好
- protected void onStart(){
- super.onStart();
- view.post(new Runnable(){
- public void run(){
- int width = view.getMeasuredWidth();
- int height = new .getMeasuredHeight();
- }
- })
- }
ViewTreeObserver:
使用ViewTreeObserver众多回调接口来完成,如OnGlobalLayoutListener,当view树状态发生改变时或内部view可见性发生改变时会回调
- ViewObserver obserber = view.getViewObserver ();
- obserber.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
- public void onGlobalLayout(){
- obserber.removeOnGlobalLayoutListener(this);
- int width = view.getMeasuredWidth();
- int height = new .getMeasuredHeight();
- }
- })
通过view进行measure来得到view的宽高
- int width = MeasureSpec.makeMeasureSpec(100,Measure.EXACTLY);//确定值
- int height= MeasureSpec.makeMeasureSpec(100,Measure.EXACTLY);//确定值
- view.measure(width,height);
- //对于wrap_content:
- int width = MeasureSpec.makeMeasureSpec((1<<30)-1,Measure.AT_MOST);//wrap_content
- int height= MeasureSpec.makeMeasureSpec((1<<30)-1,Measure.AT_MOST);//wrap_content
- view.measure(width,height);
自定义View需要注意的事项:
如果是继承view或者viewGroup,让view支持wrap_content。
如果有必要,让view支持padding。
View中如果有动画或者线程,要在onDetachedFromWindow中及时停止。当view的Activity退出或者当前view被remove时,调用它。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。