Android如何绘制View

Posted Jason_Wang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android如何绘制View相关的知识,希望对你有一定的参考价值。

应用程序Activity启动时,android是如何对每个Activity中的View进行绘制的了?接下来,我们就来看一看整个绘制的过程。

当activity获得焦点(Focus)时,系统会发送消息要求绘制布局。Android中间层会负责整个绘制过程,但在这之前,Activity需要提供布局结构的根节点(root node)。

获取到布局的根节点后,开始对布局树(所有的View组成一个View Tree)进行测量与绘制。绘制是通过遍历整个布局树以及对每个在非有效区域里的View进行着色(render)完成的。而每个ViewGroup(布局)则负责调用其子View的绘制函数onDraw(),具体的绘制由子View自身来完成。整个绘制的遍历过程是采用中序遍历的方式进行的,因此parent总是在子view之前绘制,而同一个ViewGroup中姊妹view则根据他们在布局中出现的顺序来绘制。

若需要强制一个view进行绘制,可以调用 invalidate()

绘制一个布局主要有两个过程:测量与布局。测量过程通过函数measure(int, int)实现,其从上而下对整个View tree进行遍历:每个view都将尺寸大小往树的底部传递。当测试过程结束后,每个view都保存了自己的尺寸数据。第二个布局过程,同样是一个从上之下的遍历过程,通过layout(int,int,int,int)来实现,在此过程中,每个父节点的View根据已经计算出的尺寸,负责将所有子view放到合适的位置上。

要初始化一个布局,调用requestLayout()。若视图认为布局不能够适配当前的边界时,这个方法会被视图自身调用。

当一个view的measure()函数返回时,它的宽度与高度必须被确定下来,与其相关的子view大小也需要确定下来,同时还需要满足父节点view对其宽度与高度的约束。这能够确保在测量过程结束后,程序中父视图可以接受子视图的尺寸。一个父视图有可能多次调用子视图的measure()函数。

测量过程通过两个类来交换彼此的尺寸信息:ViewGroup.LayoutParams被子视图用于通知父视图其自身想要如何被测量以及放置在什么位置。对于每个尺寸,可以指定如下几个参数:

  • 一个具体的数字,如8dp,10dp;
  • MATCH_PARENT,保持子视图跟父视图一样的大小(减去padding);
  • WRAP_CONTENT,视图刚好能够包含它的内容(加上padding)

MeasureSpec对象用于将尺寸信息从父视图传递给子视图,一个MeasureSpec有如下三种模式:

  • UNSEPCIFIED: 用于一个父视图确定一个子视图的期望的尺寸。例如,一个高度指定为 UNSPECIFIED而高度指定为EXACTLY 240LinearLayout也许会调用子视图的measure()函数,以确定在给定的240像素宽度下,子视图想要多大的尺寸;
  • EXACTLY: 父视图强制给子视图一个确定的尺寸,而子视图必须要保证使用该尺寸,并且确保其子视图的大小也同样在该尺寸范围内;
  • AT_MOST: 父视图给子视图指定一个最大的尺寸,子视图需要确保其自身以及子视图在该尺寸的范围之内。

Android怎么开始绘制View

在Activity的启动过程中,一个应用程序的UI界面是从什么时候开始绘制的了? 我们知道,在写一个Activity的程序时,重载onCreate(Bundle savedState)函数后,会调用 setContentView()函数,将Layout资源文件传递给系统,正是从这里,Android开始了View的绘制。

关于Activity启动流程:

View绘制真正的幕后由三个火枪手组成: View, ViewRootImpl, Choerographer; View就是要绘制的视图了,ViewRootImpl则负责将绘制的View与对应的Window绑定起来,Choerographer则是协调窗口中input,animation以及drawing的魔法师了。

ViewRootImpl: the top of a view hierarchy, implementing the needed protocol between View and the WindowManager

直接上图。看一看Activity启动后,UI绘制的流程简图:

  • 系统创建一个Activity实例后,会将该Activity与一个窗口(PhoneWindow)绑定在一起。同时PhoneWindow会创立一个DecorView(Window的根视图),并设置相应的参数(背景,颜色,标题等);

    final void attach(Context context, ActivityThread aThread,Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,CharSequence title, Activity parent, String id,NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor) 
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) 
            mWindow.setSoftInputMode(info.softInputMode);
        
        if (info.uiOptions != 0) 
            mWindow.setUiOptions(info.uiOptions);
        
        mUiThread = Thread.currentThread();
        ...
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) 
            mWindow.setContainer(mParent.getWindow());
        
        mWindowManager = mWindow.getWindowManager();
    
  • 调用onCreate()函数,通过setContentView()将Layout资源文件传递给PhoneWindow

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

Window将Layout对应的子View添加到对应的ViewGroup中:


    public void setContentView(View view, ViewGroup.LayoutParams params) 
        // 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.
        if (mContentParent == null) 
            installDecor();
         else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) 
            mContentParent.removeAllViews();
        

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) 
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
         else 
            // 将子View添加到窗口中(DecorView)
            mContentParent.addView(view, params);
        
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) 
            cb.onContentChanged();
        
    
  • 添加子View后,View会调用ViewRootImpl中的requestLayout函数;

    public void addView(View child, LayoutParams params) 
        addView(child, -1, params);
    

    public void addView(View child, int index, LayoutParams params) 
        if (DBG) 
            System.out.println(this + " addView");
        

        if (child == null) 
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        

        // addViewInner() will call child.requestLayout() when setting the new LayoutParams
        // therefore, we call requestLayout() on ourselves before, so that the child's request
        // will be blocked at our level
        // 开始布局的工作了
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    
  • 到这一步,就开始了真正的绘制工作:ViewRootImpl会调用scheduleTraversal()TraverasalRunnable放置到Choreographer消息队列里,等待调用:

    void scheduleTraversals() 
        if (!mTraversalScheduled) 
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // mTraversalRunnable --> 就是进行布局遍历的一把手了
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) 
                scheduleConsumeBatchedInput();
            
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        
    


    // mTraveralRunnable
    final class TraversalRunnable implements Runnable 
        @Override
        public void run() 
            doTraversal();
        
    
  • Choreographer 接着着手准备Frame绘制的工作了,

    private void scheduleFrameLocked(long now) 
        // 当前没有Frame请求
        if (!mFrameScheduled) 
            mFrameScheduled = true;
            //判断是否支持VSYNC帧同步,默认为TRUE
            if (USE_VSYNC) 

                // If running on the Looper thread, then schedule the vsync immediately,
                // otherwise post a message to schedule the vsync from the UI thread
                // as soon as possible.
                if (isRunningOnLooperThreadLocked()) 
                    // 触发一个Vsync同步信号
                    scheduleVsyncLocked();
                 else 
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                
             else 
                final long nextFrameTime = Math.max(
                        mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
                if (DEBUG_FRAMES) 
                    Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
                
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
            
        
    

在接收到Kernel发送过来的Vsync信号后,Choreographer会开始真正的绘制了:


        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) 

            // Post the vsync event to the Handler.
            // The idea is to prevent incoming vsync events from completely starving
            // the message queue.  If there are no messages in the queue with timestamps
            // earlier than the frame time, then the vsync event will be processed immediately.
            // Otherwise, messages that predate the vsync event will be handled first.
            long now = System.nanoTime();
            if (timestampNanos > now) 
                Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                        + " ms in the future!  Check that graphics HAL is generating vsync "
                        + "timestamps using the correct timebase.");
                timestampNanos = now;
            
            ...
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        

        @Override
        public void run() 
            mHavePendingVsync = false;
            // Frame 绘制的工作就是在这里
            doFrame(mTimestampNanos, mFrame);
        
    

好了,准备就绪后,doFrame()函数将Cheorographer消息队列的Callbacks取出来,分别运行:


    void doFrame(long frameTimeNanos, int frame) 
        final long startNanos;
        synchronized (mLock) 
            if (!mFrameScheduled) 
                return; // no work to do
            
        ...

        try 
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            // 处理InputEvent
            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
            // 处理动画
            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            // View 布局遍历,绘制,这里运行的就是那个TranversalRunnable了
            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
         finally 
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        
    

以上是关于Android如何绘制View的主要内容,如果未能解决你的问题,请参考以下文章

Android绘制源码分析(下)

[译]Android view 测量布局和绘制的流程

android基础-viewgroup的测量,布局,绘制

Android View 的测量流程详解

android View的测量和绘制

Android面试收集录12 View测量布局及绘制原理