View的可见性和绘制流程

view的绘制流程主要有三个部分,onMeasure,onLayout,onDraw. 其中,onMeasure是决定该view的大小,onLayout决定view的位置,onDraw决定view的显示。

除了在该view第一次出现在屏幕时,会调用这几个方法,其他的一些事件也会导致这几个方法的调用。 今天主要研究设置view的可见性和键盘不同模式下弹起和隐藏时,导致哪些重要的方法被调用。

另外其中有一个接口ViewTreeObserver.OnGlobalLayoutListener经常被我们用到,它的回调方法onGlobalLayout官方解释:

Callback method to be invoked when the global layout state or the visibility of views within the view tree changes

即:全局的布局或者该view树下的view的可见性发生变化时会被触发。

那么接下来就用具体的代码和日志来验证该回调的调用。
首先,先从源码看一下该回调在哪里触发:

一. onGlobalLayoutListener

1. 该回调是在ViewTreeObserver的方法dispatchOnGlobalLayout()中被调用
那么dispatchOnGlobalLayout在哪里被分发呢?

#ViewTreeObserver
 public final void dispatchOnGlobalLayout() {
        // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
        // perform the dispatching. The iterator is a safe guard against listeners that
        // could mutate the list by calling the various add/remove methods. This prevents
        // the array from being modified while we iterate it.
        final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
        if (listeners != null && listeners.size() > 0) {
            CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
            try {
                int count = access.size();
                for (int i = 0; i < count; i++) {
                    access.get(i).onGlobalLayout();
                }
            } finally {
                listeners.end();
            }
        }
    }

2. 在ViewRootImpl的方法peformTraversals()中调用
如果标志位didLayouttrue,那么会调用performLayout,一旦didLayout = true,那么标志位triggerGlobalLayoutListener一定为true,就必定会调用ViewTreeeObserver.dispatchOnGlobalLayout()方法。

#ViewRootImpl.performTraversals()
  final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
  boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
  if (didLayout) {
            performLayout(lp, mWidth, mHeight);
            ...
    }
  if (triggerGlobalLayoutListener) {
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}

所以得出结论:能引起重新布局的方法都将会触发onGlobalLayoutListener事件,且该事件在onLayout回调之后。

二. View.setVisibility() 方法

查看源码,发现view.setVisibility方法里面仅仅调用了setFlag()

   @RemotableViewMethod
    public void setVisibility(@Visibility int visibility) {
        setFlags(visibility, VISIBILITY_MASK);
    }

继续进setFlags()方法中看

 void setFlags(int flags, int mask) {
        int old = mViewFlags;
        mViewFlags = (mViewFlags & ~mask) | (flags & mask);
        int changed = mViewFlags ^ old;
        ...
        final int newVisibility = flags & VISIBILITY_MASK;
        if (newVisibility == VISIBLE) {
            if ((changed & VISIBILITY_MASK) != 0) {
                /*
                 * If this view is becoming visible, invalidate it in case it changed while
                 * it was not visible. Marking it drawn ensures that the invalidation will
                 * go through.
                 */
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(true);

                needGlobalAttributesUpdate(true);

                // a view becoming visible is worth notifying the parent
                // about in case nothing has focus.  even if this specific view
                // isn't focusable, it may contain something that is, so let
                // the root view try to give this focus if nothing else does.
                if ((mParent != null) && (mBottom > mTop) && (mRight > mLeft)) {
                    mParent.focusableViewAvailable(this);
                }
            }
        }

        /* Check if the GONE bit has changed */
        if ((changed & GONE) != 0) {
            needGlobalAttributesUpdate(false);
            requestLayout();

            if (((mViewFlags & VISIBILITY_MASK) == GONE)) {
                if (hasFocus()) {
                    clearFocus();
                    if (mParent instanceof ViewGroup) {
                        ((ViewGroup) mParent).clearFocusedInCluster();
                    }
                }
                clearAccessibilityFocus();
                destroyDrawingCache();
                if (mParent instanceof View) {
                    // GONE views noop invalidation, so invalidate the parent
                    ((View) mParent).invalidate(true);
                }
                // Mark the view drawn to ensure that it gets invalidated properly the next
                // time it is visible and gets invalidated
                mPrivateFlags |= PFLAG_DRAWN;
            }
            if (mAttachInfo != null) {
                mAttachInfo.mViewVisibilityChanged = true;
            }
        }

        /* Check if the VISIBLE bit has changed */
        if ((changed & INVISIBLE) != 0) {
            needGlobalAttributesUpdate(false);
            /*
             * If this view is becoming invisible, set the DRAWN flag so that
             * the next invalidate() will not be skipped.
             */
            mPrivateFlags |= PFLAG_DRAWN;

            if (((mViewFlags & VISIBILITY_MASK) == INVISIBLE)) {
                // root view becoming invisible shouldn't clear focus and accessibility focus
                if (getRootView() != this) {
                    if (hasFocus()) {
                        clearFocus();
                        if (mParent instanceof ViewGroup) {
                            ((ViewGroup) mParent).clearFocusedInCluster();
                        }
                    }
                    clearAccessibilityFocus();
                }
            }
            if (mAttachInfo != null) {
                mAttachInfo.mViewVisibilityChanged = true;
            }
        }

        if ((changed & VISIBILITY_MASK) != 0) {
            // If the view is invisible, cleanup its display list to free up resources
            if (newVisibility != VISIBLE && mAttachInfo != null) {
                cleanupDraw();
            }

            if (mParent instanceof ViewGroup) {
                ((ViewGroup) mParent).onChildVisibilityChanged(this,
                        (changed & VISIBILITY_MASK), newVisibility);
                ((View) mParent).invalidate(true);
            } else if (mParent != null) {
                mParent.invalidateChild(this, null);
            }

            if (mAttachInfo != null) {
                dispatchVisibilityChanged(this, newVisibility);

                // Aggregated visibility changes are dispatched to attached views
                // in visible windows where the parent is currently shown/drawn
                // or the parent is not a ViewGroup (and therefore assumed to be a ViewRoot),
                // discounting clipping or overlapping. This makes it a good place
                // to change animation states.
                if (mParent != null && getWindowVisibility() == VISIBLE &&
                        ((!(mParent instanceof ViewGroup)) || ((ViewGroup) mParent).isShown())) {
                    dispatchVisibilityAggregated(newVisibility == VISIBLE);
                }
                notifySubtreeAccessibilityStateChangedIfNeeded();
            }
        }
...
}
    public static final int VISIBLE = 0x00000000;
    public static final int INVISIBLE = 0x00000004;
    public static final int GONE = 0x00000008;

这段代码有点长,省略掉其他无关的代码,newVisibilityflagsVISIBILITY_MASK位与,而VISIBILITY_MASK = 0x0000000C, C对应与1100。 所以flagsVISIBILITY_MASK位与之后,还是原来的值。

changed是现在的mViewFlagsold进行位或,而现在的mViewFlagsold只有第3位和第4位有区别。

根据InVisibleGONE的值,可以将第3位理解为控制invisiblie位,而第4位理解为Gone的位。所有如果第3位不一样,那么changed = 4, 如果第4位不一样,changed = 8.

changed = 4 意味着view有可能发生了如下的变化:

  1. visibility -> invisible
  2. gone -> invisible

同样 changed = 8, 意味着view 发生了如下的变化:

  1. invisible -> gone
  2. visible -> gone

这样一分析,后面的代码就好理解多了。

     if (newVisibility == VISIBLE) {
          if ((changed & VISIBILITY_MASK) != 0) {
                /*
                 * If this view is becoming visible, invalidate it in case it changed while
                 * it was not visible. Marking it drawn ensures that the invalidation will
                 * go through.
                 */
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(true);

这一段说明,如果设置了可见,且之前不是可见,那么必定要invalidate. 而invalidate只会调用onDraw,并不会引起onLayout的调用。

if ((changed & GONE) != 0) {
  needGlobalAttributesUpdate(false);
  requestLayout();

这一段说明,只要控制Gone位的标志位发生了变化,就一定会引起requestLayout, 而requestLayout则会重新调用performTraversals(),自然会引起ViewTreeObserval.OnGlobalLayoutListener的回调。这也不难理解,因为View.Gone说明该view不会占用屏幕空间,那么GONE位的变化,必定有屏幕的布局发生变化。

  if ((changed & INVISIBLE) != 0) {
  ...
  }

这一段说明,如果invisible发生变化,则没有调用requestLayout,而从gone->invisible这一种变化,上一段的逻辑已经包含到了。所以这一段if逻辑没有requestLayout也是情理之中。

       if ((changed & VISIBILITY_MASK) != 0) {
            // If the view is invisible, cleanup its display list to free up resources
            if (newVisibility != VISIBLE && mAttachInfo != null) {
                cleanupDraw();
            }

            if (mParent instanceof ViewGroup) {
                ((ViewGroup) mParent).onChildVisibilityChanged(this,
                        (changed & VISIBILITY_MASK), newVisibility);
                ((View) mParent).invalidate(true);
            } else if (mParent != null) {
                mParent.invalidateChild(this, null);
            }
...
}
protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) {
        if (mTransition != null) {
            if (newVisibility == VISIBLE) {
                mTransition.showChild(this, child, oldVisibility);
            } else {
                mTransition.hideChild(this, child, newVisibility);
                if (mTransitioningViews != null && mTransitioningViews.contains(child)) {
                    // Only track this on disappearing views - appearing views are already visible
                    // and don't need special handling during drawChild()
                    if (mVisibilityChangingChildren == null) {
                        mVisibilityChangingChildren = new ArrayList<View>();
                    }
                    mVisibilityChangingChildren.add(child);
                    addDisappearingView(child);
                }
            }
        }

这一段是控制其子view的变化,如果newVisibility == VISIBLE, 那么最终会调用到mTransition.showChild()

public void showChild(ViewGroup parent, View child, int oldVisibility) {
        addChild(parent, child, oldVisibility == View.GONE);
 }

而其他,则会调用hideChild()

public void hideChild(ViewGroup parent, View child, int newVisibility) {
       removeChild(parent, child, newVisibility == View.GONE);
   }

所以总结如下:

  1. visible-> invisible, 只会引起invalidate, 不会引起requestLayout, 即不会引起布局变化
  2. xx -> Gone 或者Gone -> xx, 则会引起requestLayout, 即引起布局变化。

三. 实验验证

分别写了两个自定义view, 一个为继承自TextView, 一个自定义继承自LinearLayout
1. visible-> invisible

visible->invisible.png

2. visible->gone

visible->gone.png

3. invisible->visible

invisible->visible.png

4. invisible->gone

invisible->gone.png

5. gone->visible

gone->visible.png

6. gone->invisible

gone->invisible.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342