Android 视图 View (一)

1,066 阅读7分钟

View 的生命周期

了解生命周期在我们自定义View的时候发挥着很大的作用

  • 在Activity启动时获取View的宽高,但是在onCreate、onStart和onResume均无法获取正确的结果。这是因为在Activity的这些方法里,Viewed绘制可能还没有完成,我们可以在View的生命周期方法里获取,如onSizeChanged()。
  • 在Activity生命周期发生变化时,View也要做响应的处理,典型的有VideoView保存进度和恢复进度。
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
    super.onVisibilityChanged(changedView, visibility);
    //TODO do something if activity lifecycle changed if necessary
    //Activity onResume()
    if(visibility == VISIBLE){
        
    }
    //Activity onPause()
    else {
        
    }
}

@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
    super.onWindowFocusChanged(hasWindowFocus);

    //TODO do something if activity lifecycle changed if necessary
    //Activity onResume()
    if (hasWindowFocus) {
    }
    //Activity onPause()
    else {
    }
}
  • 释放线程、资源
@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    //TODO release resources, thread, animation
}

View 的测量流程

view大小

View的大小有两对值来表示,getMeasuredWidth()/getMeasureHeight(),这组值表示了他在父View里期望的大小,在measure()方法完成后可获得

View内边距

View内边距:View的内边距用padding来表示,它表示View的内容距离View边缘的距离。通过getPaddingXXX()方法获取。需要注意的是我们在自定义View的时候需要单独处理 padding,否则它不会生效.

View外边距

View外边距:View的外边距用margin来表示,它表示View的边缘离它相邻的View的距离。

在做测量时,measure() 方法会被父View调用,在measure()中做一些准备和优化工作后,调用onMeasure()来进行实际的自我测量。对于onMeasure(),View和ViewGroup有所区别:

  • View:View 在 onMeasure() 中会计算出自己的尺寸然后保存;
  • ViewGroup:ViewGroup在onMeasure()中会调用所有子View的measure()让它们进行自我测量,并根据子View计算出的期望尺寸来计算出它们的实际尺寸和位置然后保存。同时,它也会 根据子View的尺寸和位置来计算出自己的尺寸然后保存.

View measure过程 ViewGroup 首先通过getChildMeasureSpec() 获取child的MeasureSpec(childWidthMeasureSpec, childHeightMeasureSpec)

  • getChildMeasureSpec(ViewGroup的spec,ViewGroup padding,view 宽高)
   最大size = 父view大小-padding > 0? ..:0
父view mode = exactly
   子 viewsize 为具体>0数值,则子viewsize 确定 , spec_mode = exactly
   子 View 为 match_parent,则子view大小 = 最大size,spec_mode = exactly
   子 View 为 wrap_content,则子view大小 = 最大size, spec_mode = AT_MOST
父view mode = AT_MOST
   子 viewsize 为具体>0数值,则子viewsize 确定 , spec_mode = exactly
   子 View 为 match_parent,则子view大小 = 最大size,spec_mode = AT_MOST
   子 View 为 wrap_content,则子view大小 = 最大size, spec_mode = AT_MOST
父view mode = UNSPECIFIED
   子 viewsize 为具体>0数值,则子viewsize 确定 , spec_mode = exactly
   子 View 为 match_parent,则子view大小 = 0,spec_mode = UNSPECIFIED
   子 View 为 wrap_content,则子view大小 = 0, spec_mode = UNSPECIFIED
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    
     public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
    
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
    
            switch (specMode) {
            // Parent has imposed an exact size on us
            case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent has imposed a maximum size on us
            case MeasureSpec.AT_MOST:
                if (childDimension >= 0) {
                    // Child wants a specific size... so be it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size, but our size is not fixed.
                    // Constrain child to not be bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size. It can't be
                    // bigger than us.
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
    
            // Parent asked to see how big we want to be
            case MeasureSpec.UNSPECIFIED:
                if (childDimension >= 0) {
                    // Child wants a specific size... let him have it
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size... find out how big it should
                    // be
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // Child wants to determine its own size.... find out how
                    // big it should be
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
            }
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
        
}

childWidthMeasureSpec,childHeightMeasureSpec的测量参数由个ViewGroup实现类自己实现或者直接集成ViewGroup的测量方法如measureChild 和 measureChildWithMargins指定的测量参数

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) {
        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);
    }

View 调用子View的measure(childWidthMeasureSpec,childHeightMeasureSpec),实际的测量工作是由View的onMeasure()来完成的。我们来看看 onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法的实现

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    
       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
           setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                   getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
       }

       //设置View宽高的测量值
       protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
       
       //measureSpec指的是View测量后的大小
       public static int getDefaultSize(int size, int measureSpec) {
           int result = size;
           int specMode = MeasureSpec.getMode(measureSpec);
           int specSize =  MeasureSpec.getSize(measureSpec);
   
           switch (specMode) {
           //MeasureSpec.UNSPECIFIED一般用来系统的内部测量流程
           case MeasureSpec.UNSPECIFIED:
               result = size;
               break;
           //我们主要关注着两种情况,它们返回的是View测量后的大小
           case MeasureSpec.AT_MOST:
           case MeasureSpec.EXACTLY:
               result = specSize;
               break;
           }
           return result;
       }
       
       //如果View没有设置背景,那么返回android:minWidth这个属性的值,这个值可以为0
       //如果View设置了背景,那么返回android:minWidth和背景最小宽度两者中的最大值。
       protected int getSuggestedMinimumHeight() {
           int suggestedMinHeight = mMinHeight;
   
           if (mBGDrawable != null) {
               final int bgMinHeight = mBGDrawable.getMinimumHeight();
               if (suggestedMinHeight < bgMinHeight) {
                   suggestedMinHeight = bgMinHeight;
               }
           }
   
           return suggestedMinHeight;
       }
}

如果我们直接继承View来自定义View时,需要重写onMeasure()方法,并设置wrap_content时的大小。
通过上面的描述我们知道,当LayoutParams为wrap_content时,SpecMode为AT_MOST,而在 关于getDefaultSize(int size, int measureSpec) 方法需要说明一下,通过上面的描述我们知道getDefaultSize()方法中AT_MOST与EXACTLY模式下,返回的 都是specSize,这个specSize是父View当前可以使用的大小,如果不处理,那wrap_content就相当于match_parent。

@Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      Log.d(TAG, "widthMeasureSpec = " + widthMeasureSpec + " heightMeasureSpec = " + heightMeasureSpec);

      //指定一组默认宽高,至于具体的值是多少,这就要看你希望在wrap_cotent模式下
      //控件的大小应该设置多大了
      int mWidth = 200;
      int mHeight = 200;

      int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
      int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

      int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
      int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

      if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
          setMeasuredDimension(mWidth, mHeight);
      } else if (widthSpecMode == MeasureSpec.AT_MOST) {
          setMeasuredDimension(mWidth, heightSpecSize);
      } else if (heightSpecMode == MeasureSpec.AT_MOST) {
          setMeasuredDimension(widthSpecSize, mHeight);
      }
  }

们再来看看ViewGroup的measure过程。ViewGroup继承于View,是一个抽象类,它并没有重写onMeasure()方法,因为不同布局类型的测量 流程各不相同,因此onMeasure()方法由它的子类来实现。

以下是FrameLayout的onMeasure()实现,顶层的DecorVeiw也是集成了FrameLayout

protected void onMeasurre(in widthMeasureSpec, int height MeasureSpec){
    final int cont = getChildCount()
    int maxHeight = 0;
    int maxWidth = 0;
    
    for( int i=0; i<count; i++){
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
                   measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                   maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
                   maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
               }
    }
    
     // Account for padding too
   maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight;
   maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom;

   // Check against our minimum height and width
   maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());//min_height属性或者background的minheight属性的最大值
   maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
   
   final Drawable drawable = getForeground();
   if (drawable != null) {
       maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
       maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
   }

   setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),
           resolveSize(maxHeight, heightMeasureSpec));
}
public static int resolveSize(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: //Framelayout 宽高为wrap_content 或者match_parent,specSize为父View的大小,所以不能取size
       result = Math.min(size, specSize);
       break;
   case MeasureSpec.EXACTLY:
       result = specSize;
       break;
   }
   return result;
}
  1. 调用measureChildWithMargins()去测量每一个子View的大小,找到最大高度和宽度保存在maxWidth/maxHeigth中。
  2. 将上一步计算的maxWidth/maxHeigth加上padding值,mPaddingLeft,mPaddingRight,mPaddingTop ,mPaddingBottom表示当前内容区域的左右上下四条边分别到当前视图的左右上下四条边的距离, mForegroundPaddingLeft ,mForegroundPaddingRight,mForegroundPaddingTop ,mForegroundPaddingBottom表示当前视图的各个子视图所围成的区域的左右上下四条边到当前视图前景区域的 左右上下四条边的距离,经过计算获得最终宽高。
  3. 当前视图是否设置有最小宽度和高度。如果设置有的话,并且它们比前面计算得到的宽度maxWidth和高度maxHeight还要大,那么就将它们作为当前视图的宽度和高度值。
  4. 当前视图是否设置有前景图。如果设置有的话,并且它们比前面计算得到的宽度maxWidth和高度maxHeight还要大,那么就将它们作为当前视图的宽度和高度值。
  5. 经过以上的计算,就得到了正确的宽高,先调用resolveSize()方法,获取MeasureSpec,接着调用父类的setMeasuredDimension()方法将它们作为当前视图的大小。