美文网首页
View的工作原理

View的工作原理

作者: 要学的东西太多了 | 来源:发表于2018-09-26 17:39 被阅读0次
  • ViewRoot对应于ViewRootImpl类,是连接Windowmanager和DecorView的纽带,View的三大流程(measure-测量View的宽高,通过getMeasureWidth和getMeasureHeight方法能拿到宽高,一般来说等于最终宽高、layout-确定View的位置即四个顶点的坐标和View的最终宽高、draw-决定View在屏幕上的显示)均由ViewRoot完成。

  • MeasureSpec用来测量View的宽高,它是一个32位int值,高两位表示测量模式(specMode),低30表示这个模式下的规格大小(specSize),由父容器和LayoutParams共同决定。规则如下图: View创建Measure值的规则.png
  • specMode分三类:无限制(UNSPECIFIED),要多大给多大;精确大小(EXACTLY),对应`LayoutParams的match_parent和具体数值;最大值(ATMOST),不超过这个范围,对应LayoutParams的wrap_parent。

  • 直接继承View的自定义控件,要在OnMeasure方法中对wrap_content做特殊处理,如下所示:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int defautWidth=20,defaultHeight=20;//wrap_content下设置的默认值
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(defautWidth,defaultHeight);
        }else if(widthMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(defautWidth,heightSize);
        }else if(heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize,defaultHeight);
        }else{
            super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        }
    }
  • ViewGroup是一个抽象类,自定义 Viewgroup要重写OnMeasure方法,同时提供了一个measureChildren的方法,在里面去循环遍历调用子元素的measure方法。

  • 系统可能会多次measure才能确定宽高,要拿到View的宽高最好在onlayout方法里取。

  • 在Activity的创建过程中去拿View的宽高都是0,因为Activity创建过程和View的测量过程不是同步的,要拿到宽高可以用下面几种方案:

(1)onWindowFoucsChanged,不过要注意,这个方法会被频繁调用,典型代码如下:
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            int width = view.getMeasuredWidth();
            int heigt = view.getMeasuredHeight();
        }
    }

(2)view.post(runnable)。典型代码如下:
    @Override
    protected void onStart() {
        super.onStart();
        view.post(new Runnable(){
            @Override
            public void run() {
                int width = view.getMeasuredWidth();
                int heigt = view.getMeasuredHeight();
            }
        });
    }

(3)ViewTreeObserver。典型代码如下:
    @Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = view.getMeasuredWidth();
                int heigt = view.getMeasuredHeight();
            }
        });
    }

(4)view.measure(int widthMeasureSpec , int heightMeasureSpec)。根据view的LayoutParams来处理,具体情况如下:
      1.match_parent,直接放弃,无法拿到父容器的剩余空间。
      2.具体数值,如下:
            int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
            int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); 
            view.measure(widthMeasureSpec,heightMeasureSpec);
      3.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);
  • 这几种方案里面,post是最常用的,GlobalLayoutListener的方式在布局发生变化的时候都会调用,在里面尽量不要生成大量对象,且一定记得remove。此外,post还可以切换到主线程。post为什么能拿到宽高并切换线程呢,具体看下它的实现:
public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }
        getRunQueue().post(action);
        return true;
    }

下面分情况分析:

  • 如果attachInfo不等于空,就直接调用它的handler发送消息,不等于空就放入HandlerActionQueue的mActions里面等待调用。HandlerActionQueue有一个executeActions方法,会把队列里面的消息全部发送出去。executeActions是在什么时候调用呢,view的dispatchAttachedToWindow,这个方法是在ViewRootImpl的performTraversals调用。在performTraversals里面,executeActions之后才会调用performMeasure、performLayout和performDraw,那么为什么post能拿到宽高呢,因为executeActions只是把消息放到handler的消息队列里面,performTraversals本来就是一个消息的处理,执行完才会去执行下一个消息,所以post的消息被处理的时候,已经测量好了。

  • 那么attachInfo 是在什么时候初始化的呢?答案也在dispatchAttachedToWindow里面,dispatchAttachedToWindow又是在哪里调用的呢?viewParent的实现类ViewRootImpl的performTraversals里面调用,并把mAttachInfo传进去,这个mAttachInfo是在ViewRootImpl的构造方法里面初始化的,它持有的mHandler是一个常量,由于ViewRootImpl是在主线程里面初始化的,所以handler处理消息也是在主线程,那么post就能做到线程切换。

  • 其实viewTreeObserver.addOnGlobalLayoutListener也在performTraversals有调用,都在performMeasure和performLayout之后,所以也能拿到宽高。如下:

mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
  • 自定义View主要分为四种方式:继承View重写onDraw方法;继承ViewGroup派生特殊的Layout;继承特定的View(如TextView);继承特定的ViewGroup(如LinearLayout)。

  • 自定义View的注意事项:
    (1)让View支持wrap_content。如果不在onMeasure中对wrap_content做处理,那么wrap_content是不起作用的,相当于match_parent。因为view默认的onMeasure方法如下:

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

getDefaultSize方法就是根据mode返回不同的size值,可以看出AT_MOST和EXACTLY都是返回的父容器可提供的最大值,UNSPECIFIED一般用于系统测量,这里返回的是传入的getSuggestedMinimumWidth,如下:

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

getSuggestedMinimumWidth里面返回的判断逻辑如下:

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

mMinWidth /mMinHeight ,即xml文件中的minWidth和minHeight,mBackground.getMinimumWidth()表示背景图片的实际大小,例如shape这一类背景是没有实际大小的,bitmap则有。
(2)如果有必要,支持padding。继承View的控件不在draw中处理padding的话,padding不起作用;继承ViewGroup的控件在onMeasure和onLayout中要考虑padding和子元素的margin带来的影响。
(3)尽量不要在View中使用handler,因为View本身提供了post系列方法。
(4)View中有线程和动画要及时停止,在onDetachedFromWindow方法中处理,否则可能内存泄漏。
(5)合理处理滑动冲突。

示例:
1.继承View:

public class CircleView extends View{
    private int defalultColor = Color.parseColor("#000000");
    private int circleColor = defalultColor;
    private float defalultRidus = 5.0f;
    private float ridus = defalultRidus;
    private Paint paint;
    private Context context;
    public CircleView(Context context) {
        this(context,null);
    }

    public CircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.context=context;
        init(attrs);
    }

    private void init(AttributeSet attrs){
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        if(attrs!=null){
            TypedArray typedArray = context.getResources().obtainAttributes(attrs,R.styleable.CircleView);
            circleColor = typedArray.getColor(R.styleable.CircleView_circleColor, defalultColor);
            ridus = typedArray.getFloat(R.styleable.CircleView_ridus,defalultRidus);
            typedArray.recycle();
        }
        paint.setColor(circleColor);
        ridus = DisPlayUtils.dp2px(context,ridus);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int defautWidth=200,defaultHeight=200;//wrap_content下设置的默认值
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(defautWidth,defaultHeight);
        }else if(widthMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(defautWidth,heightSize);
        }else if(heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize,defaultHeight);
        }else{
            super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int width = getWidth()-getPaddingLeft()-getPaddingRight();
        int height = getHeight()-getPaddingBottom()-getPaddingTop();
        canvas.drawCircle(getPaddingLeft()+width/2,getPaddingTop()+height/2,ridus,paint);
    }
}

2.继承ViewGroup:

public class HorizontalScrollView extends ViewGroup{
    private Context context;
    private Scroller scroller;
    private VelocityTracker velocityTracker;//记录滑动速度
    private int lastX=0,lastY=0;//记录上次滑动的坐标
    private int lastInterceptX=0,lastInterceptY=0;//记录上次intercept中的坐标
    private int mChildIndex=0;
    private int mChildWidth=0;
    private int mChildSize = 0;
    public HorizontalScrollView(Context context) {
        this(context,null);
    }

    public HorizontalScrollView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context=context;
        init();
    }

    private void init(){
        scroller = new Scroller(context);
        velocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercept =false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                isIntercept = false;
                if(!scroller.isFinished()){
                    scroller.abortAnimation();
                    isIntercept=true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int delX = x-lastInterceptX;
                int delY = y-lastInterceptY;
                if(Math.abs(delX)>Math.abs(delY)){//横向移动的距离大于纵向的就拦截自己处理
                    isIntercept = true;
                }else{
                    isIntercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                isIntercept=false;
                break;
        }
        lastX = x;
        lastY = y;
        lastInterceptX = x;
        lastInterceptY = y;
        return isIntercept;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        velocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if(!scroller.isFinished()){
                    scroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int delX = x-lastX;
                scrollBy(-delX,0);
                break;
            case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                velocityTracker.computeCurrentVelocity(1000);
                float xVelocity = velocityTracker.getXVelocity();
                if(Math.abs(xVelocity)>50){
                    mChildIndex = xVelocity>0 ? mChildIndex-1 : mChildIndex+1;
                }else{
                    mChildIndex = (scrollX+mChildWidth/2)/mChildWidth;
                }
                mChildIndex = Math.max(0,Math.min(mChildIndex,mChildSize-1));
                int dx = getTotalViewWidth() - scrollX;
                smoothScrollBy(dx,0);
                velocityTracker.clear();
                break;
        }
        lastX = x;
        lastY = y;
        return true;
    }

    private int getTotalViewWidth(){
        int total = 0;
        for(int i=0;i<mChildIndex;i++){
            View view = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
            total+=view.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
        }
        total += getPaddingLeft() + getPaddingRight();
        return total;
    }

    private void smoothScrollBy(int dx,int dy){
        scroller.startScroll(getScrollX(),0,dx,0,500);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if(scroller.computeScrollOffset()){
            scrollTo(scroller.getCurrX(),scroller.getCurrY());
            postInvalidate();
        }
    }

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

        int widthSpedMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpedMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        if(childCount==0){
            if(widthSpedMode == MeasureSpec.AT_MOST && heightSpedMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(paddingLeft+paddingRight,paddingTop+paddingBottom);
            }else if(widthSpedMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(paddingLeft+paddingRight,heightSpecSize );
            }else if(heightSpedMode == MeasureSpec.AT_MOST){
                setMeasuredDimension(widthSpecSize ,paddingTop+paddingBottom);
            }else{
                setMeasuredDimension(widthSpecSize ,heightSpecSize );
            }
        }else{
            if(widthSpedMode == MeasureSpec.AT_MOST){
                for(int i=0;i<childCount;i++) {
                    View view = getChildAt(i);
                    MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
                    measureWidth+=view.getMeasuredWidth()+lp.leftMargin+lp.rightMargin;
                }
                measureWidth+=paddingLeft+paddingRight;
            }else{
                measureWidth = widthSpecSize;
            }
            if(heightSpedMode == MeasureSpec.AT_MOST){
                for(int i=0;i<childCount;i++) {
                    View view = getChildAt(i);
                    MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
                    measureHeight = Math.max(measureHeight,view.getMeasuredHeight()+lp.topMargin+lp.bottomMargin);
                }
                measureHeight+=paddingBottom+paddingTop;
            }else{
                measureHeight = heightSpecSize;
            }
            setMeasuredDimension(measureWidth,measureHeight);
        }
    }

    @Override
    protected void onLayout(boolean isChanged, int left, int top, int right, int bottom) {
        int mLeft = getPaddingLeft();
        int childCount = getChildCount();
        mChildSize = childCount;
        for(int n=0;n<childCount;n++){
            View childView = getChildAt(n);
            mChildWidth = childView.getMeasuredWidth();
            MarginLayoutParams lp =(MarginLayoutParams) childView.getLayoutParams();
            if(childView.getVisibility() != GONE){
                int childWidth = childView.getMeasuredWidth();
                int mTop = getPaddingTop()+lp.topMargin;
                mLeft+=lp.leftMargin;
                childView.layout(mLeft,mTop,mLeft+childWidth,mTop+childView.getMeasuredHeight());
                mLeft+=lp.rightMargin+childWidth;
            }
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {//要支持margin必须重写这个方法
        return new MarginLayoutParams(context,attrs);
    }

    @Override
    protected void onDetachedFromWindow() {//释放资源,关闭线程等
        velocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

相关文章

  • View 的测量

    接着上篇 View 基础 来讲 View 的工作原理,View 的工作原理中最重要的就是测量、布局、绘制三大过程,...

  • 【Android】自定义ViewGroup

    关于View的工作原理、绘制流程等,在第4章 View的工作原理[https://www.jianshu.com/...

  • View 工作原理

    1、 ViewRoot 和 DecorView 介绍 ViewRoot 对应于 ViewRootImpl 类,它...

  • View工作原理

    参考书籍:Android开发艺术探索注:京东链接https://item.jd.com/11760209.html...

  • View工作原理

    View工作原理 首先先来说明一下要掌握的知识 View绘制工作整体流程 Measure Layout Draw ...

  • View工作原理

    1、起步分析 在Activity启动分析中 知道,Activity的创建是在ActivityThread.perf...

  • View工作原理

  • View工作原理

    view有三大工作流程:测量、布局、绘制,分别对应着方法mesure、layout、draw ViewRoot和D...

  • View工作原理

    ViewRoot对应ViewRootImpl类,它是连接WindowManager和DecorView的纽带,Vi...

  • View的工作原理

    ViewRoot对应于ViewRootImpl类,是连接Windowmanager和DecorView的纽带,Vi...

网友评论

      本文标题:View的工作原理

      本文链接:https://www.haomeiwen.com/subject/mrrjoftx.html