ps :在系统的学习自定义 view 之前,搞懂本本篇的内容会让你学习的过程顺序,简单很多
view 的分类
view 其实就2种:
- 单一视图 view
我们常用的各种具体控件都是 view ,比如 TextView - 视图容器 ViewGroup
视图容器简单说就是各种 layout 布局,里面用来存放 view ,给 view 定位的。想我们常用的 LinearLayout 就是视图容器
自定义 view 的3个核心方法
- onMeasure
根据 view 的测量模式计算确定 view 的宽高 - onLayout
ViewGroup 中对所有的子 view 排版,决定子 view 的位置 - onDraw
具体绘制 view
这3个方法就是自定义的核心了,自定义 view 不管我们怎么写,基本都是围绕这3个方法玩。view 没有 onLayout 方法,因为 view 不是容器里面放不了 view ,只有 ViewGroup 才有 onLayout 方法
自定义 view 所有生命周期及回调函数
- onFinishInflate()
当应用从XML加载该组件并用它构建界面之后调用的方法 - onMeasure()
检测View组件及其子组件的大小 - onLayout()
当该组件需要分配其子组件的位置、大小时 - onSizeChange()
当该组件的大小被改变时 - onDraw()
当组件将要绘制它的内容时 - onKeyDown
当按下某个键盘时 - onKeyUp
当松开某个键盘时 - onTouchEvent
当发生触屏事件时 - onWindowFocusChanged(boolean)
当该组件得到、失去焦点时 - onAtrrachedToWindow()
当把该组件放入到某个窗口时 - onDetachedFromWindow()
当把该组件从某个窗口上分离时触发的方法 - onWindowVisibilityChanged(int)
当包含该组件的窗口的可见性发生改变时触发的方法
自定义 view 需要注意的生命周期及回调函数
注意这里,面试会问
void onFinishInflate()
当系统解析XML中声明的View后回调此方法,调用顺序:内层View->外层View,如果是viewgroup,适合在这里获取子View。
注意点:
如果View没有在XML中声明而是直接在代码中构造的,则不会回调此方法
此时无法获取到View的宽高和位置void onAttachedToWindow()
当view 被添加到window中回调,调用顺序:外层View->内层View。在XML中声明或在代码中构造,并调用addview(this view)方法都会回调该方法。
注意点:
此时View仅仅被添加到View,而没有开始绘制所以同样获取不到宽高和位置void onDetachedFromWindow()
看名字就知道是与void onAttachedToWindow();对应的方法,在VIew从Window中移除时回调,如执行removeView()方法。
注意点:
如果一个View从window中被移除了,那么其内层View(如果有)也会被一起移除,都会回调该方法,且会先回调内层View的onDetachedFromWindow()方法void onWindowFocusChanged(boolean hasWindowFocus)
当View所在的Window获得或失去焦点时被回调此方法。除了常见的设置view的onGlobalLayoutListener,也可以通过这个方法取到VIew的宽高和位置;也适合在判断当失去焦点时停止一些工作,如图片轮播,动画执行等,当获取到焦点后继续执行。
hasWindowFocus:View所在Window是否获取到焦点,当该Window获得焦点时,hasWindowFocus等于true,否则等于false。该方法在当前View或其祖先的可见性改变时被调用
更多详细请看:
自定义 view 的种类
继承现成 view 控件
比如我们写一个自定义 view 继承 textview ,这样难度小,view 的3个核心方法我么你都不用关心,不过一般这样写都是为了给某个控件添加额外功能,难度小,但是不具有普遍适应性。直接继承 view
直接继承 view ,view 的 onMeasure 测量 ,onDraw 绘制都需要我们自己来做,很考验功底的,里面又会涉及到大量的动画操作,是非常难得,学好了能大大提升我们的代码水平继承现成 ViewGroup 容器
难度小,一般我们都是做组合类型的 view 时用,多用于封装 app 中的公共基础 UI 组件,虽然难度低,但是具有普遍性直接继承 ViewGroup 容器
难度最大,ViewGroup 容器的工作在于给子 view 确定位置,给子view 排版,需要大量的计算操作,还要精确考虑 magin,padding 的问题,很难,一般很少这样做,都是对自己有信心的人才回去尝试,技术不熟练的先不要来了
view 的多个构造方法
view 的构造方法有4个,分别面对不同的使用情况,我们在自定义 view 时要知道在哪个构造的方法里做初始化,其实一般我们都是在这4个方法里面都写初始化方法的
// 如果View是在Java代码里面new的,则调用第一个构造函数
public CarsonView(Context context) {
super(context);
}
// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
public CarsonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 不会自动调用
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//API21之后才使用,不会自动调用
public CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
注意:即使你在View中使用了Style这个属性也不会调用三个参数的构造函数,所调用的依旧是两个参数的构造函数。
1.public View(Context context)
2.public View(Context context, @Nullable AttributeSet attrs)
3.public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
4.public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
构造方法严格来说不算回调,但除了方法一外都不需要我们手动调用,而且是自定义View仅有的必须要声明的方法。
- 构造方法1
当不在布局文件中声明而在代码中创建View时调用的方法 - 构造方法2
当在布局文件中声明,且没有在styles.xml中预设主题级或item级的默认属性时调用。attrs就是一组布局文件中的值(包括默认属性和自定义属性) - 构造方法3
当在布局文件中声明,在attrs.xml中有声明一个属性,并在styles.xml中的主题item声明这个属性的值(即View的一组默认属性)调用 - 构造方法4
当在布局文件中声明,在styles.xml中的主题里没有声明但单独声明了View的一组默认属性时调用。
属性赋值优先级:Xml定义 (方法二)> Xml的style定义(方法二) > defStyleAttr (方法三)> defStyleRes> theme直接定义(方法四)
构造方法四要求api21以上,所以我们一般采用构造方法二(没有默认属性)或构造方法三(有默认属性)
view 的视图层级
我们连带着把 Actvity 的 视图层级一起写一下吧,下面这张就是 Actvity 视图层级
我们在 xml 布局文件中声明的布局根节点并不是 Activity 的视图根节点,是上图中的 contentView 的位置,contentView 上面的都是 Activity 内部添加的,我们控制不了,但是我们需要了解,一些页面效果我们需要操作 DecorView
这张图是常见的 view 视图层级
ViewGroup 里面还可以再放 ViewGroup,但是 view 里面就不能放任何view 了
无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性
上面图中有一层就表示视图有一个层级,视图层级越多就会加重 cpu 计算负荷,这个不是线性的关系,是几何层级的关系。ViewGroup 宽高采用 warp_content 时,会跑2次这个 ViewGroup 所属子 view 的 onMeasur 方法,会大大增加任务量。所以我们在写布局时,层级过多或是 warp_content 应用过多,都会造成页面加载计算大,页面卡顿,这是我们需要优化的一个点。
Android 坐标系
- 屏幕的左上角为坐标原点
- 向右为x轴增大方向
- 向下为y轴增大方向
详细看下图:
和数学坐标系的 Y 轴方向是不同的
view 的坐标
view 有3套描述坐标位置的方式:
- Left,Top,Right,Bottom
- x,y,translationX、translationY
- rawX ,rawY
1. Left,Top,Right,Bottom
这4个描述的是 view 的左右上下到 view 所在父控件左上角的位置
- Top:子View上边界到父view上边界的距离
- Left:子View左边界到父view左边界的距离
- Bottom:子View下边距到父View上边界的距离
- Right:子View右边界到父view左边界的距离
相关的 API :
getTop(); //获取子View左上角距父View顶部的距离
getLeft(); //获取子View左上角距父View左侧的距离
getBottom(); //获取子View右下角距父View顶部的距离
getRight(); //获取子View右下角距父View左侧的距离
详细看下图:
需要注意的是 right = left + view 的 width , bottom = top + view 的 height
2. rawX ,rawY
描述的是 view 左上角到屏幕左上角的距离
这个可以用 MotionEvent 中 get 和 getRaw 的区别来学习
相关 API :
event.getX(); //触摸点相对于其所在组件坐标系的坐标
event.getY();
event.getRawX(); //触摸点相对于屏幕默认坐标系的坐标
event.getRawY();
详细看下图:
3. x,y,translationX、translationY
从android3.0开始,View增加了额外几个参数:x,y,translationX、translationY。其中x和y是View左上角的坐标,translationX和translationY是View左上角相对于父容器的偏移量,它们默认值是0。这些参数也是相对于View父容器。具体关系见下图:
x = left + translationX,y = top + translationY
x和left不同体现在:left是View的初始坐标,在绘制完毕后就不会再改变;而x是View偏移后的实时坐标,是实际坐标。y和top的区别同理。
如何获取 view 的宽高
获取 view 的宽高有2套 API:
- getWidth() / getHeight():获得View最终的宽 / 高
- getMeasuredWidth() / getMeasuredHeight():获得 View测量的宽 / 高
他们的区别:
getMeasuredWidth() 方法可以在 view 的 onLayout 方法里使用,onLayout 在 onMeasure 之后跑,这时候 measuredWidth view 的宽是计算出来的,但是我们要考虑 view 申请的大小超过父控件最大值的问题。
我们可以考虑在 onSizeChange 方法内记录 view 的大小,这也是一种办法。这2套获取宽高的 API 最终的结果值都一样,区别在于产生数据的时机不同。
getWidth() / getHeight() 只有在 view 计算完并显示之后才能返回具体的值,其他时候返回的都是 0,所以 getWidth() / getHeight() 一般我们都是做延迟使用,等待 view 计算显示完毕
获取 view 宽高的时机不同,所依赖的方法也是不同的,具体的我就不写了,大家看这个:
另外还有一点要清楚:getMeasuredXXX() 有时并不 = getXXX() ,下面这段话足以解释
getMeasuredXXX() 与 getXXX() 的区别和联系所在。说得直白一点,measuredWidth 与 width 分别对应于视图绘制 的 measure 与 layout 阶段。很重要的一点是,我们要明白,View 的宽高是由 View 本身和 parent 容器共同决定的,要知道有这个 MeasureSpec 类的存在。
比如,View 通过自身 measure() 方法向 parent 请求 100x100 的宽高,那么这个宽高就是 measuredWidth 和 measuredHeight 值。但是,在 parent 的 onLayout() 阶段,通过 childview.layout() 方法只分配给 childview 50x50 的宽高。那么,这个 50x50 宽高就是 childview 实际绘制并显示到屏幕的宽高,也就是 width 和 height 值。
如果你对自定义 View 过程很熟练的话,理解这部分内容就比较轻松一些。事实上,开发过程中,getWidth() 和 getHeight() 方法用的更多一些。
Android 的角度 (angle) 与弧度 (radian)
android 的角度和弧度有其需要说上一说, android 里的角度和我们平时的习惯是嫌烦的,这点很坑爹
另外这块涉及到画布的相关操作(旋转)、正余弦函数计算等,即会涉及到角度(angle)与弧度(radian)的相关知识。
另外记住缩写:
- deg --> 角度
- rad --> 弧度
角度,弧度的详细描述:
android 角度方向是顺时针的:
Android 中颜色部分
Android 支持一下几种颜色模式:
ARGB 表示4位颜色通道,RGB 表示3位颜色通道,RGB 相比 ARGB 少了透明的颜色通道,需要注意的是 ARGB8888 4位通道的图片若是转成 RGB565 3位通道的图片格式,是会造成图片色差的,A 透明颜色通道用的越多色差越严重
4位颜色通道含义:
- java中定义颜色
//java中使用Color类定义颜色
int color = Color.GRAY; //灰色
//Color类是使用ARGB值进行表示
int color = Color.argb(127, 255, 0, 0); //半透明红色
int color = 0xaaff0000; //带有透明度的红色
- xml文件中定义颜色
<?xml version="1.0" encoding="utf-8"?>
<resources>
//定义了红色(没有alpha(透明)通道)
<color name="red">#ff0000</color>
//定义了蓝色(没有alpha(透明)通道)
<color name="green">#00ff00</color>
</resources>
- java文件中引用xml中定义的颜色
//方法1
int color = getResources().getColor(R.color.mycolor);
//方法2(API 23及以上)
int color = getColor(R.color.myColor);
- xml文件(layout或style)中引用或者创建颜色
<!--在style文件中引用-->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/red</item>
</style>
<!--在layout文件中引用在/res/values/color.xml中定义的颜色-->
android:background="@color/red"
<!--在layout文件中创建并使用颜色-->
android:background="#ff0000"