前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >View的工作原理

View的工作原理

作者头像
提莫队长
发布2019-02-21 11:17:06
4960
发布2019-02-21 11:17:06
举报
文章被收录于专栏:刘晓杰刘晓杰

1.绘制流程

View的绘制流程是从ViewRoot的PerformTraversals方法开始的。它经过measure,layout,draw三个过程将view绘制出来。mesure用来测量view的宽高,layout用来确定位置,draw绘制。流程图如下

这里写图片描述
这里写图片描述

performTraversals会依次调用performMeasure, performLayout, performDraw三个方法,这三个方法分别完成顶层View的measure,layout,draw方法,onMeasure又会调用所有子元素的measure过程,直到完成整个View树的遍历。同理,performLayout, performDraw的传递流程与performMeasure相似。唯一不同在于,performDraw的传递过程在draw方法中通过dispatchDraw实现,但没有本质区别。

Measure过程后可以调用getMeasureWidth和getMeasureHeight方法获取View测量后的宽高,与getWidth和getHeight的区别是:getMeasuredHeight()返回的是原始测量高度,与屏幕无关,getHeight()返回的是在屏幕上显示的高度。几乎在所有情况下两者相等

Layout过程确定View四个顶点的位置和实际的宽高。

Draw过程确定View的显示,只有draw方法完成后View的内容才会出现在屏幕上。

2.理解MeasureSpec

MeasureSpec代表一个32位int值,高2位表示SpecMode,低30位表示SpecSize。SpecMode表示测量模式,SpecSize表示在对应模式下的大小。

代码语言:javascript
复制
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * 没有限制。要多大有多大
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * 精确模式,对应LayoutParams中的match_parent和具体数值这两种模式
         */
        public static final int EXACTLY = 1 << MODE_SHIFT;

        /**
         * 最大模式,大小不定,但是不能超过窗口的大小。对应LayoutParams中的wrap_content
         */
        public static final int AT_MOST = 2 << MODE_SHIFT;

        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return 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) {
            return (measureSpec & ~MODE_MASK);
        }

3.MeasureSpec和LayoutParams的关系

普通View的MeasureSpec的创建规则

这里写图片描述
这里写图片描述

4.View的工作流程

(1)measure过程 要分情况,如果只是一个view,那么measure就完了。如果是ViewGroup,除了自己测量外,还会去遍历调用所有子元素的measure方法。 (1.1)view的measure过程 过程我就不详述了,我就说明一下注意点 直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。 如何解决呢

代码语言:javascript
复制
@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);
  //当mode=AT_MOST时,设置默认的宽高
  if (widthSpecMode == MeasureSpec.AT_MOST
          && heightSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(200, 200);
  } else if (widthSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(200, heightSpecSize);
  } else if (heightSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(widthSpecSize, 200);
  }
}

(2)ViewGroup的measure过程 ViewGroup是一个抽象类,并没有重写onMeasure方法,而是提供了一个measureChildren方法,会对每一个子元素进行measure。 原文以LinearLayout为例,可以看看

注意:现在你应该明白为什么onCreate,onStart,onResume里面获取view的宽高为0了吧,因为只有measure完成才能获取宽高,而onCreate,onStart,onResume的时候view还没有生成,自然取不到。当然,还是有方法获取的

代码语言:javascript
复制
(1)Activity/view:onWindowFocusChanged
这个函数的含义是:view已经初始化完毕,宽高已经准备好,获取自然没问题。这个函数会被平频繁调用.在Activity窗口得到或者失去焦点时时候被调用,即onResume和onPause都会调用这个函数
    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if(hasWindowFocus){
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }

(2)view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化了
@Override
protected void onStart() {
 super.onStart();
 view.post(new Runnable() {
     @Override
     public void run() {
         int width = view.getMeasuredWidth();
         int height = view.getMeasuredHeight();
     }
 });
}

(3)ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener接口,当View树的状态发生改变或者View树内部的View的可见性发现改变时,onGlobalLayout方法将被调用,因此是获得View宽高的好时机。需要注意的是,伴随View树的状态改变等,onGlobalLayout会被多次调用。典型代码:
@Override
protected void onStart() {
 super.onStart();
 ViewTreeObserver observer = view.getViewTreeObserver();
 observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
     @SuppressWarnings("deprecation")
     @Override
     public void onGlobalLayout() {
         view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
         int width = view.getMeasuredWidth();
         int height = view.getMeasuredHeight();
     }
 });
}

(4)view.measure(int widthMeasureSpec, int heightMeasureSpec)
手动调用View的measure方法,通过手动并正确调用View的measure过程后,就可以通过View.getMeasureWidth()方法得到测量后的宽高。这种方法比较复杂,需要根据View的LayoutParams来分:
1.match_parent
直接放弃,无法measure出具体的宽高。原因在于根据View的Measure过程,需要构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小。
2.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);
3.具体的数值(比如宽/高都是100px)
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

(2)layout过程 大致流程如下:首先通过setFrame确定四个顶点的位置,接着调用onLayout方法,确定子元素位置

(3)draw过程 遵循如下四步:

代码语言:javascript
复制
绘制背景background.draw(canvas)
绘制自己(onDraw)
绘制children(dispatchDraw)
绘制修饰(onDrawScrollBars)

view有个特殊的方法setWillNotDraw

代码语言:javascript
复制
    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

可以看出中间有个标记位,通过设置这个标记为再调用invalidate()可以自动调用onDraw函数

5.自定义view

分类 1.继承View重写onDraw方法 采用这种方法需要自己支持wrap_content,并且padding也需要自己处理 2.继承ViewGroup派生特殊的Layout 处理ViewGroup的测量、布局两个过程,子元素的测量和布局 3.继承特定的View(比如TextView) 比较简单,不需要自己支持wrap_content和padding 4.继承特定的ViewGroup(比如LinearLayout) 不需要自己处理ViewGroup的测量、布局两个过程

注意点 1.让View支持wrap_content 继承自view的控件,如果不对wrap_content处理,相当于设置了match_parent 2.如果有必要,让View支持padding 继承view的控件,如果不处理padding,那么padding属性不起作用 3.尽量不要在View中使用Handler,没必要 view有post系列方法 4.View中如果有线程或动画,需要及时停止,参考View#onDetachedFromWindow 5.View带有滑动嵌套情形时,需要处理好滑动冲突

自定义view示例

1.继承view重写onDraw 第一步,创建自定义属性的xml(attrs.xml)

代码语言:javascript
复制
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
    </declare-styleable>

</resources>

第二步:在构造函数中解析这个xml文件

代码语言:javascript
复制
    private void init(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs,
                R.styleable.CircleView);
        color = array.getColor(R.styleable.CircleView_circle_color, Color.RED);
        array.recycle();
        mPaint.setColor(color);
    }

第三步:使用自定义属性

代码语言:javascript
复制
    <com.example.androidtest.CircleView
        android:id="@+id/circleView1"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:background="#000000"
        android:padding="20dp"
        app:circle_color="#FFD700" />

注意,有wrap_content和padding 最后贴出源码:

代码语言:javascript
复制
public class CircleView extends View {
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs);
    }

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

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

    public CircleView(Context context) {
        super(context);
        init(context, null);
    }

    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private int color;

    private void init(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs,
                R.styleable.CircleView);
        color = array.getColor(R.styleable.CircleView_circle_color, Color.RED);
        array.recycle();
        mPaint.setColor(color);
    }

    /**
     * 处理wrap_content,设置默认的200宽度
     */
    @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(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

    /**
     * 在onDraw中处理padding
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int getPaddingLeft = getPaddingLeft();
        int getPaddingRight = getPaddingRight();
        int getPaddingTop = getPaddingTop();
        int getPaddingBottom = getPaddingBottom();
        int width = getWidth() - getPaddingLeft - getPaddingRight;
        int height = getHeight() - getPaddingTop - getPaddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(getPaddingLeft + width / 2, getPaddingTop + height
                / 2, radius, mPaint);
    }
}

2.继承viewgroup派生特殊的layout 我这里贴源码吧,可以看注释去理解

代码语言:javascript
复制
public class MyHorizontalScrollView extends ViewGroup {
    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;

    // 上次滑动的坐标
    private int mLastX, mLastY;
    // 上次滑动的坐标 onInterceptTouchEvent
    private int mLastXIntercept, mLastYIntercept;

    private Scroller scroller;
    private VelocityTracker mVelocityTracker;

    public MyHorizontalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        if (scroller == null) {
            scroller = new Scroller(getContext());
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.e("ACTION_DOWN", "ACTION_DOWN");
            intercepted = false;
            if (!scroller.isFinished()) {
                scroller.abortAnimation();
                intercepted = true;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            Log.e("ACTION_MOVE", "ACTION_MOVE");
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            //模板有介绍,这里写父容器需要滑动事件的条件
            //如果x方向上的距离大于y,则认为是滑动,要拦截。之后就调用onTouchEvent函数
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            Log.e("ACTION_UP", "ACTION_UP");
            intercepted = false;
            break;
        }

        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        Log.e("intercepted", "" + intercepted);

        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:
            Log.i("ACTION_DOWN", "ACTION_DOWN");
            if (!scroller.isFinished()) {
                scroller.abortAnimation();
            }
            break;
        case MotionEvent.ACTION_MOVE:
            Log.i("ACTION_MOVE", "ACTION_MOVE");
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            scrollBy(-deltaX, 0);
            break;
        case MotionEvent.ACTION_UP:
            Log.i("ACTION_UP", "ACTION_UP");
            int scrollX = getScrollX();
            mVelocityTracker.computeCurrentVelocity(1000);
            float xVelocity = mVelocityTracker.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, mChildrenSize - 1));
            int dx = mChildIndex * mChildWidth - scrollX;
            Log.e("dx", "" + dx);
            smoothScrollBy(dx, 0);
            mVelocityTracker.clear();
            break;
        }

        mLastX = x;
        mLastY = y;

        return true;
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.i("onMeasure", "onMeasure");
        Log.i("width", "" + getWidth());
        Log.i("MeasuredWidth", "" + getMeasuredWidth());

        int measureWidth = 0;
        int measureHeight = 0;
        int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

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

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.i("onLayout", "onLayout");
        Log.e("width", "" + getWidth());

        int childLeft = 0;
        int childCount = getChildCount();
        mChildrenSize = childCount;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            int childWidth = childView.getMeasuredWidth();
            mChildWidth = childWidth;
            childView.layout(childLeft, 0, childLeft + childWidth,
                    childView.getMeasuredHeight());
            childLeft += childWidth;
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

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

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

参考资料http://www.jianshu.com/p/3b3335223425

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016年08月25日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.绘制流程
  • 2.理解MeasureSpec
  • 3.MeasureSpec和LayoutParams的关系
  • 4.View的工作流程
  • 5.自定义view
  • 自定义view示例
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档