1. 程式人生 > >Android標題欄、狀態列圖示文字顏色及背景動態變化

Android標題欄、狀態列圖示文字顏色及背景動態變化

android中沉浸式狀態列的文章已經滿大街了,可是在實現某些效果時,還是得各種搜尋,測試一通後,最後還常常滿足不了要求,即使好不容易在一部手機上滿足了需求,放在另外一手機上,發現效果還各種不適配。今天把自己這幾天學到的關於沉浸式狀態列知識進行總結下。

問題

比如我想實現以下效果:

  1. 同一個Activity需要動態變換標題欄和狀態列文字字型色值,該如何實現?

  2. 一個Activity包含多個Fragment切換時,不同的Fragment的狀態列背景,狀態列文字顏色和圖示要求不一樣怎麼實現?

  3. 設定沉浸式狀態列,各個android版本之間差別如何,那麼多flag,長得都一樣,都有什麼區別?

無圖無真相,帶著這幾個問題,先上兩張我實現的效果圖。

下面是同一個activity切換不同fragment時,狀態列文字顏色跟著變化的效果圖:

同一個activity切換不同fragment時,狀態列文字顏色跟著變化

下圖是同一個Activity向上滾動時,標題欄和狀態列文字顏色根據變化的效果:

Activity向上滾動時,標題欄和狀態列文字顏色根據變化

1. 實現透明狀態列常規方法

protected boolean useThemestatusBarColor = false;//是否使用特殊的標題欄背景顏色,android5.0以上可以設定狀態列背景色,如果不使用則使用透明色值
    protected boolean useStatusBarColor = true;//是否使用狀態列文字和圖示為暗色,如果狀態列採用了白色系,則需要使狀態列和圖示為暗色,android6.0以上可以設定

protected void setStatusBar() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {//5.0及以上
            View decorView = getWindow().getDecorView();
            int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
            decorView.setSystemUiVisibility(option);
            //根據上面設定是否對狀態列單獨設定顏色
            if (useThemestatusBarColor) {
                getWindow().setStatusBarColor(getResources().getColor(R.color.colorTheme));
            } else {
                getWindow().setStatusBarColor(Color.TRANSPARENT);
            }
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {//4.4到5.0
            WindowManager.LayoutParams localLayoutParams = getWindow().getAttributes();
            localLayoutParams.flags = (WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS | localLayoutParams.flags);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !withoutUseStatusBarColor) {//android6.0以後可以對狀態列文字顏色和圖示進行修改
            getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        }
    }

在Activity佈局的根節點處加上android:fitsSystemWindows=”true”屬性就可以了,要不佈局會跑到狀態列和導航欄下面,與導航欄和狀態列重疊,這當然不是我們希望的。

Activity通過上面的設定,可以實現如下效果:

狀態列文字顏色和圖示為黑色

上面設定狀態列文字顏色和圖示為暗色主要採用了以下兩個標誌:

//設定狀態列文字顏色及圖示為深色
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
  • View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 是從API 16開始啟用,實現效果:
    檢視延伸至狀態列區域,狀態列懸浮於檢視之上

  • View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 是從 API 23開始啟用,實現效果:
    設定狀態列圖示和狀態列文字顏色為深色,為適應狀態列背景為淺色調,該Flag只有在使用了FLAG_DRWS_SYSTEM_BAR_BACKGROUNDS,並且沒有使用FLAG_TRANSLUCENT_STATUS時才有效,即只有在透明狀態列時才有效。

2. 同一個Activity包含多個Fragment時,如何實現不同fragment的狀態列背景和文字顏色不一樣

如下面的效果圖:

同一個activity切換不同fragment時,狀態列文字顏色跟著變化

就是設定了狀態列為暗色後,還得設定回來,這其實主要靠下面兩個flag標識,結全上面的兩個flag標識就能實現。

//設定狀態列文字顏色及圖示為淺色
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
  • View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 前面說過了,是為了讓檢視能延伸到狀態列區域,使狀態列懸浮在檢視佈局之上。

  • View.SYSTEM_UI_FLAG_LAYOUT_STABLE
    保持整個View穩定, 常和控制System UI懸浮, 隱藏的Flags共用, 使View不會因為System UI的變化而重新layout。

將上面的程式碼放在不同fragment切換處即可實現上面的效果了:

private void switchTo(int position) {
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        switch (position) {            
            case 0://首頁                
            hideShowFragment(transaction, fourFragment, thirdFragment, secondFragment, homeFragment);//展示第一個fragment
getWindow().getDecorView().setSystemUiVisibility(
                        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
                getWindow().getDecorView().findViewById(android.R.id.content).setPadding(0, 0, 0, CommonUtils.navigationHeight);
                break;

            case 1:  //活動                
            hideShowFragment(transaction, homeFragment, thirdFragment, fourFragment, secondFragment);//展示第二個fragment
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//實現狀態列圖示和文字顏色為暗色                     
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
                }
                getWindow().getDecorView().findViewById(android.R.id.content).setPadding(0, 0, 0, CommonUtils.navigationHeight);
                break;
       case 2:  //所有圖片    
hideShowFragment(transaction, homeFragment, fourFragment, secondFragment, thirdFragment);
//展示第三個fragment
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//實現狀態列圖示和文字顏色為暗色                          
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
                }
                getWindow().getDecorView().findViewById(android.R.id.content).setPadding(0, 0, 0, CommonUtils.navigationHeight);
                break;

            case 3://我的
              hideShowFragment(transaction, homeFragment, secondFragment, thirdFragment, fourFragment);//展示第四個fragment
 //實現狀態列圖示和文字顏色為淺色                     
 getWindow().getDecorView().setSystemUiVisibility(
                        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
                getWindow().getDecorView().findViewById(android.R.id.content).setPadding(0, 0, 0, CommonUtils.navigationHeight);
                break;
            default:
                break;
        }

    }

//fragment切換實現
private void hideShowFragment(FragmentTransaction transaction, Fragment fragment1, Fragment fragment2, Fragment fragment3, Fragment fragment4) {
        transaction.hide(fragment1);
        transaction.hide(fragment2);
        transaction.hide(fragment3);
        transaction.show(fragment4);
        transaction.commitAllowingStateLoss();
    }

大家可能會注意到,我這裡切換每個fragment時,有下面這樣一行程式碼:

getWindow().getDecorView().findViewById(android.R.id.content).setPadding(0, 0, 0, CommonUtils.navigationHeight);

這行程式碼幹什麼用的,因為我們這裡首頁和我的頁面,需要背景圖片填充到狀態列,故不能使用android:fitsSystemWindows屬性,故在實現上面效果時帶有底部導航欄手機上就會存在一個大坑,解決辦法見第3章節。同時不使用android:fitsSystemWindows屬性,怎麼讓佈局不遮擋狀態列文字,解決辦法見第4章節。

3. 帶有底部導航欄手機底部導航按鈕會和navigationbar重疊

如下圖所示:

底部導航欄與底部按鈕重疊

全屏時,由於檢視佈局會填充到狀態列和導航欄下方,如果不使用android:fitsSystemWindows=”true”屬性,就會使底部導航欄和應用底部按鈕重疊,導視按鈕點選失效,這該怎麼辦?
經過網上搜索相關資料,其實實現方法和實現透明狀態列效果方法一致。
解決的方法:

  1. 先判斷手機是否有物理按鈕判斷是否存在NavigationBar;

  2. 計算底部的NavigationBar高度;

  3. 最後設定檢視邊距。

3.1 通過反射判斷手機是否有物理按鈕NavigationBar

//判斷是否存在NavigationBar
public static boolean checkDeviceHasNavigationBar(Context context) {
    boolean hasNavigationBar = false;
    Resources rs = context.getResources();
    int id = rs.getIdentifier("config_showNavigationBar", "bool", "android");
    if (id > 0) {
        hasNavigationBar = rs.getBoolean(id);
    }
    try {
        Class systemPropertiesClass = Class.forName("android.os.SystemProperties");
        Method m = systemPropertiesClass.getMethod("get", String.class);
        String navBarOverride = (String) m.invoke(systemPropertiesClass, "qemu.hw.mainkeys");
        if ("1".equals(navBarOverride)) {
            hasNavigationBar = false;
        } else if ("0".equals(navBarOverride)) {
            hasNavigationBar = true;
        }
    } catch (Exception e) {

    }
    return hasNavigationBar;

}

3.2 計算底部的NavigationBar高度

/**
 * 獲取底部導航欄高度
 * @return
 */
public static int getNavigationBarHeight(Context context) {
    Resources resources = context.getResources();
    int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
    //獲取NavigationBar的高度
    navigationHeight = resources.getDimensionPixelSize(resourceId);
    return navigationHeight;
}

3.3 設定檢視邊距

getWindow().getDecorView().findViewById(android.R.id.content).setPadding(0, 0, 0, CommonUtils.navigationHeight);

通過上面的設定,會使佈局距離底部導航欄的高度。
最後實現效果如下:

導航欄與底部按鈕不重疊

參考文章:android 6.0導航欄 NavigationBar影響檢視解決辦法

4. 不使用fiySystemWindow屬性,佈局怎麼能不遮擋狀態列文字

跟第三章節類似,在主頁中,需要使佈局中帶文字的佈局向上margin狀態列的高度。

4.1 先在佈局中設定一個佔空LinearLayout

我們先來看看第三個Fragment的佈局實現

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <!--這個是隱藏的佈局,然後通過動態的設定高度達到效果-->
        <LinearLayout
            android:id="@+id/ll_bar"
            android:layout_width="fill_parent"
            android:layout_height="1dp"
            android:orientation="vertical"
            android:background="@color/white"
            android:visibility="gone">
        </LinearLayout>

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="@dimen/toollbar_height"
            android:background="@drawable/topbar_generic">
            <TextView
                android:id="@+id/title_tv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:layout_gravity="center"
                android:textColor="@color/text_main_color"
                android:text="@string/all_photo"
                android:textSize="20sp" />
            <TextView
                android:id="@+id/titlebar_right_tv"
                style="@style/main_content_text_style"
                android:layout_alignParentRight="true"
                android:layout_margin="@dimen/margin_10dp"
                android:gravity="center_vertical"
                android:text="@string/confirm"/>
        </RelativeLayout>
        <FrameLayout
            android:id="@+id/fmImageList"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/white" />
    </LinearLayout>
</layout>

4.2 在程式碼中動態設定佔空佈局高度

/**
 * 動態的設定狀態列  實現沉浸式狀態列
 */
private void initState() {

    //當系統版本為4.4或者4.4以上時可以使用沉浸式狀態列
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        //透明狀態列
        getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        //透明導航欄
        getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
        bindingView.llBar.setVisibility(View.VISIBLE);
        //獲取到狀態列的高度
        int statusHeight = CommonUtils.getStatusBarHeight(getActivity());
        //動態的設定隱藏佈局的高度
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) bindingView.llBar.getLayoutParams();
        params.height = statusHeight;
        bindingView.llBar.setLayoutParams(params);
       }
}

/**
 * 通過反射的方式獲取狀態列高度
 *
 * @return
 */
public static int getStatusBarHeight(Context context) {
    try {
        Class<?> c = Class.forName("com.android.internal.R$dimen");
        Object obj = c.newInstance();
        Field field = c.getField("status_bar_height");
        int x = Integer.parseInt(field.get(obj).toString());
        return context.getResources().getDimensionPixelSize(x);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return 0;
}

對於上面的第二個和第三個fragment的實現,為了讓檢視佈局不遮擋狀態列文字,主要是通過先給介面設定佔位佈局,然後在程式碼中動態設定該佈局為狀態列高度,這其實就是讓狀態列懸浮在這個佔空佈局上面。檢視佈局位於佔空佈局下方,從而達到檢視佈局不遮擋狀態列效果。
上面對於版本的判斷,如果android版本大於4.4, 則讓該佈局顯示出來,而版本低於4.4, 由於沒有沉浸式狀態列效果,則不需要給介面設定佔空佈局。

而對於第一個首頁和第四個我的fragment,則需要佈局的圖片填充到狀態列底下,而標題欄要位於狀態列下方,這其實只需要一種取巧實現,一般手機狀態列高度都是在25dp左右,當然在程式碼中動態獲取狀態列高度,動態設定也可以。我這裡是簡單實現,讓標題欄marginTop狀態列高度即可,對於android不同版本,可以如下設定。
對於values中dimens.xml設定狀態列的高度:

<dimen name="status_bar_height">0dp</dimen>

對於values-v19中dimens.xml設定狀態列的高度:

<dimen name="status_bar_height">25dp</dimen>

5. 同一個Activity上下滑動動態變換標題欄和狀態列文字字型色值

效果如下:

Activity向上滾動時,標題欄和狀態列文字顏色根據變化

這種佈局實現主要是依靠CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout+Toolbar+NestedScrollView來實現,之前我也寫過類似的博文來介紹CoordinatorLayout的使用方法。感興趣的小夥伴可以參下:android沉浸式狀態列、fitsSystemWindows、標題欄摺疊
下面我們說說怎麼在介面滑動時,修改狀態列和標題欄文字顏色。

這個主要通過監聽AppBarLayout滑動的距離,向上滑動,如果大於標題欄的高度,則要動態改變標題欄文字顏色,當標題欄摺疊時,改變狀態列文字顏色及返回銨鈕圖示,同時狀態列文字顏色變成暗色。
向下滑動時,隨著標題欄慢慢消失,需要把狀態列文字顏色變成淺色調。

private void setAppBarListener() {
        measureHeight();
        bindingView.appbar.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
            if (verticalOffset == 0) {
                if (state != CollapsingToolbarLayoutState.EXPANDED) {
                    state = CollapsingToolbarLayoutState.EXPANDED;//修改為展開狀態
                    bindingView.titleTv.setVisibility(View.GONE);
                    bindingView.toolbar.setNavigationIcon(R.drawable.nav_icon_white_return);
                    getWindow().getDecorView().setSystemUiVisibility(
                            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_VISIBLE);
                }
            } else if (Math.abs(verticalOffset) >= appBarLayout.getTotalScrollRange()) {
                bindingView.titleTv.setVisibility(View.VISIBLE);
                bindingView.toolbar.setNavigationIcon(R.drawable.nav_icon_return);
                state = CollapsingToolbarLayoutState.COLLAPSED;//修改為摺疊狀態
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
                }
            } else {
                if (Math.abs(verticalOffset) > height) {
                    bindingView.titleTv.setVisibility(View.VISIBLE);
                    float scale =  1- height / (float) Math.abs(verticalOffset);
                    if (state != CollapsingToolbarLayoutState.INTERNEDIATE) {
                        if (state == CollapsingToolbarLayoutState.COLLAPSED &amp;&amp; scale < 0.55) {//由摺疊變為展開
                            bindingView.toolbar.setNavigationIcon(R.drawable.nav_icon_white_return);
                            getWindow().getDecorView().setSystemUiVisibility(
                                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_VISIBLE);
                        } else {
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                                getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
                            }
                        }

                        state = CollapsingToolbarLayoutState.INTERNEDIATE;
                    }
                    float alpha = (255 * scale);
                                      bindingView.titleTv.setTextColor(Color.argb((int) alpha, 53,55,58));

                    bindingView.toolbar.setNavigationIcon(R.drawable.nav_icon_return);
                } else {
                    bindingView.titleTv.setVisibility(View.GONE);
                    bindingView.toolbar.setNavigationIcon(R.drawable.nav_icon_white_return);
                }
            }
        });
    }

//獲取標題欄高度
    private void measureHeight() {
        ViewTreeObserver vto = bindingView.coordinatorlayout.getViewTreeObserver();

        vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            public boolean onPreDraw() {
                if (hasMeasured == false) {

                    height = bindingView.toolbar.getMeasuredHeight();
                    hasMeasured = true;

                }
                return true;
            }
        });
    }

總結

根據android提供的widnow的flag,狀態列淺色調和深色調,我們可以實時動態變換一個Activity的狀態列顏色,同時結合CoordinatorLayout,我們可以實現更加複雜的效果。

程式碼傳送門:
https://github.com/xiewenfeng/statusbartextcolorchange