問題描述
一般我們設(shè)置透明狀態(tài)欄的時候都是通過下面代碼進(jìn)行設(shè)置
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
然后在使用Toolbar的時候設(shè)置fitsSystemWindows="true"就可以成功設(shè)置透明狀態(tài)欄了,這里還通過設(shè)置android:minHeight和maxButtonHeight將默認(rèn)的Toolbar的高度56dp改為48dp。
<style name="toolbarStyle" parent="Base.Widget.AppCompat.Toolbar" >
<item name="android:background">@color/colorPrimary</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">match_parent</item>
<item name="android:fitsSystemWindows">true</item>
<item name="theme">@style/ToolbarTheme</item>
<item name="popupTheme">@style/ToolbarPopupTheme</item>
<item name="titleTextAppearance">@style/ToolbarTitle</item>
<item name="android:minHeight">48dp</item>
<item name="maxButtonHeight">48dp</item>
</style>
然而這種設(shè)置方法在有EditText的布局中就會出現(xiàn)問題,假如有以下帶有EditText的布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/toolbarStyle">
</android.support.v7.widget.Toolbar>
<EditText
android:id="@+id/edittext"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:background="#cccccc"
android:hint="我是EditText"
android:textColor="@android:color/black"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:layout_below="@id/toolbar"
android:layout_above="@id/edittext"
android:text=""/>
</RelativeLayout>
沒有彈出鍵盤的情況下,這個布局的效果如下圖所示。

當(dāng)Activity設(shè)置android:windowSoftInputMode="adjustPan"時,則會出現(xiàn)Toolbar被移出屏幕的情況。

當(dāng)Activity設(shè)置android:windowSoftInputMode="adjustResize"時,則會出現(xiàn)EditText被鍵盤覆蓋、Toolbar被拉伸的情況

問題分析
fitsSystemWindow屬性
根據(jù)官方對fitsSystemWindows屬性(鏈接)的描述,當(dāng)View的fitsSystemWindows設(shè)置為true的時候,系統(tǒng)會自動為該View設(shè)置相應(yīng)的padding以適應(yīng)鍵盤、狀態(tài)欄、導(dǎo)航欄等系統(tǒng)窗口,這就可以解釋為什么給Toolbar設(shè)置fitsSystemWindows之后Toolbar會自動加上paddingTop以適應(yīng)狀態(tài)欄,如果沒有加上fitsSystemWindows=true,Toolbar則會有部分被狀態(tài)欄覆蓋。
Called by the view hierarchy when the content insets for a window have changed, to allow it to adjust its content to fit within those windows. The content insets tell you the space that the status bar, input method, and other system windows infringe on the application's window.
You do not normally need to deal with this function, since the default window decoration given to applications takes care of applying it to the content of the window. If you use SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION this will not be the case, and your content can be placed under those system elements. You can then use this method within your view hierarchy if you have parts of your UI which you would like to ensure are not being covered.
關(guān)于fitsSystemWindows的源碼實(shí)現(xiàn)可以看這篇文章(鏈接)
EditText問題分析
當(dāng)設(shè)置android:windowSoftInputMode="adjustPan"時,根據(jù)官方對adjustPan(鏈接)的描述,設(shè)置這個屬性之后 Activity 主窗口的尺寸不會調(diào)整,而是會自動平移窗口的內(nèi)容使EditText永遠(yuǎn)不會被鍵盤覆蓋,這就是為什么Toolbar會被移出屏幕的原因。
“adjustPan”
不調(diào)整 Activity 主窗口的尺寸來為軟鍵盤騰出空間, 而是自動平移窗口的內(nèi)容,使當(dāng)前焦點(diǎn)永遠(yuǎn)不被鍵盤遮蓋,讓用戶始終都能看到其輸入的內(nèi)容。 這通常不如尺寸調(diào)正可取,因?yàn)橛脩艨赡苄枰P(guān)閉軟鍵盤以到達(dá)被遮蓋的窗口部分或與這些部分進(jìn)行交互。
當(dāng)設(shè)置設(shè)置android:windowSoftInputMode="adjustResize"時,根據(jù)官方的解釋,此時Activity窗口的尺寸會調(diào)整而為屏幕上的軟鍵盤騰出空間,由于Toolbar設(shè)置了fitsSystemWindows為true且Toolbar的高度設(shè)置為wrap_content,因此Toolbar會被設(shè)置了一定的paddingBottom造成被拉伸。
“adjustResize”
始終調(diào)整 Activity 主窗口的尺寸來為屏幕上的軟鍵盤騰出空間。
問題解決
首先不能在Toolbar設(shè)置fitsSystemWindows="true"讓系統(tǒng)自動給Toolbar設(shè)置paddingTop,我們可以自己手動給Toolbar添加狀態(tài)欄高度的paddingTop,具體代碼如下。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
toolbar.setTitle("測試");
//設(shè)置透明狀態(tài)欄
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
toolbar.setPadding(0, getStatusBarHeight(this), 0, 0); //給Toolbar設(shè)置paddingTop
}
}
//通過反射獲取狀態(tài)欄高度,默認(rèn)25dp
private static int getStatusBarHeight(Context context) {
int statusBarHeight = dip2px(context, 25);
try {
Class<?> clazz = Class.forName("com.android.internal.R$dimen");
Object object = clazz.newInstance();
int height = Integer.parseInt(clazz.getField("status_bar_height")
.get(object).toString());
statusBarHeight = context.getResources().getDimensionPixelSize(height);
} catch (Exception e) {
e.printStackTrace();
}
return statusBarHeight;
}
//根據(jù)手機(jī)的分辨率從 dp 的單位 轉(zhuǎn)成為 px(像素)
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}
首先是android:windowSoftInputMode="adjustPan",其實(shí)我們不應(yīng)該這一個布局文件設(shè)置為adjustPan,如果確實(shí)需要解決這個問題,那么應(yīng)該給Toolbar下面的所有View用ScrollView給包括進(jìn)去,這樣自動平移只會移動ScrollView里面的內(nèi)容。
對于android:windowSoftInputMode="adjustResize",由于現(xiàn)在我們沒有對Toolbar設(shè)置fitsSystemWindows="true",Toolbar沒有被拉伸,但是EditText卻被鍵盤覆蓋住,解決這個問題最好的方法就是給最外層的View設(shè)置fitsSystemWindows="true",此時雖然EditText在鍵盤上面沒有被覆蓋住,但是最外層的View由于設(shè)置了fitsSystemWindows="true"從而導(dǎo)致系統(tǒng)會給最外層的View設(shè)置paddingTop,導(dǎo)致的效果如下。

解決這個問題的方法就是讓最外層的View不去添加系統(tǒng)給的padding,通過重寫View的兩個方法就可以實(shí)現(xiàn),這兩個方法中fitSystemWindows在5.0以后就不支持了,因此5.0以后需要重寫onApplyWindowInsets來進(jìn)行適配,代碼如下。
@Override
protected boolean fitSystemWindows(Rect insets) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
insets.left = 0;
insets.top = 0;
insets.right = 0;
}
return super.fitSystemWindows(insets);
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return super.onApplyWindowInsets(insets.replaceSystemWindowInsets(0, 0, 0, insets.getSystemWindowInsetBottom()));
} else {
return insets;
}
}
最終的效果圖如下,順利達(dá)到我們想要的效果。

代碼
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<com.sparrow.toolbar.SoftInputRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/toolbarStyle">
</android.support.v7.widget.Toolbar>
<EditText
android:id="@+id/edittext"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:background="#cccccc"
android:hint="我是EditText"
android:textColor="@android:color/black"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="16sp"
android:layout_below="@id/toolbar"
android:layout_above="@id/edittext"
android:text="The default implementation works well for a situation where it is used with a container that covers the entire window, allowing it to apply the appropriate insets to its content on all edges. If you need a more complicated layout (such as two different views fitting system windows, one on the top of the window, and one on the bottom), you can override the method and handle the insets however you would like. Note that the insets provided by the framework are always relative to the far edges of the window, not accounting for the location of the called view within that window. (In fact when this method is called you do not yet know where the layout will place the view, as it is done before layout happens.)The default implementation works well for a situation where it is used with a container that covers the entire window, allowing it to apply the appropriate insets to its content on all edges. If you need a more complicated layout (such as two different views fitting system windows, one on the top of the window, and one on the bottom), you can override the method and handle the insets however you would like. Note that the insets provided by the framework are always relative to the far edges of the window, not accounting for the location of the called view within that window. (In fact when this method is called you do not yet know where the layout will place the view, as it is done before layout happens.)"
android:textColor="@android:color/black"/>
</com.sparrow.toolbar.SoftInputRelativeLayout>
SoftInputRelativeLayout.java
public class SoftInputRelativeLayout extends RelativeLayout{
public SoftInputRelativeLayout(Context context) {
super(context);
}
public SoftInputRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SoftInputRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public SoftInputRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
insets.left = 0;
insets.top = 0;
insets.right = 0;
}
return super.fitSystemWindows(insets);
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return super.onApplyWindowInsets(insets.replaceSystemWindowInsets(0, 0, 0, insets.getSystemWindowInsetBottom()));
} else {
return insets;
}
}
}
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
toolbar.setTitle("測試");
//設(shè)置透明狀態(tài)欄
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
toolbar.setPadding(0, getStatusBarHeight(this), 0, 0);
}
}
//獲取狀態(tài)欄高度
private static int getStatusBarHeight(Context context) {
int statusBarHeight = dip2px(context, 25);
try {
Class<?> clazz = Class.forName("com.android.internal.R$dimen");
Object object = clazz.newInstance();
int height = Integer.parseInt(clazz.getField("status_bar_height")
.get(object).toString());
statusBarHeight = context.getResources().getDimensionPixelSize(height);
} catch (Exception e) {
e.printStackTrace();
}
return statusBarHeight;
}
//根據(jù)手機(jī)的分辨率從 dp 的單位 轉(zhuǎn)成為 px(像素)
public static int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}
styles.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item> <!-- 標(biāo)題欄顏色 -->
<item name="colorPrimaryDark">@color/colorAccent</item> <!-- 狀態(tài)欄顏色 -->
<item name="colorAccent">@color/colorAccent</item> <!-- 控件顏色 -->
<item name="android:textColorSecondary">#ffffff</item> <!-- 返回按鈕和三點(diǎn)更多按鈕顏色 -->
<item name="android:textColorPrimary">@android:color/white</item> <!--標(biāo)題文字顏色-->
</style>
<!-- toolbar標(biāo)題樣式 -->
<style name="ToolbarTitle" parent="@style/TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:textSize">16sp</item>
</style>
<style name="myPopupMenu" parent="@style/TextAppearance.AppCompat.Widget.PopupMenu.Small"></style>
<style name="MyPopupMenu" parent="Base.Widget.AppCompat.PopupMenu">
</style>
<!-- toolbar菜單文字顏色 -->
<style name="ToolbarTheme" parent="@style/ThemeOverlay.AppCompat.ActionBar">
<item name="actionMenuTextColor">@android:color/white</item> <!-- 菜單文字顏色 -->
<item name="actionMenuTextAppearance">@style/ToolbarMenuTextSize</item>
</style>
<!-- toolbar菜單文字尺寸 -->
<style name="ToolbarMenuTextSize" parent="@style/TextAppearance.AppCompat.Menu">
<item name="android:textSize">10sp</item>
</style>
<!-- toolbar彈出菜單樣式 -->
<style name="ToolbarPopupTheme" parent="@style/ThemeOverlay.AppCompat">
<item name="android:colorBackground">#212121</item> <!-- 彈窗菜單背景顏色 -->
<item name="actionOverflowMenuStyle">@style/OverflowMenuStyle</item>
</style>
<style name="OverflowMenuStyle" parent="Widget.AppCompat.Light.PopupMenu.Overflow">
<item name="overlapAnchor">false</item> <!--把該屬性改為false即可使menu位置位于toolbar之下-->
</style>
<style name="toolbarStyle" parent="Base.Widget.AppCompat.Toolbar" >
<item name="android:background">@color/colorPrimary</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">match_parent</item>
<!--<item name="android:fitsSystemWindows">true</item>-->
<item name="theme">@style/ToolbarTheme</item>
<item name="popupTheme">@style/ToolbarPopupTheme</item>
<item name="titleTextAppearance">@style/ToolbarTitle</item>
<item name="android:minHeight">48dp</item>
<item name="maxButtonHeight">48dp</item>
</style>
</resources>
新的問題
在使用的過程中發(fā)現(xiàn)有用戶反饋,說只要進(jìn)入我們采用該布局的頁面就會崩潰,我們查看了崩潰日志,發(fā)現(xiàn)有部分手機(jī)都使用了相同的一個安卓系統(tǒng),并且版本都是19,android4.4.x,YunOS系統(tǒng),異常信息為 java.lang.ClassNotFoundException: Didn't find class "android.view.WindowInsets" 。
這應(yīng)該是該系統(tǒng)的虛擬機(jī)加載類的方法不一樣,在加載一個類的時候也將函數(shù)涉及到的其他類也一起進(jìn)行加載了,因?yàn)閃indowInsets是Api為20才添加的,所有才會出現(xiàn)ClassNotFoundException這一個異常,解決方法網(wǎng)上有人給出方法,就是增加layout_v20文件夾,針對不同的版本寫不一樣的布局,分別為api 20以上與20以下提供不同的布局,這是采用系統(tǒng)的限定符實(shí)現(xiàn)的,之后20以上的原樣采用上述的方式,20以下去掉onApplyWindowInsets復(fù)寫,這樣不同的版本加載不同的代碼就OK了。
具體可以查看一下這篇博客(鏈接)。