Android 透明状态栏实现方案

10,955 阅读5分钟
原文链接: blog.csdn.net

透明状态栏(Translucent Bars)是从 Android 4.4 开始出现的,到了 Android 5.0 的时候,Android 又推出了沉浸模式(Immersive Mode),于是混淆了不少人。

其实像QQ、QQ音乐这种效果的叫做透明状态栏;而沉浸模式多用于视频或阅读类 APP ,在用户需要显示状态栏时,用手指在屏幕顶部向下划动可以显示一个透明状态栏,几秒后会自动隐藏;沉浸模式最早出现在 MIUI 2.3 上,当时泛称叫做全屏下拉状态栏。

本文主要目的是提供一种实现透明状态栏的比较完善的思路。

下面是样例代码,后面会有讲解:

values-v19


    
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryDark">@color/primary_dark</item>
        <item name="colorAccent">

        <item name="android:windowTranslucentStatus">true</item>
    

MainActivity 布局代码:




    

    

MainActivity 代码:

public class MainActivity extends AppCompatActivity {

    private Toolbar mToolbar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mToolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(mToolbar);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_statusbar, menu);
        return super.onCreateOptionsMenu(menu);
    }
}

DetailActivity 布局代码




    

        

            

            

        

    

    

    

DetailActivity

public class DetailActivity extends AppCompatActivity {

    private Toolbar mToolbar;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_detail);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }

        mToolbar = (Toolbar) findViewById(R.id.id_toolbar);
        setSupportActionBar(mToolbar);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                ActivityCompat.finishAfterTransition(this);
                return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

在 style.xml 中设置了 true 属性后,会发现出现了下面这种情况:
Toolbar 到了状态栏的下面

图一

那么这种情况,网上也已经给出了相关的解决方法:在布局里面设置 android:fitsSystemWindows="true" 属性。

但是这不是最终解决方案,因为我们想要这样的效果:

图二

当然,这在 5.0 以上是可以轻松实现的,但是在 Android 4.4 上,却是这样子的:

图三

背景图并没有铺满到状态栏的位置,所以,为了兼容 Android 4.4 ,我们不使用 android:fitsSystemWindows="true" 属性。

有研究过的朋友,可能已经知道我要做什么了。

原理也很简单:就是给 Toolbar 设置一个 PaddingTop

  • 如果有使用过 SystemBarTint
    这个库的朋友,应该能看的出来,SystemBarTint 也是通过获取 状态栏高度 来实现的。
  • SystemBarTint
    会新建一个高度等于状态栏高度的 View ,并把 View 添加到 DecorView 。
  • SystemBarTint 这种方法不是很好,这样 ContentView
    区域就少了一块,而且给 Toolbar 设置背景图片也不能铺满到状态栏位置。

自定义 Toolbar:

public class CompatToolbar extends Toolbar {

    private boolean mLayoutReady;

    public CompatToolbar(Context context) {
        this(context, null);
    }

    public CompatToolbar(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, android.support.v7.appcompat.R.attr.toolbarStyle);
    }

    public CompatToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);

        if (!mLayoutReady) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                if ((getWindowSystemUiVisibility() &
                        (SYSTEM_UI_FLAG_LAYOUT_STABLE|SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)) ==
                        (SYSTEM_UI_FLAG_LAYOUT_STABLE|SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)) {
                    int statusBarHeight = getStatusBarHeight();
                    ViewGroup.LayoutParams params = getLayoutParams();
                    params.height = getHeight() + statusBarHeight;
                    setPadding(0, statusBarHeight, 0, 0);
                }
            }

            mLayoutReady = true;
        }
    }

    private int getStatusBarHeight() {
        Resources res = Resources.getSystem();
        int resId = res.getIdentifier("status_bar_height", "dimen", "android");
        if (resId > 0) {
            return res.getDimensionPixelSize(resId);
        }
        return 0;
    }
}

自定义 CompatToolbar 主要通过 (getWindowSystemUiVisibility() &
(SYSTEM_UI_FLAG_LAYOUT_STABLE|SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)) ==
(SYSTEM_UI_FLAG_LAYOUT_STABLE|SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
判断是不是透明状态栏,如果是就执行里面的内容:

  • 获取状态栏高度。
  • 修改 父ViewGroup 的 LayoutParams 的高度为 原高度 + 状态栏高度
  • 最后给 Toolbar 设置 PaddingTop 以保证 Toolbar 显示在状态栏下方。

下面修改一下布局,把我们的自定义 CompatToolbar 替换掉原的 Toolbar:

效果图一

效果图二

Android 4.4 上效果出来了,已经实现了我们想要的效果。

但是, Android 5.0+ 上却出问题了;如果已经尝试过自定义 Toolbar 解决 Android 4.4 兼容的朋友可能已经遇到过了:
(由于不会搞 GIF 图,所以只截了两张图,一张是 Toolbar 收起前,一张是 Toolbar 收起后)

Toolbar 收起前

Toolbar 收起后

这又是什么情况??

经过多次查看源码和 Debug 调试,终于发现了问题所在:

在 CollapsingToolbarLayout 的 onLayout 方法中:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

        ......

        
        for (int i = 0, z = getChildCount(); i < z; i++) {
            final View child = getChildAt(i);

            if (mLastInsets != null && !ViewCompat.getFitsSystemWindows(child)) {
                final int insetTop = mLastInsets.getSystemWindowInsetTop();
                if (child.getTop() < insetTop) {
                    
                    
                    ViewCompat.offsetTopAndBottom(child, insetTop);
                }
            }

            getViewOffsetHelper(child).onViewLayout();
        }

        ......
    }

在 Android 5.0 以上的系统中, mLastInsets 这个参数是不为空的:

  • 先通过mLastInsets.getSystemWindowInsetTop() 拿到状态栏高度。
  • 然后通过 ViewCompat.offsetTopAndBottom(child, insetTop) 去给子 View (CollapsingToolbarLayout 布局内的 ImageView 和 Toolbar)设置顶部偏移量为 状态栏高度。

那么 mLastInsets 又是什么呢?

private WindowInsetsCompat mLastInsets;

从名字可以看出来:窗口插图。那么它是什么时候初始化的呢?

    public CollapsingToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr) {

        ......

        ViewCompat.setOnApplyWindowInsetsListener(this,
                new android.support.v4.view.OnApplyWindowInsetsListener() {
                    @Override
                    public WindowInsetsCompat onApplyWindowInsets(View v,
                            WindowInsetsCompat insets) {
                        return setWindowInsets(insets);
                    }
                });
    }

    private WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) {
        if (mLastInsets != insets) {
            mLastInsets = insets;
            requestLayout();
        }
        return insets.consumeSystemWindowInsets();
    }

从上面的代码可以看出来, CollapsingToolbarLayout 是在初始化的时候给自己设置了一个窗口插图监听器

    /**
     * Set an {@link OnApplyWindowInsetsListener} to take over the policy for applying
     * window insets to this view. This will only take effect on devices with API 21 or above.
     */
    public static void setOnApplyWindowInsetsListener(View v,
            OnApplyWindowInsetsListener listener) {
        IMPL.setOnApplyWindowInsetsListener(v, listener);
    }

查看 ViewCompat.setOnApplyWindowInsetsListener() 源码可以看到,注释里面写着:此方法只对 Api 21 以上有效

那么现在可以理解为什么 Android 4.4 上的 mLastInsets 为空了;ViewCompat.setOnApplyWindowInsetsListener() 原来就是为了实现 Android 5.0 以上的插图效果的。

现在清楚了,只要 mLastInsets 为空就能解决这个问题了,可是翻遍了 CollapsingToolbarLayout 的源码只有一个设置 mLastInsets 方法:

private WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) {
        if (mLastInsets != insets) {
            mLastInsets = insets;
            requestLayout();
        }
        return insets.consumeSystemWindowInsets();
    }

可以看到这是一个私有的方法,也就是说我们用不了这个方法(就算能用也不能传个 null 进去吧……)。

自定义 CompatCollapsingToolbarLayout:

public class CompatCollapsingToolbarLayout extends CollapsingToolbarLayout {

    private boolean mLayoutReady;

    public CompatCollapsingToolbarLayout(Context context) {
        this(context, null);
    }

    public CompatCollapsingToolbarLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CompatCollapsingToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (!mLayoutReady) {
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) {
                if ((getWindowSystemUiVisibility() &
                        (SYSTEM_UI_FLAG_LAYOUT_STABLE|SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)) ==
                        (SYSTEM_UI_FLAG_LAYOUT_STABLE|SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)) {
                    try {
                        Field mLastInsets = CollapsingToolbarLayout.class.getDeclaredField("mLastInsets");
                        mLastInsets.setAccessible(true);
                        mLastInsets.set(this, null);
                    } catch (NoSuchFieldException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }

            mLayoutReady = true;
        }

        super.onLayout(changed, left, top, right, bottom);
    }
}

还是老方法,通过 (getWindowSystemUiVisibility() &
(SYSTEM_UI_FLAG_LAYOUT_STABLE|SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)) ==
(SYSTEM_UI_FLAG_LAYOUT_STABLE|SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
判断是不是透明状态栏,如果是:

  • 通过反射拿到 CollapsingToolbarLayout 中的变量 mLastInsets ,然后置为空。

下面是 Androi 5.0+ 效果图:

效果图三

效果图四

至此,大功告成!

/**************************************************/

也许有人会考虑另一种方法:

  • 只自定义CollapsingToolbarLayout,通过反射给 private WindowInsetsCompat setWindowInsets(windowinsets) 方法设值, 结合 android:fitsSystemWindows="true" ,去兼容 Android 4.4

这样不是更简单吗?

我只能很遗憾的告诉你,WindowInsets 相关的类和方法是 API 20+ 才有的。

源码地址:github.com/fanxin92/Tr…