【Android】View和ViewGroup

0. 前言

本文将对自定义View的原理和方法进行简要讲解,通过此文,你将学到:

  • 安卓的View架构
  • View的绘图机制
  • 自定义View的方法步骤

1. View控件的架构

1.1 View和ViewGroup

Android中,控件大致可以分为两大类:

  • View控件
  • ViewGroup控件

它们都会在界面中占得一块矩形区域。View控件是单个的视图控件,是一个独立的最小个体,View控件之间互不相容,比如系统的Button、TextView等控件;ViewGroup控件便是包容View控件的容器,比如系统的LinearLayout、FrameLayout、以及安卓5.0后增加的基于FrameLayout的CardView等。

请注意,这里说的是View控件而不是说安卓源码中的View类,因为在源码中,ViewGroup其实是继承自View的,View在源码中的类继承关系如图所示:
View继承关系

1.2 View树

由于ViewGroup和View之间存在缠绵的包容关系,便有了View树这一说法,什么是View树呢,其实就是View容器和View构成多层次视图所形成的树形结构,也就是树根是ViewParent,其子为ViewGroup,ViewGroup又可以有View和ViewGroup子树。安卓界面的View树如图所示:

View树

每个Activity都包含一个Window对象,在Android中Window对象通常由PhoneWindow来实现。PhoneWindow将一个DecorView设置为整个应用窗口的根View。DecorView作为窗口界面的顶层视图,封装了一些窗口操作的通用方法。可以说,DecorView将要显示的具体内容呈现在了PhoneWindow上,这里面的所有View的监听事件,都通过WindowManagerService来进行接收,并通过Activity对象来回调相应的onClickListener。在显示上,它将屏幕分成两部分,一个是TitleView,另一个是ContentView。看到这里,大家一定看见了一个非常熟悉的布局——ContentView。它是一个ID为content的Framelayout,activity_main.xml就是设置在这样一个Framelayout里。

通过上述结构便可以推导出,如过用户通过调用requestWindowFeature(…)来设置窗口的属性,那么必须在setContentView(…)之前调用才能生效,因为Window是在ContentView之前绘制的。

2. 自定义View

通常情况下,有以下三种方法来实现自定义View:

  • 对现有控件进行扩展
  • 通过组合来实现新的控件
  • 完全重写View来实现全新控件

2.1 View绘制流程

在自定义View中,我们需要对系统的绘图机制作一定了解:
整个View树的绘图流程是在ViewRootImpl类的performTraversals()方法开始的,该函数做的执行过程主要是根据之前设置的状态,判断是否重新计算视图大小(measure)、是否重新放置视图的位置(layout)、以及是否重绘 (draw),其核心也就是通过判断来选择顺序执行这三个方法。

2.1.1 第一步: 递归测量View大小

在现实生活中,如果我们要去画一个图形,就必须知道它的大小和位置。同样,Android系统在绘制View前,也必须对View进行测量,即告诉系统该画一个多大的View。这个过程在onMeasure()方法中进行,View中的onMeasure方法如下:

<code class="language-java hljs  has-numbering"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onMeasure</span>(<span class="hljs-keyword">int</span> widthMeasureSpec, <span class="hljs-keyword">int</span> heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}</code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li></ul><div class="save_code tracking-ad" data-mod="popu_249"><a target=_blank href="javascript:;"><img src="http://static.blog.csdn.net/images/save_snippets.png" /></a></div><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li></ul>

Android系统给我们提供了一个设计短小精悍却功能强大的类——MeasureSpec类,通过它来帮助我们测量View。MeasureSpec是一个32位的int值,其中高2位为测量的模式,低30位为测量的大小,在计算中使用位运算的原因是为了提高并优化效率。
测量的模式可以为以下三种:

  • EXACTLY
    即精确值模式,当我们将控件的layout_width属性或layout_height属性指定为具体数值时,比如andorid:layout_width=”100dp”,或者指定为match_parent属性时(占据父View的大小),系统使用的是EXACTLY模式。
  • AT_MOST
    即最大值模式,当控件的layout_width属性或layout_height属性指定为wrap_content时,控件大小一般随着控件的子空间或内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。
  • UNSPECIFIED
    这个属性比较奇怪——它不指定其大小测量模式,View想多大就多大,通常情况下在绘制自定义View时才会使用。

View类默认的onMeasure()方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure()方法的话,就只能使用EXACTLY模式。

此处贴上一段模板代码:

<code class="language-java hljs  has-numbering"><span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onMeasure</span>(<span class="hljs-keyword">int</span> widthMeasureSpec, <span class="hljs-keyword">int</span> heightMeasureSpec) {
    <span class="hljs-keyword">super</span>.onMeasure(widthMeasureSpec, heightMeasureSpec);
    setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
<span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> <span class="hljs-title">measureHeight</span>(<span class="hljs-keyword">int</span> heightMeasureSpec) {
    <span class="hljs-keyword">int</span> result = <span class="hljs-number">0</span>;
    <span class="hljs-comment">// 取得高2位和低30位</span>
    <span class="hljs-keyword">int</span> specMode = MeasureSpec.getMode(heightMeasureSpec);
    <span class="hljs-keyword">int</span> specSize = MeasureSpec.getSize(heightMeasureSpec);
    <span class="hljs-keyword">if</span> (specMode == MeasureSpec.EXACTLY) {
        <span class="hljs-comment">// 如果是match_parent属性,不需要修改</span>
        result = specSize;
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// 如果是wrap_content属性,则需要一个固定值</span>
        result = <span class="hljs-number">100</span>;
        <span class="hljs-keyword">if</span> (specMode == MeasureSpec.AT_MOST) {
            <span class="hljs-comment">// 为防止显示不全,需要取固定值和测量值当中的小者</span>
            result = Math.min(result, specSize);
        }
    }
    <span class="hljs-keyword">return</span> result;
}
<span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> <span class="hljs-title">measureWidth</span>(<span class="hljs-keyword">int</span> widthMeasureSpec) {
    <span class="hljs-keyword">int</span> result = <span class="hljs-number">0</span>;
    <span class="hljs-keyword">int</span> specMode = MeasureSpec.getMode(widthMeasureSpec);
    <span class="hljs-keyword">int</span> specSize = MeasureSpec.getSize(widthMeasureSpec);
    <span class="hljs-keyword">if</span> (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } <span class="hljs-keyword">else</span> {
        result = <span class="hljs-number">200</span>;
        <span class="hljs-keyword">if</span> (specMode == MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
    }
    <span class="hljs-keyword">return</span> result;
}</code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li></ul><div style="display: none;" class="save_code tracking-ad" data-mod="popu_249"><a target=_blank href="javascript:;"><img src="http://static.blog.csdn.net/images/save_snippets.png" /></a></div><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li></ul>

另外,ViewGroup在测量时通过遍历所有子View,调用子View的Measure方法来获得每一个子View的测量结果,从而确定自身的大小。

2.1.2 第二步: 递归确定View位置

整个layout过程比较容易理解,也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。具体layout核心主要有以下几点:

  • View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。
  • measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。
  • 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的。
  • 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

2.1.3 第三步: 递归绘制View

绘制View过程发生在onDrawe(canvas)方法中,在方法中使用Canvas对象作为参数,并通过它来绘制图形和文字来实现各种复杂的效果,也是自定义View中非常关键的一步,比如TextView中,在onDraw中实现绘制文本:

<code class="language-java hljs  has-numbering">    <span class="hljs-annotation">@Override</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onDraw</span>(Canvas canvas) {
        restartMarqueeIfNeeded();
        <span class="hljs-comment">// Draw the background for this view</span>
        <span class="hljs-keyword">super</span>.onDraw(canvas);
        ...
        <span class="hljs-keyword">int</span> color = mCurTextColor;

        mTextPaint.setColor(color);
        mTextPaint.drawableState = getDrawableState();

       ...

        <span class="hljs-keyword">if</span> (mEditor != <span class="hljs-keyword">null</span>) {
            mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
        } <span class="hljs-keyword">else</span> {
            layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
        }

        <span class="hljs-keyword">if</span> (mMarquee != <span class="hljs-keyword">null</span> && mMarquee.shouldDrawGhost()) {
            <span class="hljs-keyword">final</span> <span class="hljs-keyword">float</span> dx = mMarquee.getGhostOffset();
            canvas.translate(layout.getParagraphDirection(<span class="hljs-number">0</span>) * dx, <span class="hljs-number">0.0</span>f);
            layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
        }

        canvas.restore();
    }</code><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li></ul><div style="display: none;" class="save_code tracking-ad" data-mod="popu_249"><a target=_blank href="javascript:;"><img src="http://static.blog.csdn.net/images/save_snippets.png" /></a></div><ul style="" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li></ul>

所以在自定义View中需要对onDraw方法进行重写,但在自定义ViewGroup则不需要,除非ViewGroup需要有背景。

2.2 View常见回调方法

在View中通常有以下一些比较重要的回调方法:

  • onFinishInflate():从XML加载组件后回调。
  • onSizeChanged():组件大小改变时回调。
  • onMeasure():回调该方法来进行测量。
  • onLayout():回调该方法来确定显示的位置。
  • onTouchEvent():监听到触摸事件时回调。

当然,创建自定义View的时候,并不需要重写所有的方法,只需要重写特定条件的回调方法即可。这也是Android控件架构灵活性的体现。

3. 自定义View的一般步骤

自定义View一般采用一下步骤:

  1. 定义attrs.xml属性
  2. 继承View,在构造函数中获取属性
  3. 在onSizeChanged方法中初始化
  4. 重写onMeasure方法
  5. 重写onDraw方法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值