Android开发艺术探索——第四章View的工作原理

Android开发艺术探索——第四章View的工作原理

4.1

(一)初识ViewToot和DecorView

基本概念
  ViewRoot对应于ViewRootImpl类,是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完成后,会将DecorView添加到View中。同时,会创建ViewRootImpl对象,并将ViewTootImpl对象和DecorView建立关联。

源码如下:
root = new ViewRootImpl(view,getContext(),dispaly);
root.setView(view,panelParentView);

  View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的位置,而draw则负责将View绘制在屏幕上。

  performMeasure ——>  measur  ——> onMeasure      
                        ↓
  preformLayout  ——>  layout  ——> onLayout
                        ↓
  performDraw    ——>   draw   ——> onDraw

4.2

(二)理解MeasureSpec

  MeasureSpec翻译为”测量规格”。MeasureSpec在很大程度上决定一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,父容器影响View的MeasureSpec。在测量过程中,系统会将View的LayoutParams根据这个measureSpec来测量出View的宽/高,不一定等于View的最终宽/高。

4.2.1MeasureSpec

  MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode是指测量模式,SpecSize是指某种测量模式下的规格大小。

  部分源码:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public stataic int makeMeasureSpec(int size,int mode){
    if( sUseBrokenMakeMeasureSpec){
          retrurn size + mode ;
    }else{
         return (size & ~Mode_MASK)|(mode & MODE_MASK)
    }
}
public static int getMode(int measureSpec){
    return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec){
    rerurn (measureSpec & ~MODE_MASK);
}

  MeaureSpec通过将SpecMode个SpecSize打包打包一个int值避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode和SpecSize可以打包一个MeasureSpec,而一个MeasureSpec可以通过解包的形式来得出原始的SpecMode和SpecSize,需要注意的是这里提到的MeasureSpec是指MeaureSpec所代表的int值,而非MeasureSpec本身。

SpecMode

  • UNSPECIFIED
      父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。

  • EXACTLY
      父容器已经检测出View所要的精确的大小,这个时候View的最终大小就是SpecSize所指定的值。对应于LayoutParams中的match_parent和具体的数值这两种模式。

  • AT_MOST
      父容器指定了一个可用大小的SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应LayoutParams中的warp_content。

4.2.2MeasureSpec和LayoutParams的对应关系

  系统内部是通过MeasureSpec来进行View的测量,但正常情况下使用View指定MeasureSpec,可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后在根据这个MeasureSpec来确定View测量后的宽/高。 注意: MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽/高。
  另外,对于顶级的View也就是DecorView普通View来说,MeasureSpec的转换过程略有不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定;对于普通View,其MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。
  DecorView在ViewRootImpl中的measureHierarchy方法有如下一段代码,展示了DecorView的MeasureSpec的创建过程,其中desiredWindowWidth和desiredWindowHeight是屏幕尺寸:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height);
performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);

  getRootMeasureSpec方法的实现:

private static int getRootMeasureSpec(int windowSize,int rootDimension){
    int measureSpec;
    switch(rootDimension){
        case ViewGroup.LayoutParams.MATCH_PARENT:
             measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
        break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
             measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.AT_MOST);
        break;
        default:
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension,MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec ;
}

DecorView的MeasureSpec的产生过程就很明确了,具体遵守如下规则,根据它的LayoutParams中的宽/高的参数划分。

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小
  • LayoutParams.WRAP_CONTENT:最大模式,大小为LayoutParams中的指定的大小。
  • 固定大小:精确模式,大小LayoutParams中指定的大小。

4.3

(三)View的工作原理

  View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽/高,layout确定View的最高宽/高和四个顶点的位置,而draw则将View绘制到屏幕上。

4.3.1Measure过程

  measure过程要分情况来看,如果只是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去请用所有子元素的measure方法,各个子元素
再递归去执行这个过程。

1. View的measure过程

   View的measure过程由measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会调用View的onMeasure方法。

  View的onMeasure方法:
protected void onMeasure(int widthMeasureSpec , int heightMeasureSpec){
    setMearsureDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
}

View的最终的大小是在Layout阶段确定。

4.3View的工作原理一遍没理解多看几遍。

4.3.3draw过程

  Draw过程就是将View绘制到屏幕上。

* (1)绘制背景backgroud。draw(canvas)*

* (2)绘制自己(onDraw)*

* (3)绘制children(dispatchDraw)*

* (4)绘制装饰(onDrawScrollBars)*
  

4.4

(三)自定义View

4.4.1 自定义View的分类

  
根据个人理解分为了四类:

* 1. 继承View重写onDraw*
  这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,及重写onDraw方法。采用这种方法需要自己支持wrap_content,并且padding也需要自己处理。

* 2. 继承ViewGroup派生特殊的Layout*
  这种方法主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统的布局之外,重新定义一中新布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些。需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。

* 3. 继承特定的View(比如TextView)*
  一般是用于扩展某种已有的View功能,比如TextView,这种方法比较容易实现。这种方法不需要自己支持wrap_content和padding。

* 4. 继承特定的ViewGroup(比如LinearLayout)*
  当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和方法2的区别,一般来说方法2能实现的效果方法4都能实现,两者主要的差别在于方法2更接近底层。

4.4.2自定义View须知

* 1.让View支持wrap_content*
  因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,当外界在布局中使用wrap_content时就无法达到预期的效果。

* 2.如果有必要,让你的View支持padding*
  因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性无法起作用的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响不然将导致padding和子元素的margin失效。

* 3.尽量不要在View在中使用Handler,没必要*
  View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你明确地要使用Handler来发送消息。

* 4.View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWinow*
  如果线程或者动画需要停止时,onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,此方法对应的是onAttachedToWindow,当包含此View的Activity启动时,View的onAttachedToWindow方法会被调用。同时,当View变得不可见时,我们也需要停止线程和动画,如果不及时处理这种问题,可能会造成内存泄露。

* 5.View带有滑动嵌套情形时,需要处理好滑动冲突*

4.4.3自定义View实例

1.继承View重写onDraw方法
package view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

import com.superdaxue.customviewdemo.R;

/** * Created by ZX_CC on 2016/4/26. */

public class CircleView extends View {
    //WRAP_CONTENT 下的默认高度
    private int mWidth = 100;
    private int mHeight = 100;
    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    public CircleView(Context context) {
        super(context);
        init();
    }
    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
        a.recycle();
        init();
    }
    private void init() {
        mPaint.setColor(mColor);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        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 && heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,mHeight);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth,heightSpecSize);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,mHeight);
        }
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft  = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop   = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width,height)/2;
        canvas.drawCircle(paddingLeft+width/2,paddingTop +height/2,radius,mPaint);
    }
}
2.继承ViewGroup派生特殊的Layout
package view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/** * Created by ZX_CC on 2016/4/26. */

public class HorizontalScrollViewEx extends ViewGroup {
    private  static final String TAG = "HorizontalScrollViewEx";
    private int mChildrenSize ;
    private int mChildrenWidth ;
    private int mChildrenIndex ;
    private Scroller mScroller ;
    private VelocityTracker mVelocityTracker ;

    //分别记录上次滑动坐标
    private int mLastX = 0;
    private int mLastY = 0;
    //分别记录上次滑动的坐标(在onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    public HorizontalScrollViewEx(Context context) {
        super(context);
        init();
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }



    public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        if (mScroller == null){
            mScroller = new Scroller(getContext());
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                if ( !mScroller.isFinished() ){
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (Math.abs(deltaX) > Math.abs(deltaY)){
                    intercepted = true;
                }else{
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        Log.e(TAG,"--intercepted = " + intercepted);
        mLastX = x;
        mLastY = y;

        mLastXIntercept = x ;
        mLastYIntercept = y ;
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if ( !mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy( - deltaX ,0);
                break;
            case MotionEvent.ACTION_UP :
                int scrollX = getScrollX();
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) >= 50){
                    mChildrenIndex = xVelocity > 0 ? mChildrenIndex -1 : mChildrenIndex + 1;
                }else {
                    mChildrenIndex  = (scrollX + mChildrenWidth/2) /mChildrenWidth;
                }
                mChildrenIndex = Math.max(0,Math.min(mChildrenIndex,mChildrenIndex-1));
                int dx = mChildrenIndex * mChildrenWidth - scrollX ;
                smoothScrollBy(dx,0);
                mVelocityTracker.clear();
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = 0;
        int measureHeight = 0;
        final int childCount = getChildCount();

        measureChildren(widthMeasureSpec,heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        if (childCount == 0){
            setMeasuredDimension(0,0);
        }else if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            final View childView = getChildAt(0);
            measureWidth = childView.getMeasuredWidth() * childCount;
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measureWidth,measureHeight);
        }else if (heightSpecMode == MeasureSpec.AT_MOST){
            final View childView = getChildAt(0);
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize,measureHeight);
        }else if (widthSpecMode == MeasureSpec.AT_MOST){
            final View childView = getChildAt(0);
            measureWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measureWidth,heightSpaceSize);
        }
    }

// @Override
// protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
// final int childCount = getChildCount();
//
// }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
              int childLeft = 0;
              final int childCount = getChildCount();
              mChildrenSize = childCount;

              for (int i = 0; i < childCount; i ++){
                  final View childView = getChildAt(i);
                  if (childView.getVisibility() != View.GONE){
                      final int childWidth = childView.getMeasuredWidth();
                      mChildrenWidth = childWidth;
                      //MarginLayoutParams lm = (MarginLayoutParams) childView.getLayoutParams();
                     // int m = min(lm.leftMargin,lm.rightMargin,lm.topMargin,lm.bottomMargin);
                      childView.layout(childLeft,0,childLeft + childWidth  ,childView.getMeasuredHeight());
                      childLeft += childWidth;
                  }
              }
    }

    private int min(int...params){
        int min  = params[0];
        for (int p : params){
            if (p < min){
                min = p;
            }
        }
        return min;
    }

    private void smoothScrollBy(int dx, int dy){
        mScroller.startScroll(getScrollX(),0,dx,0,500);
        invalidate();
    }
    @Override
    public void computeScroll(){
        if (mScroller.computeScrollOffset()){
              scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
              postInvalidate();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

你可能感兴趣的:(android)