五、INVISIBLE、GONE、VISIBLE的实现逻辑

INVISIBLE、GONE、VISIBLE这三个变量,应该是我们最常用的了,有没有思考系统是怎么实现的呢?

ViewGroup

首先要明确一点,通常使用的View都是放在ViewGroup以及ViewGroup子类的,大小都是在父控件的onMeasure和onLayout来进行确定。我们从最简单的FrameLayout原来来看看,它是怎么实现View的GONE操作的。

onMeasure阶段:
   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
		//判断自己是不是EXACTLY,对应的就是判断自己是不是Match_Parent
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
		//获取最大宽度与高度
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //需要测量所有子view||不为GONE时,才需要测量
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
		//加上pandding
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
		//.....
		//设置好FrameLayout的高度和宽度.此时FrameLayout的measureHeight与MeasureWidth已经有值了
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
         //.....               
        }
    }

上面的onMeasure其实就做一件事,就是把FrameLayout的maxWidth和maxHeight算出来。在计算时过滤掉mMeasureAllChildren = false是的child是gone的情况,那么问题来了,这个mMeasureAllChildren是做什么的呢?看源码的赋值,我们找到方法

  /**
     * Sets whether to consider all children, or just those in
     * the VISIBLE or INVISIBLE state, when measuring. Defaults to false.
     *
     * @param measureAll true to consider children marked GONE, false otherwise.
     * Default value is false.
     *
     * @attr ref android.R.styleable#FrameLayout_measureAllChildren
     */
    @android.view.RemotableViewMethod
    public void setMeasureAllChildren(boolean measureAll) {
        mMeasureAllChildren = measureAll;
    }

看注释的意思是,当为true时,为GONE的所有children也会被测量。所以,也并不是说GONE了,我们就不会去测量这个child。得是FrameLayout这个measureAllChildren的属性为false时,GONE才不会去测量。为了验证我们的阅读的源码。我写出了以下例子:
假设我们有如下xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="wrap_content"
    android:layout_height="wrap_content"
    android:measureAllChildren="true"
    android:background="@color/black"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:text="Hello World!"
        android:background="@android:color/holo_red_dark"
      />
    <TextView
        android:layout_width="200dp"
        android:visibility="gone"
        android:layout_height="200dp"
        android:text="Hello World!"
        android:background="@android:color/holo_red_dark"
        />

</FrameLayout>

按照前面我们对measureAllChildren的解读,measureAllChildren为true,此时FrameLayout的黑色应该是200dp。看实验结果:
在这里插入图片描述
从实验结果看,我们的假设阅读源码得到的结论和实验结果是一致的。默认情况下mMeasureAllChildren是false,所以我们日常使用时,GONE实际上是不会计算实际大小的。

    @UnsupportedAppUsage
    boolean mMeasureAllChildren = false;

GONE的问题,搞清楚了,现在我们来看看测量里面INVISIBLE

 for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //需要测量所有子view||不为GONE时,才需要测量
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

在上一段代码中,并未有对INVISIBLE做一个拦截,也就是说,INVISIBLE在测量阶段和VISIBLE是一样的。

onLayout阶段

onLayout我们之前已经讲过,它不仅可以控制子view的位置,实际上,也可以控制子view的大小。翻开FrameLayout的源代码


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //和onMeasure相似的GONE判断
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

上述的代码,是Android SDK30中,FrameLayout的onLayout全部代码,其实上述代码做的事情也很简单,就是计算各种gravity.仅需关注child.getVisibility() != GONE 这一行,表示GONE的时候,child是不参与layout的。不参与layout代表什么呢?不参与layout即表示,子view不会被放到摆放在viewgroup中,即使你已经测量过了。

通过通读FrameLayout的源代码可以给我们提供的思考是什么?

  1. 如果我们自定义了ViewGroup.在测量阶段,我们需要处理View的GONE事件,否则View仍然会进入onMeasure。
  2. 并非GONE了,就不会被测量了。
  3. 分析一类问题,要从最简单的样本开始。

看完了ViewGroup的onMeasure和onLayout,我们也仅能知道为GONE并且mMeasureAllChildren == false时,child不会被layout.但是INVISIBLE还是会Layout的啊,那INVISIBLE这个漏网之鱼,他的控制显然就不是在ViewGroup层面来做的了。那我们就要去看看View的setVisibility

View

上面已经分析完了GONE,这里我们着重分析一下INVISIBLE。INVISIBLE的控制,一般我们通过

 /**
     * Set the visibility state of this view.
     *
     * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
     * @attr ref android.R.styleable#View_visibility
     */
    @RemotableViewMethod
    public void setVisibility(@Visibility int visibility) {
        setFlags(visibility, VISIBILITY_MASK);
    }

内部调用的其实是

  /**
     * Set flags controlling behavior of this view.
     *
     * @param flags Constant indicating the value which should be set
     * @param mask Constant indicating the bit range that should be changed
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    void setFlags(int flags, int mask) {
    	//判断现在的flags和之前的flag是否有变化
        int old = mViewFlags;
        mViewFlags = (mViewFlags & ~mask) | (flags & mask);
        int changed = mViewFlags ^ old;
        if (changed == 0) {
            return;
        }
        int privateFlags = mPrivateFlags;
        boolean shouldNotifyFocusableAvailable = false;

       
        final int newVisibility = flags & VISIBILITY_MASK;
        if (newVisibility == VISIBLE) {
            if ((changed & VISIBILITY_MASK) != 0) {
          		 //... 省略和焦点相关的代码 

                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(true);
                needGlobalAttributesUpdate(true);
            }
        }

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

            if (((mViewFlags & VISIBILITY_MASK) == GONE)) {
                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;
            }
        }

        /* 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)) {
                //... 省略和焦点相关的代码 
            }
        }

        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 parent = (ViewGroup) mParent;
                parent.onChildVisibilityChanged(this, (changed & VISIBILITY_MASK),
                        newVisibility);
                parent.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);
                }
            }
        }
    }

首先我们来看一下第一段源码:

  int old = mViewFlags;
  mViewFlags = (mViewFlags & ~mask) | (flags & mask);
  int changed = mViewFlags ^ old;
  if (changed == 0) {
      return;
  }

哦吼,一堆位运算。头大,我们来,弄个对照表

符号描述运算规则
&两个位都为 1 时,结果才为 1
I两个位都是 0 时,结果才为 0
^异或两个位相同时为 0,相异为 1
~取反0 变 1,1 变 0
<<左移各二进位全部左移若干位,高位丢弃,低位补 0
>>右移各二进位全部右移若干位,对无符号数,高位补 0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补 0 (逻辑右移)

我们知道mask的值是VISIBILITY_MASK = 0x0000000C ,GONG,VISIBLE,INVISIBLE的值:

	//二进制 0000
    public static final int VISIBLE = 0x00000000;

	//二进制 0100
    public static final int INVISIBLE = 0x00000004;

   	//二进制 1000
    public static final int GONE = 0x00000008;
	
	//二进制 1100
   static final int VISIBILITY_MASK = 0x0000000C;

首先来看这行

 mViewFlags = (mViewFlags & ~mask) | (flags & mask);

我们先来分析前半部分 (mViewFlags & ~mask) 这个mask的值是通过setVisibility 传过来的,值为VISIBILITY_MASK,二进制位1100,取反"~“后,0011,神奇的事情来了。此时无论mViewFlags是GONE、VISIBLE、INVISIBLE与”&"上mask的反码,都是0,那证明这段代码的意思就是把前面的标志位数据清除掉。

//推导过程
INVISIBLE & ~ VISIBILITY_MASK
0100 & ~1100
0100 & 0011
0000

VISIBLE & ~VISIBLE_MASK
0000 & ~1100
0000 & 0011
0000

GONE & ~VISIBLE_MASK
1000 & ~1100
1000 & 0011
0000

现在来分析后半部分 (flags & mask) ,上面我们讲到,&~是清除,那&是什么呢,推导一下吧

VISIBLE & ~VISIBLE_MASK
0000 & 1100
0000

INVISIBLE & VISIBILITY_MASK
0100 & 1100
0100

GONE & VISIBILITY_MASK
1000 & 1100
1000

相信你发现了吧,INVISIBLE、VISIBLE、GONE这三个标志位,&上mask之后,都是自己。前面讲了&~mask都是0,现在这个&mask都是自己,那不是相当于直接给mViewFlags赋值为flag就完事儿了吗?事情当然不会这么简单啦,接下来我们继续看

  int old = mViewFlags;
  mViewFlags = (mViewFlags & ~mask) | (flags & mask);
  int changed = mViewFlags ^ old;
  if (changed == 0) {
      return;
  }

这里面还有一个关键代码:int changed = mViewFlags ^ old;,old表示的是上一轮的flags,mViewFlags表示的是当前设置成的flag,老规矩推导一下吧

INVISIBILE ^ VISIBLE
0100 ^ 0000
0100

GONE ^ VISIBLE
1000 ^ 0000
1000

GONE ^ INVISIBILE
1000 ^ 0100
1100

GONE ^ GONE
1000 ^ 1000
0000

INVISIBLE^INVISIBLE
0100 ^ 0100
0000

从推导可以看出,如果上一轮和这一轮的flag不同,那change >0,如果相同,chang==0。change等于0就直接return了。那么现在可以进行总结一下了,这里的位运算主要是用来计算和上一轮的标志位是否一致。

接下来,我们接着分析

final int newVisibility = flags & VISIBILITY_MASK;
if (newVisibility == VISIBLE) {
    if ((changed & VISIBILITY_MASK) != 0) {
  		 //... 省略和焦点相关的代码 

        mPrivateFlags |= PFLAG_DRAWN;
        invalidate(true);
        needGlobalAttributesUpdate(true);
    }
}

前面我们讲到了flags&mask的结果最后还是等于flag,changed&mask同理,这里代码就很好理解了,当心的flags为visible,且上一轮不是visible,那这里就执行invalidate刷新界面。
那我还很好奇invalidate里面是怎么写的。


    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        if (mGhostView != null) {
            mGhostView.invalidate(true);
            return;
        }

        if (skipInvalidate()) {
            return;
        }

      //...将view画到屏幕上
    }

mGhostView这个是view上面的一层overlay先不用管,通常情况下是为空的。着重看一下skipInvalidate.源码如下


    /**
     * Do not invalidate views which are not visible and which are not running an animation. They
     * will not get drawn and they should not set dirty flags as if they will be drawn
     */
    private boolean skipInvalidate() {
        return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
                (!(mParent instanceof ViewGroup) ||
                        !((ViewGroup) mParent).isViewTransitioning(this));
    }

这段代码就比较简单了。如果不是VISIBLE状态和没有动画并且父view不是viewgroup,就跳过。假设我们将一个viewgroup的子view设置为gone,并且没有动画,那我们就直接skip了,即不会再显示。到此为止,INVISIBLE、GONE、VISIBLE的的实现已经全线完了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
当将视图从 `INVISIBLE`(不可见)状态转换为 `VISIBLE`(可见)状态时,有可能会出现短暂的黑屏问题。这是因为在转换期间,视图的重新绘制可能需要一些时间。 要解决这个问题,可以尝试以下方法: 1. 使用 `ViewPropertyAnimator`: 使用 `ViewPropertyAnimator` 可以实现平滑的动画过渡效果,同时避免出现黑屏问题。下面是一个示例代码,演示如何使用 `ViewPropertyAnimator` 将视图从 `INVISIBLE` 转换为 `VISIBLE`: ```java View view = findViewById(R.id.my_view); // 先将视图的透明度设置为 0 view.setAlpha(0f); // 将视图设置为可见,并通过 ViewPropertyAnimator 将透明度渐变为 1 view.setVisibility(View.VISIBLE); view.animate().alpha(1f).setDuration(500).start(); ``` 2. 使用延迟开启过渡动画: 可以使用延迟开启过渡动画的方式,等待视图完成重新绘制后再播放动画。下面是一个示例代码,演示如何使用延迟开启过渡动画来避免黑屏问题: ```java View view = findViewById(R.id.my_view); // 将视图设置为可见 view.setVisibility(View.VISIBLE); // 延迟一段时间后播放过渡动画 view.postDelayed(new Runnable() { @Override public void run() { TransitionManager.beginDelayedTransition((ViewGroup) view.getParent()); // 这里可以添加其他视图变化的动画效果 } }, 100); // 延迟 100 毫秒后播放动画 ``` 通过延迟开启过渡动画,可以给视图足够的时间进行重新绘制,从而避免黑屏问题。 希望以上方法能够帮助你解决 `INVISIBLE` 到 `VISIBLE` 过程中的黑屏问题!如果有任何进一步的问题,请随时提问!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值