Android开发艺术探索 - 第4章 View的工作原理

46 篇文章 0 订阅
39 篇文章 0 订阅
1.ViewRoot和DecorView

ViewRoot对应ViewRootImpl,实现了DecorView和WindowManager之间的交互。
View的绘制流程从ViewRoot#performTraversals开始,经过measure、layout、draw最终将一个View绘制出来:

例,measur过程:performMeasure->measure->onMeasure->子View的measure。
measure决定了View的宽高,measure完成后可以通过getMeasureWidth/getMeasureHeight获取测量后的宽高;layout决定了View四个顶点的坐标和实际View的宽高,可调用getLeft/getTop/getRight/getBottom/getWidth/getHeight获取对应属性;draw决定了View的显示,draw完成后View的内容才显示在屏幕上。
Activity中通过setContentView设置的view位于DecorView的content部分,可以通过android.R.id.content索引到该View的父容器,然后通过getChildAt(0)定位到该View:

2.MeasureSpec
  1. 用于parent向child传递layout要求。真正传递的实际上是一个32位int存储,高2位代表mode,低30位代表size,MeasureSpec只是一个工具类,帮助拼装和拆解这个int。
    mode:
    • UNSPECIFIED
      parent不对child强加任何限制。child想要多大就多大。
    • EXACTLY
      parent已经决定了child准确的大小,child要依据这个大小。
    • AT_MOST
      child可以想多大就多大,但是被指定了一个上限。
  2. 与LayoutParams的关系
    对于一个View,可以设置LayoutParams来指定宽高,系统会综合该LayoutParams和parent施加的MeasureSpec,得出最后应用于该View的MeasureSpec;而对于DecorView,因为其没有parent,所以取而代之的是Window的size,结合自己的LayoutParams得出最后的MeasureSpec。MeasureSpec一旦确定,onMeasure中就可以确定View的宽高。
    • DecorView的MeasureSpec计算过程:
      在ViewRootImpl的measureHierarchy中,计算了DecorView的MeasureSpec。desiredWindow*为window的size:
      childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
      childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
      performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
      
      getRootMeasureSpec中根据window size和DecorView的LayoutParams计算出MeasureSpec。规则很简单,如果是MATCH_PARENT或者固定的值,则spec mode为EXACTLY,同时size设置为相应的值;如果是WRAP_CONTENT,则spec mode为AT_MOST,size为window size:
      private static int getRootMeasureSpec(int windowSize, int rootDimension) {
          int measureSpec;
          switch (rootDimension) {
              case ViewGroup.LayoutParams.MATCH_PARENT:
                  // Window can't resize. Force root view to be windowSize.
                  measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                  break;
              case ViewGroup.LayoutParams.WRAP_CONTENT:
                  // Window can resize. Set max size for root view.
                  measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                  break;
              default:
                  // Window wants to be an exact size. Force root view to be that size.
                  measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                  break;
          }
          return measureSpec;
      }
      
    • 普通View的MeasureSpec计算过程:
      以ViewGroup的measureChildWithMargins为例,在该方法中会计算child的MeasureSpec。计算完成后,会直接对该view进行measure。计算时也会考虑parent的padding,child的margin:
      protected void measureChildWithMargins(View child,
              int parentWidthMeasureSpec, int widthUsed,
              int parentHeightMeasureSpec, int heightUsed) {
          final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
      
          final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                  mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                          + widthUsed, lp.width);
          final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                  mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                          + heightUsed, lp.height);
      
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
      
      具体的计算过程在getChildMeasureSpec中进行:
      • child指定确定的size,则遵从child的这个size设置。
      • child指定match_parent,如果parent表示可以exactly,则其size为parent size;如果parent表示atmost,即其size也不确定,则其atmost为parent size。
      • child指定wrap_content,则此时size由child自己决定,所以只限制其atmost为parent size。
3.View的渲染流程

measure确定测量宽高->layout确定最终宽高和四个顶点位置->draw绘制到屏幕上

  1. measure
    • View的measure
      View的measure过程由其measure方法执行,其中会调用onMeasure,具体的计算在onMeasure中进行。measure用final修饰,所以只有onMeasure可以而且必须被子类复写。onMeasure的默认实现,是通过setMeasuredDimension(onMeasure中一定要调用该方法设置measure出的值)设置测量值为view默认的size:
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                  getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
      }
      
      getSuggestedMinimumWidth中会根据android:minWidth和是否设置了background得出minwidth。getSuggestedMinimumHeight原理一样:
      protected int getSuggestedMinimumHeight() {
          return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
      }
      protected int getSuggestedMinimumWidth() {
          return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
      }
      
      默认的size在getDefaultSize中计算。对于parent的spec mode为UNSPECIFIED的情况,最终的size即为minwidth;在AT_MOST和EXACTLY的情况下默认的size就是parent中指定的size:
      public static int getDefaultSize(int size, int measureSpec) {
          int result = size;
          int specMode = MeasureSpec.getMode(measureSpec);
          int specSize = MeasureSpec.getSize(measureSpec);
      
          switch (specMode) {
          case MeasureSpec.UNSPECIFIED:
              result = size;
              break;
          case MeasureSpec.AT_MOST:
          case MeasureSpec.EXACTLY:
              result = specSize;
              break;
          }
          return result;
      }
      
      所以,onMeasure的默认实现都会将measure size设置为parent size。对于child使用wrap_content的情况,这通常不是符合预期的设置。所以在自定义View的时候,需要重写onMeasure方法,将View的measure size设置为预期的默认值,一般是该View的默认最小值。(这也是为什么一定要重写onMeasure)
      具体的处理方式,在onMeasure中,针对AT_MOST的情况,将对应的size(width或者height)设置为默认最小值。因为在ViewGroup的getChildMeasureSpec方法中,针对child为wrap_contentchild为match_parent+parent为wrap_content这两种情况,最终的spec mode都会是AT_MOST,即针对无法由parent决定child的情况,最终都会是AT_MOST。
    • ViewGroup的measure
      ViewGroup除了完成自身的measure之外,还要遍历子View去执行其measure方法。因为ViewGroup不同的派生类具有不同布局特性,所以测量方式也不同,故没有提供默认的onMeasure方法。但是ViewGroup中提供了简单的measure其child的方法,提供给其派生类使用(在其onMeasure中调用);复杂的情况下,其派生类一般是自己实现。
      protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
          final int size = mChildrenCount;
          final View[] children = mChildren;
          for (int i = 0; i < size; ++i) {
              final View child = children[i];
              if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                  measureChild(child, widthMeasureSpec, heightMeasureSpec);
              }
          }
      }
      
      protected void measureChild(View child, int parentWidthMeasureSpec,
              int parentHeightMeasureSpec) {
          final LayoutParams lp = child.getLayoutParams();
      
          final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                  mPaddingLeft + mPaddingRight, lp.width);
          final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                  mPaddingTop + mPaddingBottom, lp.height);
      
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
      
      protected void measureChildWithMargins(View child,
              int parentWidthMeasureSpec, int widthUsed,
              int parentHeightMeasureSpec, int heightUsed) {
          ...
      }
      
    • 获取View的宽高的tips:
      • 因为系统可能多次measure之后,才能确定最终的宽高,所以measure之后的measureWidth和measureHeight可能是不准确的,这个时候就要在onLayout之后去获取宽高。
      • 在Activity的生命周期回调中无法直接获取view的宽高,因为View的渲染过程和其声明周期回调不是同步执行的,可以通过如下方法:
        • Activity/View#onWindowFocusChanged
          该方法会在Activity的窗口获得和失去焦点的时候被调用;伴随着焦点的变化,该方法会被调用多次:
          @Override
          public void onWindowFocusChanged(boolean hasFocus) {
              super.onWindowFocusChanged(hasFocus);
              
              if (hasFocus) {
                  int width = view.getMeasuredWidth();
                  int height = view.getMeasuredHeight();
              }
          }
          
        • view.post(Runnable)
          当View初始化完毕之后,Looper就开始执行各个post进去的Runnable:
          @Override
          protected void onStart() {
              super.onStart();
              
              view.post(new Runnable() {
                  @Override
                  public void run() {
                      int width = view.getMeasuredWidth();
                      int height = view.getMeasuredHeight();
                  }
              });
          }
          
        • ViewTreeObserver
          如果一个View的view tree的layout状态或者view的可见性发生了变化,onGlobalLayout就会被回调;伴随着view tree的变化,该方法会被调用多次:
          ViewTreeObserver observer = view.getViewTreeObserver();
          observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
              @Override
              public void onGlobalLayout() {
                  view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
          
                  int width = view.getMeasuredWidth();
                  int height = view.getMeasuredHeight();
              }
          });
          
        • view.measure
          手动调用view.measure去测量,然后得到宽高。对于该View的LayoutParams为match_parent的情况,无法使用该方法,因为此时parent的MeasureSpec是不确定的:
          // dp/px
          int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
          int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
          view.measure(widthMeasureSpec, heightMeasureSpec);
          
          // wrap_content
          int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
          int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 < 30) - 1, MeasureSpec.AT_MOST);
          view.measure(widthMeasureSpec, heightMeasureSpec);
          
  2. layout
    该步骤用来确定View的位置,依据就是measure之后存储的measure值。由View的layout方法处理:首先会通过setFrame设置自身的l/t/r/b(位置/宽高被确定)然后调用onLayout,在onLayout中需要遍历其所有的子View,计算其layout数据,然后调用其layout方法,直至所有View都layout完成。所以,对于ViewGroup一定要实现onLayout方法。
        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);
    
    ViewGroup和View都没有onLayout的默认实现,因为其实现也与具体的布局有关。
    以LinearLayout的layout过程为例来看。首先LinearLayout的layout的起点也是其layout方法,被parent调用之后,设置了l/t/r/b;之后调用自己的onLayout向子View发起layout。根据布局方式的不同,水平和垂直的layout也不同:
    @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);
        }
    }
    
    对于垂直的情况:childTop会随着child一个个的layout逐渐增大,其表现就是后面child会被放置在更下面;拿到child的measureWidth和measureHeight之后,调用setChildFrame将layout工作传递给该child。
    void layoutVertical(int left, int top, int right, int bottom) {
        ...
    
        final int count = getVirtualChildCount();
    
        ...
    
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
    
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
    
                ...                
    
                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }
    
                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
    
                i += getChildrenSkipCount(child, i);
            }
        }
    }
    
    setChildFrame则直接调用了child的layout方法。而这里的layout方法传递的r和b参数,对应的是l+measureWidth和t+measureHeight计算出来的。所以通常情况下,调用View的getWidth方法(返回的是r-l)和getMeasureWidth方法,其返回值是一致的,即measureWidth的值。而在一些情况下,多次进行measure会导致layout阶段与measure阶段的width不同,但总的来说两者基本上是相等的。height也是一样的情况。
    private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }
    
  3. draw
    • 流程:
      1. 绘制背景:background.draw(canvas)
      2. 绘制自己:onDraw
      3. 绘制children:dispatchDraw->child.draw
      4. 绘制装饰:onDrawScrollBars
      public void draw(Canvas canvas) {
          final int privateFlags = mPrivateFlags;
          final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                  (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
          mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
      
          /*
           * Draw traversal performs several drawing steps which must be executed
           * in the appropriate order:
           *
           *      1. Draw the background
           *      2. If necessary, save the canvas' layers to prepare for fading
           *      3. Draw view's content
           *      4. Draw children
           *      5. If necessary, draw the fading edges and restore layers
           *      6. Draw decorations (scrollbars for instance)
           */
      
          // Step 1, draw the background, if needed
          int saveCount;
      
          if (!dirtyOpaque) {
              drawBackground(canvas);
          }
      
          // skip step 2 & 5 if possible (common case)
          final int viewFlags = mViewFlags;
          boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
          boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
          if (!verticalEdges && !horizontalEdges) {
              // Step 3, draw the content
              if (!dirtyOpaque) onDraw(canvas);
      
              // Step 4, draw the children
              dispatchDraw(canvas);
      
              drawAutofilledHighlight(canvas);
      
              // Overlay is part of the content and draws beneath Foreground
              if (mOverlay != null && !mOverlay.isEmpty()) {
                  mOverlay.getOverlayView().dispatchDraw(canvas);
              }
      
              // Step 6, draw decorations (foreground, scrollbars)
              onDrawForeground(canvas);
      
              // Step 7, draw the default focus highlight
              drawDefaultFocusHighlight(canvas);
      
              if (debugDraw()) {
                  debugDrawFocus(canvas);
              }
      
              // we're done...
              return;
          }
      
    • tips
      setWillNotDraw方法:该方法用于设置表示其是否会进行draw,以便系统进行优化。ViewGroup会默认设置为true,所以如果一个自定义VIewGroup有draw的需求,要将其设置为false。
4.自定义View
  1. 分类
    • 继承View
      实现一些特殊的效果,需要重写onDraw。同时需要处理wrap_content和padding。
    • 继承ViewGroup
      实现自定义布局,实现组合的效果。需要处理自身的measure、layout,以及children的measure、layout。
    • 继承特定View
      扩展某个已有View的功能。wrap_content和padding不需要自己处理。
    • 继承特定ViewGroup
      实现组合的效果。不需要处理measure、layout。
  2. 自定义View须知
    • 处理wrap_content
      直接继承View或ViewGroup,默认的onMeasure无法正确处理wrap_content。
    • 处理padding
      直接继承View需要在draw方法中处理padding;直接继承ViewGroup需要在onMeasure和onLayout中考虑padding和margin的影响。
    • 尽量不要在View中使用Handler
      View内部提供的post系列方法可以满足需求。
    • View中的线程和动画,需要及时停止
      当View不可见时,需要及时停止,否则可能造成内存泄漏。可以根据回调方法去处理。当View被remove或者VIew所在的Activity退出时,View#onDetachedFromWindow会被调用;当View的Activity启动时,View#onAttactedToWindow会被调用。
    • VIew嵌套滚动时,处理好滚动冲突
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值