安卓开发之View工作原理

前面已经介绍了View的事件分发以及处理机制,这次来学习下View的工作流程,学习之前要先了解一下View坐标系的相关知识。

1. View位置参数

我们知道安卓中坐标系是以屏幕的左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向。View坐标系也是这样,内部关系如下图所示:
在这里插入图片描述
其中getLeft()getTop()getRight()getBottom()分别对应View的四个属性:left、top、right、bottom,分别代表相对于View父容器的左上角的横坐标(left)、左上角的纵坐标(top)、右下角的横坐标(right)、右下角的纵坐标(bottom)。可以看出View的宽度width = getRight() - getLeft(),高度height = getBottom() - getTop(),安卓中还可以直接通过getWidth()和getHeight()方法用来获取View的宽度和高度。getX()、getY()、getRawX()、getRawY()分别代表触摸点相对于父容器以及屏幕而言的横坐标和纵坐标。

从安卓3.0开始,View增加了额外的几个参数:x,y,translationX、translationY。其中x和y是View左上角的坐标,translationX和translationY是View左上角相对于父容器的偏移量,这几个参数也是相对父容器的坐标,并且translationX和translationY的默认值为0。换算关系如下:

  • x = left + translationX y = top + translationY

View 在平移的过程中,top 和left 表示的是原始左上角的位置信息,其值在绘制完毕后就不会再改变,此时发生改变的是x、y、translationX和translationY 这四个参数。具体关系见下图(图来自要点提炼|开发艺术之View):
在这里插入图片描述

2. View工作流程

在学习之前,首先对View的整体工作流程有个大概的了解,View的绘制基本由measure、layout、draw这三个步骤完成。

  • measure:测量View的宽高
  • layout:计算当前View以及子View的位置即确定View的最终宽高和四个顶点的位置
  • draw:将View 绘制到屏幕上

安卓开发之事件分发机制中我们提到过Android中通过在Activity中(具体来说在onCreate生命周期方法中)使用setContentView()方法来设置一个布局,在调用该方法后,ActivityManagerService会回调onResume()方法, 此时系统才会把整个DecorView 添加到PhoneWindow中,并让其显示出来,从而最终完成界面的绘制,布局内容就真正显示出来。而在DecorView被添加到Window的过程中,WindowManager起到了关键性的作用,最后交给ViewRootImpl做详细处理。

在之前的文章关于Hook相关知识的学习一中我们学习了Activity的启动过程,最后提到attachApplicationLocked(app)最终通过调用scheduleLaunchActivity方法创建启动Activity,当时没有继续跟踪scheduleLaunchActivity方法,其实在ApplicationThread的scheduleLaunchActivity()方法内内,会发送一个"LAUNCH_ACTIVITY"消息, mH (H对象,H继承自Handler,mH用来发送和处理ApplicationThread通过binder接受的AMS请求)处理"LAUNCH_ACTIVITY"时会调用handleLaunchActivity(), 而handleLaunchActivity()会分两步, 第一步调performLaunchActivity(),创建Activity的对象,依次调用它的onCreate(), onStart();第二步调handleResumeActivity(), 调用Activity对象的onResume()。那么结合前面说的在onCreate()方法中调用setContentView()将布局添加到PhoneWindow的内部类DecorView类之后,AMS会回调onResume()方法, 将DecorView 添加到PhoneWindow中,这里的回调onResume()方法,实际上还得从handleResumeActivity()进行分析。

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
       ActivityClientRecord r = mActivities.get(token);
       ...
       //在这里执行performResumeActivity的方法中会执行Activity的onResume()方法
        r = performResumeActivity(token, clearHide, reason);
       ...
       if (r.window == null && !a.mFinished && willBeVisible) {
          //获得当前Activity的PhoneWindow对象
          r.window = r.activity.getWindow();
          //获得当前phoneWindow内部类DecorView对象
          View decor = r.window.getDecorView();
          //设置窗口顶层视图DecorView可见度
          decor.setVisibility(View.INVISIBLE);
          //获取ViewManager对象,这里getWindowManager()实质上获取的是ViewManager的子类对象WindowManager
          ViewManager wm = a.getWindowManager();
          ...
          //获取ViewRootImpl对象
          ViewRootImpl impl = decor.getViewRootImpl();
           ...
          }
          if (a.mVisibleFromClient) {
              if (!a.mWindowAdded) {
                   //标记根布局DecorView已经添加到窗口
                   a.mWindowAdded = true;
                   //在这里WindowManager将DecorView添加到PhoneWindow中
                   wm.addView(decor, l);
                   } 
                   ...
          }
          ...
    }

handleResumeActivity代码中,重要的在wm.addView(decor, l)这块,这里就是将DecorView添加到PhoneWindow中,那我们继续跟踪addView方法:

 public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow);
 }

继续跟踪WindowManagerGlobal类的实例方法addView:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        View panelParentView = null;
        ...
        //获得ViewRootImpl对象root
         root = new ViewRootImpl(view.getContext(), display);
        ...
        // do this last because it fires off messages to start doing things
        try {
            //将传进来的参数DecorView设置到root中
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
          ...
        }
 }

addView中获得ViewRootImpl对象root,并调用了ViewRootImpl的setView方法,继续跟踪setView:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
            //将顶层视图DecorView赋值给全局的mView
                mView = view;
            ...
            //标记已添加DecorView
             mAdded = true;
            ...
            //请求布局
            requestLayout();
            ...   
        }
 }

继续跟踪requestLayout()方法:

public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
}

跟踪scheduleTraversals方法:

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
        }
    }

...

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

...

 void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);

            try {
                performTraversals();
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
    }
...

可以看到,ViewRootImpl的setView方法最终调用了performTraversals方法,该方法中重要的代码为:

private void performTraversals() {
        //mView就是DecorView根布局
        final View host = mView;
        if (host == null || !mAdded)
            return;
        //是否正在遍历
        mIsInTraversal = true;
        //是否马上绘制View
        mWillDrawSoon = true;
         ...
        //顶层视图DecorView所需要窗口的宽度和高度
        int desiredWindowWidth;
        int desiredWindowHeight;
         ...
        //在构造方法中mFirst已经设置为true,表示是否是第一次绘制DecorView
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            //如果窗口的类型是有状态栏的,那么顶层视图DecorView所需要窗口的宽度和高度就是除了状态栏
            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {
                //否则顶层视图DecorView所需要窗口的宽度和高度就是整个屏幕的宽高
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }
    }
  ...
  //获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.width和lp.height表示DecorView根布局宽和高
 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

  //执行测量操作
 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  ...
  //执行布局操作
 performLayout(lp, desiredWindowWidth, desiredWindowHeight);
  ...
  //执行绘制操作
 performDraw();
}

可以看到View的具体绘制就在performTraversals()方法中展开了,通过依次调用performMeasure()、performLayout()和performDraw()三个方法分别完成顶级View的measure、layout、draw三大流程。大致流程图如下图所示:
在这里插入图片描述
由上图可以看到在performMeasure()中又会调用measure()方法,在measure()方法中又会调用onMeasure()方法,在onMeasure()方法中则会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父容器的measure过程,如此反复完成整个View树的遍历。performLayout()、performDraw()的传递流程和performMeasure()是类似的。

接下来我们接着从源码角度对这个流程图进行分析,首先来看View的一个重要内部类MeasureSpec。

2.1. MeasureSpec

MeasureSpec是View的一个重要内部类,它参与了View的measure测量过程,在很大程度上决定了一个View的尺寸规格。我们就从performMeasure()方法入手看下MeasureSpec的作用:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
}

可以看到performMeasure方法中传入了childWidthMeasureSpec、childHeightMeasureSpec两个int类型的值,和前面说的一样,performMeasure中又调用了measure方法:

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            //根据原有宽高计算获取不同模式下的具体宽高值
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        ...
        if (forceLayout || needsLayout) {
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                //在该方法中子控件完成具体的测量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                ...
            } 
         ...
    }

可以看到,widthMeasureSpec, heightMeasureSpec是MeasureSpec根据原有宽高计算获取不同模式下的具体宽高值。那么我们跟进看下MeasureSpec类:

public static class MeasureSpec {
        //int类型占4个字节,其中高2位表示尺寸测量模式,低30位表示具体的宽高信息
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        ...
        //如下所示是MeasureSpec中的三种模式:UNSPECIFIED、EXACTLY、AT_MOST                  
        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 static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
       ...
        //获取尺寸模式
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }
        
        //获取宽高信息
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

        //将控件的尺寸模式、宽高信息进行拆解查看,并对不同模式下的宽高信息进行不同的处理
        static int adjust(int measureSpec, int delta) {
            final int mode = getMode(measureSpec);
            int size = getSize(measureSpec);
            if (mode == UNSPECIFIED) {
                // No need to adjust size for UNSPECIFIED mode.
                return makeMeasureSpec(size, UNSPECIFIED);
            }
            size += delta;
            if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
            }
            return makeMeasureSpec(size, mode);
        }

        public static String toString(int measureSpec) {
            int mode = getMode(measureSpec);
            int size = getSize(measureSpec);

            StringBuilder sb = new StringBuilder("MeasureSpec: ");

            if (mode == UNSPECIFIED)
                sb.append("UNSPECIFIED ");
            else if (mode == EXACTLY)
                sb.append("EXACTLY ");
            else if (mode == AT_MOST)
                sb.append("AT_MOST ");
            else
                sb.append(mode).append(" ");

            sb.append(size);
            return sb.toString();
        }
    }

可以看到MeasureSpec的值保存在一个int值(4个字节)当中。其中4字节的前两位表示模式mode,后30位表示大小size。即MeasureSpec = mode + size,在MeasureSpec当中一共存在三种mode:UNSPECIFIED、EXACTLY 和AT_MOST

  • UNSPECIFIED:无限制,View对尺寸没有任何限制,View设置为多大就应当为多大,一般系统内部使用
  • EXACTLY:精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size所指定的值,对应LayoutParams中的match_parent
  • AT_MOST:最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值,对应LayoutParams中的wrap_content

对于每一个View,MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同影响。而对顶级View来说,其MeasureSpec受窗口尺寸和自身的LayoutParams影响,MeasureSpec确定下来后,在下面要说的onMeasure方法中就可以确定View的测量宽高。

2.2. measure过程

2.2.1 View的measure

上面已经介绍过performMeasure方法调用了measure方法,这里就接着measure方法分析,该方法最后调用了onMeasure()方法完成子View的具体测量:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

可以看到onMeasure方法中涉及到以下三种方法:

  • setMeasuredDimension:用来设置View的宽度、高度
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
  • getDefaultSize:用来获取View测量后的宽高,View的最终大小在layout步骤确定,但是基本相等
 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()/getSuggestedMinimumHeight():当View没有设置背景时,默认大小就是mMinWidth,这个值对应Android:minWidth属性,如果没有设置时默认为0;如果有设置背景,则默认大小为mMinWidth和mBackground.getMinimumWidth()当中的较大值。
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

View的measure完成以后,通过getMeasuredWidth/Height方法就可以正确地获取到View的测量宽/高。需要注意的是,在某些极端情况
下,系统可能需要多次measure才能确定最终的测量宽/高,在这种情形下,最好在onLayout方法中去获取View的最终宽/高。

2.2.2 ViewGroup的measure

ViewGroup的测量过程与View有区别,因为不同的布局测量方式也都不同,除了要完成自己的measure过程外还需要遍历调用所有子元素的measure方法,各个子元素再去递归执行这个过程,因此没有对View的measure方法以及onMeasure方法进行重写。但是它提供了measureChildren()以及measureChild()方法帮助我们对子View进行测量。

 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);//分别对单个view进行测量
            }
        }
    }

measureChildren方法又最终调用了measureChild方法,对单个view进行测量:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

measureChild方法中会取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec 直接传递给View的measure方法来进行测量。getChildMeasureSpec 方法如下:

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

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        //当父View要求一个精确值时,为子View赋值
        case MeasureSpec.EXACTLY:
            //如果子view有自己的尺寸,则使用自己的尺寸
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
                //当子View是match_parent,将父View的大小赋值给子View
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
                //如果子View是wrap_content,设置子View的最大尺寸为父View
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父布局给子View了一个最大界限
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //如果子view有自己的尺寸,则使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // 父View的尺寸为子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //父View的尺寸为子View的最大尺寸
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父布局对子View没有做任何限制
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
            //如果子view有自己的尺寸,则使用自己的尺寸
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
    
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

和前面说的一样,子元素的MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同影响:

  • 父容器的MeasureSpec为UNSPECIFIED模式:父容器没有做出限制,如果子View有大小(自身的LayoutParams)则使用,如果没有则为0
  • 父容器的MeasureSpec为EXACTLY模式:父容器采用精准模式有确切的大小,如果子View有大小则直接使用(自身的LayoutParams),如果子View没有大小,那么子View不得超出父view的大小范围
  • 父容器的MeasureSpec为AT_MOST模式:父容器采用最大模式存在确切的大小,如果子View有大小则直接使用(自身LayoutParams),如果子View没有大小,子View不得超出父view的大小范围

综上所述,普通View的MeasureSpec的创建规则如下表所示:
在这里插入图片描述
了解完ViewGroup的measure过程后,我们返回到View measure过程中的getDefaultSize方法,从该方法中我们可以看到View 的宽/高由specSize决定,所以直接继承View的自定义控件需要重写onMeasure方法并设置wrap_ content时的自身大小,否则在布局中使用wrap_ content就相当于使用match_ parent。 因为如果自定义View在布局中使用wrap_ content,那么它的那么它的specMode是AT_ MOST模式,结合上表可知这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View 的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用match_ parent 完全一致。

2.2.3 View的Measure过程中遇到的问题

例如我们想在Activity已启动的时候执行一个任务, 但是这一件任务需要获取某个View的宽/高。但是由于View的measure过程和Activity 的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了。如果View还没有测量完毕,那么获得的宽/高都是0。解决这个问题大致有四种办法:

  • 1.Activity/View的onWindowsChanged()方法
    onWindowFocusChanged()方法表示View已经初始化完毕了,宽/高已经准备好,这个时候去获取宽/高是没问题的。
    onWindowFocusChanged()方法会被调用多次,具体来说,当Activity继续执行和暂停执行时,onWindowFocusChanged()均会被调用,如果频繁地进行onResume和onPause,那么onWindowFocusChanged()也会被频繁地调用。代码如下:
    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也已经初始化好了。代码如下:
	protected void onStart() {  
	    super.onStart();  
	    view.post(new Runnable() {  
	        @Override  
	        public void run() {  
	            int width=view.getMeasuredWidth();  
	            int height=view.getMeasuredHeight();  
	        }  
	    });  
	}  
  • 3.ViewTreeObsever
    使用 ViewTreeObserver 的众多回调方法可以完成这个功能,比如使用onGlobalLayoutListener 这个接口,当 View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout 方法将被回调,这是获取View宽/高的好时机。伴随着View树的变化,这个方法也会被多次调用。
	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 height=view.getMeasuredHeight();  
	        }  
	    });  
	} 
  • 4.view.measure(int widthMeasureSpec, int heightMeasureSpec)
    通过手动对View进行measure来得到View的宽/高。这种方法比较复杂,要分情况处理,根据View自身的LayoutParams来分:
    (1)如果是match_parent的话,那么无法measure出具体的宽/高;
    (2)如果是具体的数值(dp/px),比如宽/高都是100px,可通过如下方法:
	int widthMeasureSpec = MeasureSpec.makeMeasureSpec (100, MeasureSpec. EXACTLY);
	int heightMeasureSpec = MeasureSpec.makeMeasureSpec (100, MeasureSpec.EXACTLY);
	view.measure (widthMeasureSpec, heightMeasureSpec);

(3)如果是wrap_parent

	int widthMeasureSpec = MeasureSpec . makeMeasureSpec( (1 << 30)- 1 ,MeasureSpec.AT_MOST);
	int heightMeasureSpec = MeasureSpec . makeMeasureSpec( (1 << 30) - 1 ,MeasureSpec.AT_MOST);
	view.measure (widthMeasureSpec, heightMeasureSpec);

详细说明可以参考《Android开发艺术探索》。

2.3 layout过程

layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup 的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout 方法,在layout方法中onLayout方法又会被调用。简单来说layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置,这里看下layout方法:

  public void layout(int l, int t, int r, int b) {
        ...
        //记录 view 原始位置  
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        //调用 setFrame 方法 设置新的 mLeft、mTop、mBottom、mRight 值,  
        //设置 View 本身四个顶点位置  
        //并返回 changed 用于判断 view 布局是否改变  
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //第二步,如果 view 位置改变那么调用 onLayout 方法设置子 view 位置  
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //调用 onLayout  
            onLayout(changed, l, t, r, b);
            ...
        }
        ...
    }

layout方法中首先通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、 mTop和mBottom这四个值,View的四个顶点一旦确定, 那么View在父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。感兴趣的话可以去看下《Android开发艺术探索》中对于LinearLayout的onLayout方法的分析。

2.4 draw过程

draw的作用是将View绘制到屏幕上面。View的draw过程大致包含以下几步:

  1. 绘制背景。
  2. 如果有必要,保存当前canvas层。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果有必要,绘制边缘、阴影等效果。
  6. 绘制装饰,例如滚动条等。

直接看draw的源码:

 public void draw(Canvas canvas) {
        int saveCount;
        // 1. 绘制背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // 2.如果可能的话跳过第2步和第5步(一般情况)
        final int viewFlags = mViewFlags;
      
        if (!verticalEdges && !horizontalEdges) {
            // 3. 绘制View的内容
            if (!dirtyOpaque) onDraw(canvas);
            
            // 4. 绘制子View
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // 6. 绘制装饰,如滚动条等等。
            onDrawForeground(canvas);
            return;
        }
    }
    
    /**
    *  1.绘制View背景
    */
    private void drawBackground(Canvas canvas) {
        //获取背景
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        setBackgroundBounds();

        //获取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,则会在平移后的canvas上面绘制背景
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
    
    /**
    * 3.绘制View的内容,该方法是一个空的实现,根据不同的内容进行不同的设置,自定义View中就需要重写该方法
    */
    protected void onDraw(Canvas canvas) {
    }
    
    /**
    * 4. 绘制子View。该方法在View当中是一个空的实现,在各个业务当中自行处理。
    * 在ViewGroup当中对dispatchDraw方法做了实现,主要是遍历子View并调用子元素的draw方法,如此draw事件就一层层地传递了下去。
    */
    protected void dispatchDraw(Canvas canvas) {

    }
        

3.自定义View

《Android开发艺术探索》中把自定义View大致分为了4类,我们通过Demo学习下前两类,后两类和前两类是相似的:

  • 1.继承View重写onDraw方法
    该方法主要用于实现一些不规则的效果,即这种效果需要静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_ content, 并且padding也需要自己处理。

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

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

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

在写Demo之前,还要了解下自定义View的一些注意事项:

  • 1.View支持wrap_ content
    直接继承View或者ViewGroup的控件,需要需要重写onMeasure方法并设置wrap_ content时的自身大小。

  • 2.View支持padding
    直接继承View的控件,需要在draw方法中处理padding;直接继承自ViewGroup 的控件需要在onMeasure和onLayout
    中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。

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

  • 4.View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
    View 中如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View 的onDetachedFromWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,否则有可能会造成内存泄漏。

  • 5.避免View滑动冲突

接下来通过Demo学习一下。

3.1.继承View重写onDraw方法,并对wrap_ content、padding进行处理

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#ffffff"
    tools:context=".MainActivity">

    <com.demo.myview.CircleView
        android:id="@+id/circleView"
        android:background="#000000"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:padding="20dp"
        app:circle_color="@color/colorAccent"/>

</LinearLayout>
 

这里可以看到自定义View的layout_width为wrap_content,前面介绍过这里其实还是match_parent的效果,要在onMeasure中进行处理;然后这里指定的padding其实是不会起作用的,需要在draw方法中处理padding;最后 app:circle_color是自定义的属性,自定义属性的添加方法如下:

1)首先在values目录下新建attrs.xml,支持的自定义属性为color,除此之外还支持string、float、boolean等:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
    </declare-styleable>
</resources>

2)在View的构造方法中解析自定义属性的值(这里是circle_color)并做相应处理
3)在布局文件中使用自定义属性,例如上面布局文件中app:circle_color=@color/colorAccent

接下来看自定义View,在这个自定义View的构造方法中要解析自定义属性的值,onDraw方法中要处理padding,onMeasure中要处理自定义View的layout_width为wrap_content的问题:

public class CircleView extends View {
    private Paint paint;
    private int color = Color.RED;

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

    public CircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
        color = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
        a.recycle();
        init();
    }

    private void init() {
        paint = new Paint();
        paint.setColor(color);
    }

    @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,paint);

    }

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

}

在onMeasure中,我们给View指定了一个默认的内部宽/高( 200/200),并在wrap_ content 时设置此宽/高。对于非wrap_ content 情形,则沿用系统的测量值。最终效果如下:
在这里插入图片描述

3.2.继承ViewGroup派生特殊的Layout

前面提到过该方法需要合适地处理ViewGroup自己的测量、布局这两个过程,还要同时处理子元素的测量和布局过程。这里就通过继承ViewGroup模仿实现LinearLayout的垂直布局。

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.demo.myviewgroup.MyViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorAccent">
        
        <Button
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:text="Button1"/>

        <Button
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="Button2"/>

        <Button
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:text="Button3"/>
    </com.demo.myviewgroup.MyViewGroup>
    
</LinearLayout>

MyViewGroup代码:

public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context) {
        super(context);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //对所有的子View进行测量,会触发每个子View的onMeasure函数 measureChildren中又会调用measureChild分别对单个View进行测量
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childCount = getChildCount();
        if(childCount == 0){//判断是否有子元素
            setMeasuredDimension(0, 0);//没有子元素就直接将宽高设置为0
        }else if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            //如果宽高都采用了wrap_content的话,将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度
            int height = getTotalHeight();
            int width = getMaxChildWidth();
            setMeasuredDimension(width, height);
        }else if(heightMode == MeasureSpec.AT_MOST){
            //如果只有高采用了wrap_content,宽度设置为ViewGroup自己的测量宽度,高度设置为所有子View的高度总和
            setMeasuredDimension(widthSize, getTotalHeight());
        }else if(widthMode == MeasureSpec.AT_MOST){
            //如果只有宽采用了wrap_content,宽度设置为子View中宽度最大的值,高度设置为ViewGroup自己的测量值
            setMeasuredDimension(getMaxChildWidth(), heightSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {//完成子元素的定位
        int count = getChildCount();
        //记录当前的高度位置
        int curHeight = t;

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if(child.getVisibility()!=View.GONE){//如果这个子元素不处于GONE状态
                int height = child.getMeasuredHeight();
                int width = child.getMeasuredWidth();
                //把子View放到合适的位置上,参数分别是子View矩形区域的左、上、右、下边
                child.layout(l, curHeight, l + width, curHeight + height);
                curHeight += height;//高度叠加 所以效果和垂直方向的LinearLayout相似
            }
        }
    }

    private int getMaxChildWidth() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getMeasuredWidth() > maxWidth)
                maxWidth = childView.getMeasuredWidth();
        }
        return maxWidth;
    }

    private int getTotalHeight() {
        int childCount = getChildCount();
        int height = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            height += childView.getMeasuredHeight();

        }
        return height;
    }

}

效果如下:
在这里插入图片描述

参考以下内容,如有理解错误之处还请指出:

猜你喜欢

转载自blog.csdn.net/weixin_42011443/article/details/107616919