您的位置:首页 > 其它

自定义View(一)---View的基础概念、工作流程以及生命周期的理解

2017-01-12 22:26 465 查看
转载请注明出处:From李诗雨—-http://blog.csdn.net/cjm2484836553/article/details/54358072

不诗意的女程序猿不是好厨师~

:最近在工作中使用到了各种自定义控件,也更深刻的理解了自定义控件的重要性,所以就建了一个专栏来专门整理自定义控件的相关知识。我打算先从理论知识说起,然后再把项目中使用的自定义控件整理后写为博客发表,并且源码也会一并上传。理论知识部分,个人觉得整理的还是很详细的而且重点分明,无论是对面试还是对代码的理解都能起到很好的辅助作用。

:本文于2017/4/30号进行了更新。增加了一些新内容,又绘制了一些新的图形,更有助于对知识点的理解。

View相关知识的理解

在说自定义控件之前让我先来了解一下ViewViewGroup

整个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()
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  View的生命周期
相关文章推荐