目录:

1、前语 2、View原理 3、ViewRoot 4、自界说view

1、前语

在Android使用开发中,经常会用到以下3点,自界说View动画Touch事情分发。自界说View,能够写出十分漂亮的界面。良好的动画,会提升app的质感。Touch事情分发,影响着与用户的互动。

如需求写自界说view,最重要的是了解view原理,本文今日尝试从源码视点解析View原理。

2、View原理

从本文标题结合内容,部分同学在看完后可能会觉得博主在装13,View原理是什么?应该比较高深。其实View原理一切人都懂。

Android应用开发三部曲-----View原理

如上,view原理便是measure、layout、draw的三个进程。measure,确认view的巨细。layout确认view的方位,draw,制作view。

3、ViewRoot

当Activity履行onResume后,界面便是可见的了,为什么是这样呢?本文盯梢这条头绪来检查view是怎样被增加的?view是怎么被改写的?

调用时序图:

Android应用开发三部曲-----View原理

上代码

            //ActivityThread的handleResumeActivity办法,将Activity的DecorView经过WindowManager增加,所以界面可见了
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            }

追踪wm.addView办法,终究调用WindowManagerGlobal类的addView办法

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ViewRootImpl root;
    View panelParentView = null;
    synchronized (mLock) {
        //假如View的窗口类型是子窗口类型,则找出其父View
        if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            final int count = mViews.size();
            for (int i = 0; i < count; i++) {
                if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                    panelParentView = mViews.get(i);
                }
            }
        }
        //初始化ViewRoot
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        //将view和ViewRoot保存到列表中
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }
    try {
    	//ViewRoot设置View
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
    	//View增加出错,则删除此View
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

WindowManagerGlobal的addView办法中,初始化了ViewRoot对象,并且调用了setView办法。ViewRoot能够了解为View的管理者,View的改写、制作等都是经过ViewRoot调用的,且View与WMS之间的跨进程交互,也是经过ViewRoot完结的。继续检查setView办法。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            int res; /* = WindowManagerImpl.ADD_OKAY; */
            //请求界面改写,要履行measure、layout、draw那套流程了
            requestLayout();
            try {
            	//经过WindowSession与WMS交互,告诉WMS,这个窗口需求被增加,需求被显示了
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mInputChannel);
            } catch (RemoteException e) {
            }
            //WindowSession.addToDisplay的结果值,假如返回值不等于add_ok,则增加失利,抛出反常
            if (res < WindowManagerGlobal.ADD_OKAY) {
                throw new RuntimeException(
                    "Unable to add window -- unknown error code " + res);
            }
        }
    }
}

ViewRoot与WMS使用WindowSession跨进程交互。从以上代码中能够看出,一个Activity中只有一个ViewRoot,并不是一个View对应着一个View。当然,假如是相似状态栏这种直接经过WindowManager增加的View,这类View也会对应着一个ViewRoot。

ViewRoot的requestLayout办法比较简单,一路盯梢,最后会履行ViewRoot的performTraversals办法,此办法十分复杂,十分长。

private void performTraversals() {
    final View host = mView;
    //被增加view的希望宽高
    int desiredWindowWidth;
    int desiredWindowHeight;
    //可见性是否变化
    boolean viewVisibilityChanged = mViewVisibility != viewVisibility || mNewSurfaceNeeded;
    //是否需求从头布局
    boolean layoutRequested = mLayoutRequested && !mStopped;
    //窗口是否需求从头确认巨细
    boolean windowShouldResize = layoutRequested && windowSizeMayChange
        && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
            || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
                    frame.width() < desiredWindowWidth && frame.width() != mWidth)
            || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
                    frame.height() < desiredWindowHeight && frame.height() != mHeight));
    //核算view的巨细
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    //是否要layout
    final boolean didLayout = layoutRequested && !mStopped;
    if (didLayout) {
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    }
    //假如没有撤销制作,则制作view
    if (!cancelDraw && !newSurface) {
        performDraw();
    }
}

performTraversals办法中,依据各种条件,核算是否需求measure、layout以及draw,view的改写完结。至此,Activity从onResume之后发生的故事,都解释清楚了。

挑选一个办法从头看看,performMeasure的具体完结具体是什么:

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办法中直接调用view的measure办法,measure办法是个final办法,无法被子类重写,measure办法中调用onMeasure办法,调用子view的measure办法,完结整个view树的measure操作。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	//现在的MeasureSpec与老的MeasureSpec不相同时,则需求检测判别是否调用onMeasure
    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
            widthMeasureSpec != mOldWidthMeasureSpec ||
            heightMeasureSpec != mOldHeightMeasureSpec) {
        onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

onMeasure办法,直接调用setMeasuredDimension办法,确认view的宽和高,所以在自界说View中,必定要对自己调用setMeasuredDimension办法,确认自己的宽和高。同时只需求调用子view的measure办法即可。

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

在performMeasure办法中,调用了Trace.traceBegin办法,只有调用此办法,才能在SysTrace工具中看到对应的办法的履行时间。

4、自界说view

自界说view是一个系统性的工作,有必要对view原理、元素制作等都有必定掌握才行。本博中对canvas制作以及camera使用等进行过相关总结,不再复述。自界说view中文字的制作较为特别,本文以两行文字控件举例。

检查canvas.drawText接口说明:

Android应用开发三部曲-----View原理

y值意义是,被制作文字的baseline的y坐标,baseline终究是什么呢?

Android应用开发三部曲-----View原理

Baseline是基线,在Android中,文字的制作都是从Baseline处开始的,Baseline往上至字符“最高处”的间隔咱们称之为ascent(上坡度),Baseline往下至字符“最低处”的间隔咱们称之为descent(下坡度);

leading(行间距)则表明上一行字符的descent到该行字符的ascent之间的间隔;

top和bottom文档描绘地很模糊,其实这里咱们能够学习一下TextView对文本的制作,TextView在制作文本的时分总会在文本的最外层留出一些内边距,为什么要这样做?由于TextView在制作文本的时分考虑到了相似读音符号,下图中的A上面的符号便是一个拉丁文的相似读音符号的东西:

Android应用开发三部曲-----View原理

top的意思其实便是,除了Baseline到字符顶端的间隔外还应该包括这些符号的高度,bottom的意思也是相同。一般情况下咱们很少使用到相似的符号,所以往往会忽略掉这些符号的存在,但是Android仍然会在制作文本的时分在文本外层留出必定的边距,这便是为什么top和bottom总会比ascent和descent大一点的原因。而在TextView中咱们能够经过xml设置其特点android:includeFontPadding=”false”去掉必定的边距值但是不能彻底去掉。

本文将自界说一个显示两行文字的控件,文字居中显示,作用如下图:

Android应用开发三部曲-----View原理

直接上代码,检查draw办法

public void draw(Canvas canvas){
  //Log.i("okunu"," firsttext = " + mFirstText + "  msecond = " + mSecondText);
  int totalTextHeight = mFirstTextHeight + mGap + mSecondTextHeight;
  TextPaint paint = getPaint();
  paint.setTextSize(mFirstSize);
  paint.setColor(mFirstColor);
  paint.setTypeface(mFirsTypeface);
  float x1 = (mWidth - mFirstTextWidth)/2;
  float y1 = (mHeight - totalTextHeight) - paint.ascent();
  //float y1 = (mHeight - totalTextHeight);
  canvas.drawText(mFirstText, 0, mFirstText.length(), x1, y1, paint);
  paint.setTextSize(mSecondSize);
  paint.setColor(mSecondColor);
  paint.setTypeface(mSecondTypeface);
  float x2 = (mWidth - mSecondTextWidth)/2;
  float y2 = (mHeight - totalTextHeight) + mFirstTextHeight + mGap - paint.ascent();
  canvas.drawText(mSecondText, 0, mSecondText.length(), x2, y2, paint);
}

x坐标的处理很容易了解,中间方位即可。y坐标的核算比较特别,从作用图上看,文字的制作起点便是view的极点处,y坐标应该是0,但依据canvas.drawText接口的剖析,此处传递的y坐标真实意义是baseline坐标值,而不是文字的极点坐标值。所以y1值核算时需求减去ascent值。假如去掉这一步骤,那么第一行文字就看不见了。

ps:在核算y值时,极点是0,结合前文对baseline的介绍,baseline和极点之间相差一个ascent,那么baseline的值便是极点坐标加上ascent即可。由于ascent值为负,所以加负号即可。核算baseline值可由极点值推导得到。

核算baseline的方位,首先咱们得知道文字的top方位,假如文字在view的正中心,top方位也能够确认,便是view的正中心和文字高度相关。假如文字在顶部,top方位就在view的顶部,确认了top方位之后,再来核算baseline的方位就适当容易了,baseline和top之前相隔的间隔便是 (-top) ,于是就能够确认文字的制作方位了

再次总结下相关点:

  • 文字的宽能够由paint核算得出
  • 文字的高能够由paint核算得出,为求准确,一般要求是 bottom - top
  • 先确认文字的top方位,再来核算baseline方位

一切代码均已上传至自己的github,欢迎拜访。