复习:Android自定义View

135 阅读4分钟

自定义控件的类型

  1. 组合控件。这种自定义控件不需要我们自己绘制,而是使用原生控件组合成新的控件。
  2. 继承原有控件。这种自定义控件在原生控件提供的方法外,可以自己添加一些方法。如制作圆角图片
  3. 完全自定义控件。这种view需要我们自己绘制出来。比如阅读器的翻页控件

View的绘制流程

onMeasure() -> onLayout() -> onDraw()

第一步:measure测量视图大小,从顶层父视图向子视图递归调用measure方法,完成视图测量。测量视图有个关键内容部MeasureSpec,用于解析width/height MeasureSpec的int值,它是一个32位的int值,通过高2位获取测量模式,低30位获取规格大小。

第二步:layout确定View的位置,进行页面布局。从顶层父视图向子视图递归调用layout方法,即父视图根据上一步measure子视图所得到的布局大小和参数,将子视图放到合适的位置。

第三步:draw绘制视图。绘制的过程:背景(drawBackground) - 主体(onDraw) - 子视图(dispatchDraw) - 装饰(onDrawForeground 如滚动条)- 前景(onDrawForeground 前景的支持是在 Android 6.0(也就是 API 23)才加入的;之前其实也有,不过只支持 FrameLayout,而直到 6.0 才把这个支持放进了 View 类里。)

MeasureSpec

widthMeasureSpec/heightMeasureSpec是32位int类型,高2位为测量模式,低30位代表大小Size

有三种测量模式:

  • EXACTLY 精确模式:match_parent或者精确数值
  • AT_MOST 最大值模式:wrap_content,View的尺寸不超过MeasureSpec当中的Size值
  • UNSPECIFIED 无限制模式:View设置多大就给多大。
ViewGroup中没有measure()也没有onMeasure()

自定义view的注意事项

在ViewGroup的子类中重写dispatchDraw以外的绘制方法是,可能需要调用setWillNotDra(false);

出于效率的考虑,ViewGroup默认会绕过draw()方法,直接执行dispatchDraw(),以此简化绘制流程。如果自定义了某个ViewGroup的子类(比如LinearLayout)并且需要在它的除dispatchDraw()以外的任何一个绘制方法内绘制内容,可能需要调佣setWillNotDraw(false)这行代码来切换到完整的绘制流程。

绘制代码优先写在onDraw方法内

当绘制代码可以写在onDraw方法内,也可以写在其他绘制方法里,那么优先写在onDraw里面,因为Android有相关优化,可以在不需要重绘的时候自动跳过onDraw()的重复执行,以提升开发效率。享受这种优化的只有 onDraw() 一个方法。

Activity、Window、DecorView之间关系

首先,来看一下Activity中setContentView的源代码。

 public void setContentView(@LayoutRes int layoutResID) {
        //将xml布局传递到Window当中
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

可以看到, Activity的 setContentView实质是将 View传递到 Window的 setContentView()方法中, Window的 setContenView会在内部调用 installDecor()方法创建 DecorView,代码如下。

 public void setContentView(int layoutResID) { 
        if (mContentParent == null) {
            //初始化DecorView以及其内部的content
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        ...............
        } else {
            //将contentView加载到DecorVoew当中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...............
    }
  private void installDecor() {
        ...............
        if (mDecor == null) {
            //实例化DecorView
            mDecor = generateDecor(-1);
            ...............
            }
        } else {
            mDecor.setWindow(this);
        }
       if (mContentParent == null) {
            //获取Content
            mContentParent = generateLayout(mDecor);
       }  
        ...............
 }
 protected DecorView generateDecor(int featureId) {
        ...............
        return new DecorView(context, featureId, this, getAttributes());
 }

通过 generateDecor()的new一个 DecorView,然后调用 generateLayout()获取 DecorView中 content,最终通过 inflate将 Activity视图添加到 DecorView中的 content中,但此时 DecorView还未被添加到 Window中。添加操作需要借助 ViewRootImpl。

ViewRootImpl的作用是用来衔接 WindowManager和 DecorView,在 Activity被创建后会通过 WindowManager将 DecorView添加到 PhoneWindow中并且创建 ViewRootImpl实例,随后将 DecorView与 ViewRootImpl进行关联,最终通过执行 ViewRootImpl的 performTraversals()开启整个View树的绘制。

Requestlayout,onlayout,onDraw,DrawChild区别与联系

  • requestLayout() :会导致调用 measure()过程 和 layout()过程,将会根据标志位判断是否需要onDraw。
  • onLayout() :如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局。
  • onDraw() :绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)。
  • drawChild() :去重新回调每个子视图的draw()方法。

invalidate() 和 postInvalidate()的区别

invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。