Android自定义View

1、自定义View的基本方法

自定义View的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw(); View在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout和draw。

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

2、自定义控件分类

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

3、View的分类

类别 解释 特点
单一视图 即一个View,如TextView 不包含子View
视图组 即多个View组成的ViewGroup,如LinearLayout 包含子View

4、View类简介

  • View类是Android中各种组件的基类,如View是ViewGroup基类
  • View表现为显示在屏幕上的各种视图

Android中的UI组件都由View、ViewGroup组成。

View的构造函数:共有4个

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

// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的   
 public  CarsonView(Context context, AttributeSet attrs) {       
      super(context, attrs);  
 }

// 不会自动调用
// 一般是在第二个构造函数里主动调用// 如View有style属性时    
public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {        
    super(context, attrs, defStyleAttr);   
}


// 不会自动调用
// 一般是在第二个构造函数里主动调用// 如View有style属性时    
public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {       
    super(context, attrs, defStyleAttr);  
 }    

//API21之后才使用    
// 不会自动调用    
// 一般是在第二个构造函数里主动调用    
// 如View有style属性时    
public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {       
    super(context, attrs, defStyleAttr, defStyleRes);
}

5、AttributeSet与自定义属性

系统自带的View可以在xml中配置属性,对于写的好的自定义View同样可以在xml中配置属性,为了使自定义的View的属性可以在xml中配置,需要以下4个步骤:

    1. 通过<declare-styleable>为自定义View添加属性
    1. 在xml中为相应的属性声明属性值
    1. 在运行时(一般为构造函数)获取属性值
    1. 将获取到的属性值应用到View

6、View视图结构

    1. PhoneWindow是Android系统中最基本的窗口系统,继承自Windows类,负责管理界面显示以及事件响应。它是Activity与View系统交互的接口
    1. DecorView是PhoneWindow中的起始节点View,继承于View类,作为整个视图容器来使用。用于设置窗口属性。它本质上是一个FrameLayout
    1. ViewRoot在Activtiy启动时创建,负责管理、布局、渲染窗口UI等等
image.png
  • 4.对于多View的视图,结构是树形结构:最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup或View,如下图:
image.png

一定要记住:无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性。

7、Android坐标系

Android的坐标系定义为:

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

8、View位置(坐标)描述

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

请记住:View的位置是相对于父控件而言的)

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

9、位置获取方式

View的位置是通过view.getxxx()函数进行获取:(以Top为例)

// 获取Top位置
public final int getTop() {     
   return mTop;  
}  
// 其余如下:  
getLeft();      //获取子View左上角距父View左侧的距离  
getRight();     //获取子View右下角距父View左侧的距离
getTop():    //获取子View右上角距父View顶部的距离
getBottom();    //获取子View右下角距父View顶部的距离  

与MotionEvent中 get()和getRaw()的区别

//get() :触摸点相对于其所在组件坐标系的坐标 
event.getX();        
event.getY();
//getRaw() :触摸点相对于屏幕默认坐标系的坐标 
event.getRawX();     
event.getRawY();
image.png

10、View树的绘制流程是谁负责的?

  • view树的绘制流程是通过ViewRoot去负责绘制的;
  • ViewRoot这个类的命名有点坑,最初看到这个名字,翻译过来是view的根节点,但是事实完全不是这样,ViewRoot其实不是View的根节点,它连view节点都算不上;
  • 它的主要作用是View树的管理者,负责将DecorView和PhoneWindow“组合”起来,而View树的根节点严格意义上来说只有DecorView;
  • 每个DecorView都有一个ViewRoot与之关联,这种关联关系是由WindowManager去进行管理的;

11、view的添加

image.png

12、view的绘制流程

image.png

13、measure

    1. 系统为什么要有measure过程?
    1. measure过程都干了点什么事?
    1. 对于自适应的尺寸机制,如何合理的测量一颗View树?
    1. 那么ViewGroup是如何向子View传递限制信息的?
    1. ScrollView嵌套ListView问题?


      image.png

14、layout

    1. 系统为什么要有layout过程?
    1. layout过程都干了点什么事?
image.png

15、draw

    1. 系统为什么要有draw过程?
    1. draw过程都干了点什么事?
image.png

16、LayoutParams

  • LayoutParams翻译过来就是布局参数,子View通过LayoutParams告诉父容器(ViewGroup)应该如何放置自己。
  • 从这个定义中也可以看出来LayoutParams与ViewGroup是息息相关的,因此脱离ViewGroup谈LayoutParams是没有意义的。
  • 事实上,每个ViewGroup的子类都有自己对应的LayoutParams类,典型的如LinearLayout.LayoutParams和FrameLayout.LayoutParams等,可以看出来LayoutParams都是对应ViewGroup子类的内部类。

17、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

18、LayoutParams与View如何建立联系

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

19、addView

/** * 重载方法1:添加一个子View * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
 */
public void addView(View child) {    
    addView(child, -1);
}
/** * 重载方法2:在指定位置添加一个子View 
* 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
 * @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾) 
*/
public void addView(View child, int index) {   
     if (child == null) {        
         throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");  
     }   
       LayoutParams params = child.getLayoutParams();   
       if (params == null) {       
           params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams        
          if (params == null) {            
                    throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");      
       }  
 }    
  addView(child, index, params);
}

/** * 重载方法3:添加一个子View 
* 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height 
*/
public void addView(View child, int width, int height) {   
   final LayoutParams params = generateDefaultLayoutParams();  // 生成当前ViewGroup默认的LayoutParams    
     params.width = width;   
     params.height = height;    
     addView(child, -1, params);
}

/** * 重载方法4:添加一个子View,并使用传入的LayoutParams */

@Override
public void addView(View child, LayoutParams params) {   
      addView(child, -1, params);
}

/** * 重载方法4:在指定位置添加一个子View,并使用传入的LayoutParams 
*/
public void addView(View child, int index, LayoutParams params) {   
         if (child == null) {        
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");   
}
     requestLayout();    
     invalidate(true);    
     addViewInner(child, index, params, false);
}

20、自定义LayoutParams

1. 创建自定义属性

<resources>    
         <declare-styleable name="xxxViewGroup_Layout">       
                  <!-- 自定义的属性 -->        
                  <attr name="layout_simple_attr" format="integer"/>       
                  <!-- 使用系统预置的属性 -->       
                 <attr name="android:layout_gravity"/>    
        </declare-styleable>
</resources>

2. 继承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) {        s
       uper(width, height);  
 }   

 public LayoutParams(MarginLayoutParams source) {
       super(source);
}

 public LayoutParams(ViewGroup.LayoutParams source) {         
      super(source);
  }
}

3. 重写ViewGroup中几个与LayoutParams相关的方法

// 检查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {      return p instanceof SimpleViewGroup.LayoutParams;
}

// 生成默认的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {     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进行转化
@Overridepublic ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {     
    return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}

21、LayoutParams常见的子类

在为View设置LayoutParams的时候需要根据它的父容器选择对应的LayoutParams,否则结果可能与预期不一致,这里简单罗列一些常见的LayoutParams子类:

  • ViewGroup.MarginLayoutParams
  • FrameLayout.LayoutParams
  • LinearLayout.LayoutParams
  • RelativeLayout.LayoutParams
  • RecyclerView.LayoutParams
  • GridLayoutManager.LayoutParams
  • StaggeredGridLayoutManager.LayoutParams
  • ViewPager.LayoutParams
  • WindowManager.LayoutParams

22、MeasureSpec

image.png

测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指在某种SpecMode下的参考尺寸,其中SpecMode 有如下三种:

  • UNSPECIFIED 父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大)
  • EXACTLY 父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。
  • AT_MOST 你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。
image.png

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

  • 对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定
  • 对于不同的父容器和view本身不同的LayoutParams,view就可以有多种MeasureSpec。
    1. 当view采用固定宽高的时候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小;
    1. 当view的宽高是match_parent时,这个时候如果父容器的模式是精准模式,那么view也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么view也是最大模式并且其大小不会超过父容器的剩余空间;
    1. 当view的宽高是wrap_content时,不管父容器的模式是精准还是最大化,view的模式总是最大化并且大小不能超过父容器的剩余空间。
    1. Unspecified模式,这个模式主要用于系统内部多次measure的情况下,一般来说,我们不需要关注此模式(这里注意自定义View放到ScrollView的情况 需要处理)。

23、Drawable是什么?

一种可以在Canvas上进行绘制的抽象的概念 颜色、图片等都可以是一个
Drawable Drawable可以通过XML定义,或者通过代码创建 Android中 Drawable是一个抽象类,每个具体的Drawable都是其子类

24、Drawable的优点

使用简单,比自定义View成本低 非图片类的Drawable所占空间小,能减 小apk大小 Drawable的内部宽/高

一般getIntrinsicWidth/Height能获得内部宽/高 图片Drawable其内部宽高就是图
片的宽高 颜色Drawable没有内部宽高的概念 内部宽高不等同于它的大小,一般
Drawable没有大小概念(作为View背景时,会被拉伸至View的大小)

25、自定义Drawable

一般作为ImageView的图像来显示 另一个是作为View的背景 自定义 Drawable主要就是实现draw方法 setAlpha、setColorFilter、getOpacity也 需要重写,但是模板固定 当自定义Drawable有固定大小时(比如绘制一 张图片),需要重写getIntrinsicWidth()/getIntrinsicHeight()方法(默认返 回-1),会影响到View的wrap_content布局 内部固定大小不等于Drawable 的实际区域大小,getBounds能获得实际区域大小

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容