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

第四章 view的工作原理

  • 目标:
  1. 掌握view的底层工作原理,如measure,layout,draw。
  2. 通过继承view, viewGroup或现有的系统控件来实现自定义view。
  • 再次需要注意的是viewGroup继承自view, 所以view中的方法viewGroup中都有.

ViewRoot与DecorView

  1. DecorView是根view。在ActivityTread中, 当Activity被创建后,会将DecorView添加到window中,
	View decor = r.window.getDecorView();
	wm.addView(decor, l);
  1. 同时创建ViewRootImpl对象(是ViewRoot的实现)并与DecorView建立联系。 ViewRootImpl对象在WindowManagerGlobal中被创建.
	root = new ViewRootImpl(view.getContext(), display);//这里的view就是decorview
	root.setView(view, wparams, panelParentView);
  1. View的三大流程均是通过ViewRootImpl来完成。
  2. performTraversals的工作流程图performTraversals的工作流程图

MeasureSpec

  1. 是一个32位的int值,高2位代表SpecMode, 低30位代表SpecSize.
  2. 是一种类似于测量规格的东西, 对view进行测量时需要用到.
  3. 子view的测量需要父view的MeasureSpec(或窗口大小)和自身的LayoutParams来共同确定.
  4. SpecMode有三种类型UNSPECIFIED, EXACTLY, AT_MOST

view的工作过程

从DecorView开始分析

  1. DecorView是窗口大小和自身LayoutParams两者共同确定自己的MeasureSpec. 此处是通过getRootMeasureSpec来获取返回值.
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
  1. 调用performMeasure方法执行measure函数.
 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);//在view中measure方法为final,不可更改
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
 }
  1. measure方法中会调用onMeasure方法
  2. 所以DecorView(继承于FrameLayout)中重写了onMeasure. 同时FrameLayout(继承于viewGroup, viewGroup继承于view)自己也重写了onMeasure方法. 之后DecorView将处理结果(自身宽高的精确值)通过super.onMeasure传递到FrameLayout中处理.
  3. FrameLayout中的onMeasure方法会对子view进行遍历同时调用measureChildWithMargins. measureChildWithMargins又会调用view.measure, measure又会调用onMeasure. 直到最后只剩下view(不是viewGroup)时, 在onMeasure中调用setMeasuredDimension设置单独view的宽高.
  4. 当每个字view宽高都计算完时, 加上子view各自的margin, 父view的padding后, viewGroup就能得出自身的大小.
  5. 在第5条中的measureChildWithMargins, 调用了getChildMeasureSpec方法. 这个方法是普通view利用父MeasureSpec和自身lp来创建自己MeasureSpec的规则.
	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);//父容器对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 = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
  1. 以上总结的表格
    普通view的MeasureSpec的创建过程
  2. 根据规则可知, 当自定义view的属性为wrap时, SpecSize都是parentSize(即父view的剩余空间大小).所以此时在自定义view中, 要重写onMeasure方法, 在SpecMode模式为AT_MOST时设定一个默认的内部宽高.

view工作过程的总结

  1. ViewRootImpl中调用performTraversals -> performMeasure
  2. performMeasure -> view.measure -> onMeasure(decorView重写了view的onMeasure) -> super.OnMeasure(FrameLayout中). 因为decorView继承自FrameLayout.
  3. 到了FrameLayout后, 遍历子view并调用measureChildWithMargins, measureChildWithMargins -> view.measure.
  4. decorView的工作结束(其MeasureSpec是由窗口和自身lp决定), 此时view.measure -> onMeasure
  5. 系统对后面的处理是分为单独的view还是viewGroup来考虑
  6. 如果是view, onMeasure -> setMeasuredDimension 从而确定好这个view的宽高.
  7. 如果是viewGroup, 它没有提供一致的onMeasure方法. 因为不同的viewGroup其特性不同导致工作的逻辑不同.所以像在上面提到的FrameLayout或是LinearLayout等继承了viewGroup都是根据自己的特性重写了OnMeasure(可以查看相关的源码得到验证).
  8. 在viewGroup中提供了便捷的方法 measureChildren(参数是父MeasureSpec) -> measureChild(参数是父MeasureSpec, 此方法中通过getChildMeasureSpec得到子MeasureSpec) -> child.measure(参数是子MeasureSpec), 这样形成循环, 一直到只有单个view的时候. 此时是6 中的情况.
  9. 所以当自定义viewGroup时可以在重写的onMeasure中分析好逻辑, 然后灵活调用measureChildren方法. (在LinearLayout中的measureChildrenBeforeLayout方法实际上就是调用了measureChildren)
  10. 不懂view测量过程看这里

获取view宽高

  1. 获取view的测量宽高: getMeasuredWidth, getMeasuredHeight
  2. 获取view的最终宽高: getWidth, getHeight
  3. 测量宽高和最终宽高基本上相同, 但在极端情况下系统需要多次测量才能确定最终宽高. 所以比较好的做法是在onLayout方法中去获取view的测量宽高或最终宽高
  4. 如果想要在Activity已启动的时候获取某个view的宽高, 但由于Activity的生命周期与view的measure过程不是同步执行的, 所以不能直接调用1,2 中的方法.
    解决方法:
    1.在onWindowFocusChanged中使用
    2.在view.post(runnable)的run方法中使用
    3.手动使用view.measure.(P182)
    4.使用ViewTreeObserver
	ViewTreeObserver observer = view.getViewTreeObserver();
	observer.addGlobalLayoutListener(new OnGlobalLayoutListener(){
		public void onGlobalLayout(){
			view.getViewTreeObserver().removeGlobalLayoutListener(this);
			//这里使用
		}
	})

layout过程

  1. performTraversals 中 经过了performMeasure 到了 performLayout
  2. performLayout -> layout -> setFrame(设置自身位置) -> onLayout(确定所有子view的位置)
  3. view和viewGroup都没有实现onLayout方法
  4. 所以自定义的时候需要自己实现onLayout方法
  5. onLayout -> child.layout

draw过程

  • draw 在源码中做了如下几步
  1. 绘制背景background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)
  • setWillNotDraw(Boolean )
  1. 如果一个view不需要绘制任何内容,那么设置这个标志位为true, 系统会进行相应的优化
  2. 默认情况view没有启用, viewGroup启用了
  3. 在继承viewGroup的自定义控件需要通过onDraw来绘制内容时, 我们需要显示的关闭

自定义view

  • 自定义view
  1. 继承view重写onDraw方法
    1.重写onDraw方法,并且需要自己支持wrap_content(设置默认宽高setMeasuredDimension), padding.
  2. 继承ViewGroup派生自己的layout
    1.需要合理处理ViewGroup的 测量, 布局过程. 并同时处理子元素的测量和布局过程.
  3. 继承特定的View(TextView之类)
    1.较为容易实现, 不需要自己支持wrap_content, padding.
  4. 继承特定的ViewGroup(LinearLayout之类)
    1.比2中的方法方便一些, 一般2中能实现的效果, 4都能实现. 但2更接近底层. 此方法不需要自己处理ViewGroup的测量和布局过程
  • 注意事项
  1. view中如果有线程或动画, 需要及时停止, 否则会造成内存泄漏
  2. Activity中的onAttachedToWindowonDetachedFromWindow分别是开启和关闭的好地方.
  3. 处理好滑动嵌套中的滑动冲突问题
  • 提供自定义属性
  1. values目录下创建自定义属性的xml文件, 命名最好以attrs_开头
	<resource>
		<declare-styleable name="xxx"> <--!自定义属性集合名称-->
			<attr name="circle_color" format="color"/> <--!格式(format)是相应的类型,可以查看相应的文档-->
		</declare-styleable>
	</resource>
  1. 在自定义的view构造方法中(有三种, 是其中attributeSet那种)
	TypeArray a = context.obtainStyledAttributes(自定义xml名, R.styleable.xxx)
	my = a.getColor(styleable.xxx_circle_color, 默认值)
	my.recycle();//实现资源
  1. 添加schemas声明(xmlns:app=http://schemas.android.com/apk/res-auto), app是前缀, 可以修改
  • 继承ViewGroup实现自己的Layout
    主要看onMeasureonLayout方法, 不规范的地方有
  1. 没有处理自己的padding和子元素的margin(在onMeasureonLayout中都要去处理)
  2. 没有子元素时不应该直接把宽高设置为0, 而应该根据LayoutParams中的宽高来做相应的处理

手机上看不全下面代码HorizontalScrollViewEx

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;

public class HorizontalScrollViewEx extends ViewGroup {
    private static final String TAG = "HorizontalScrollViewEx";

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;

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

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

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

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

    public HorizontalScrollViewEx(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
        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;
        }
        default:
            break;
        }

        Log.d(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) {
                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;
            smoothScrollBy(dx, 0);
            mVelocityTracker.clear();
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 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);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        }
    }

    @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();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    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();
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值