专栏首页刘晓杰View的工作原理

View的工作原理

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表示在对应模式下的大小。

        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。 如何解决呢

@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还没有生成,自然取不到。当然,还是有方法获取的

(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过程 遵循如下四步:

绘制背景background.draw(canvas)
绘制自己(onDraw)
绘制children(dispatchDraw)
绘制修饰(onDrawScrollBars)

view有个特殊的方法setWillNotDraw

    /**
     * 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)

<?xml version="1.0" encoding="utf-8"?>
<resources>

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

</resources>

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

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

第三步:使用自定义属性

    <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 最后贴出源码:

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 我这里贴源码吧,可以看注释去理解

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • View的事件体系

    2.MotionEvent 手指触摸屏幕后的一系列事件,包括ACTION_DOWN,ACTION_MOVE,ACTION_UP

    提莫队长
  • 16(套接字)

    套接字描述符在Unix系统中是用文件描述符实现的。事实上,许多处理文件描述符函数(read和write)都可以处理文件描述符 要创建一个套接字,可以调用so...

    提莫队长
  • 嵌套滑动机制详解

    提莫队长
  • CodeForces #549 Div.2 C Queen

    ShenduCC
  • 洛谷P4383 [八省联考2018]林克卡特树lct(DP凸优化/wqs二分)

    小L 最近沉迷于塞尔达传说:荒野之息(The Legend of Zelda: Breath of The Wild)无法自拔,他尤其喜欢游戏中的迷你挑战。

    attack
  • LeetCode 593. 有效的正方形(数学)

    Michael阿明
  • 南京网络预选赛 The Preliminary Contest for ICPC Asia Nanjing 2019 H. Holy Grail 多源最短路

    用户2965768
  • 2038:[2009国家集训队]小Z的袜子(hose)

    用户2965768
  • HDU4609 3-idiots(生成函数)

    但是如果直接算合法的方案的话会出现一点问题。我们在算的时候维护了一个后缀和表示乘起来大于等于这个数的方案。我们要求的方案需要满足i < j < k,但是这样计算...

    attack
  • HDU5036 Explosion(期望 bitset)

    首先根据期望的线性性,可以转化为求每个点的期望打开次数,又因为每个点最多会被打开一次,只要算每个点被打开的概率就行了

    attack

扫码关注云+社区

领取腾讯云代金券