状态栏UI效果仿写沉浸式

沉浸式状态栏,全屏模式原理解析一篇就够了

2019-09-15  本文已影响0人  暴走的小青春

现在很多app都有了沉浸式状态栏和全屏的样式,大多用的是第三方的库
包括但不仅限于:

https://github.com/niorgai/StatusBarCompat
https://github.com/jgilfelt/SystemBarTint

其实这两个库本质都是对android本身设置状态栏颜色或者全屏模式的封装
下面来分析下andoid原生设置状态栏颜色的方式

android 5.0以上设置状态栏颜色的方法:
getWindow().setStatusBarColor(Color.BLUE);

可以说这个代码是5.0以后设置状态栏颜色的方法,我们走到源码里看下
getWindow()返回的就是phoneWindow了
其实是在Activity的attacth里被初始化的

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,
            Window window, ActivityConfigCallback activityConfigCallback) {
...
        attachBaseContext(context);

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

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
...

可以看到在attach里phonewinow被初始化完成的
也就是调用了phonewindow的setStatusBarColor方法

@Override
  public void setStatusBarColor(int color) {
      mStatusBarColor = color;
      mForcedStatusBarColor = true;
      if (mDecor != null) {
          mDecor.updateColorViews(null, false /* animate */);
      }
  }

也就是在mDecor里被调用的,decorView其实是在setContentView中被创建出来的

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
...

当其不为null的时候就调用了其updateColorViews方法也是核心代码

private WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
        WindowManager.LayoutParams attrs = getAttributes();
        int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();

       int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();

        // IME is an exceptional floating window that requires color view.
        final boolean isImeWindow =
                mWindow.getAttributes().type == WindowManager.LayoutParams.TYPE_INPUT_METHOD;
        if (!mWindow.mIsFloating || isImeWindow) {
            boolean disallowAnimate = !isLaidOut();
            disallowAnimate |= ((mLastWindowFlags ^ attrs.flags)
                    & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
            mLastWindowFlags = attrs.flags;
            ...
            boolean statusBarNeedsRightInset = navBarToRightEdge
                    && mNavigationColorViewState.present;
            int statusBarRightInset = statusBarNeedsRightInset ? mLastRightInset : 0;
         
            updateColorViewInt(mStatusColorViewState, sysUiVisibility, mStatusBarColor,
                    mLastTopInset, false /* matchVertical */, statusBarRightInset,
                    animate && !disallowAnimate);
        }
        ...
    }

其中调用了updateColorViewInt这个方法,就是改变状态栏颜色的方法

 private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color,
            int dividerColor, int size, boolean verticalBar, boolean seascape, int sideMargin,
            boolean animate, boolean force) {
     //关键点1
        state.present = state.attributes.isPresent(sysUiVis, mWindow.getAttributes().flags, force);
        boolean show = state.attributes.isVisible(state.present, color,
                mWindow.getAttributes().flags, force);
     //关键点2
        boolean showView = show && !isResizing() && size > 0;

        boolean visibilityChanged = false;
        View view = state.view;

        int resolvedHeight = verticalBar ? LayoutParams.MATCH_PARENT : size;
        int resolvedWidth = verticalBar ? size : LayoutParams.MATCH_PARENT;
        int resolvedGravity = verticalBar
                ? (seascape ? state.attributes.seascapeGravity : state.attributes.horizontalGravity)
                : state.attributes.verticalGravity;

        if (view == null) {
            if (showView) {
                state.view = view = new View(mContext);
                setColor(view, color, dividerColor, verticalBar, seascape);
                view.setTransitionName(state.attributes.transitionName);
                view.setId(state.attributes.id);
                visibilityChanged = true;
                view.setVisibility(INVISIBLE);
                state.targetVisibility = VISIBLE;

                LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight,
                        resolvedGravity);
                if (seascape) {
                    lp.leftMargin = sideMargin;
                } else {
                    lp.rightMargin = sideMargin;
                }
                addView(view, lp);
                updateColorViewTranslations();
            }
        } else {
...

可以看到showView成立的条件是state. present成立,并且show成立
state. present的条件如下

 public boolean isPresent(int sysUiVis, int windowFlags, boolean force) {
            return (sysUiVis & systemUiHideFlag) == 0
                    && (windowFlags & hideWindowFlag) == 0
                    && ((windowFlags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
                    || force);
        }

其中systemUiHideFlag就是SYSTEM_UI_FLAG_FULLSCREEN, hideWindowFlag就是FLAG_FULLSCREEN
而FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS这个属性系统默认就是有的,后面那个force是大于等于7.0才有的,可以忽略不计
所以说满足isPresent的条件的话,必须是不设置全屏的属性
那满足show的条件呢???

public boolean isVisible(boolean present, int color, int windowFlags, boolean force) {
            return present
                    && (color & Color.BLACK) != 0
                    && ((windowFlags & translucentFlag) == 0  || force);
        }

首先showpresent为true,然后设置的颜色都是非透明的
black是##ff000000前两位代表了透明度
还有windowFlags不设置translucentFlag标签
也就是FLAG_TRANSLUCENT_STATUS
当然默认也没有设置过
所以才会给decorView添加一个有颜色的view

沉浸式状态栏在5.0以上能显示出来的条件是

不设置全屏的flag和FLAG_TRANSLUCENT_STATUS透明的flag

既然说到了沉浸式状态栏,就要说下android全屏的实现原理了
这里先用layout inspector抓取布局


屏幕快照 2019-09-01 下午7.46.59.png

这是创建默认的项目,可以看到phoneWindow下面是一个LinearLayout


屏幕快照 2019-09-01 下午7.46.44.png
这个Linearlayout有126像素的bottomMargin
屏幕快照 2019-09-01 下午7.46.52.png
还有个63像素的paddingtop

每个手机可能状态栏和下方的导航栏高度会有不同,那找到了系统的view

<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>

可以看到系统的LinearLayout并没有设置过marginBottom和paddingTop
那它的由来呢?
其实不难发现在我们设置状态栏颜色的方法中updateColorViews


private WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
       //设置状态栏颜色
   ... 
 boolean consumingNavBar =
                (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
                        && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
                        && (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0
                || mLastShouldAlwaysConsumeNavBar;
     boolean consumingStatusBar = (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
                && (sysUiVisibility & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0
                && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
                && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
                && mForceWindowDrawsStatusBarBackground
                && mLastTopInset != 0;

        int consumedTop = consumingStatusBar ? mLastTopInset : 0;
        int consumedRight = consumingNavBar ? mLastRightInset : 0;
        int consumedBottom = consumingNavBar ? mLastBottomInset : 0;
        int consumedLeft = consumingNavBar ? mLastLeftInset : 0;

        if (mContentRoot != null
                && mContentRoot.getLayoutParams() instanceof MarginLayoutParams) {
            MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams();
            if (lp.topMargin != consumedTop || lp.rightMargin != consumedRight
                    || lp.bottomMargin != consumedBottom || lp.leftMargin != consumedLeft) {
                lp.topMargin = consumedTop;
                lp.rightMargin = consumedRight;
                lp.bottomMargin = consumedBottom;
                lp.leftMargin = consumedLeft;
                mContentRoot.setLayoutParams(lp);

                if (insets == null) {
                    // The insets have changed, but we're not currently in the process
                    // of dispatching them.
                    requestApplyInsets();
                }
            }
            if (insets != null) {
                insets = insets.inset(consumedLeft, consumedTop, consumedRight, consumedBottom);
            }
        }

        if (insets != null) {
            insets = insets.consumeStableInsets();
        }
        return insets;
    }

在设置颜色下面有个consumingNavBar一旦为true就会并且
mContentRoot不为null就会设置mContentRoot的layoutParams
那上面的consumingNavBar这三个条件其实只要我们不主动设置隐藏导航栏的标记就是属于消耗掉了
关键是mContentRoot何时不为null和这个方法何时被调用的问题
其实mContentRoot在setContentView时就被赋值了

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        if (mBackdropFrameRenderer != null) {
            loadBackgroundDrawablesIfNeeded();
            mBackdropFrameRenderer.onResourcesLoaded(
                    this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
                    getCurrentColor(mNavigationColorViewState));
        }

        mDecorCaptionView = createDecorCaptionView(inflater);
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {

            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }


mContentParent = generateLayout(mDecor)中会调用onResourcesLoaded
所以mContentRoot就是Linearlayout,那我们默认也没有调用这个改变状态栏颜色方法啊
其实在activity的onResume时,也就是生成viewRootImp时

@Override
   public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
           String reason) {
       // If we are getting ready to gc after going to the background, well
       // we are back active so skip it.
       unscheduleGcIdler();
       mSomeActivitiesChanged = true;

       // TODO Push resumeArgs into the activity for consideration
       final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
       if (r == null) {
           // We didn't actually resume the activity, so skipping any follow-up actions.
           return;
       }

       final Activity a = r.activity;

       if (localLOGV) {
           Slog.v(TAG, "Resume " + r + " started activity: " + a.mStartedActivity
                   + ", hideForNow: " + r.hideForNow + ", finished: " + a.mFinished);
       }

       final int forwardBit = isForward
               ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

       // If the window hasn't yet been added to the window manager,
       // and this guy didn't finish itself or start another activity,
       // then go ahead and add the window.
       boolean willBeVisible = !a.mStartedActivity;
       if (!willBeVisible) {
           try {
               willBeVisible = ActivityManager.getService().willActivityBeVisible(
                       a.getActivityToken());
           } catch (RemoteException e) {
               throw e.rethrowFromSystemServer();
           }
       }
       if (r.window == null && !a.mFinished && willBeVisible) {
           r.window = r.activity.getWindow();
           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 (r.mPreserveWindow) {
               a.mWindowAdded = true;
               r.mPreserveWindow = false;
               // Normally the ViewRoot sets up callbacks with the Activity
               // in addView->ViewRootImpl#setView. If we are instead reusing
               // the decor view we have to notify the view root that the
               // callbacks may have changed.
               ViewRootImpl impl = decor.getViewRootImpl();
               if (impl != null) {
                   impl.notifyChildRebuilt();
               }
           }
           if (a.mVisibleFromClient) {
               if (!a.mWindowAdded) {
                   a.mWindowAdded = true;
                   wm.addView(decor, l);
               } else {
                   // The activity will get a callback for this {@link LayoutParams} change
                   // earlier. However, at that time the decor will not be set (this is set
                   // in this method), so no action will be taken. This call ensures the
                   // callback occurs with the decor set.
                   a.onWindowAttributesChanged(l);
               }
           }

           // If the window has already been added, but during resume
           // we started another activity, then don't yet make the
           // window visible.

来自activityThread的handleResumeActivity方法
可以看到在activity的resume方法以后,decorview才被添加到windowManager中,而windowManager通过代理调用了
WindowManagerGlobal的addview方法
里面有个

root = new ViewRootImpl(view.getContext(), display);

            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 {
                root.setView(view, wparams, panelParentView);

也就是setView方法,从而进行了第一次的requestlayout方法
当然requestlayout会遍历view树,从而调用了viewRootImp的performTraversals方法

private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;

        if (DBG) {
            System.out.println("======================================");
            System.out.println("performTraversals");
            host.debug();
        }

        if (host == null || !mAdded)
            return;

        mIsInTraversal = true;
        mWillDrawSoon = true;
        boolean windowSizeMayChange = false;
        boolean newSurface = false;
        boolean surfaceChanged = false;
        WindowManager.LayoutParams lp = mWindowAttributes;

        int desiredWindowWidth;
        int desiredWindowHeight;

        final int viewVisibility = getHostVisibility();
        final boolean viewVisibilityChanged = !mFirst
                && (mViewVisibility != viewVisibility || mNewSurfaceNeeded
                // Also check for possible double visibility update, which will make current
                // viewVisibility value equal to mViewVisibility and we may miss it.
                || mAppVisibilityChanged);
        mAppVisibilityChanged = false;
        final boolean viewUserVisibilityChanged = !mFirst &&
                ((mViewVisibility == View.VISIBLE) != (viewVisibility == View.VISIBLE));

        WindowManager.LayoutParams params = null;
        if (mWindowAttributesChanged) {
            mWindowAttributesChanged = false;
            surfaceChanged = true;
            params = lp;
        }
        CompatibilityInfo compatibilityInfo =
                mDisplay.getDisplayAdjustments().getCompatibilityInfo();
        if (compatibilityInfo.supportsScreen() == mLastInCompatMode) {
            params = lp;
            mFullRedrawNeeded = true;
            mLayoutRequested = true;
            if (mLastInCompatMode) {
                params.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
                mLastInCompatMode = false;
            } else {
                params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
                mLastInCompatMode = true;
            }
        }

        mWindowAttributesChangesFlag = 0;

        Rect frame = mWinFrame;
        if (mFirst) {
            mFullRedrawNeeded = true;
            mLayoutRequested = true;

            final Configuration config = mContext.getResources().getConfiguration();
            if (shouldUseDisplaySize(lp)) {
                // NOTE -- system code, won't try to do compat mode.
                Point size = new Point();
                mDisplay.getRealSize(size);
                desiredWindowWidth = size.x;
                desiredWindowHeight = size.y;
            } else {
                desiredWindowWidth = mWinFrame.width();
                desiredWindowHeight = mWinFrame.height();
            }

            // We used to use the following condition to choose 32 bits drawing caches:
            // PixelFormat.hasAlpha(lp.format) || lp.format == PixelFormat.RGBX_8888
            // However, windows are now always 32 bits by default, so choose 32 bits
            mAttachInfo.mUse32BitDrawingCache = true;
            mAttachInfo.mHasWindowFocus = false;
            mAttachInfo.mWindowVisibility = viewVisibility;
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mLastConfigurationFromResources.setTo(config);
            mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
            // Set the layout direction if it has not been set before (inherit is the default)
            if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
                host.setLayoutDirection(config.getLayoutDirection());
            }
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
            dispatchApplyInsets(host);

也就是measure和layout的开始,也是分发状态栏的开始
大家可以看我的这篇文章https://www.jianshu.com/p/8ad3dea371bc
里面有详细的调用过程
而在decorview重写onApplyWindowInsets时

@Override
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        final WindowManager.LayoutParams attrs = mWindow.getAttributes();
        mFloatingInsets.setEmpty();
        if ((attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0) {
            // For dialog windows we want to make sure they don't go over the status bar or nav bar.
            // We consume the system insets and we will reuse them later during the measure phase.
            // We allow the app to ignore this and handle insets itself by using
            // FLAG_LAYOUT_IN_SCREEN.
            if (attrs.height == WindowManager.LayoutParams.WRAP_CONTENT) {
                mFloatingInsets.top = insets.getSystemWindowInsetTop();
                mFloatingInsets.bottom = insets.getSystemWindowInsetBottom();
                insets = insets.inset(0, insets.getSystemWindowInsetTop(),
                        0, insets.getSystemWindowInsetBottom());
            }
            if (mWindow.getAttributes().width == WindowManager.LayoutParams.WRAP_CONTENT) {
                mFloatingInsets.left = insets.getSystemWindowInsetTop();
                mFloatingInsets.right = insets.getSystemWindowInsetBottom();
                insets = insets.inset(insets.getSystemWindowInsetLeft(), 0,
                        insets.getSystemWindowInsetRight(), 0);
            }
        }
        mFrameOffsets.set(insets.getSystemWindowInsets());
        insets = updateColorViews(insets, true /* animate */);
        insets = updateStatusGuard(insets);
        if (getForeground() != null) {
            drawableChanged();
        }
        return insets;
    }

调用了updateColorViews进行margin的设置,关于fitSystemWindows的分发在以前的文章介绍过了

最后再来说下4.4手机设置状态栏颜色方法
由于4.4手机没有setStatusBarColor方法,所以是采用FLAG_TRANSLUCENT_STATUS加fitSystemWindows的方法的

上一篇下一篇

猜你喜欢

热点阅读