Android動態改變佈局

遇到這麼個需求,先看圖:android

    

實際上是一個軟件的登陸界面,初始是第一個圖的樣子,當軟鍵盤彈出後變爲第二個圖的樣子,由於登陸界面有用戶名、密碼、登陸按鈕,不這樣的話軟鍵盤彈出後會遮住登陸按鈕(其實以前的實現放到了ScrollView裏面,監聽軟鍵盤彈出後滾動到底部,軟鍵盤隱藏後滾動到頂部,也是能夠的)。數組

最簡單的方法就是多加幾個冗餘的View,根據軟鍵盤的狀態隱藏不須要的View,顯示須要的View,但這樣感受太挫了,而後就想起了前兩年研究的RelativeLayout佈局,RelativeLayout中子控件的佈局都是相對位置,只須要在軟鍵盤彈出隱藏時改變應用的位置規則就好了。ide

先來看一下佈局文件佈局

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dp"
    tools:context="${packageName}.${activityClass}" >

    <RelativeLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true" >

        <ImageView
            android:id="@+id/logo"
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:layout_centerHorizontal="true"
            android:scaleType="centerCrop"
            android:src="@drawable/ic_launcher"
            tools:ignore="ContentDescription" />

        <TextView
            android:id="@+id/label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/logo"
            android:layout_centerHorizontal="true"
            android:layout_marginLeft="10dp"
            android:layout_marginTop="10dp"
            android:text="@string/hello_world"
            android:textSize="20sp" />
    </RelativeLayout>

    <EditText
        android:id="@+id/input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/container"
        android:layout_margin="16dp"
        android:hint="Input sth."
        tools:ignore="HardcodedText" />

</RelativeLayout>

軟鍵盤的彈出隱藏用OnGlobalLayoutListener監聽實現,對Activity應用android:windowSoftInputMode="stateHidden|adjustResize",這樣開始時軟鍵盤不顯示,當軟鍵盤彈出時佈局被Resize。spa

接下來是代碼,全部的代碼都在這裏了code

public class MainActivity extends Activity {

    private View root; // 最外層佈局
    private View logo; // Logo圖標
    private View label; // Logo附近的文字

    private int rootBottom = Integer.MIN_VALUE;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        root = findViewById(R.id.root);
        logo = findViewById(R.id.logo);
        label = findViewById(R.id.label);
        root.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

            @Override
            public void onGlobalLayout() {
                Rect r = new Rect();
                root.getGlobalVisibleRect(r);
                // 進入Activity時會佈局,第一次調用onGlobalLayout,先記錄開始軟鍵盤沒有彈出時底部的位置
                if (rootBottom == Integer.MIN_VALUE) {
                    rootBottom = r.bottom;
                    return;
                }
                // adjustResize,軟鍵盤彈出後高度會變小
                if (r.bottom < rootBottom) {
                    RelativeLayout.LayoutParams lp = (LayoutParams) logo.getLayoutParams();
                    // 若是Logo不是水平居中,說明是由於接下來的改變Logo大小位置致使的再次佈局,忽略掉,不然無限循環
                    if (lp.getRules()[RelativeLayout.CENTER_HORIZONTAL] != 0) {
                        // Logo顯示到左上角
                        lp.addRule(RelativeLayout.CENTER_HORIZONTAL, 0); // 取消水平居中
                        lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT); // 左對齊

                        // 縮小Logo爲1/2
                        int height = logo.getHeight(); // getMeasuredHeight()
                        int width = logo.getWidth();
                        lp.width = width / 2;
                        lp.height = height / 2;
                        logo.setLayoutParams(lp);

                        // Logo下的文字
                        RelativeLayout.LayoutParams labelParams = (LayoutParams) label.getLayoutParams();
                        labelParams.addRule(RelativeLayout.CENTER_HORIZONTAL, 0); // 取消水平居中
                        labelParams.addRule(RelativeLayout.BELOW, 0); // 取消顯示到logo的下方
                        labelParams.addRule(RelativeLayout.RIGHT_OF, R.id.logo); // 顯示到Logo的右方
                        labelParams.addRule(RelativeLayout.CENTER_VERTICAL); // 垂直居中
                        label.setLayoutParams(labelParams);
                    }
                } else { // 軟鍵盤收起或初始化時
                    RelativeLayout.LayoutParams lp = (LayoutParams) logo.getLayoutParams();
                    // 若是沒有水平居中,說明是軟鍵盤收起,不然是開始時的初始化或者由於此處if條件裏的語句修改控件致使的再次佈局,忽略掉,不然無限循環
                    if (lp.getRules()[RelativeLayout.CENTER_HORIZONTAL] == 0) {
                        // 居中Logo
                        lp.addRule(RelativeLayout.CENTER_HORIZONTAL);
                        lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT, 0);

                        // 還原Logo爲原來大小
                        int height = logo.getHeight();
                        int width = logo.getWidth();
                        lp.width = width * 2;
                        lp.height = height * 2;
                        logo.setLayoutParams(lp);

                        // Logo下的文字
                        RelativeLayout.LayoutParams labelParams = (LayoutParams) label.getLayoutParams();
                        labelParams.addRule(RelativeLayout.CENTER_HORIZONTAL); // 設置水平居中
                        labelParams.addRule(RelativeLayout.BELOW, R.id.logo); // 設置顯示到Logo下面
                        labelParams.addRule(RelativeLayout.RIGHT_OF, 0); // 取消顯示到Logo右面
                        labelParams.addRule(RelativeLayout.CENTER_VERTICAL, 0); // 取消垂直居中
                        label.setLayoutParams(labelParams);
                    }
                }
            }
        });
    }
}

當Activity啓動時也會進行Layout,此時用rootBottom記錄了初始時最外層佈局底部的位置,此後當軟鍵盤彈出時,佈局被壓縮,再次獲取同一個View底部的位置,若是比rootBottom小說明軟鍵盤彈出了,若是大於或等於rootBottom說明軟鍵盤隱藏了。server

全部的代碼都在上面,也有詳細註釋,有兩點須要注意一下:xml

  1. Activity啓動時會進行Layout,此時會調用onGlobalLayout,並且通常會調用兩次,這樣第二次時會進入else語句,要注意過濾
  2. 軟鍵盤彈出或隱藏時進入onGlobalLayout,此時根據須要縮放Logo的大小,並改變Logo和Label的位置,這些操做會引發再次onGlobalLayout,須要將以後的onGlobalLayout過濾掉,否則就無限循環了。

能夠看到上面代碼中的過濾條件,以else語句中的爲例,Activity啓動時會進入else,此時Logo是水平居中狀態,會跳過else裏面的if語句,這樣就處理掉了第一種狀況。blog

當由於軟鍵盤收起進入else時,Logo已經由於if語句塊變爲了顯示在左上角,因此會進入else中的if語句,從新改變Logo爲水平居中,因爲修改了Logo的大小和位置,會致使再次進入onGlobalLayout,還是進入else,但此時已經設置Logo爲水平居中了,不會再次進入else中的if語句,這樣經過一個條件判斷就處理了上面提到的兩點注意事項。索引

關於addRule

RelativeLayout中每個子控件所應用的規則都是經過數組保存的,以下所示:

public static final int TRUE = -1;

public void addRule(int verb) {
    mRules[verb] = TRUE;
    mInitialRules[verb] = TRUE;
    mRulesChanged = true;
}

public void addRule(int verb, int anchor) {
    mRules[verb] = anchor;
    mInitialRules[verb] = anchor;
    mRulesChanged = true;
}

以某一規則的索引爲下標,值就是規則對應的anchor,若是是相對於另外一個子控件,值就是另外一個子控件的ID,若是是相對於父控件,值就是`TRUE`,即-1,若是沒有應用某一規則值就是0,能夠看到,removeRule就是把相應位置的值改成了0:

public void removeRule(int verb) {
    mRules[verb] = 0;
    mInitialRules[verb] = 0;
    mRulesChanged = true;
 }

removeRuleAPI 17才加的方法,爲了在API 17前也能使用,能夠使用它的等價方法,像上面的例子中的同樣,使用addRule(verb, 0)