深入理解Android View《一》—IT蓝豹分享

做android其实也有一段时间了,我们每个人都会碰到一些这样或那样的问题,碰到问题了就拼命百度,可是发现,我们解决问题的能力并没有提升很多,所以我才有想总结一下我项目中所用过的相关知识,并了解一下Android源代码中是如何定义这些属性的,如何去实现的。以后再碰到类似的问题,我该如何实现。本人也不常写博客,希望各位博友能指点,分享,并提出博客中不正确的地方,共勉! 
   首先我发一份我做的关于Android View深入实现的的XMind的思维导图,可以帮助我一起整理思路,若是博友有什么想到的地方可以帮我一起补充。 

深入理解Android View《一》—IT蓝豹分享_第1张图片 

    今天首先来介绍下View,View字面意思理解就是视图,什么是视图,说白了UI,你在屏幕上看到的,呈现在你眼前的东西都是属于视图。但是有个问题:我们在做刷新视图的时候,要用Handler机制来对UI组件进行相应的刷新,为什么不能直接在UI线程中做相关的刷新呢?若是在做一些比较费时的操作,比如读取数据库,网络访问,这个时候我们基本都会在UI线程中开一个子线程,然后利用Handler机制去子线程中取相应的数据,利用消息通知机制来UI主线程中刷新数据。我的理解是:其实UI,View,这些可以看得到的组件在运行的过程中,首先他要创建相关的UI的属性,比如要显示的图形的坐标点,大小,长宽,然后有一个画笔和画布,画上去的。就是说,电脑屏幕上你所有看到的东西都是画上去的。但是你发现,一旦断电,屏幕上的东西立马就不见了,是的!屏幕其实一直在不断的重绘,主要是利用了人眼的视觉暂留效果,两次重绘在0.05秒-0.2秒的时候,人不会发现发现屏幕在闪烁!计算机里有一个叫UI线程,其实他只做绘画,只管不停地在屏幕上不断画画,而且要再那么短的时间不断画,他是一个死循环,他很繁忙,还有一点,比如你操作数据库的数据,通过网络访问数据这些都是比较慢的操作,内存取数据>文件取数据>网络访问请求取数据。这个时候若是在UI主线程中去做非时操作,UI就阻塞了。那这个时候需要开辟一个子线程,他来做数据相关的操作,Handler的作用就好像是UI线程和子线程之间的信使,UI主线程这个时候发出一个通知:郑打理朝政繁忙,哪个子民可以为我平息边疆的战争,这个时候,主线程不是直接把这个话通知到下面的所有臣子,而是用了个中间的对象,就好比皇帝身边的人,先告诉他:最近要平息边疆战争,你给我告诉传达到每一个下面的臣子,务必传达到!好了,于是Handler就忙起来了,他忙着通知,接受大臣的反馈,大臣接受到这个间接的通知之后,带兵大战,驰骋沙场。那老皇帝等的着急吧,战争胜利了还是失败了呢?还是通过Handler把子线程运行的结果告诉Handler,Handler把结果拿给UI线程,一做刷新就完美呈现了!这个过程有什么好处呢,就是保证每个事物,每个人都分工很明确,各干各的事情。UI线程只做UI组件的创建,渲染,重绘,而Handler只管理UI线程和子线程之间的交互和反馈,子线程只管做自己擅长的具体事情就好了,他唯一要做的是把运行的结果告诉Handler,Handler再拿着结果反馈给UI线程,UI再拿着这个结果去刷新屏幕。 
      其实不光光是UI绘制这个过程是这样的,比如:我们在点击一个Button的时候,会给他注册一个setOnClickListener(),但是UI组件自己是不知道自己干什么的,UI是不知道自己是被点击了,还是被滑动了,他只管自己绘画,而真正的处理这个事情的过程是由另一个接口来处理的,相应的接口做相应的处理,处理的结果再由回调来响应。 
      说了那么多,都只是刚开始,其实这是一种很好的思想,叫做委托,比如你因为各种原因,没时间,没有资源,我委托一个中间的代理来帮我找能帮我做相应事情的人,然后这个代理再把找到的结果反馈给你。你要租房子,但是找不到很多的房源,那就要通过第三方比如中介来做这个事情了。 
    在说View之前,我提出了很多问题:我的学习方法是带着问题从源代码中去找解决方式。这样才能知其然而知其所以然,然后用理论再在结合实际的一些小例子 
   1)首先一个View要绘制之前,肯定要定义相应的属性和方法,比如坐标点,长,宽,高,绘制的方式,绘制的风格,这些是哪里定义的呢? 
(2)View是如何被创建出来的呢,绘图绘制的机制是如何的?是如何把相应的属性结合起来呈现视图效果的? 
(3)一个View他要占地方,他的位置如何确定的。他的大小如何确定的? 
(4)一个View画出来之后,其实就像水氢原子+氧原子+电产生的,可是产生了之后用什么来存放呢? 
(5)就一个图形放在那里,我们不知道他如何和外部交互啊?UI叫做UserInterface,用户接口,既然是给用户用的,那我怎么和计算机交互呢?怎么样赋予它生命呢,和外界感知? 
(6)如何用这些知识在项目中实战 
总的来说,分成三个方面,(1)UI的外部形态?(2)谁来产生UI,谁来去创造它?(3)事件机制,能被外界感知到 
  先来说第一个问题,在说道UI的外部形态的时候,有三个决定他外部形态的函数 
-> onDraw()方法 
-> onMeasure()方法 
-> onLayout()方法 
从字面上来 onDraw()和onLayout()很好理解 一个是用来绘制的,一个是用来确定他的位置的,但是这个onMeasure()我查了下英文字典,measure是测量,尺寸的意思,就是测量UI组件的大小的。 
还是先来看onDraw()的源代码 
咋一看 
  <pre name="code" class="java">  /** 
     * Implement this to do your drawing. 
     * 
     * @param canvas the canvas on which the background will be drawn 
     */ 
    protected void onDraw(Canvas canvas) { 

    }</pre> 
其中什么代码也没有?是不是很奇怪,这个时候就应该想是哪个地方调用这个函数的。可以在AndroidStudio下按下ctrl+g 
你可以看到有一个方法里调用了onDraw()方法,这个方法就是draw()方法。 
首先来看一下英文的解释:注意其中的2点,draw()是通过Canvas来实现的(2)当这个方法draw()方法被调用的时候,这个View其实已经生成了一个完整的布局了,然后后面说的,在编程的时候如果你要在一个子类中需要调用重写该方法,要使用onDraw()方法来替代这个draw()方法 
这里要注意2点,第一,View的相关绘制追踪到再底层是由Canvas绘制的,Canvas是画布,可以在画布上绘制各种雏形,然后可以通过跟踪代码的方式按F3,其实在绘制View的时候会首先绘制一个矩形这样的雏形。第二,我有问我网友,我为什么不能重写draw(),若是重写父类的draw()方法我要重写这个onDraw()方法呢,他给我的解释是这样的: 
draw()方法是系统的方法,是一个framework层的方法,onDraw()方法是用户的方法,是给用户间接调用实现特定用户需求的方法的 
如果要重写draw()方法的话就变成了 
<pre name="code" class="java"> public void draw(){ 
   //framework code 
   //custom code 
   //framework code 
}</pre> 
但是若是采用重写onDraw()方法的话就变成了 
<pre name="code" class="java">public void draw(){ 
   //framework code 
   onDraw(); 
   //framework code</pre> 
   其实后面要说到的layout()方法,官方中也是这么解释的,要你使用onLayout()方法来重写,而不是调用layout()方法。这个就是所谓的钩子函数!不知道说的准不准确,欢迎博友一起纠正!继续往下读代码,你可以知道要绘制一个View 需要6个步骤:我简要总结一下, 
1。绘制背景。 
2.绘制View或者该View存在的子View。 
3.还要对View加上相应的滚动条,比如有些View ScrollView就是带有滚动条的,还有有些列表也是带有的。 

     那倒这里我又有一个疑惑了,为什么有些View是带有滚动条的呢,而有些View就是没有呢,比如我在项目中用一个ListView,若是ListView中有很多很多条数据,加入数据超过了相应的显示长度,这个时候我们是不是要在ListView里加一个滚动条组件,这样就可以允许用户通过滚动条的滑动来查看下面的数据,但是:有些组件,比如我要上传自己的一个照片到服务器端,但是照片太大了 ,我要进行相应的裁剪,这个时候View其实是做了一个裁剪的操作,就是说:每个不同的View其实根据实际的应用需求来对View做相应的处理。那具体是根据什么方式来划分的呢?别着急,这些源代码里都有下面有一个方法确定View大小的方法 onMeasure()里就有涉及到这方面的处理! 
     当然draw()方法还有一个重载的方法 ,这个英文的描述: 
这个方法是被ViewGroup.drawChild()方法来调用的,用来绘制依次在ViewGroup里的childView。这样一说其实也就明白了,原来Viewgroup就是一个View的容器,他是用来装View的地方的。那可以联想到我们常用的LinearLayout,RelativeLayout布局容器,他们其实也不就是ViewGroup嘛,在写XML的时候他们是作为根节点的,他们下面有很多子节点作为父容器的孩子节点。 
再来看一下确定View位置的相关方法Layout()方法,为什么把这2个方法放在一起说呢,就是因为刚才说到一点,layout()方法和draw()方法一样,若是子类要重写父类这个方法的时候,也是要调用onLayout()方法来重写,所以他的代码结构其实和draw()方法是一样的。 
<pre name="code" class="java">public void layout(int l, int t, int r, int b) { 
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { 
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); 
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; 
        } 

        int oldL = mLeft; 
        int oldT = mTop; 
        int oldB = mBottom; 
        int oldR = mRight; 

        boolean changed = isLayoutModeOptical(mParent) ? 
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); 

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { 
            onLayout(changed, l, t, r, b); 
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; 

            ListenerInfo li = mListenerInfo; 
            if (li != null && li.mOnLayoutChangeListeners != null) { 
                ArrayList<OnLayoutChangeListener> listenersCopy = 
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); 
                int numListeners = listenersCopy.size(); 
                for (int i = 0; i < numListeners; ++i) { 
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); 
                } 
            } 
        } 

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; 
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; 
    } 
在这个方法里要分三段来看,(1)setFrame() 要设置改组件距离左边,上边,底部和右边的距离。这几个值构成的矩形的区域就是该View显示的位置,不过这里的具体位置都是相对父容器的(2)此处有一个判断位置是否改变的返回boolean值的函数,若是位置改变,则进行相应的重绘。然后回调onLayout函数,最后回调所有注册过的listener的onLayoutChange函数。对于View来说,onLayout只是一个空实现,一般情况下我们也不需要重载该函数: 
然后在看代码的时候我发现一个细节要特别注意,setFrame()有一个这个方法中notifySubtreeAccessibilityStateChangedIfNeeded() 
该方法我单从字面意思上去理解就是说,这个方法是父容器通知子容器的有效重绘的通知,为了不必要的系统开销而建立的通知机制。 
然后按F3跟踪进去 
** 
     * Notifies that the accessibility state of this view changed. The change 
     * is *not* local to this view and does represent structural changes such 
     * as children and parent. For example, the view size changed. The 
     * notification is at at most once every 
     * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()} 
     * to avoid unnecessary load to the system. Also once a view has a pending 
     * notification this method is a NOP until the notification has been sent. 
     * 
     * @hide  
     */ 
    public void notifySubtreeAccessibilityStateChangedIfNeeded() { 
        if (!AccessibilityManager.getInstance(mContext).isEnabled()) { 
            return; 
        } 
        if ((mPrivateFlags2 & PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED) == 0) { 
            mPrivateFlags2 |= PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED; 
            if (mParent != null) { 
                try { 
                    mParent.notifySubtreeAccessibilityStateChanged( 
                            this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); 
                } catch (AbstractMethodError e) { 
                    Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() + 
                            " does not fully implement ViewParent", e); 
                } 
            } 
        } 
    }</pre> 
然后,我这里还会提出一个问题: 
你在创建对象的时候,都会用该对象的构造方法。我们每次都是通过构造方法来实现对该对象的内存分配,属性赋值等相关操作,那View的构造方法上有什么可以值得推敲的地方呢,这个时候促使我想起,View是怎么来的,如何被创建的。 
先小看一下View的三个构造方法: 
1.public void CustomView(Context context) {} 
2.public void CustomView(Context context, AttributeSet attrs) {} 
3.public void CustomView(Context context, AttributeSet attrs, int defStyle) {} 
那这个时候我们需要好好的问问自己:为什么要定义三个构造方法呢,2个不够用嘛?然后再猜想一下这个AttributeSet和 defStyle是用来干嘛的?从名字看是关于View的设计风格的文件的,那他又是如何加载这些设计风格的文件呢?程序员去编程的时候如何使用这些用户接口呢?将再下一块涉及到这些问题。 

 

文章来自IT蓝豹:http://www.itlanbao.com  请转发时明标注出处,谢谢。


你可能感兴趣的:(UI,android,IT蓝豹)