Android基础知识 View绘制相关

一、DecorView、Window、ViewRootImpl 等概念

View的三大流程:measure、layout、draw

1、 ViewRootImpl

连接 WindowManager 和 DecorView
View 三大流程都是通过 ViewRootImpl 完成的

  • 在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,
    同时会创建 ViewRootImpl 对象,将 ViewRootImp 与 DecorView 建立关联

    位于:WindowManagerGlobal.addView()

    root = new ViewRootImpl(view.getContext(), display);
    root.setView(view, wparams, panelParentView);
    
  • View的绘制流程, 从 ViewRoot 的 performTraversals() 方法开始,经过 measure、layout、draw
    将一个View绘制出来

    measure:测量 View的宽高
    layout:确定 View在父容器中的位置
    draw:将 View绘制在屏幕上

  • performTraversals() 会依次调用 performMeasure、performLayout、performDraw 三个方法

    这三个方法分别完成 顶级View 的 measure、layout、draw三大流程

    performMeasure -> measure -> onMeasure
    performLayout -> layout -> onLayout
    performDraw -> draw -> dispatchDraw -> onDraw

  • 顶级View 在 onMeasure 方法中,会对所有子元素进行 measure 过程
    顶级View 在 onLayout 方法中,会对所有子元素进行 layout 过程
    顶级View 在 onDraw 方法中,会 通过 dispatchDraw 传递到子元素 进行 draw 过程

动作结果
measure 完成getMeasuredWidth / getMeasuredHeight 可拿到View宽高
layout 完成getTop、getBottom、getLeft、getRight 可拿到View的四个顶点位置
getWidth、getHeight 可拿到View的最终宽高
draw 完成View的内容显示在屏幕上

View绘制过程

WindowManagerGlobal
ActivityThread
ActivityThread# handleResumeActivity
activity.makeVisible
WindowManagerGlobal.addView
new ViewRootImpl.setView<DecorView...>
ViewRootImpl.requestLayout
ViewRootImpl.scheduleTraversals
执行 mTraversalRunnable
ViewRootImpl.doTraversal
ViewRootImpl.performTraversals
ActivityThread.performLaunchActivity
classloader.loadClass<activity>.newInstance
activity.attach
attach 方法中会创建 PhoneWindow
2、 DecorView

顶级View
内部包含一个竖直方向的LinearLayout,其中分为上下两部分
上部分是标题栏
下面是内容栏 FrameLayout (id 为 android.R.id.content)

Activity.setContentView,设置的 View,会被添加到内容栏

内容栏id:android.R.id.content

3、 Window

窗口,Window是View的管理者,唯一的实现类是 PhoneWindow
WindowManager 是访问 Window 的入口。

Window 的具体实现在 WindowManagerService 中

Android中所有试图都是通过Window 呈现

事件传递,由 Activity -> Window -> DecorView -> View

二、MeasureSpec 概念

  • MeasureSpec 代表
    一个 32 位的int 值
    高 2 位比代表 SpecMode
    低 30 位代表 SpecSize

  • SpecMode 测量模式 ,int值

  • SpecSize 某种测量模式下的尺寸,int 值

    MeasureSpec 将 SpecMode 、SpecSize 拼成一个 int 值,来避免过多对象内存分配

  • 打包、解包的方法:
    打包:

    public static int makeMeasureSpec(int size, int mode);
    

    解包:

    public static int getMode(int measureSpec);
    public static int getSize(int measureSpec);
    
1、SpecMode 有三类:
  • UNSPECIFIED
    父容器不限制 View 的尺寸,要多大给多大。
    用于【系统内部】,表示一种测量状态。

  • EXACTLY
    父容器检测出 View 所需要的精确大小,此时 View最终大小就是 SpecSize 所指定的值
    对应 LayoutParams 中的 match_parent具体的数值

  • AT_MOST
    父容器指定一个可用大小 SpecSize,View大小不能大于这个值,具体值要看不同View的具体实现。
    对应 LayoutParams 中的 wrap_content

    根据【ViewGroup.getChildMeasureSpec() 源码】推导
    普通View的 MeasureSpec 创建规则
    父容器的 MeasureSpec 和 View 的 LayoutParams 共同确定View的MeasureSpec

    child
    LayoutParams
    parentSpecMode
    EXACTLY
    parentSpecMode
    AT_MOST
    parentSpecMode
    UNSPECIFIED
    dp/pxEXACTLY
    childSize
    EXACTLY
    childSize
    EXACTLY
    childSize
    match_parentEXACTLY
    parentSize
    AT_MOST
    parentSize
    UNSPECIFIED
    API<23?0:parentSize
    wrap_contentAT_MOST
    parentSize
    AT_MOST
    parentSize
    UNSPECIFIED
    API<23?0:parentSize

    由上表得出结论:
    View的 LayoutParams有确切值(dp/px)时,View的测量结果就是该确切值

    View的LayoutParams为 match_parent时,View的测量结果是【父容器的剩余尺寸】

    View的LayoutParams为 wrap_content时,View的测量结果是【父容器的剩余尺寸】

    自定义View时,需要对 SpecMode 为 AT_MOST 也就是 wrap_content 的情况做计算处理
    否则自定义View 在布局中使用 wrap_content 时,会撑满父容器


    源码 ViewGroup.measureChildWithMargins() -> ViewGroup.getChildMeasureSpec()

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
      	//父容器的 specMode
      	int specMode = MeasureSpec.getMode(spec);
      	//父容器的 specSize
      	int specSize = MeasureSpec.getSize(spec);
      	// 父容器减去边距后的剩余 specSize, 也就是父容器的剩余尺寸
      	int size = Math.max(0, specSize - padding);
      	int resultSize = 0;//子View的 尺寸
      	int resultMode = 0;//子View的 specMode
    
      	switch (specMode) {
      		case MeasureSpec.EXACTLY: //父容器EXACTLY,dp/px/match_parent
      			if (childDimension >= 0) { //子View,dp/px
      				resultSize = childDimension;//子View 测量尺寸为 LayoutParams中的dp/px
      				resultMode = MeasureSpec.EXACTLY;//子View Mode为 EXACTLY
      				
      			} else if (childDimension == LayoutParams.MATCH_PARENT) {//子View match_parent
      				resultSize = size;//子View测量尺寸为父容器剩余尺寸
      				resultMode = MeasureSpec.EXACTLY;//子View Mode为 EXACTLY
      				
      			} else if (childDimension == LayoutParams.WRAP_CONTENT) {//子View wrap_content
      				resultSize = size;//子View 测量尺寸为父容器剩余尺寸
      				resultMode = MeasureSpec.AT_MOST;//子View Mode AT_MOST
      			}
      			break;
      			
      		case MeasureSpec.AT_MOST://父容器AT_MOST,wrap_content
      			if (childDimension >= 0) {//子View,dp/px
      				resultSize = childDimension;//子View测量尺寸为LayoutParams中的dp/px
      				resultMode = MeasureSpec.EXACTLY;//子View Mode为 EXACTLY
      				
      			} else if (childDimension == LayoutParams.MATCH_PARENT) {//子View match_parent
      				resultSize = size;//子View 测量尺寸为父容器剩余尺寸
      				resultMode = MeasureSpec.AT_MOST;//子View Mode为 AT_MOST
      				
      			} else if (childDimension == LayoutParams.WRAP_CONTENT) {//子View wrap_content
      				resultSize = size;//子View 测量尺寸为父容器剩余尺寸
      				resultMode = MeasureSpec.AT_MOST;//子View Mode 为 AT_MOST
      			}
      			break;
      			
      		case MeasureSpec.UNSPECIFIED://父容器UNSPECIFIED
      			if (childDimension >= 0) {//子View,dp/px
      				resultSize = childDimension;//子View测量尺寸为LayoutParams中的dp/px
      				resultMode = MeasureSpec.EXACTLY;//子View Mode为 EXACTLY
      				
      			} else if (childDimension == LayoutParams.MATCH_PARENT) {//子View match_parent
      				//View.sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M;
      				// 5.0以下,resultSize 为0,5.0及以上为父容器剩余尺寸
      				resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
      				resultMode = MeasureSpec.UNSPECIFIED;//子View Mode为 UNSPECIFIED
      				
      			} else if (childDimension == LayoutParams.WRAP_CONTENT) {//子View wrap_content
      				//View.sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M;
      				resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
      				resultMode = MeasureSpec.UNSPECIFIED;//子View Mode为 UNSPECIFIED
      			}
      			break;
      	}
      	
      	//把子View的 specSize 和 specMode 打包成 MeasureSpec 返回
      	return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    

三、MeasureSpec 和 LayoutParams 的对应关系

  • View 测量时,系统会将 LayoutParams 在父容器的约束下转换成 对应的 MeasureSpec
    再根据这个 MeasureSpec 来确定 View测量后的宽高

  • 普通View 的 MeasureSpec 是由 LayoutParams 和父容器的 MeasureSpec 共同决定的,从而进一步决定View 的宽高。

  • 对于顶级View – DecorView 与普通 View略有不同

    DecorView 的 MeasureSpec 由 窗口的尺寸 和 DecorView 的 LayoutParams 共同决定

  • MeasureSpec一旦确定后,onMeasure中就可以确定View 的测量宽高

四、View的工作流程,measure过程、layout过程、draw过程

一般情况下 measure 完成后,通过 getMeasuredWidth / Height 就可以正确获取View的测量宽高
极端情况下,系统需要多次 measure 才能确定最终测量宽高,此时 onMeasure中拿到的测量宽高可能不准确。

比较好的习惯是,在 onLayout 中去获取View的测量宽高/最终宽高

1、 View的 measure过程

由View的 measure 方法完成,measure方法是 final 类型方法,重写 onMeasure来测量

  • View.onMeasure() 源码:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //setMeasuredDimension 方法,会设置View 的 宽/高值
        setMeasuredDimension(
      		getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
      		getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    
    /**
     * 返回的大小就是 measureSpec 中的 specSize
     * specSize 就是View 测量后的大小
     */
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);//取出specSize
    
        switch (specMode) {
      	  case MeasureSpec.UNSPECIFIED: //UNSPECIFIED 一般用于系统内部的测量过程
      		  //size 是 getSuggestedMinimumWidth()/getSuggestedMinimumHeight()返回的
      		  result = size;
      	  	  break;
      	  case MeasureSpec.AT_MOST: // wrap_content
      	  case MeasureSpec.EXACTLY: //明确大小 和 match_parent
      		  result = specSize; // 就是specSize 的值
      		  break;
        }
        return result;
    }
    

    getSuggestedMinimumWidth()
    View 没背景, 那么View的宽度为 mMinWidth 对应 android:minWidth 属性 的值
    如果 这个属性 不指定,那么 mMinWidth 就默认为 0

    View 有背景,View的宽度为 max(mMinWidth, mBackground.getMinimumWidth())

     protected int getSuggestedMinimumWidth() {
         return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
     }
    
    /* Drawable.getMinimumWidth()
     * 返回 Drawable 的原始宽度,如果Drawable 没有原始宽度,就返回 0
     * ShapDrawable 没有原始宽高;BitmapDrawable 有原始宽高
     */
    public int getMinimumWidth() {
    	  final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }
    
  • 继承 View 的自定义控件,需要重写 onMeasure 方法,并设置 wrap_content 时的自身大小
    否则在布局中使用 wrap_content 就相当于使用 match_parent。

    原因
    如果View在布局中使用 wrap_content,它的 specMode 是 AT_MOST 模式。
    此模式下,它的宽高等于 specSize,这种情况下 View的 specSize 是parentSize也就是父容器剩余空间大小

    解决方式:

    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(mWidth, mHeight);
      		
      	} else if(widthSpecMode == MeasureSpec.AT_MOST) {
      		setMeasuredDimension(mWidth, heightSpecSize);
      		
      	} else if(heightSpecMode == MeasureSpec.AT_MOST) {
      		setMeasuredDimension(widthSpecSize, mHeight);
      		
      	}
    }
    

    给View 指定一个默认的内部宽高(mWidth和mHeight),并在wrap_content 时设置此宽高

2、 ViewGroup的 measure 过程

ViewGroup 完成自己的measure过程之外,还要遍历调用所有子元素的measure方法。
各个子元素再递归执行这个过程。

  • ViewGroup 的 measureChild

    protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
      	final LayoutParams lp = child.getLayoutParams();
      
      	//根据ViewGroup的 MeasureSpec 和 child 的 LayoutParams 生成 child的 MeasureSpec
      	final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
      			mPaddingLeft + mPaddingRight, lp.width);
      
      	final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
      			mPaddingTop + mPaddingBottom, lp.height);
      
      	//根据child的 MeasureSpec 测量 child 尺寸		
      	child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
  • Activity 中获取View尺寸
    View的measure过程 与 Activity 的生命周期方法不同步,
    在Activity生命周期方法中获取View尺寸可能是0

    1)Activity/View#onWindowFocusChanged
    View已初始化完毕,宽高已经准备好了。

    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
      	if (hasFocus) {
      		int width = view.getMeasuredWidth();
      		int height = view.getMeasuredHeight();
      	}	
    }
    

    2)view.post(runnable)

    View.post() 将 一个 runnable 消息塞到UI线程消息队列尾部

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

    3)使用 ViewTreeObserver 的回调

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

    4)手动 measure 获取View宽高

    view.measure(int widthMeasureSpec, int heightMeasureSpec)
    

    >> View.LayoutParams == match_parent,无法获取。
    measure方法,依赖父容器的MeasureSpec,此时父容器也可能还没有绘制完毕

    >>View.LayoutParams 值 为具体 dp/px
    假设 宽高100px:

    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
    view.measure(widthMeasureSpec, heightMeasureSpec);
    

    >>View.LayoutParams == wrap_content
    MeasureSpec为32位,其中高2位为specMode,低30位为specSize
    specSize最大值为 30个1 即 2^30 - 1 , 也就是 (1<<30)-1

    //这里表示用View理论上能支持的最大specSize 去构造 MesureSpec
    int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<< 30) - 1, MeasureSpec.AT_MOST);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<< 30) - 1, MeasureSpec.AT_MOST);
    view.measure(widthMeasureSpec, heightMeasureSpec);
    
  • 关于View的 measure 网络上的 错误用法
    违背了系统内的 MeasureSpec 实现规范,也不能保证一定能measure 出正确结果。

    i. 错误一

    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
    view.measure(widthMeasureSpec, heightMeasureSpec);
    

    ii. 错误二

    view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    
3、 layout过程

layout()方法:确定View本身的位置

onLayout()方法:确定所有子元素的位置

当ViewGroup 调用layout()确定自身位置后,
再调用onLayout 遍历子元素、并调用子元素的layout()方法。
子元素调用layout()确认自身位置后,又会继续调用 onLayout去遍历孙元素。
这样层层传递完成整个View树的layout过程

  • View的 getMeasuredWidth() 、 getWidth() 获取宽度区别
    默认,getMeasuredWidth() == getWidth()
    区别:getMeasuredWidth() 在 measure过程赋值,getWidth()是 layout过程赋值

    重写了 layout()方法,可能导致 测量宽度 与 最终宽度不同

    public void layout(int l, int t, int r, int b) {
      	super.layout(l, t, r + 100, b + 100);
    }
    

    此时,最终宽/高 比 测量宽/高 大100px

    layout过程

ViewGroup.layout确定自身位置
ViewGroup.onLayout遍历子元素
子元素layout确定自身位置
子元素onLayout 遍历孙元素
层层传递...
最终确定所有View的位置
4、 draw过程

将View 绘制在屏幕上

  • 1)绘制背景 drawBackground(canvas)

  • 2)绘制自己 onDraw(canvas)

  • 3)绘制 children dispatchDraw(canvas)
    View的 draw 过程,通过 dispatchDraw 来传递
    dispatchDraw 遍历调用所有子元素的 draw 方法
    draw 中 再调用 dispatchDraw,层层传递

  • 4)绘制装饰 onDrawScrollBars(canvas)

    public void draw(Canvas canvas) {
      	...
      	// Step 1, draw the background, if needed
      	int saveCount;
      	if (!dirtyOpaque) {
      		drawBackground(canvas);//绘制背景
      	}
      	// skip step 2 & 5 if possible (common case)
      	...
      	if (!verticalEdges && !horizontalEdges) {
      		// Step 3, draw the content
      		if (!dirtyOpaque) onDraw(canvas);//绘制自身内容
      		// Step 4, draw the children
      		dispatchDraw(canvas);//分发绘制children,内部会调用child.draw(canvas)
      		drawAutofilledHighlight(canvas);
      		// Overlay is part of the content and draws beneath Foreground
      		if (mOverlay != null && !mOverlay.isEmpty()) {
      			mOverlay.getOverlayView().dispatchDraw(canvas);
      		}
      		// Step 6, draw decorations (foreground, scrollbars)
      		onDrawForeground(canvas);//绘制绘制装饰
      		// Step 7, draw the default focus highlight
      		drawDefaultFocusHighlight(canvas);
      		if (debugDraw()) {
      			debugDrawFocus(canvas);
      		}
      		// we're done...
      		return;
      	}
    }
      
    public void onDrawForeground(Canvas canvas) {
      	onDrawScrollIndicators(canvas);
      	onDrawScrollBars(canvas);
    }
    

draw过程
draw -> drawBackground绘制背景 -> onDraw绘制自身
-> dispatchDraw遍历调用
子元素draw方法(内部子元素的 draw过程)-> onDrawScrollBars 绘制装饰

Children
Parent
ViewGroup.dispatchDraw 遍历绘制子布局
child.draw 绘制入口
child.drawBackground 绘制背景
child.onDraw 绘制自己
child.dispatchDraw 遍历绘制孙布局
层层传递...
child.onDrawScrollBars 绘制装饰
ViewGroup.draw 绘制入口方法
ViewGroup.drawBackground 绘制背景
ViewGroup.onDraw 绘制自己
ViewGroup.onDrawScrollBars 绘制装饰

四、自定义View

1、 自定义View的分类
  • 1)继承 View 重写 onDraw
    这种方式,需要自己支持 wrap_content,并且 padding 也需自己处理

    protected void onDraw(Canvas canvas) {
      	super.onDraw(canvas);
      	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 + widt/2, paddingTop + height/2, radius, mPaint);
    }
      
    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);
      	}
    }
    
  • 2)继承 ViewGroup 派生特殊的 Layout
    实现一种新的容器布局。
    需要处理ViewGroup的 measure 和 layout 过程、同时需要处理子元素的 measure 和 layout 过程

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      	int measureWidth = 0;
      	int measureHeight = 0;
      	final int childCount = getChildCount();
      	//测量子元素尺寸
      	measureChildren(widthMeasureSpec, heightMeasureSpec);
    
      	int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
      	int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
      	int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
      	int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    
      	if (childCount == 0) {
      		setMeasuredDimension(0, 0);
      	} else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
      		final View childView = getChildAt(0);
      		measureWidth = childView.getMeasuredWidth() * childCount;
      		measureHeight = childView.getMeasuredHeight();
      		setMeasuredDimension(measureWidth, measureHeight);
      	} else if (heightSpecMode == MeasureSpec.AT_MOST) {
      		final View childView = getChildAt(0);
      		measureHeight = childView.getMeasuredHeight();
      		setMeasuredDimension(widthSpecSize, measureHeight);
      	} else if (widthSpecMode == MeasureSpec.AT_MOST) {
      		final View childView = getChildAt(0);
      		measureWidth = childView.getMeasuredWidth() * childCount;
      		setMeasuredDimension(measureWidth, heightSpecSize);
      	}
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
      	int childLeft = 0;
      	final int childCount = getChildCount();
      	mChildrenSize = childCount;
      	for (int i = 0; i < childCount; i++) {
      		final View childView = getChildAt(i);
      		if (childView.getVisibility() != View.GONE) {
      			final int childWidth = childView.getMeasuredWidth();
      			mChildWidth = childWidth;
      			childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
      			childLeft += childWidth;
      		}
      	}
    }
    
  • 3)继承特定的View (比如 TextView)
    用于扩展已有的View的功能
    不需要自己支持 wrap_content 和 padding 等

  • 4)继承特定的 ViewGroup(比如 LienarLayout)
    扩展特定ViewGroup(比如 LienarLayout)的功能。
    不需要自己处理ViewGroup的测量和布局过程

自定义View的方式

方式特点
继承 View 重写 onDraw需要实现 AT_MOST模式(wrap_content)的 measure过程
需处理 padding
继承 ViewGroup需要处理 ViewGroup的 measure 和 layout 过程
需要处理子元素的 measure 和 layout 过程
继承特定的View(比如 TextView)不需处理 AT_MOST模式(wrap_content)的 measure过程 和 padding 等
继承特定的 ViewGroup(比如 LienarLayout)不需处理 ViewGroup 的 measure 和 layout 过程
2、 需要注意的事项
  • 1)让View 支持 wrap_content

    直接继承View 或者 ViewGroup 的控件,如果不在 onMeasure 中 对wrap_content 特殊处理,
    那么当父布局中使用wrap_content时,实际上会使用父容器的剩余尺寸作为View的尺寸。
    达不到wrap_content所要的效果。

  • 2)让View 支持 padding

    直接继承View的控件,如果不在 draw 方法中处理 padding,
    那么 padding 属性无法起作用。

    直接继承ViewGroup 的控件,需要在 onMeasure 和 onLayout 中考虑 padding 和子元素的 margin
    不然会导致 padding 和 子元素的 margin 失效。

  • 3)不要在 View 中使用 Handler,没有必要。
    因为View 内部本身有提供 post 系列方法,可替代 Handler 的作用。

  • 4)View 中如果有线程池或者动画,需要及时停止。View#onDetachedFromWindow()
    onDetachedFromWindow()停止线程和动画。
    View依赖的Activity退出、或者当前View被remove时,onDetachedFromWindow()会被调用。
    否则,有可能造成内存泄露。

    对应的方法是 onAttachedToWindow(),View依赖的Activity启动时 View#onAttachedToWindow()会被调用。

  • 5)View带有滑动嵌套情形时,需要处理好滑动冲突
    onInterceptTouchEvent(event) 外部拦截、内部拦截,两种解决冲突的方式。

3、View绘制 双缓冲 实现
缓冲:系统会在 onDraw 执行完之后,才将数据交给GPU处理展示
双缓冲:创建一个 Canvas 和对应的 Bitmap 提前绘制在 Bitmap上,然后在 onDraw 方法里,
默认的 Canvas 通过 drawBitmap 画刚才new 的那个 Bitmap

推荐阅读:
《Android开发艺术探索》第四章 View的工作原理
《Android开发艺术探索》第八章 理解Window和WindowManager
Android自定义控件三部曲文章索引

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值