Android面试笔记——View

View(视图)的内容很多,这里只总结了一些我认为比较重要的点。

1.1View基础

1.1.1视图分类

视图View主要分为两类:

  • 单一视图:即一个View、不包含子View,如TextView
  • 视图组,即多个View组成的ViewGroup、包含子View,如LinearLayout

Android中的UI组件都由View、ViewGroup共同组成。

1.1.2视图结构

  • 对于包含子View的视图组(ViewGroup),结构是树形结构
  • ViewGroup下可能有多个ViewGroup或View,如下图:


    image.png

这里需要特别注意的是:在View的绘制过程中,永远都是从View树结构的根节点开始(即从树的顶端开始),一层一层、一个个分支地自上而下遍历进行(即树形递归),最终计算整个View树中各个View,从而最终确定整个View树的相关属性。

1.1.3Android坐标系

Android的坐标系定义为:

  • 屏幕的左上角为坐标原点
  • 向右为x轴增大方向
  • 向下为y轴增大方向

具体如下图:

img

视图的位置由四个顶点决定,如图所示的A、B、C、D。

image.png

视图的位置是相对于父控件而言的,四个顶点的位置描述分别由四个与父控件相关的值决定:

  • 顶部(Top):视图上边界到父控件上边界的距离;
  • 左边(Left):视图左边界到父控件左边界的距离;
  • 右边(Right):视图右边界到父控件左边界的距离;
  • 底部(Bottom):视图下边界到父控件上边界的距离。

具体如图所示。

image.png

可根据视图位置的左上顶点、右下顶点进行记忆:

  • 顶部(Top):视图左上顶点到父控件上边界的距离;
  • 左边(Left):视图左上顶点到父控件左边界的距离;
  • 右边(Right):视图右下顶点到父控件左边界的距离;
  • 底部(Bottom):视图右下顶点到父控件上边界的距离。

1.1.4位置获取方式

视图的位置获取是通过View.getXXX()方法进行获取。

获取顶部距离(Top):getTop()
获取左边距离(Left):getLeft()
获取右边距离(Right):getRight()
获取底部距离(Bottom):getBottom()
  • 与MotionEvent中 get()getRaw()的区别
//get() :触摸点相对于其所在组件坐标系的坐标
 event.getX();       
 event.getY();

//getRaw() :触摸点相对于屏幕默认坐标系的坐标
 event.getRawX();    
 event.getRawY();

具体如下图:

image.png

1.2自定义View工作流程

1.2.1Window、Activity、DecorView 与 ViewRoot的关系

1.2.2绘制流程概述

View的绘制流程开始于:ViewRootImpl对象的performTraversals(),源码如下:

/**
  * 源码分析:ViewRootImpl.performTraversals()
  */
  private void performTraversals() {

        // 1. 执行measure流程
        // 内部会调用performMeasure()
        measureHierarchy(host, lp, res,desiredWindowWidth, desiredWindowHeight);

        // 2. 执行layout流程
        performLayout(lp, mWidth, mHeight);

        // 3. 执行draw流程
        performDraw();
    }
  • 从上面的performTraversals()可知:View的绘制流程从顶级View(DecorView)ViewGroup开始,一层一层从ViewGroup至子View遍历测绘

即:自上而下遍历、由父视图到子视图、每一个 ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身

  • 绘制的流程 = measure过程、layout过程、draw过程,具体如下:

1.2.3Measure 过程

  • 作用
    测量View的宽 / 高
  1. 在某些情况下,需要多次测量(measure)才能确定View最终的宽/高;
  2. 该情况下,measure过程后得到的宽 / 高可能不准确;
  3. 此处建议:在layout过程中onLayout()去获取最终的宽 / 高
  • 具体流程
img

单一view的getDefaultSize()

单一View的measure过程对onMeasure()有统一的实现(如下代码),但为什么ViewGroup的measure过程没有呢?

原因是:onMeasure()方法的作用是测量View的宽/高值,而不同的ViewGroup(如LinearLayout、RelativeLayout、自定义ViewGroup子类等)具备不同的布局特性,这导致它们的子View测量方法各有不同,所以onMeasure()的实现也会有所不同。

因此,ViewGroup无法对onMeasure()作统一实现。这个也是单一View的measure过程与ViewGroup的measure过程最大的不同。

复写onMeasure()

针对Measure流程,自定义ViewGroup的关键在于:根据需求复写onMeasure(),从而实现子View的测量逻辑。复写onMeasure()的步骤主要分为三步:

  1. 遍历所有子View及测量:measureChildren()
  2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值:需自定义实现
  3. 存储测量后View宽/高的值:setMeasuredDimension()

具体如下所示。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  

      //仅展示关键代码
      ...

      // 步骤1:遍历所有子View & 测量 -> 分析1
      measureChildren(widthMeasureSpec, heightMeasureSpec);

      // 步骤2:合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值
       void measureCarson{
           ... // 需自定义实现
       }

      // 步骤3:存储测量后View宽/高的值
      setMeasuredDimension(widthMeasure,  heightMeasure);  
      // 类似单一View的过程,此处不作过多描述
}

LinearLayout的onMeasure()实现如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    // 根据不同的布局属性进行不同的计算
    // 此处只选垂直方向的测量过程,即measureVertical() ->分析1
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

/**
  * 分析1:measureVertical()
  * 作用:测量LinearLayout垂直方向的测量尺寸
  */ 
  void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
      
      // 获取垂直方向上的子View个数
      final int count = getVirtualChildCount();

      // 遍历子View获取其高度,并记录下子View中最高的高度数值
      for (int i = 0; i < count; ++i) {
          final View child = getVirtualChildAt(i);

          // 子View不可见,直接跳过该View的measure过程,getChildrenSkipCount()返回值恒为0
          // 注:若view的可见属性设置为VIEW.INVISIBLE,还是会计算该view大小
          if (child.getVisibility() == View.GONE) {
             i += getChildrenSkipCount(child, i);
             continue;
          }

          // 记录子View是否有weight属性设置,用于后面判断是否需要二次measure
          totalWeight += lp.weight;

          if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {

            // 如果LinearLayout的specMode为EXACTLY且子View设置了weight属性,在这里会跳过子View的measure过程
            // 同时标记skippedMeasure属性为true,后面会根据该属性决定是否进行第二次measure
            // 若LinearLayout的子View设置了weight,会进行两次measure计算,比较耗时
            // 这就是为什么LinearLayout的子View需要使用weight属性时候,最好替换成RelativeLayout布局
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            skippedMeasure = true;

          } else {
              
              int oldHeight = Integer.MIN_VALUE;

              // 步骤1:该方法内部最终会调用measureChildren(),从而 遍历所有子View & 测量
              measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec,totalWeight == 0 ? mTotalLength : 0);
              
              ...
            }

        // 步骤2:合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值(需自定义实现)
        final int childHeight = child.getMeasuredHeight();
        // 1. mTotalLength用于存储LinearLayout在竖直方向的高度
        final int totalLength = mTotalLength;
        // 2. 每测量一个子View的高度, mTotalLength就会增加
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
               lp.bottomMargin + getNextLocationOffset(child));
        // 3. 记录LinearLayout占用的总高度
        // 即除了子View的高度,还有本身的padding属性值
        mTotalLength += mPaddingTop + mPaddingBottom;
        int heightSize = mTotalLength;

        // 步骤3:存储测量后View宽/高的值
        setMeasureDimension(resolveSizeAndState(maxWidth,width))
      
        ...
  }

1.2.3Layout过程

  • 作用
    计算视图(View)的位置

即计算View的四个顶点位置:LeftTopRightBottom

  • 具体流程

单一view不需要重写onLayout,ViewGroup必须重写onLayout,接下来看一下LinearLayout的onLayout实现:

/**
  * 源码分析:LinearLayout复写的onLayout()
  * 注:复写的逻辑 和 LinearLayout measure过程的 onMeasure()类似
  */ 
  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {

      // 根据自身方向属性,而选择不同的处理方式
      if (mOrientation == VERTICAL) {
          layoutVertical(l, t, r, b);
      } else {
          layoutHorizontal(l, t, r, b);
      }
  }
      // 由于垂直/水平方向类似,所以此处仅分析垂直方向(Vertical)的处理过程 ->分析1

/**
  * 分析1:layoutVertical(l, t, r, b)
  */
  void layoutVertical(int left, int top, int right, int bottom) {
       
      // 子View的数量
      final int count = getVirtualChildCount();

      // 1. 遍历子View
      for (int i = 0; i < count; i++) {
          final View child = getVirtualChildAt(i);
          if (child == null) {
              childTop += measureNullChild(i);
          } else if (child.getVisibility() != GONE) {

              // 2. 计算子View的测量宽 / 高值
              final int childWidth = child.getMeasuredWidth();
              final int childHeight = child.getMeasuredHeight();

              // 3. 确定自身子View的位置
              // 即:递归调用子View的setChildFrame(),实际上是调用了子View的layout() ->分析2
              setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                      childWidth, childHeight);

              // childTop逐渐增大,即后面的子元素会被放置在靠下的位置
              // 这符合垂直方向的LinearLayout的特性
              childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
              i += getChildrenSkipCount(child, i);
          }
       }
    }

/**
  * 分析2:setChildFrame()
  */
  private void setChildFrame( View child, int left, int top, int width, int height){
        
    child.layout(left, top, left ++ width, top + height);
    // setChildFrame()仅仅只是调用了子View的layout()而已
    // 在子View的layout()又通过调用setFrame()确定View的四个顶点
    // 即确定了子View的位置
    // 如此不断循环确定所有子View的位置,最终确定ViewGroup的位置

  }

细节问题:getWidth() 与 getMeasuredWidth() 获取的宽 (高)有什么区别?

getWidth() / getHeight():获得View最终的宽 / 高

getMeasuredWidth() / getMeasuredHeight():获得 View测量的宽 / 高

// 获得View测量的宽 / 高
  public final int getMeasuredWidth() {  
      return mMeasuredWidth & MEASURED_SIZE_MASK;  
      // measure过程中返回的mMeasuredWidth
  }  

  public final int getMeasuredHeight() {  
      return mMeasuredHeight & MEASURED_SIZE_MASK;  
      // measure过程中返回的mMeasuredHeight
  }  


// 获得View最终的宽 / 高
  public final int getWidth() {  
      return mRight - mLeft;  
      // View最终的宽 = 子View的右边界 - 子view的左边界。
  }  

  public final int getHeight() {  
      return mBottom - mTop;  
     // View最终的高 = 子View的下边界 - 子view的上边界。
  }  

上面标红:一般情况下,二者获取的宽 / 高是相等的。那么,“非一般”情况是什么?

答:人为设置:通过重写Viewlayout()强行设置

@Override
public void layout( int l , int t, int r , int b){
  
   // 改变传入的顶点位置参数
   super.layout(l,t,r+100,b+100);

   // 如此一来,在任何情况下,getWidth() / getHeight()获得的宽/高 总比 getMeasuredWidth() / getMeasuredHeight()获取的宽/高大100px
   // 即:View的最终宽/高 总比 测量宽/高 大100px

}

虽然这样的人为设置无实际意义,但证明了View的最终宽 / 高 与 测量宽 / 高是可以不一样

1.2.4Draw过程

  • 作用
    绘制View视图
  • 具体流程

1.3自定义View的步骤

步骤1:实现Measure、Layout、Draw流程

  • 从View的工作流程(measure过程、layout过程、draw过程)来看,若要实现自定义View,根据自定义View的种类不同(单一View / ViewGroup),需自定义实现不同的方法
  • 主要是:onMeasure()onLayout()onDraw(),具体如下
image.png

步骤2:自定义属性

  1. 在values目录下创建自定义属性的xml文件
  2. 在自定义View的构造方法中加载自定义XML文件 & 解析属性值
  3. 在布局文件中使用自定义属性

使用注意点

支持特殊属性

  • 支持wrap_content
    如果不在onMeasure()中对wrap_content作特殊处理,那么wrap_content属性将失效

具体原因请看文章:为什么你的自定义View wrap_content不起作用?

  • 支持padding & margin
    如果不支持,那么paddingmargin(ViewGroup情况)的属性将失效
  1. 对于继承View的控件,padding是在draw()中处理
  2. 对于继承ViewGroup的控件,padding和margin会直接影响measure和layout过程

多线程应直接使用post方式

View的内部本身提供了post系列的方法,完全可以替代Handler的作用,使用起来更加方便、直接。

避免内存泄露

主要针对View中含有线程或动画的情况:当View退出或不可见时,记得及时停止该View包含的线程和动画,否则会造成内存泄露问题

启动或停止线程/ 动画的方式:

  1. 启动线程/ 动画:使用view.onAttachedToWindow(),因为该方法调用的时机是当包含View的Activity启动的时刻
  2. 停止线程/ 动画:使用view.onDetachedFromWindow(),因为该方法调用的时机是当包含View的Activity退出或当前View被remove的时刻

处理好滑动冲突

当View带有滑动嵌套情况时,必须要处理好滑动冲突,否则会严重影响View的显示效果。

参考:https://www.jianshu.com/p/146e5cec4863

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容