自定义View

最基本的 三个方法

  • onMeasure()
  • onLayout()
  • onDraw()

View在Activity中显示出来,要经历测量、布局和绘制三个步骤,
分别对应三个动作:measure、layout和draw。

测量:onMeasure()决定View的大小;
布局:onLayout()决定View在ViewGroup中的位置;
绘制:onDraw()决定绘制这个view

自定义控件分类

  • 自定义View: 只需要重写onMeasure()和onDraw()
  • 自定义ViewGroup :则只需要重写onMeasure()和onLayout()

视图View主要分为两类

  • 单一视图 即一个View,如TextView 不包含子View
  • 视图组 即多个View组成的ViewGroup,如LinearLayout 包含子View

View

View类是Android中各种组件的基类,如View是ViewGroup基类View表现为显示在屏幕上的各种视图
Android中的UI组件都由View、ViewGroup组成。
View的构造函数:共有4个


//如果view是在Java代码里面new的,则调用第一个构造函数
public CarsonView(context context) {
super(context);
}


//如果view是在.xm1里声明的,则调用第二个构造函数
//自定义属性是从attributeSet参数传进来的
public Carsonview(Context context, AttributeSet attrs) {
super(context, attrs);
}
//不会自动调用
//一般是在第二个构造函数里主动调用
//如view有style属性时
public Carsonview(Context context, AttributeSet attrs, int defsty1eAttr){super(context, attrs, defSty1eAttr);
}
//API21之后才使用
//不会自动调用
//一般是在第二个构造函数里主动调用
//如view有style属性时
public CarsonView(Context context, AttributeSet attrs, int defsty1eAttr, intdefstyleRes) {
super(context, attrs, defSty1eAttr, defStyleRes);
}

ViewGroup 的方法调用

image.png

View的方法调用

image.png

View的层次结构

image.png

Android 坐标系

  • 屏幕的左上角为坐标原点
  • 向右为x轴增大方向

View 位置(坐标)描述

View的位置由4个顶点决定的4个顶点的位置描述分别由4个值决定:

View的位置是相对于父控件而言的

  • Top: 子View上边界到父view上边界的距离
  • Left: 子View左边界到父view左边界的距离
  • Bottom: 子View下边距到父View上边界的距离
  • Right: 子View右边界到父view左边界的距离

位置获取方式

  • getTop()
  • getLeft();
  • getRight();
  • getBottom();

MotionEvent get()和getRaw()的区别

//get()触摸点相对于其所在组件坐标系的坐标event.getX();
event.getX();

//getRawX():触摸点相对于屏幕默认坐标系的坐标event.getRawx();
event.getRawY();

Android 支持的颜色模式

  • ARGB8888 四通道高精度(32位)
  • ARGB4444 四通道低精度(16位)
  • RGB565 Android屏幕默认模式(16位)
  • Alpha8 仅有透明通道(8位)

· 字母表示通道类型;
· 数值表示该类型用多少位二进制来描述。
· 例子:ARGB8888,表示有四个通道(ARGB);每个对应的通道均
用8位来描述。

A(Alpha) 透明度 (0-255)
R(Red) 红色 (0-255)
G(Green) 绿色 (0-255)
B(Blue) 蓝色 (0-255)

View的绘制流程

View树的绘制流程是通过ViewRoot去负责绘制的,ViewRoot这个类的命名有点坑,最初看到这个名字,翻译过来是view的根节点,但是事实完全不是这样,ViewRoot其实不是View的根节点,它连view节点都算不上。

它的主要作用是View树的管理者,负责将DecorView和PhoneWindow组合”起来,而View树的根节点严格意义上来说只有DecorView;

每个DecorView都有一个ViewRoot与之关联,这种关联关系是由WindowManager去进行管理的;

  • 依次调用如下顺序:

WindowManager.addView()
ViewRootImpl.setView
ViewRootImpl.requestLayout
ViewRootImpl.scheduleTraversals
TraversalRunnable.run
ViewRootImpl.doTraversal
ViewRootImpl.performTraversals
  • performTraversals 开始计算视图大小
image.png

https://www.cnblogs.com/xyhuangjinfu/p/5435201.html

1、系统为什么要有measure过程?

开发人员在绘制UI的时候,基本都是通过XML布局文件的方式来配置UI,而每个View必须要设置的两个群属性就是layout_width和layout_height,这两个属性代表着当前View的尺寸。

这两个属性的取值只能为三种类型:

             1、固定的大小,比如100dp。

             2、刚好包裹其中的内容,wrap_content。

             3、想要和父布局一样大,match_parent / fill_parent。

由于Android希望提供一个更优雅的GUI框架,所以提供了自适应的尺寸,也就是 wrap_content 和 match_parent 。

试想一下,那如果这些属性只允许设置固定的大小,那么每个View的尺寸在绘制的时候就已经确定了,所以可能都不需要measure过程。但是由于需要满足自适应尺寸的机制,所以需要一个measure过程。

2、measure过程都干了点什么事?

由于上面提到的自适应尺寸的机制,所以在用自适应尺寸来定义View大小的时候,View的真实尺寸还不能确定。

但是View尺寸最终需要映射到屏幕上的像素大小,所以measure过程就是干这件事,把各种尺寸值,经过计算,得到具体的像素值。

measure过程会遍历整棵View树,然后依次测量每个View真实的尺寸。

具体是每个ViewGroup会向它内部的每个子View发送measure命令,然后由具体子View的onMeasure()来测量自己的尺寸。

最后测量的结果保存在View的mMeasuredWidth和mMeasuredHeight中,保存的数据单位是像素。

3、对于自适应的尺寸机制,如何合理的测量一颗View树?

系统在遍历完布局文件后,针对布局文件,在内存中生成对应的View树结构。
这个时候,整棵View树中的所有View对象,都还没有具体的尺寸,因为measure过程最终是要确定每个View打的准确尺寸,也就是准确的像素值。

但是刚开始的时候,View中layout_width和layout_height两个属性的值,都只是自适应的尺寸,也就是match_parent和wrap_content,这两个值在系统中为负数,所以系统不会把它们当成具体的尺寸值。

所以当一个View需要把它内部的match_parent或者wrap_content转换成具体的像素值的时候,他需要知道两个信息。

1、针对于match_parent,父布局当前具体像素值是多少,因为match_parent就是子View想要和父布局一样大。

2、针对wrap_content,子View需要根据当前自己内部的content,算出一个合理的能包裹所有内容的最小值。但是如果这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不应该超过这个尺寸。

由于树这种数据结构的特殊性,我们在研究measure的过程时,可以只研究一个ViewGroup和2个View的简单场景。大概示意图如下:

image.png

也就是说,在measure过程中,ViewGroup会根据自己当前的状况,结合子View的尺寸数据,进行一个综合评定,然后把相关信息告诉子View,然后子View在onMeasure自己的时候,
一边需要考虑到自己的content大小,
一边还要考虑的父布局的限制信息,
然后综合评定,测量出一个最优的结果。

4、那么ViewGroup是如何向子View传递限制信息的?

谈到传递限制信息,那就是MeasureSpec类了,该类贯穿于整个measure过程,用来传递父布局对子View尺寸测量的约束信息。简单来说,该类就保存两类数据。

1、子View当前所在父布局的具体尺寸。
2、父布局对子View的限制类型。

那么限制类型又分为三种类型:

1、UNSPECIFIED,不限定。意思就是,子View想要多大,我就可以给你多大,你放心大胆的measure吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)

2、EXACTLY,精确的。意思就是,根据我当前的状况,结合你指定的尺寸参数来考虑,你就应该是这个尺寸,具体大小在MeasureSpec的尺寸属性中,自己去查看吧,你也不要管你的content有多大了,就用这个尺寸吧。

3、AT_MOST,最多的。意思就是,根据我当前的情况,结合你指定的尺寸参数来考虑,在不超过我给你限定的尺寸的前提下,你测量一个恰好能包裹你内容的尺寸就可以了。

ScrollView内部嵌套ListView的问题

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
  • ListView的onMeasure
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final View child = obtainView(0, mIsScrap);
        childHeight = child.getMeasuredHeight();
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
            if (heightMode == MeasureSpec.AT_MOST) {
                // TODO: after first layout we should maybe start at the first visible position, not 0 
                heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
            }
            setMeasuredDimension(widthSize, heightSize);
            mWidthMeasureSpec = widthMeasureSpec;
        }
    }

当MeasureSpec mode为UNSPECIFIED的时候,只测量第一个item打的高度,跟问题描述相符,所以我们猜测可能是因为ScrollView传递了一个UNSPECIFIED限制给ListView。

  • ScrollView的onMeasure代码:
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

调用了父类的onMeasure:

看看FrameLayout的onMeasure:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            }
        }
    }

调用了measureChildWithMargins,但是因为ScrollView覆写了该方法,所以看看ScrollView的measureChildWithMargins方法:

@Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 
                                           int parentHeightMeasureSpec, int heightUsed) {
        final int childHeightMeasureSpec = 
                MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(parentHeightMeasureSpec), 
                        MeasureSpec.UNSPECIFIED);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

果然,它向ListView的onMeasure传递了一个UNSPECIFIED的限制。

为什么呢,想想,因为ScrollView,本来就是可以在竖直方向滚动的布局,所以,它对它所有的子View的高度就是UNSPECIFIED,意思就是,不限制子View有多高,因为我本来就是需要竖直滑动的,它的本意就是如此,所以它对子View高度不做任何限制。

为什么这种解决方法可以解决这个问题?

看看ListView的onMeasure:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final View child = obtainView(0, mIsScrap);
        childHeight = child.getMeasuredHeight();
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
            if (heightMode == MeasureSpec.AT_MOST) {
                // TODO: after first layout we should maybe start at the first visible position, not 0 
                heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
            }
            setMeasuredDimension(widthSize, heightSize);
            mWidthMeasureSpec = widthMeasureSpec;
        }
    }

只要让heightMode == MeasureSpec.AT_MOST,它就会测量它的完整高度,所以第一个数据,限制mode的值就确定下来了。第二个数据就是尺寸上限,如果给个200,那么当ListView数据过多的时候,该ListView最大高度就是200了,还是不能完全显示内容,怎么办?那么就给个最大值吧,最大值是多少呢,Integer.MAX_VALUE?

先看一下MeasureSpec的代码说明:

        private static final int MODE_SHIFT = 30;
        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;

他用最高两位存储mode,用其他剩余未存储size。所以Integer.MAX_VALUE >> 2,就是限制信息所能携带的最大尺寸数据。所以最后就需要用这两个值做成一个限制信息,传递给ListView的height维度。

也就是如下代码:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

layout

https://www.cnblogs.com/xyhuangjinfu/p/5435253.html
View框架的工作流程为:测量每个View大小(measure)-->把每个View放置到相应的位置(layout)-->绘制每个View(draw)。

1、系统为什么要有layout过程?

View框架在经过第一步的measure过程后,成功计算了每一个View的尺寸。但是要成功的把View绘制到屏幕上,只有View的尺寸还不行,还需要准确的知道该View应该被绘制到什么位置。
除此之外,对一个ViewGroup而言,还需要根据自己特定的layout规则,来正确的计算出子View的绘制位置,已达到正确的layout目的。这也就是layout过程的职责。

该位置是View相对于父布局坐标系的相对位置,而不是以屏幕坐标系为准的绝对位置。

这样更容易保持树型结构的递归性和内部自治性。

而View的位置,可以无限大,超出当前ViewGroup的可视范围,这也是通过改变View位置而实现滑动效果的原理。

2、layout过程都干了点什么事?

    由于View是以树结构进行存储,所以典型的数据操作就是递归操作,所以,View框架中,采用了内部自治的layout过程。

    每个叶子节点根据父节点传递过来的位置信息,设置自己的位置数据,每个非叶子节点,除了负责根据父节点传递过来的位置信息,设置自己的位置数据外(如果有父节点的话),还需要根据自己内部的layout规则(比如垂直排布等),计算出每一个子节点的位置信息,然后向子节点传递layout过程。

    对于ViewGroup,除了根据自己的parent传递的位置信息,来设置自己的位置之外,还需要根据自己的layout规则,为每一个子View计算出准确的位置(相对于子View的父布局的位置)。

    对于View,根据自己的parent传递的位置信息,来设置自己的位置。
image.png
    View对象的位置信息,在内部是以4个成员变量的保存的,分别是mLeft、mRight、mTop、mBottom。他们的含义如图所示。
image.png

Android View框架的draw机制

1、系统为什么要有draw过程?

View框架在经过了measure过程和layout过程之后,就已经确定了每一个View的尺寸和位置。那么接下来,也是一个重要的过程,就是draw过程,draw过程是用来绘制View的过程,它的作用就是使用graphic框架提供的各种绘制功能,绘制出当前View想要的样子。

2、draw过程都干了点什么事?

View框架中,draw过程主要是绘制View的外观。ViewGroup除了负责绘制自己之外,还需要负责绘制所有的子View。而不含子View的View对象,就负责绘制自己就可以了。

    draw过程的主要流程如下:

    1、绘制 backgroud(drawBackground)     
    2、如果需要的话,保存canvas的layer,来准备fading(不是必要的步骤)
    3、绘制view的content(onDraw方法)
    4、绘制children(dispatchDraw方法)
    5、如果需要的话,绘制fading edges,然后还原layer(不是必要的步骤)
    6、绘制装饰器、比如scrollBar(onDrawForeground)

LayoutParams

LayoutParams翻译过来就是布局参数,子View通过LayoutParams告诉父容器(ViewGroup)应该如何放置自己。

从这个定义中也可以看出来LayoutParams与ViewGroup是息息相关的,因此脱离ViewGroup谈LayoutParams是没有意义的。

事实上,每个ViewGroup的子类都有自己对应的LayoutParams类,典型的如LinearLayout.LayoutParams和FrameLayout.LayoutParams等,可以看出来LayoutParams都是对应ViewGroup子类的内部类

MarginLayoutParams

MarginLayoutParams是和外间距有关的。

事实也确实如此,和LayoutParams相比,MarginLayoutParams只是增加了对上下左右外间距的支持。

实际上大部分LayoutParams的实现类都是继承自MarginLayoutParams,因为基本所有的父容器都是支持子View设置外间距的属性优先级问题MarginLayoutParams主要就是增加了上下左右4种外间距。

在构造方法中,先是获取了margin属性;
如果该值不合法,就获取horizontalMargin;如果该值不合法,再去获取leftMargin和rightMargin属性 (verticalMargin、topMargin和bottomMargin同理)。

我们可以据此总结出这几种属性的优先级
margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin

属性覆盖问题优先级更高的属性会覆盖掉优先级较低的属性。

此外,还要注意一下这几种属性上的注释
Call {@link ViewGroup#setLayoutParams(LayoutParams)}after reassigning a new value

LayoutParams与View如何建立联系

  • 在XML中定义View
  • 在Java代码中直接生成View对应的实例对象

自定义LayoutParams

  • 创建自定义属性


<!一自定义的属性 一>
<!一 使用系统预置的属性一>



  • 继承MarginLayout
public static class LayoutParams extends ViewGroup.MarginLayoutParams {

public int simpleAttr;

public int gravity;

public LayoutParams(Context c, Attributeset attrs) {
super(c, attrs);
TypedArray typedArray = c.obtainstyledAttributes(attrs,
R.styleable.SimpleviewGroup_Layout);
simpleAttr =
typedArray.getInteger(R.styleable.SimpleviewGroup_Layout_layout_simple_attr, 0);
gravity=typedArray.getInteger(R.styleable.SimpleviewGroup_Layout_android_layout_gravity,-1);
typedArray.recycle();
}

public Layoutparams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {super(source);
}
  • 重写与LayoutParams的方法
//检查LayoutParams是否合法
@override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof SimpleviewGroup.LayoutParams;
}
//生成默认的LayoutParams
@override
protected ViewGroup.LayoutParams generateDefaultLayoutParamsO {
return new simpleviewGroup. LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.WRAP_CONTENT);
}
//对传入的LayoutParams进行转化
@override
protected viewGroup. LayoutParams generatelayoutParams (viewGroup.LayoutParams p)return new simpleviewGroup.LayoutParams(p);
}

//对传入的Layoutparams进行转化
@override
public viewGroup.LayoutParams generatelayoutParams(Attributeset attrs) {
return new SimpleviewGroup.LayoutParams(getcontextO, attrs);
}

MeasureSpec

  • widthMeasureSpec
  • heightMeasureSpec

specMode

  • UNSPECIFIED 父控件不对你有任何限制,你想要多大给你多大,想上天就上天。
    这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)

  • EXACTLY 父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。

  • AT_MOST你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。

specSize

在某种specMode下的参考尺寸

MeasureSpecs 的意义

通过将 SpecMode 和 SpecSize 打包成一个int值可以避免过多的对象内存分配,为了方便操作,其提供了打包/解包方法

MeasureSpecs 值的确定

MeasureSpec (测量规格,32位的int值) =
Mode (测量模式,高2位即31,32位)+
size(具体测量大小,低30位)

子View的MeasureSpec值是
根据子View的布局参数(LayoutParams) 和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。

/**
目标是将父控件的测量规格和child view的布局参数Layoutparams相结合,得到一个最可能符合条件的child view的测量规格。
@param spec父控件的测量规格
@param padding 父控件里已经占用的大小
@param childdimension child view布局LayoutParams里的尺寸
@return child 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://父控件是精确的
//如果child的布局参数有固定值,比如"layout_width" = "100dp"
//那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是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.
//如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小,
//比如TextView根据设置的字符串大小来决定自己的大小
//那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛
//所以测量模式就是AT_MOST,测量大小就是父控件的size
                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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
image.png

注: parentSize 为父容器中目前可使用的大小

针对上表,这里再做一下具体的说明

  • 对于应用层View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定

  • 对于不同的父容器和view本身不同的LayoutParams, view就可以有多种MeasureSpec。
    1.当view采用固定宽高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;

2.当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大小不会超过父容器的剩余空间;

3.当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,view的模式总是最大化并且大小不能超过父容器的剩余空间。

  1. Unspecified模式,这个模式主要用于系统内部多次measure的情况下,一般来说,我们不需要关注此模式(这里注意自定义View放到ScrollView的情况需要处理)。

https://www.jianshu.com/p/0723ff4123e1

你可能感兴趣的:(自定义View)