Android自定义ViewAndroid自定义View学习笔记

自定义View(七)-View的工作原理- Activity的布

2018-01-03  本文已影响497人  g小志

前言

前面几篇对动画可以说是做了非常全面的总结了(上篇文章最后的4种ViewGroup相关动画相信在了解基础后看些文章也不会太难理解)。在View的工作原理 这一部分我们将对View做全面深入的解析。由于本人是菜鸟,其实无法直接看源码,也都是通过书籍与文章反复阅读,然后才去看的源码。由于怕忘记写成博客。希望和我一样不了解的朋友能在自定义View中不那么迷茫。如果那里有错误大家一定指出我将不胜感激。


Activity#setContentView

关于View的工作原理,大家可能会问:为什么不直接看View呢?因为我觉得Activty是呈现应用界面的载体,所有的View都在Acitivity中,并且在理解Activity的启动XML的加载也是一种了解View工作原理的一个很好的入口。好了废话不多说:翠花~上酸菜(代码):
注:在View的工作原理中涉及到源码为:API=23 以后不再说明 重要的一些源码或是方法较长我会标出在源码中所在行数

activity_main :

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

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="Hello World!"
        />

</LinearLayout>

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.tv);
    }

布局非常简单,首先注意两点:

其实了解的知道2者本质是没有区别的,只是AppCompatActivity做了许多的兼容性方面的处理。自然在流程上相对Activity会在分析上更复杂些。所以这里继承自Activity,当然你继承AppCompatActivity也是可以的。

这个大家应该都是比较了解,就是去除Title。也就是标题栏。

setContentView

我们查看setContentView发现他的源码如下:

  public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

我们在追踪发现是调用Window#setContentView(),但是Window是一个抽象类setContentView是一个抽象方法,并且的进去getWindow()方法中发现他里面返回mWindow,那么我只需要知道mWindow一定是Window的实现类里面实现了setContentView方法。通过查找发现在Activity中只有一个方法将mWindow赋值,如下:

mWindow = new PhoneWindow(this, window);//6619

原来是调用了PhoneWindow#setContentView(),那我们就进入PhoneWindow查看一下,发现点不了。原因是因为PhoneWindow被隐藏了,我们是看不到的。所以我们需要去sdk源码中找到这个类。这里我以我的电脑为例,目录在D:\Android\Administrator\AppData\Local\Android\sdk\sources\android-23。里面包含我们下载的所有sdk版本,也可以直接通过Android Stuido查看路径再进入\sources目录下。然后直接在右上角搜索类名。OK!我们继续。找到setConentView,如下:

 @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        //这里mContentParent就是我们布局加载的父View,activity_main就是加载到他里面
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

里面有2个setConentView方法。我们主要看我们用到的。如上。当进入Acitvity时mContentParent一定为空,那么就会进入installDecor()方法中。

 private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();   ...(1)
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);...(2)

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();
            
            ...
            }

我们这里看主要代码。首先mDecor为空。然后调用generateDecor()给mDecor赋值。在generateDecor()方法中主要是创建了一个DecorView。DecorView它是什么呢?其实他是PhoneWindow中的一个内部类。继承FrameLayout。那我们知道了,他是一个布局控件。我们回来继续看 (2) 处。在这里对mContentParent进行赋值。我们追踪generateLayout(mDecor)方法:

    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

        TypedArray a = getWindowStyle();

        .........

        /**以下这些是Activity 窗口属性特征的设置*/
        //窗口是否浮动,一般用于Dialog窗口是否浮动:是否显示在布局的正中间。
        mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
        int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
                & (~getForcedWindowFlags());
        if (mIsFloating) {
            setLayout(WRAP_CONTENT, WRAP_CONTENT);
            setFlags(0, flagsToUpdate);
        } else {
            setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
        }
        //设置窗口是否支持标题栏,隐藏显示标题栏操作在此处。
        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            // Don't allow an action bar if there is no title.
            requestFeature(FEATURE_ACTION_BAR);
        }
        //ActionBar导航栏是否不占布局空间叠加显示在当前窗口之上。
        if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
            requestFeature(FEATURE_ACTION_BAR_OVERLAY);
        }

        if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
            requestFeature(FEATURE_ACTION_MODE_OVERLAY);
        }

        if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
            requestFeature(FEATURE_SWIPE_TO_DISMISS);
        }
        //当前Activity是否支持全屏
        if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
            setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
        }

       ................

       //设置状态栏的颜色
        if (!mForcedStatusBarColor) {
            mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);
        }
        //设置导航栏的颜色
        if (!mForcedNavigationBarColor) {
            mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);
        }

        if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion
                >= android.os.Build.VERSION_CODES.HONEYCOMB) {
            if (a.getBoolean(
                    R.styleable.Window_windowCloseOnTouchOutside,
                    false)) {
                setCloseOnTouchOutsideIfNotSet(true);
            }
        }

        WindowManager.LayoutParams params = getAttributes();
        //设置输入法的状态
        if (!hasSoftInputMode()) {
            params.softInputMode = a.getInt(
                    R.styleable.Window_windowSoftInputMode,
                    params.softInputMode);
        }

        if (a.getBoolean(R.styleable.Window_backgroundDimEnabled,
                mIsFloating)) {
            /* All dialogs should have the window dimmed */
            if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
                params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
            }
            if (!haveDimAmount()) {
                params.dimAmount = a.getFloat(
                        android.R.styleable.Window_backgroundDimAmount, 0.5f);
            }
        }
        //设置当前Activity的出现动画效果
        if (params.windowAnimations == 0) {
            params.windowAnimations = a.getResourceId(
                    R.styleable.Window_windowAnimationStyle, 0);
        }

        // The rest are only done if this window is not embedded; otherwise,
        // the values are inherited from our container.
        if (getContainer() == null) {
            if (mBackgroundDrawable == null) {
                if (mBackgroundResource == 0) {
                    mBackgroundResource = a.getResourceId(
                            R.styleable.Window_windowBackground, 0);
                }
                if (mFrameResource == 0) {
                    mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0);
                }
                mBackgroundFallbackResource = a.getResourceId(
                        R.styleable.Window_windowBackgroundFallback, 0);
                if (false) {
                    System.out.println("Background: "
                            + Integer.toHexString(mBackgroundResource) + " Frame: "
                            + Integer.toHexString(mFrameResource));
                }
            }
            mElevation = a.getDimension(R.styleable.Window_windowElevation, 0);
            mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false);
            mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT);
        }

        //以下代码为当前Activity窗口添加 decor根布局。
        // Inflate the window decor.

        int layoutResource;
        int features = getLocalFeatures();
        
        ....
        
        else {
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;(1)
            // System.out.println("Simple!");
        }
        mDecor.startChanging();//3907

        View in = mLayoutInflater.inflate(layoutResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); (2)
        mContentRoot = (ViewGroup) in;

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        
        ...
        
        mDecor.finishChanging();

        return contentParent;

简单说下省略代码。里面先获取xml属性,根据设置决定加载什么样的xml属性。我们在开头requestWindowFeature(Window.FEATURE_NO_TITLE);这个设置就是在这里其中用的。我们继续看主要代码。上面的源码中根据我们的设置加载layoutResource,并将进行加载,添加到decor中然后通过ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);得到contentParent并返回。之前我们说过decor是一布局控件,那么它添加的layoutResource是什么样的布局呢?可以发现layoutResource是在 (1) 处的代码块处被赋值。那么我们来看看R.layout.screen_simple;

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

它的布局很简单。这就是我们其实decor的布局。我们 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);中的ID_ANDROID_CONTENT就是 android:id="@android:id/content",所以我们布局中的XML是添加到FrameLayout中了。那么到底是不是这样的呢?我们启动应用来测试一下。
注:这个布局在D:\Android\Administrator\AppData\Local\Android\sdk\platforms\android-23中搜索

图片.png
注:此图是通过hierarchyviewer查看的。不了解可以百度下
通过此图我们可以知道DecorView是顶级View,它包括通知栏(图3),底部导航栏(图4)。从图2中的布局我们可以看到正是我们上面加载的screen_simple布局。而我们activity_main正是加载到R.id.content中。证实了我们上面的想法。

总结

通过上面的流程,我们现在就了解了Activity的布局加载,现在我们来梳理下流程:

Android布局加载.png

层级结构关系:

Android布局加载结构图.png

总结

  1. 在实际中当使用AppCompatActivity是会发现requestWindowFeature(Window.FEATURE_NO_TITLE);无效,因为此时标题栏为ActionBar导航栏。如果要去除可以使用getSupportActionBar().hide();来隐藏导航栏。当然也可以在style.xml中设置xxxx.NoActionBar
  2. 通过上面源码得知,我们在generateLayout()方法中是先根据requestWindowFeature(Window.FEATURE_NO_TITLE);设置的属性来决定是否显示标题栏,然后才加载的布局,所以方法requestWindowFeature(Window.FEATURE_NO_TITLE);需要在 setContentView方法之前使用。

DecorView添加到窗口过程

1.ActivityThread#performResumeActivity
上面我们已经了解了,Activity的布局加载过程,当我们加载布局完成后我们是如何将我们加载的布局添加到我们的界面窗口的呢?这里我们就提到ActivityThread类。我们知道我们主线程也就是UI线程。我们的Activity就在此线程中,而ActivityThread是管理应用进程的主线程的执行。当我们的顶级View->DecorView加载完成后。回调用ActivityThread#handlerResumeActivity方法。在这里将加载完成的DecorView添加到PhoneWindow窗口。那我们就来看看这个方法:

·
    public final ActivityClientRecord performResumeActivity(IBinder token,
            boolean clearHide) {
            ActivityClientRecord r = performResumeActivity(token, clearHide); // 这里会调用到onResume()方法
            //...省略
            
            if (r.window == null && !a.mFinished && willBeVisible) {
                //得到窗口PhoneWindow实体
                r.window = r.activity.getWindow();
                //得到DecorView
                View decor = r.window.getDecorView();
                //设置DecorView可见度
                decor.setVisibility(View.INVISIBLE);
                //得到前Activity的WindowManagerImpl对象
                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) {
                //标记DecorView已经添加到窗口界面
                    a.mWindowAdded = true;
                    //将DecorView添加到当前Activity的窗口上面
                    wm.addView(decor, l);
                }
                
                //...省略
        }

2.WindowManagerImpl#addView()

上面的注解比较详细,这样我们就将布局添加到了屏幕上了。那我们来跟中下 wm.addView(decor, l);看它是如何添加的。
WindowManagerImpl#addView()

·
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

3.WindowManagerGlobal#addView()

方法里面调用了WindowManagerGlobal#addView()。继续找它里面的方法:


public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ......

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
           
            ......

            //创建ViewRootImpl
            root = new ViewRootImpl(view.getContext(), display);
            //设置LayoutParams (1)
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            //调用ViewRootImpl#setView添加布局view(参数view就是DecorView)
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }

这里面没有太多需要解释的,主要的方法都有注释。这里需要提一句,针对(1)处:设置LayoutParams,对于DecorView,其MeasureSpec有窗口的尺寸和其自身的LayoutParams来共同决定;对于普通View,其MeasureSpec由父容器和其自身的LayoutParams来共同决定。关于MeasureSpec不了解的等后续的文章会说明并结合measure()详细讲解。这里只需要知道它是测量规格即可。我们继续跟着思路往下走。

4.ViewRootImpl#setView

ViewRootImpl#setView:

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

            .............     
        }
 }

ViewRootImpl#requestLayout():

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

里面就一个方法,我们继续走:

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

这里用到一个回调:mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);我们进入这个遍历完成的回调mTraversalRunnable:

//定义mTraversalRunnable
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

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

5.performTraversals()

ViewRootImpl#doTraversal()内部调用了performTraversals();那我们就直接看此方法:

private void performTraversals() {
        // cache mView since it is used so much below...
        //上面提到mView就是DecorView根布局
        final View host = mView;
        // 成员变量mAdded赋值为true,因此条件不成立
        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) {
                // NOTE -- system code, won't try to do compat mode.
                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.widthhe和lp.height表示DecorView根布局宽和高 (1)
 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

  // Ask host how big it wants to be
  //执行测量操作
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//1864

........................
//执行布局操作
 performLayout(lp, desiredWindowWidth, desiredWindowHeight);//1931

.......................
//执行绘制操作
performDraw();//2067

}  

上面提到对于顶级View(DecorView),它的测量规格是窗口的尺寸大小。普通View则是父容器的测量规格与自身LayoutParames。那么此时的childWidthMeasureSpec,childHeightMeasureSpec就表示DecorView的测量规格。然后根据这个规格进行测量,布局,绘制。最后这三个方法都是调用View的measure,layot,draw。

剩下的就是关于测量,布局与绘制的相关知识了。这些知识点后面再系统的讲解。


总结

1. 流程总结

ViewRoot对应ViewRootImpl类。他是链接WindowManager和DecorView的纽带,View的三大流程均通过ViewRootImpl来完成,在ActivityThread中,当Activity被创建完成后会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRoot对象和DecorView建立联系。这个过程可以通过上面代码中:

 root = new ViewRootImpl(view.getContext(), display);
//调用ViewRootImpl#setView添加布局view(参数view就是DecorView)
root.setView(view, wparams, panelParentView);

下面我还只用流程图来总结下上面的流程。

Android布局加载结构图2.png

performTraversals()的工作流程图(此图来自Android开发艺术探索):

图片.png
由此我们可以看到在执行performTraversals()中里面就是View的三大流程了。这部分内容比较多,我们在后续的篇幅中来讲解。

2. 获取测量宽高为0
View的measure过程和Activity的生命周期方法不是同步执行的。因此无法保证在onCreate(),onStart(),onResume()中获取测量宽高。由上面的代码也可知道在onResume()之后才开始执行三大流程。所以我们在获取时会得到宽/高=0。解决办法有很多,提供一种常用的如下:

·
    @Override
    protected void onStart() {
        super.onStart();
        mView.post(new Runnable() {
            @Override
            public void run() {
                int width=mView.getMeasuredWidth();
                int height=mView.getMeasuredHeight();
            }
        });
    }

结语

这篇文章的核心是理顺View加载的思路与流程。以简短,清晰,易懂(和我一样工作时间短的小伙伴)来分析。

关于自定义View设计的知识点非常多我觉得也很难掌握,所以利用文章来记录想,希望对大家有些帮助。由于本人能力有限,如果有错误大家一定指出,共同进步。最后希望如果对你有帮助请评个论,点个关注,让我更有信心和动力。下篇我们将针对View的三大流程来分析下。

感谢

《Android开发艺术探索》
Android View 深度分析

上一篇下一篇

猜你喜欢

热点阅读