View学习总结

View是Android界面中的一块矩形区域。ViewGroup控件可以包含和管理多个View。通过ViewGroup可以是整个界面上的view形成控件树,上层控件负责下层子控件的测量、布局和绘制,并传递交互事件。在Activity中通过findViewById()可以在控件树中以深度优先遍历方式查找对应view。在控件树的根部会有一个ViewParent对象统一调度分配整棵树的交互管理。

每个Activity都包含一个Window对象,这个对象一般用PhoneWindow实现。 PhoneWindow将DecorView设置为整个应用窗口的root view,DecorView将要显示的内容呈现到 PhoneWindow中。DecorView将屏幕分为两部分:一个是TitleView,另一个是ContentView。ContentView是一个ID为content的Framelayout,activty_main.xml中定义的布局作为子view就会在这个加载到这上面。

1.在Activity中显示View

在Activity中显示view实例时,可以在onCreate回调方法中使用setContentView(),也可以使用LayoutInflater去动态加载布局。本质上setContentView()内部也是使用LayoutInflater去加载布局。手动使用LayoutInflater加载布局可以用以下一个例子来解释。

定义一个新布局文件button_layout.xml:

  1. <Button xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="wrap_content"
  3. android:layout_height="wrap_content"
  4. android:text="Button" >
  5. </Button>

定义一个Activity:

  1. public class MainActivity extends Activity {
  2. private LinearLayout mainLayout;
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_main); //加载activity_main.xml定义的布局
  7. mainLayout = (LinearLayout) findViewById(R.id.main_layout); //获取布局
  8. LayoutInflater layoutInflater = LayoutInflater.from(this); //从当前Activity中获取LayoutInflater实例
  9. View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null); //加载自定义的布局文件中的view
  10. mainLayout.addView(buttonLayout); //把button加入当前布局
  11. }
  12. }

显示结果:

 

 在这个过程中,LayoutInflater是使用inflate方法来加载布局的,这个方法在frameworks/base/core/java/android/view/LayoutInflater.java的451行。这个方法主要做了以下几件事情:

(1)使用Android提供的pull解析方式来解析布局文件的节点和参数;

(2)把得到的节点和参数使用反射机制构造view实例;

(3)递归去解析构造子view;

(4)把构造出的view加入各级ViewGroup;

(5)整个布局文件都解析完成后就形成了一个完整的DOM结构,最终会把最顶层的根布局返回。

inflate()方法还有个接收三个参数的方法重载,结构如下:

inflate(int resource, ViewGroup root, boolean attachToRoot)

(1). 如果root为null,attachToRoot将失去作用,设置任何值都没有意义。

(2). 如果root不为null,attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即root。

(3). 如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。

(4). 在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true。

2.View绘制

Android中提供的布局、widget、ViewGroup都是view的直接子类或者间接子类,这些控件的绘制都需要经过三个过程:onMeasure()、onLayout()和onDraw()。ViewGroup通常情况下不需要绘制,除非它指定了背景颜色。但ViewGroup会使用dispatchDraw方法绘制其子View,通过遍历所有子view,并调用子view的绘制方法来完成绘制。

2.1.onMeasure()

每个view在绘制之前需要知道它的大小,因此在绘制view前需要对view进行测量,测量的过程在onMeasure()中进行。Android提供了MeasureSpec类来帮助确定测试的模式和测量的大小,该类是一个无符号32位变量,高2位为测量的模式,低32位为测量的大小。测量的模式分为以下三种:

(1)EXACTLY:精确模式,当view的layout_width或者layout_height为具体数值,或者为match_parent时,系统使用精确模式。这是也是系统默认模式。此模式不需要重写onMeasure()测量大小。

(2)AT_MOST:最大值模式, 当view的layout_width或者layout_height为wrap_content时,使用该模式。这种模式是指,view大小会随着该view的内容变化而变化,只要不超父view的大小即可。指定此模式必须重写onMeasure()测量大小。

(3)UNSPECIFIED:不指定大小模式,可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在这里会先获取父视图大小,然后执行performMeasure(),在其内部调用View的measure()方法。measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值的类型就是MeasureSpec。measure()方法会去调用onMeasure()方法,onMeasure()方法中调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。这段代码在frameworks/base/core/java/android/view/ViewRootImpl.java的1507行。

视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。

2.2.onLayout()

measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程。

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

layout()方法接收四个参数,分别代表着左、上、右、下的坐标,这个坐标是相对于当前视图的父视图而言的。可以看到,这里还把刚才测量出的宽度和高度传到了layout()方法中。

在layout()方法中,首先会调用setFrame()方法来判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。接下来会调用onLayout()方法。这段代码在frameworks/base/core/java/android/view/View.java的16943行。View中的onLayout()方法就是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图决定子视图的显示位置。因此需要去看ViewGroup中的onLayout()方法实现。ViewGroup中的onLayout()方法是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。

2.3.onDraw()

测量完大小和位置就可以进行绘制了。ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。draw()方法内部的绘制过程总共可以分为六步:

(1). Draw the background

这一步的作用是对视图的背景进行绘制。这里会先得到一个mBGDrawable对象,然后根据layout过程确定的视图位置来设置背景的绘制区域,之后再调用Drawable的draw()方法来完成背景的绘制工作。那么这个mBGDrawable对象是从哪里来的呢?其实就是在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值。

(2). If necessary, save the canvas' layers to prepare for fading

(3). Draw view's content

这一步的作用是对视图的内容进行绘制。可以看到,这里去调用了一下onDraw()方法,这是个空方法。因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现。

(4). Draw children

这一步的作用是对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。

(5). If necessary, draw the fading edges and restore layers

(6). Draw decorations (scrollbars for instance)

这一步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是ListView或者ScrollView,为什么要绘制滚动条呢?其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。

至此,可以发现View是不会帮我们绘制内容部分的,因此需要每个视图根据想要展示的内容来自行绘制。如果你去观察TextView、ImageView等类的源码,你会发现它们都有重写onDraw()这个方法,并且在里面执行了相当不少的绘制逻辑。绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。

3.View状态

View是有状态的,比如说有一个按钮,普通状态下是一种效果,但是当手指按下的时候就会变成另外一种效果,这样才会给人产生一种点击了按钮的感觉。View状态的种类非常多,一共有十几种类型,不过多数情况下我们只会使用到其中的几种:

(1). enabled

表示当前视图是否可用。可以调用setEnable()方法来改变视图的可用状态,传入true表示可用,传入false表示不可用。它们之间最大的区别在于,不可用的视图是无法响应onTouch事件的。

(2). focused

表示当前视图是否获得到焦点。通常情况下有两种方法可以让视图获得焦点,即通过键盘的上下左右键切换视图,以及调用requestFocus()方法。而现在的Android手机几乎都没有键盘了,因此基本上只可以使用requestFocus()这个办法来让视图获得焦点了。而requestFocus()方法也不能保证一定可以让视图获得焦点,它会有一个布尔值的返回值,如果返回true说明获得焦点成功,返回false说明获得焦点失败。一般只有视图在focusable和focusable in touch mode同时成立的情况下才能成功获取焦点,比如说EditText。

(3). window_focused

表示当前视图是否处于正在交互的窗口中,这个值由系统自动决定,应用程序不能进行改变。

(4). selected

表示当前视图是否处于选中状态。一个界面当中可以有多个视图处于选中状态,调用setSelected()方法能够改变视图的选中状态,传入true表示选中,传入false表示未选中。

(5). pressed

表示当前视图是否处于按下状态。可以调用setPressed()方法来对这一状态进行改变,传入true表示按下,传入false表示未按下。通常情况下这个状态都是由系统自动赋值的,但开发者也可以自己调用这个方法来进行改变。

3.1. View状态改变后背景发生改变

当View发生状态改变之后,都会调用refreshDrawableState()方法去更新对应的背景Drawable对象。代码路径\frameworks\base\core\java\android\view\View.java 中17359行。

  1. public void refreshDrawableState() {
  2. //主要功能是根据当前的状态值去更换对应的背景Drawable对象
  3. mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
  4. drawableStateChanged();//所有功能在这个函数里去完成
  5. ViewParent parent = mParent;
  6. if (parent != null) {
  7. parent.childDrawableStateChanged(this);
  8. }
  9. }

  1. protected void drawableStateChanged() {
  2. final int[] state = getDrawableState(); //获取状态
  3. final Drawable bg = mBackground; //
  4. if (bg != null && bg.isStateful()) {
  5. bg.setState(state);
  6. }
  7. final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
  8. if (fg != null && fg.isStateful()) {
  9. fg.setState(state);
  10. }
  11. if (mScrollCache != null) {
  12. final Drawable scrollBar = mScrollCache.scrollBar;
  13. if (scrollBar != null && scrollBar.isStateful()) {
  14. scrollBar.setState(state);
  15. }
  16. }
  17. if (mStateListAnimator != null) {
  18. mStateListAnimator.setState(state);
  19. }

drawableStateChanged()调用getDrawableState(),该方法首先会判断当前视图的状态是否发生了改变,如果没有改变就直接返回当前的视图状态,如果发生了改变就调用onCreateDrawableState()方法来获取最新的视图状态。视图的所有状态会以一个整型数组的形式返回。drawableStateChanged()方法中的第4行有一个mBackground实例,看下面代码:

  1. public void setBackgroundResource(@DrawableRes int resid) {
  2. if (resid != 0 && resid == mBackgroundResource) {
  3. return;
  4. }
  5. Drawable d = null;
  6. if (resid != 0) {
  7. d = mContext.getDrawable(resid);
  8. }
  9. setBackground(d);
  10. mBackgroundResource = resid;
  11. }
  12. public void setBackground(Drawable background) {
  13. //noinspection deprecation
  14. setBackgroundDrawable(background);
  15. }

setBackgroundResource()中调用了Context的getDrawable()方法将resid转换成了一个Drawable对象,然后调用了setBackground()方法并将这个Drawable对象传入,在setBackgroundDrawable()方法中会将传入的Drawable对象赋值给mBGDrawable。在布局文件中通过android:background属性指定的selector文件,效果等同于调用setBackgroundResource()方法。也就是说drawableStateChanged()方法中的mBGDrawable对象其实就是我们指定的selector文件。

回到drawableStateChanged()中,得到View状态的数组之后,就会调用Drawable的setState()方法来对状态进行更新。这部分代码在frameworks\base\graphics\drawable\drawable.java,682行,在setState()之后会更新对应新状态的背景。

3.2. View重绘

由于view状态发生改变,该背景也应该发生变化。但是Activity早已经绘制完布局,状态改变之后之前的view已经过期,状态更新带来的背景更新需要进行一次重绘。调用视图的setVisibility()、setEnabled()、setSelected()等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用invalidate()方法来实现。当然了,setVisibility()、setEnabled()、setSelected()等方法的内部其实也是通过调用invalidate()方法来实现的

invalidate()方法最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。

参考:

http://blog.csdn.net/guolin_blog/article/details/12921889

http://blog.csdn.net/guolin_blog/article/details/16330267

http://blog.csdn.net/guolin_blog/article/details/17045157

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值