1. 程式人生 > >Android Toolbar選單動態切換item的圖示

Android Toolbar選單動態切換item的圖示

警示:本文所述的方法過於繁瑣,邏輯混亂,程式碼難以適應複雜情況,並且幾乎不可擴充套件,本文之所以沒刪是我為了記錄自己的踩坑記錄,因此如果有人想參照這方面的知識,請參考我的另一篇文章:

大家都知道,Fragment的啟動速度比Activity快很多,因此在開發中如果每一個介面都使用一個Activity顯然不那麼好,這時候我們一般用Activity來充當管理的角色,介面的內容都放在Fragment中。可是由於每個Fragment都對應一個功能介面,因此每個Fragment的頂部工具欄都應該是不同的,但是ActionBar或者ToolBar都是屬於Activity的,這時候我們就需要在切換Fragment的時候使Toolbar也做出相應的動態切換。

我們來舉一個簡單的例子,打開個人資訊Activity(MyInformationActivity),佈局檔案如下。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".myinformation.MyInformationActivity">

    <android.support.v7.widget.Toolbar
        android:id="@+id/my_information_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:theme="@style/WhiteToolBar">
    </android.support.v7.widget.Toolbar>

    <FrameLayout
        android:id="@+id/alter_my_information_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <fragment
            android:id="@+id/my_information"
            android:name="com.icon.app.myinformation.MyInformationFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </FrameLayout>

</LinearLayout>
佈局很簡單,裡面只放置了一個Toolbar和第一個Fragment (MyInformationFragment),因此,只要這個Activity一啟動,首先展示在使用者眼前並和使用者互動的就是MyInformationFragment,MyInforamtionFragment的佈局檔案我就不放了,直接放效果圖,如下:

展示的使用者資訊除了頭像以外,主要就是姓名,性別,友好度這三項。大家可以注意到 ,toolbar的標題是“我的資訊”,右上角的MenuItem是一支筆的圖片。我們要實現的是點選右上角的menuitem,建立第二個Fragment (AlterMyInformationFragment) 使其到達Activity的最頂端,覆蓋當前的MyInforamtionFragment。於是,我們在onOptionsItemSelected(MenuItem item)方法中進行監聽,當用戶點選右上角的筆圖示時,啟動新的Fragment,MyInformationActivity的程式碼如下:

public class MyInformationActivity extends AppCompatActivity {

    private Toolbar toolbar;
    private AlterMyInformationFragment fragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_information);
        toolbar = (Toolbar) findViewById(R.id.my_information_toolbar);
        toolbar.setTitle("我的資訊");
        setSupportActionBar(toolbar);
        getSupportActionBar().setHomeButtonEnabled(true);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    }

    private void setAlterMyInformationFragment() {
        fragment = new AlterMyInformationFragment();
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.alter_my_information_frame, fragment);
        transaction.addToBackStack(null);
        transaction.commit();
    }

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

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                break;
            case R.id.alter_information:
                setAlterMyInformationFragment();
                break;
            default:
                break;
        }
        return super.onOptionsItemSelected(item);
    }


}

注:android.R.id.home是左上角後退按鈕的id。

MyInformationActivity的Menu佈局檔案如下所示:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".myinformation.MyInformationActivity">

    <item
        android:id="@+id/alter_information"
        android:orderInCategory="100"
        app:showAsAction="ifRoom"
        android:icon="@drawable/ic_create_white_24dp"/>

</menu>
可以看到,MyInformationActivity的Toolbar中只有一個item。

現在,我們在動態變更選單前還要做一個事情,一般toolbar最左邊的後退按鈕相當於back鍵的功能,即點選以後銷燬當前的Activity,但是,我們現在的問題是,如果當前Activity展示的是MyInformationFragment,那自然沒有問題,單擊後退鍵,即銷燬當前活動,但是如果我們當前處在和AlterMyInforamtionFragment的互動狀態,單擊後退鍵把當前Activity銷燬了,顯然是不合理的,正確的情況是應該銷燬AlterMyInforamtionFragment,使使用者回到和MyInformationFragment互動的狀態。因此,我們需要在Activity中設定一個變數status來進行標記,用來反映當前和使用者互動的Fragment到底是哪一個。因此,新增程式碼後Activity的程式碼如下所示:

public class MyInformationActivity extends AppCompatActivity {

    private static final int MY_FRAGMENT = 0;
    private static final int ALTER_FRAGMENT = 1;

    private int status;

    private Toolbar toolbar;
    private AlterMyInformationFragment fragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_information);
        toolbar = (Toolbar) findViewById(R.id.my_information_toolbar);
        toolbar.setTitle("我的資訊");
        setSupportActionBar(toolbar);
        getSupportActionBar().setHomeButtonEnabled(true);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        status = MY_FRAGMENT;
    }

    private void setAlterMyInformationFragment() {
        fragment = new AlterMyInformationFragment();
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.alter_my_information_frame, fragment);
        transaction.addToBackStack(null);
        status = ALTER_FRAGMENT;
        transaction.commit();
    }

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

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                switch (status) {
                    case MY_FRAGMENT:
                        finish();
                        break;
                    case ALTER_FRAGMENT:
                        FragmentManager fragmentManager = getFragmentManager();
                        FragmentTransaction transaction = fragmentManager.beginTransaction();
                        transaction.remove(fragment);
                        status = MY_FRAGMENT;
                        transaction.commit();
                        break;
                    default:
                        break;
                }
                break;
            case R.id.alter_information:
                setAlterMyInformationFragment();
                break;
            default:
                break;
        }
        return super.onOptionsItemSelected(item);
    }

}
我們用兩個常量來表示當前到底處在哪一個Fragment,MY_FRAGMENT表示MyInformationFragment,ALTER_FRAGMENT表示AlterMyInformationFragment。我們可以看到Activity啟動的時候自動給status賦值MY_FRAGMENT,在setAlterMyInformationFragment執行的時候給status賦值ALTER_FRAGMENT。於是我們在監聽後退鍵的時候就可以進行判斷了,如果當前和使用者互動的是MyInformationFragment,執行finish()銷燬Activity,如果和使用者互動的是AlterMyInformationFragment,則銷燬Fragment,讓使用者回到和MyInformationFragment互動的狀態。

我們處理完了後退鍵的問題,就該來解決動態變更toolbar的問題了。切換了Fragment,Toolbar如何相應的發生改變呢?大家平常使用選單的時候,主要使用兩個方法onCreateOptionView(Menu menu)和onOptionsItemSelected(MenuItem item),前者是載入選單佈局的,只會在Activity啟動的時候呼叫一次,後者是對Menu中的item進行事件監聽。也就是說,我們是不可能在onCreateOptionView(Menu menu)中對menu進行動態變更的。那我們來試試在onOptionsItemSelected(MenuItem item)中進行。於是我在啟動AlterMyInforamtionFragment的程式碼後面加了這麼兩行:

item.setIcon(R.drawable.ic_send_white_24dp);
toolbar.setTitle("修改個人資訊");
這樣啟動AlterMyInforamtionFragment後的介面如下圖所示:


demo的介面還是很簡單,使用者可以自己修改自己的姓名和性別,看起來我們貌似實現了,這時候我們遇到一個問題,現在右上角的item的功能不再是啟動新的Fragment了,而應該是使用者修改好個人資訊後進行提交,於是我們再次修改程式碼,讓onOptionsItemSelected(MenuItem item)中的程式碼變成這樣:

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                switch (status) {
                    case MY_FRAGMENT:
                        finish();
                        break;
                    case ALTER_FRAGMENT:
                        FragmentManager fragmentManager = getFragmentManager();
                        FragmentTransaction transaction = fragmentManager.beginTransaction();
                        transaction.remove(fragment);
                        status = MY_FRAGMENT;
                        transaction.commit();
                        break;
                    default:
                        break;
                }
                break;
            case R.id.alter_information:
                switch (status) {
                    case MY_FRAGMENT:
                        setAlterMyInformationFragment();
                        item.setIcon(R.drawable.ic_send_white_24dp);
                        toolbar.setTitle("修改個人資訊");
                        break;
                    case ALTER_FRAGMENT:
                        fragment.postAlterInformation();
                        break;
                    default:
                        break;
                }
                break;
            default:
                break;
        }
        return super.onOptionsItemSelected(item);
    }
我們再次通過status來判斷當前和使用者互動的Fragment,從而決定這個item在不同狀態下的功能。在ALTER_FRAGMENT狀態下呼叫AlterMyInformationFragment類中的postAlterInformation()方法把修改好的資訊通過網路傳送給伺服器。

看起來我們已經實現這個功能了,但是還有很重要的一點沒有做,那就是點選後退鍵的時候我們的toolbar應該變回之前的樣子,但是我們現在還沒有實現。那應該怎麼做呢?修改toolbar的title很容易執行,只要在監聽android.R.id.home的ALTER_FRAGMENT的switch分支下加一條

toolbar.setTitle("我的資訊");
就可以了,但是變更圖示似乎不那麼容易,有些人說,再加一條
item.setIcon("R.drawable.ic_create_white_24dp");
不就行了嗎,但是我們仔細看,我們之所以在啟動AlterMyInforamtionFragment的時候能通過這條程式碼改變item的圖示,是因為我們這行程式碼寫在case R.id.alter_information這條switch分支下,此時的item代表的是右上角我們要修改的item。但是如果你要android.R.id.home這條switch分支下使用引數item,這個item代表的是左上角的後退鍵,即使執行上面那條程式碼,也不會有任何效果。但是,我們確實必須在點選後退鍵的時候改變右上角的item的圖示,這就麻煩了,我找了item能呼叫的所有方法,沒有發現能通過什麼方式指定它的id,讓它在非case R.id.alter_information分支下也代表這個item。後來我在網上查閱後發現,我們應該改變一種思路,因為onCreateOptionView(Menu menu)和onOptionsItemSelected(MenuItem item)這兩個方法可能不夠用了。

我在網上看到很多文章發現,onPrepareOptionsMenu(Menu menu)這個方法也可以被呼叫多次,那我們就通過它來改變item的圖示。於是MyInformationActivity的程式碼就變成了如下這樣

public class MyInformationActivity extends AppCompatActivity {

    private static final int MY_FRAGMENT = 0;
    private static final int ALTER_FRAGMENT = 1;

    private int status;

    private Toolbar toolbar;
    private AlterMyInformationFragment fragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_information);
        toolbar = (Toolbar) findViewById(R.id.my_information_toolbar);
        toolbar.setTitle("我的資訊");
        setSupportActionBar(toolbar);
        getSupportActionBar().setHomeButtonEnabled(true);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        status = MY_FRAGMENT;
    }

    private void setAlterMyInformationFragment() {
        fragment = new AlterMyInformationFragment();
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.alter_my_information_frame, fragment);
        transaction.addToBackStack(null);
        status = ALTER_FRAGMENT;
        transaction.commit();
    }

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

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                switch (status) {
                    case MY_FRAGMENT:
                        finish();
                        break;
                    case ALTER_FRAGMENT:
                        FragmentManager fragmentManager = getFragmentManager();
                        FragmentTransaction transaction = fragmentManager.beginTransaction();
                        transaction.remove(fragment);
                        status = MY_FRAGMENT;
                        transaction.commit();
                        invalidateOptionsMenu();
                        toolbar.setTitle("我的資訊");
                        break;
                    default:
                        break;
                }
                break;
            case R.id.alter_information:
                switch (status) {
                    case MY_FRAGMENT:
                        setAlterMyInformationFragment();
                        item.setIcon(R.drawable.ic_send_white_24dp);
                        toolbar.setTitle("修改個人資訊");
                        break;
                    case ALTER_FRAGMENT:
                        fragment.postAlterInformation();
                        break;
                    default:
                        break;
                }
                break;
            default:
                break;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        menu.findItem(R.id.alter_information).setIcon(R.drawable.ic_create_white_24dp);
        return super.onPrepareOptionsMenu(menu);
    }

}

呼叫invalidateOptionsMenu()方法的地方,onPrepareOptionsMenu(Menu menu)就會被呼叫一次,值得說明的是,網上許多文章的教程寫的是,如果你使用的是系統原生的ActionBar,應呼叫mActivity.getWindow().invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL); 如果使用的是ActionBarSherlock就應呼叫invalidateOptionsMenu(),目前大家可能使用ActionBar的逐漸減少,都逐步轉向了Toolbar,Toolbar和ActionBarSherlock一樣,都是呼叫invalidateOptionsMenu()方法。

2016年10月7日最新更新:又經過了兩個月的學習,我又遇到了更為複雜的問題,比如一個activity可能會管理三個甚至更多的Fragment,這時候我這篇部落格寫的解決方法就不太靈了,會讓程式碼既複雜又臃腫,而且難以閱讀。不過我已經找到了新的解決方法,通過沖分利用Fragment的返回棧模擬以及Fragment的生命週期回撥,可以更簡單易懂的解決這個問題,過段時間我會把最新的方法再寫一篇部落格。