Android自定义控件系列——View的全解析

View

View体系

坐标系

屏幕区域划分

//获取屏幕宽高
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int widthPixels = metrics.widthPixels;
int heightPixels = metrics.heightPixels;
//获取APP区域宽高
Rect rect = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
//获取状态栏高度
Rect rect= new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
int statusBarHeight = rectangle.top;
//获取view布局宽高
Rect rect = new Rect();  
getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(rect);  

绝对坐标

获取view的位置
​ getMeasuredWidth() 返回measure过程得到的mMeasuredWidth值
​ getMeasuredHeight() 返回measure过程得到的mMeasuredHeight值
​ getHeight() layout后有效,返回mRight-mLeft,一般参考measure的宽度
​ getWidth() layout后有效,返回mBottom-mTop,一般参考measure的高度
​ getLocalVisibleRect() 以自身左上角为原点,获取自身可见坐标区域
​ getGlobalVisibleRect() 以屏幕左上角为原点,获取view在屏幕中的绝对坐标可是区域
​ getLocationOnScreen() 以屏幕左上角为原点,获取view左上角绝对坐标
​ getLocationInWindow() 普通Activity时(window与屏幕一样大),获取view左上角到屏幕顶部的距离;对话框式Activity,获取view左上角到activity的标题栏顶部的距离

获得View到其父控件(ViewGroup)的距离
​ getTop() 获取View自身顶边到其父布局顶边的距离
​ getLeft() 左 左
​ getRight() 右 左
​ getBottom() 底 顶

获取焦点坐标(onTouchEvent())
​ getX() 获取点击事件距离控件左边的距离,即视图坐标
​ getY() 顶 视图
​ getRawX() 左 绝对
​ getRawY() 顶 绝对

滑动相关坐标
滑动整个view与滑动view中的内容的区别:会改变getLeft()等API的值
​ offsetLeftAndRight(int offset) 水平方向移动整个view,以X轴正向移动为正
​ offsetTopAndBottom(int offset) 垂直方向移动整个View,以Y轴正向移动为正
​ scrollTo(int x, int y) 将view中的内容移动到指定位置
​ scrollBy(int x, int y) 将view中的内容在当前基础上继续移动X,Y
​ setScrollX(int value) 相当于scrollTo(0,value)
​ setScrollY(int value) 相当于scrollTo(value,0)
​ getScrollX()/getScrollY() 获取当前滑动位置偏移量

补充:scrollBy()和scrollTo()参数传递正数却向坐标系负方向移动的原因
​ 以上两个API移动的是view中内容,要想移动自身,添加一层父布局,再移动父布局的内容即可

绘制流程

measure(测量)–>layout(布局)–>draw(绘制)

Measure

measure过程:从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程
MeasureSpec(测量规格)
​ MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:
​ MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定
​ MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值
​ MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定

最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec()确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)
ViewGroup类提供了measureChild()和measureChildWithMargins(),简化了父子View的尺寸计算
只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数
获取View测量的宽高:View的getMeasuredWidth()和getMeasuredHeight(),这两个方法需在onMeasure流程后调用
View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑
View的布局大小由父View和子View共同决定

操作后得到的是对每个View经测量过的measuredWidth和measuredHeight

Layout

从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上

layout核心
View.layout()可被重载,ViewGroup.layout()为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑
操作后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的
凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的
获取View测量的宽高:View的getWidth()和getHeight(),这两个方法需在onLayout流程之后调用

Draw

绘制过程就是把View对象绘制到屏幕上

需注意的细节:
如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View
View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现
View的绘制是借助onDraw()传入的Canvas实例来进行的
View动画:view自身的动画,可通过setAnimation添加
ViewGroup布局动画:专指内部子View显示时的动画,对应xml布局中的layoutAnimation属性,如LinearLayout设置子View在显示时出现逐行、随机显示等动画效果
在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可
默认情况下子View的ViewGroup.drawChild绘制顺序和子View添加顺序一致,但可重载ViewGroup.getChildDrawingOrder()方法提供不同顺序

源码解析

https://blog.csdn.net/yanbober/article/details/46128379/

performTraversals()

private void performTraversals() {
    ......
    //最外层的根视图的widthMeasureSpec和heightMeasureSpec由来
    //lp.width和lp.height在创建ViewGroup实例时等于MATCH_PARENT
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ......
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ......
    mView.draw(canvas);
    ......
}
ViewRootImpl类,绘制的开始。根据之前设置的状态,判断是否重新计算视图大小(measure)、是否重新放置视图的位置(layout)、以及是否重绘 (draw),其核心也就是通过判断来选择顺序执行这三个方法

getRootMeasureSpec()

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
	int measureSpec;
	switch (rootDimension) {
		case ViewGroup.LayoutParams.MATCH_PARENT:
			measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
			break;
		......
	}
	return measureSpec;
}
用来测Root View,最终走的是MATCH_PARENT,使用MeasureSpec.makeMeasureSpec()创建一个MeasureSpec对象。其specMode = EXACTLY,specSize = windowSize。因此根视图总是全屏

第一步递归measure源码分析

//final方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ......
    //回调onMeasure()方法
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ......
}
为整个View树计算实际的大小,然后设置实际的高和宽,每个View控件的实际宽高都是由父视图和自身决定的。

实际的测量是在onMeasure方法进行,所以在View的子类需要重写onMeasure方法,这是因为measure方法是final的
这个方法的两个参数都是父View传递过来的,也就是代表了父view的规格,由两部分组成。
​ 高2位表示MODE,定义在MeasureSpec类(View的内部类)中,三种类型
​ MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定
​ MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值
​ MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定
​ 低30位表示size,也就是父View的大小
​ 对于系统Window类的DecorVIew对象Mode一般都为MeasureSpec.EXACTLY ,而size分别对应屏幕宽高
​ 对于子View来说大小是由父View和子View共同决定的

//View的onMeasure默认实现方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
		getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
对于非ViewGroup的View,通过调用上面默认的onMeasure()即可完成View的测量,也可重载onMeasure()并调用setMeasuredDimension()来设置任意大小的布局,但一般不这么做

默认实现仅调用了setMeasuredDimension(),它会对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值。由于measure目的就是对View树中每个View的mMeasuredWidth和mMeasuredHeight进行赋值。因此,这两个变量被赋值,标志View的测量工作结束
//如果specMode等于AT_MOST或EXACTLY就返回specSize,这就是系统默认的规格 
public static int getDefaultSize(int size, int measureSpec) {
	int result = size;
	//通过MeasureSpec解析获取mode与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的Background尺寸与通过设置View的miniXXX属性共同决定的。
protected int getSuggestedMinimumWidth() {
	return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
	return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

以上为最基础的元素View的measure过程。

如View为ViewGroup的子类,需递归对每个View进行measure。因此ViewGroup中定义了measureChildren(), measureChild(),measureChildWithMargins()来对子视图进行测量
measureChildren()内部实质只是循环调用measureChild()
measureChild()和measureChildWithMargins()的区别:是否把margin和padding也作为子视图的大小
protected void measureChildWithMargins(View child,
	int parentWidthMeasureSpec, int widthUsed,
	int parentHeightMeasureSpec, int heightUsed) {
	//获取子视图的LayoutParams
	final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
	//调整MeasureSpec
	//通过这两个参数以及子视图本身的LayoutParams来共同决定子视图的测量规格
	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);
	//调运子View的measure方法,子View的measure中会回调子View的onMeasure方法
	child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
对父视图提供的measureSpec参数结合自身的LayoutParams参数进行了调整,然后再调用child.measure(),具体通过getChildMeasureSpec()来进行参数调整
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //获取当前Parent View的Mode和Size
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    //获取Parent size与padding差值(也就是Parent剩余大小),若差值小于0直接返回0
    int size = Math.max(0, specSize - padding);
    //定义返回值存储变量
    int resultSize = 0;
    int resultMode = 0;
    //依据当前Parent的Mode进行switch分支逻辑
    switch (specMode) {
            // Parent has imposed an exact size on us
            //默认Root View的Mode就是EXACTLY
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                //如果child的layout_wOrh属性在xml或者java中给予具体大于等于0的数值
                //设置child的size为真实layout_wOrh属性值,mode为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //如果child的layout_wOrh属性在xml或者java中给予MATCH_PARENT
                // Child wants to be our size. So be it.
                //设置child的size为size,mode为EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //如果child的layout_wOrh属性在xml或者java中给予WRAP_CONTENT
                //设置child的size为size,mode为AT_MOST
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
            ......
            //其他Mode分支类似
    }
    //将mode与size通过MeasureSpec方法整合为32位整数返回
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
通过其父View提供的MeasureSpec参数得到specMode和specSize,然后根据计算出来的specMode以及子View的childDimension(layout_width或layout_height)来计算自身的measureSpec
如果其包含子视图,则计算出来的measureSpec将作为调用其子视图measure()的参数,同时也作为自身调用setMeasuredDimension()的参数
如果其不包含子视图则默认情况下最终会调用onMeasure的默认实现,并最终调用到setMeasuredDimension()
最终决定View的measure大小:View的setMeasuredDimension(),可通过设定固定值来设置View的mMeasuredWidth和mMeasuredHeight大小。但考虑到灵活性,一般不设定固定值

问题:inflate方法加载一些布局显示时指定的大小失效问题

setMeasuredDimension(),它会对View的成员变量mMeasuredWidth和mMeasuredHeight变量赋值,因此自定义的View或者使用现成的View想通过getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值

第二步:递归layout源码分析

private void performTraversals() {
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    //参数:相对Parent的左上右下坐标,左上为0,右下分别为上面刚测量的width和height
    ......
}

//VIewGroup的layout方法
@Override
public final void layout(int l, int t, int r, int b) {
	......
	super.layout(l, t, r, b);
	......
}
//View的layout方法
public void layout(int l, int t, int r, int b) {
	......
	//实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量
	//判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout
	boolean changed = isLayoutModeOptical(mParent) ?
	setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
	//需要重新layout
	if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
		//回调onLayout
		onLayout(changed, l, t, r, b);
		......
	}
	......
}
//VIewGroup的onLayout方法
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
//View的onLayout方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
所有ViewGroup的子类都必须重写onLayout()。所以在自定义ViewGroup控件中,onLayout()配合onMeasure()一起使用可以实现自定义View的复杂布局。
自定义View首先调用onMeasure()进行测量,然后调用onLayout()动态获取子View和子View的测量大小,然后进行layout布局
重载onLayout()的目的就是安排其children在父View的具体位置,重载onLayout()通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置

以LinearLayout的onLayout()为例

public class LinearLayout extends ViewGroup {
    @Override
    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);
...
//以VERTICAL模式为例
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
    //计算父窗口推荐的子View宽度
    final int width = right - left;
    //计算父窗口推荐的子View右侧位置
    int childRight = width - mPaddingRight;

    // Space available for child
    //child可使用空间大小
    int childSpace = width - paddingLeft - mPaddingRight;
    //通过ViewGroup的getChildCount方法获取ViewGroup的子View个数
    final int count = getVirtualChildCount();
    //获取Gravity属性设置
    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    //依据majorGravity计算childTop的位置值
    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) {
            //LinearLayout中其子视图显示的宽和高由measure过程来决定的,因此measure过程的意义就是为layout过程提供视图显示范围的参考值
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            //获取子View的LayoutParams
            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);
            //依据不同的absoluteGravity计算childLeft位置
            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;
            //通过垂直排列计算调运child的layout设置child的位置
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                          childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
...
一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidth和mMeasuredHeight来安排子View在父View中显示的位置,但measure过程得到的结果可能完全没用,特别是对一些自定义的ViewGroup,其子View的个数、位置和大小都是固定的。这时候可以忽略整个measure过程,只在layout函数中传入的4个参数来安排每个子View的具体位置

getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()两对方法的区别

getMeasuredWidth()、getMeasuredHeight()必须在onMeasure之后使用才有效 
getWidth()与getHeight()方法必须在layout(int l, int t, int r, int b)执行之后才有效 

    **同时也解释了,他们宽高值不一致** 
public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

    public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }

    public final int getWidth() {
        return mRight - mLeft;
    }

    public final int getHeight() {
        return mBottom - mTop;
    }

    public final int getLeft() {
        return mLeft;
    }

    public final int getRight() {
        return mRight;
    }

    public final int getTop() {
        return mTop;
    }

    public final int getBottom() {
        return mBottom;
    }

第三步:递归draw源码分析

private void performTraversals() {
    ......
    final Rect dirty = mDirty;
    ......
    canvas = mSurface.lockCanvas(dirty);
    ......
    //mView对于Actiity来说就是PhoneWindow.DecorView
    mView.draw(canvas);
    ......
}

//共分6步,根据注释(skip step 2 & 5 if possible (common case))第2和5步可以跳过 
public void draw(Canvas canvas) {
        ......
        /*
         * 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
        ......
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        ......

        // Step 2, save the canvas' layers
        //canvas.save
        ......
            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }
        ......

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
    	//canvas.restore
        ......
        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }
        ......

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
        ......
    }

第一步,对View的背景进行绘制

private void drawBackground(Canvas canvas) {
	//获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable
	final Drawable background = mBackground;
	......
	//根据layout过程确定的View位置来设置背景的绘制区域
	if (mBackgroundSizeChanged) {
		background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
		mBackgroundSizeChanged = false;
		rebuildOutline();
	}
	......
	//调用Drawable的draw()方法来完成背景的绘制工作
	background.draw(canvas);
	......
}

第三步,对View的内容进行绘制

//空方法,每个View的内容均不相同,所以需要子类去实现
protected void onDraw(Canvas canvas) {}

第四步,对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制

//View的dispatchDraw,空方法,如果View包含子类需要重写
protected void dispatchDraw(Canvas canvas) {}
//ViewGroup的dispatchDraw
@Override
protected void dispatchDraw(Canvas canvas) {
    ......
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    ......
    for (int i = 0; i < childrenCount; i++) {
		......
		if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
		}	
	}
	......
	// Draw any disappearing views that have animations
	if (mDisappearingChildren != null) {
		......
		for (int i = disappearingCount; i >= 0; i--) {
			......
			more |= drawChild(canvas, child, drawingTime);
		}
	}
	......
}
重写了View的dispatchDraw()方法,该方法内部会遍历每个子View,然后调用drawChild()方法 
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
	return child.draw(canvas, this, drawingTime);
}	
调用了子View的draw()方法。所以ViewGroup类已经为我们重写了dispatchDraw()的功能实现,一般不需要重写,但可以重载父类函数实现具体的功能

第六步,对View的滚动条进行绘制

protected final void onDrawScrollBars(Canvas canvas) {
	//绘制ScrollBars分析不是我们这篇的重点,所以暂时不做分析
	......
}
任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已 

canvas.save和canvas.restore

确保对坐标系的改动只发生在save、restore之间的代码
save可以调用canvas的旋转rotate、缩放,平移,剪裁和错切
restore恢复canvas上述的改动,避免对后续绘制进行的影响
save和restore配对使用,restore只能比save少,不能多。多了引发error
save将当前canvas状态入栈,restore把最后一个状态弹出栈
例如:canvas.rotate(45)画一个矩形,这个不用save、restore罩住。那么如果以后再画的矩形也是45旋转的

invalidate()源码分析

public void invalidate(Rect dirty) {
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    //实质还是调运invalidateInternal方法
    invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
    	dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}
public void invalidate(int l, int t, int r, int b) {
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    //实质还是调运invalidateInternal方法
    invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - 					scrollY, true, false);
}
public void invalidate() {
    //invalidate的实质还是调运invalidateInternal方法
    invalidate(true);
}
void invalidate(boolean invalidateCache) {
    //实质还是调运invalidateInternal方法
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, 						invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean 					invalidateCache, boolean fullInvalidate) {
	......
	// Propagate the damage rectangle to the parent view.
	final AttachInfo ai = mAttachInfo;
	final ViewParent p = mParent;
	if (p != null && ai != null && l < r && t < b) {
		final Rect damage = ai.mTmpInvalRect;
		//设置刷新区域
		damage.set(l, t, r, b);
		//传递调运Parent ViewGroup的invalidateChild方法
		p.invalidateChild(this, damage);
	}
	......
}

invalidate方法只能在UI线程调用,最终均调用invalidateInternal()。此方法实质是将要刷新的区域直接传递给父ViewGroup的invalidateChild(),这是一个从当前向上级父View回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集,直到传递到ViewRootImpl后触发了scheduleTraversals方法,然后整个View树开始重新按照上面分析的View绘制流程进行重绘任务 

如果View大小没有发生变化就不会调用layout过程,并且只绘制那些“需要重绘的”View,也就是哪个View(View只绘制该View;ViewGroup请求invalidate系列方法,绘制整个ViewGroup

常见的引起invalidate方法操作的原因

1、直接调用invalidate(),setSelection()或setVisibility(),请求重新draw,但只会绘制调用者本身
2、触发setEnabled(),请求重新draw,但不会重新绘制任何View包括该调用者本身
3、触发requestFocus方法,请求View树的draw过程,只会绘制“需要重绘”的View
public final void invalidateChild(View child, final Rect dirty) {
	ViewParent parent = this;
	final AttachInfo attachInfo = mAttachInfo;
	......
	do {
		......
		//循环层层上级调运,直到ViewRootImpl会返回null
		parent = parent.invalidateChildInParent(location, dirty);
	......
	} while (parent != null);
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    ......
    //View调运invalidate最终层层上传到ViewRootImpl后最终触发了该方法
    scheduleTraversals();
    ......
    return null;
}

postInvalidate()源码分析

用于非UI线程
public void postInvalidate() {
	postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        //核心,实质就是调运了ViewRootImpl.dispatchInvalidateDelayed方法
        attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, 						delayMilliseconds);
    }
}
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
    Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
    //发送一条延时消息
    mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
public void handleMessage(Message msg) {
    ......
    switch (msg.what) {
    case MSG_INVALIDATE:
        //最终在UI线程调用View的invalidate()
        ((View) msg.obj).invalidate();
        break;
    ......
    }
    ......
}

requestLayout()源码分析

//触发View的requestLayout()的实质是层层向上传递,直到ViewRootImpl为止,然后触发ViewRootImpl的requestLayout()
public void requestLayout() {
	......
	if (mParent != null && !mParent.isLayoutRequested()) {
		//由此向ViewParent请求布局
		//从这个View开始向上一直requestLayout,最终到达ViewRootImpl的requestLayout
		mParent.requestLayout();
	}
	......
}
//ViewRootImpl的requestLayout()
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        //View调运requestLayout最终层层上传到ViewRootImpl后最终触发了该方法
        scheduleTraversals();
    }
}
requestLayout()在layout过程中执行setFrame()时,如果控件的位置或者大小发生改变时,会主动调用draw过程。所以应当说成requestLayout()方法会调用measure过程和layout过程,可能调用draw过程,也可能不调用draw过程

为什么ViewRootImpl类的performTraversals()被认为是View绘制的源头?

以PhoneWindow的setContentView()为例

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    ......
        //如果mContentParent为空进行一些初始化,实质mContentParent是通过findViewById(ID_ANDROID_CONTENT);获取的id为content的FrameLayout的布局
        if (mContentParent == null) {
            installDecor();
        } 
    ......
        //把我们的view追加到mContentParent
        mContentParent.addView(view, params);
    ......
}
public void addView(View child) {
     addView(child, -1);
 }

public void addView(View child, int index) {
    ......
        addView(child, index, params);
}

public void addView(View child, int index, LayoutParams params) {
	......
	//该方法稍后后面会详细分析
	requestLayout();
	//调用invalidate(true)触发ViewRootImpl的performTraversals(),至此递归绘制自定义的所有布局
	invalidate(true);
	......
}

自定义View

方式:对原View进行扩展方式;多个View的组合方式;重写View的方式

分类:自定义View继承View或其子类;自定义View继承ViewGroup或其子类

使用场景

类型使用场景实现方式注意点
继承View实现不规则效果,自定义控件绘制(onDraw())需自己支持wrap_content&padding(不需要支持margin,是因为其由父容器决定)
继承View子类扩展现有功能在原有基础上增加功能需要自己支持wrap_content&padding
继承ViewGroup实现不规则效果,自定义布局ViewGroup&子View的measure、layout过程需要自己支持wrap_content&padding&margin
继承ViewGroup子类扩展某种布局的布局方式在原有ViewGroup的基础组合与自定义ViewGroup,更简单,但自由度不高

注意事项

1、支持特殊属性:wrap_content,padding,margin
https://www.jianshu.com/p/ca118d704b5e
​ 对于继承View的控件,padding是在draw()中处理
​ 对于继承ViewGroup的控件,padding和margin会直接影响measure和layout过程
2、多线程直接使用post(View内部提供)方式,避免使用Hanlder等其他方式
3、避免内存泄露,线程、动画应及时停止
​ 启动线程/ 动画:view.onAttachedToWindow();停止线程/动画:view.onDetachedFromWindow()
​ 这两个方法的调用时机分别为包含View的Activity启动和退出或remove时刻
4、处理好滑动冲突,View带有滑动嵌套情况

自定义View优化

onDraw()
	不应做内存分配的事情,因为会导致GC,从而导致卡顿。
	在初始化或者动画间隙期间做分配内存的动作
	减少调用次数,即减少调用invaildate()的次数。优先调用含有4个参数的invalidate(...),因为无参invalidate()会强制绘制整个view
layout()
	任何时候执行requestLayout(),系统去遍历整个View的层级来计算出每一个view的大小。如有冲突,会重新计算好几次
	保持View的层级扁平化
	如果UI复杂,自定义的ViewGroup来执行layout操作。与内置view不同,自定义view可以使得程序仅仅测量这一部分,这避免了遍历整个view的层级结构来计算大小

抗锯齿设置

//方法一:Paint设置抗锯齿
//绘制线条等非位图图形,如drawLine,drawCircle
paint.setAntiAlias(true);
//绘制位图图形,Drawable及其子类
paint.setFilterBitmap(true)//方法二:Canvas设置抗锯齿
canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
//方法三:在上述方法上优化
camera.save();
 // 绕y轴旋转
camera.rotateY(0.3f);
camera.getMatrix(matrix);
camera.restore();
// 获取手机像素密度 (即dp与px的比例)
float scale = context.getResources().getDisplayMetrics().density;
// 修正失真,主要修改 MPERSP_0 和 MPERSP_1
float[] mValues = new float[9];
matrix.getValues(mValues);              	//获取数值
mValues[6] = mValues[6] / scale;          	//数值修正
mValues[7] = mValues[7] / scale;          	//数值修正
matrix.setValues(mValues);             		//重新赋值
 // 调节中心点
matrix.preTranslate(-0, -0);
matrix.postTranslate(0, 0);
canvas.drawBitmap(bmp, matrix, null);
//方法三,在图片四周设置一层透明像素
//方法四:使用.9.path图片

问题

计算View的层级数量

private int getParents(ViewParents view){ 
	if(view.getParents() == null) {
		return 0; 
	}else{ 
		return (1 + getParents(view.getParents)); 
	}
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值