转载请注明出处:From李诗雨—-http://blog.csdn.net/cjm2484836553/article/details/54358072
不诗意的女程序猿不是好厨师~
序:最近在工作中使用到了各种自定义控件,也更深刻的理解了自定义控件的重要性,所以就建了一个专栏来专门整理自定义控件的相关知识。我打算先从理论知识说起,然后再把项目中使用的自定义控件整理后写为博客发表,并且源码也会一并上传。理论知识部分,个人觉得整理的还是很详细的而且重点分明,无论是对面试还是对代码的理解都能起到很好的辅助作用。
注:本文于2017/4/30号进行了更新。增加了一些新内容,又绘制了一些新的图形,更有助于对知识点的理解。
View相关知识的理解
在说自定义控件之前让我先来了解一下View及ViewGroup
整个View树的结构图:
关于View和ViewGroup我们需要注意以下几点:
1. 手机屏幕上的整个界面只有一个根View
如何得到它:activity.getWindow().getDecorview() —>PhoneWindow$decorView
本质类型: FrameLayout
注意: setContentView():执行添加的视图不是整个界面的根View
2.一个View只会有一个父View(ViewGroup),一个ViewGroup可以有多个子View
a.得到父视图: view.getParent(),可以将返回的ViewParent强转为指定的ViewGroup
b.不是所有的View都能添加子view,只有ViewGroup 及其子类才能添加
View是什么
- View类是所有用来构建用户界面的组件的基类
- 一个View对象占用屏幕上的一个矩形区域, 它负责界面的绘制和事件处理
- 手机屏幕上所有看得见摸得着的都是View
- 常见的View:TextView,EditText,Button,ImageView,ProgressBar…
ViewGroup是什么
- ViewGroup类是View的一个子类, 是各种布局(五大布局)的基类
- 一个ViewGroup可以包含多个子View(ViewGroup)
- 作用: 控制子View的布局,view.layout(left, top, right, bottom)
- ViewManager及相关方法:
① addView():添加子View
② removeView():删除子View
③ updateViewLayout():更新子View - 常见的ViewGroup:LinearLayout,RelativeLayout,FrameLayout,ListView…
View的位置怎么确定
- Veiw的位置是由它(左上右下)四个顶点确定的。
top:左上角纵坐标
bottom:右下角纵坐标
left:左上角横坐标
right:右下角横坐标
它们都是相对坐标,都是相对于View的父容器来说的
如上图可得View的宽高与坐标的关系:
width= right - left
height= bottom - top
- 那么,How to 得到View的这四个参数?
View源码中它们对应mLeft,mTop,mRight,mBottom四个成员变量,获取方式:
left = getLeft();
top = getTop();
right = getRight();
bottom = getBottom();
- 注:Android 3.0开始额外增加了几个参数: x ,y —是View左上角的坐标
translationX,translationY—是View左上角相对于父容器的偏移量(默认值为0)
这几个参数也是相对于父容器的相对坐标。
它们的换算关系:
x = left +translationX;
y = top + translationY;
需注意,View在平移过程中,top,left表示的是原始坐上角的位置信息,其值不会发生变化。
此时发生改变的是x,y,translationX,translationY这四个参数。
View(及其子类)的生命周期及其他知识点回顾
1. 创建对象
- 创建方式(2种):
new MyView(context)
加载布局文件,即自定义View必须使用全类名标签 流程方法
①构造器
Xxx(Context context)
Xxx(Context context, AttributeSet set)
②onFinishInflate()
只有布局的方式才会调用
重写的目的: 得到子View –>getChildAt(int index):index按照加载顺序排列
onAttachedToWindow()–>重写: 得到子View补充 Activity的onResume()执行之后才会进入后面的流程
2.View的工作流程
- View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制。无图不欢,这里给大家画了一个图:
通过上面的图形,相信你就可以很明白的知道,各个环节是干嘛的,可以得到什么,以及它的意义所在了。下面再让我们进行具体的分析。
2.1 测量
作用:计算并确定视图的大小(测量的宽/高)
流程方法:
①.mesure() :系统在此方法中测量计算出当前视图的宽高,此方法不能重写
②.onMesure():
当mearure()中 计算出的视图的宽高就会调用此方法, 在此方法默认保存的
视图测量的宽高
注意:视图测量的宽高不等同于视图的宽高。获取的时机不同
重写的意义:得到当前视图/子视图测量的宽高;保存我们自己指定的宽高
2.2 布局
作用:确定视图显示的坐标(left, top, right, bottom)以及View最终的宽/高
流程方法:
①.layout() :layout(l, t, r, b),不会重写此方法, 只会调用视图对象的此方法, 指
定其新的显示位置
②.onLayout()
在layout()的过程中 如果①视图的位置change或②强制重新布局就会调
此方法
重写它: 可以对子View进行重新布局,调用childView.layout(left, top, right,
bottom)强制重新布局: view.requestLayout()
2.3.绘制
作用:画出视图的样子,决定View的显示
流程方法:
draw(),绘制视图通用的部分,确定绘制的流程,一般不会重写此方法
onDraw(),重写此方法,绘制自己需要的样子,一些具体的View类(如:TextView,ImageView)都重写了此方法强制重绘:
invalidate():只能在主线程执行
postInvalidate():可以在主线程或分线程执行
注意!
细心的朋友可能注意到了,我这里提到了measure过程中我们得到的是View的测量的宽/高,layout过程我们得到的是View的最终的宽/高。那这测量的宽/高和最终的宽/高它们有什么区别呢?
我们可以这么理解,由于两者的赋值时机不同,即一个measure过程,一个layout过程,这就导致了一个先后的问题,即测量的宽/高先出生,最终的宽/高后出生。在这里我举一个具体点的例子,可能不是非常的合理,但确实有助于理解:
如图,在布局中你可能在外部设置了一个ScrollerView,并且设置为match_parent,然后它的内部的内容如虚线内所示,比屏幕的高度要长,那么这时候我们在measure过程中的到的测量高/宽最大也只能为屏幕的高,而在layout过程中得到的实际宽/高,确是比屏幕的高要长的。
当然大多数情况下,测量的宽/高和最终实际的宽/高是相同的,但是当遇到一些特殊情境时我们还是要多多小心。这里我们还要注意一点:那就是View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate,onStart, onResume时某个View就测量完毕了。如果View还没有测量完毕,那么获得的宽/高就是0.
所以如果我们想在Activity已启动的时候就做一件任务,但是这一件任务需要获取某个View的宽/高。那么这个时候你就不要在天真地告诉我:在onCreate或者onResume里面去获取这个View的宽/高不就行了?真是too young too simple,不可以的哈!
这里给大家几个解决办法以供参考:
①我首推的是ViewTreeObserver,没错视图树。大家可以看下我高仿各大商城引导页面的那篇文章,我在处理下部的小圆点,需要获得它的间距的时候,我就使用了视图树的OnGlobalLayoutListener这个接口,当View树的状态发生改变或者Veiw树内部的View的可见性发生改变是,onGlobalLayout方法就会被调用,因此我在这里面去获取小圆点间的间距就是一个很好的时机,对应部分的代码如下:
//获取树形视图,每次页面布局完成时会调用,获取点间的距离
ivWhitePoint.getViewTreeObserver().addOnGlobalLayoutListener(new MyOnGlobalLayoutListener());
private class MyOnGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener {
@Override
public void onGlobalLayout() {
//默认会调用俩次,只需要一次,第一次进入就移除
ivWhitePoint.getViewTreeObserver().removeGlobalOnLayoutListener(MyOnGlobalLayoutListener.this);
//间距 = 第1个点距离左边距离 - 第0个点距离左边距离
leftmax = llPointGroup.getChildAt(1).getLeft() - llPointGroup.getChildAt(0).getLeft();
}
}
private class MyOnPageChangeListener implements ViewPager.OnPageChangeListener {
/**
* 当页面滑动回调会调用此方法
*
* @param position 当前页面位置
* @param positionOffset 当前页面滑动百分比
* @param positionOffsetPixels 滑动的像素数
*/
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//红点移动的距离 = ViewPager页面的百分比* 间距
//坐标 = 起始位置 + 红点移动的距离;
int leftmagin = (int) (position * leftmax + (positionOffset * leftmax));
...
}
...
}
②当然你也可以使用onWindowFocusChanged这个方法,这个方法的含义是:View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没有问题的。
③view.post(runnable),通过psot可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。
3.事件处理
流程方法:
dispatchTouchEvent():
分发事件,从外向里一层一层分发, 分发到事件发生的最里面的视图对象
boolean onInterceptTouchEvent():
拦截请求, 只有return true才拦截成功,如果事件被拦截,事件不会再向内层分
发, 交给当前的视图处理
boolean onTouchEvent():
处理事件:消费事件的条件: return true
requestDisallowInterceptTouchEvent(true)
反拦截–>view.getParent().requestDisallowInterceptTouchEvent(true)事件机制:
①分发: 将TouchEvent对象从Activity对象开始, 由外向内分发给对应的布局和子View对象(由外向内分发)。②处理: 回调OnTouchListener的boolean onTouch()
回调View的boolean onTouchEvent()③消费: 回调方法返回true
④拦截: onInterceptTouchEvent()执行返回true
如果返回true, TouchEvent就不会再传入子View对象⑤反拦截: view.getParent().requestDisallowInterceptTouchEvent(true)
使父View不能再拦截, 事件就会分发到当前View对象
拦截与反拦截,都是在分发的时候就要决定的。
4.死亡
什么时候死亡:
Activity死亡之前
视图对象被移除流程方法
onDetachedFromWindow()