如何客製化Menu

如何客製化Menu

情境

在之前如何自訂Dialog之二-客製化Menu這篇文章
是透過自己做的假Toolbar
在上面蓋一層Dialog, 模擬出假Menu的效果
那如果在真的Toolbar想要客製化自己的Menu Popupwindow該怎麼做呢?

程式碼說明

首先我們建立一個新專案
接著在MainActivity內建立一個Toolbar

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context="com.example.givemepass.custommenudemo.MainActivity">

    <android.support.v7.widget.Toolbar
        android:background="?attr/colorPrimary"
        android:id="@+id/toolbar"
        app:titleTextAppearance="@style/toolbar_style"
        android:minHeight="?actionBarSize"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </android.support.v7.widget.Toolbar>
</RelativeLayout>

而這個Toolbar讓它呈現白色的字樣, 因此我們定義一個Theme

<style name="toolbar_style" >
    <item name="android:textColor">@android:color/white</item>
</style>

接著在menufest內的AppTheme進行一些修改
將原本的ActionBar改成NoActionBar
避免之後再將ToolBar塞進ActionBar的時候會閃退

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>

所以當我們的程式碼將Toolbar設定為ActionBar

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);

}

就會出現這樣的畫面


接著讓我們產生一個menu的按鈕,
首先要先產生一個icon是垂直的點點點,
可以透過之前如何透過Android Studio產生向量圖來產生一個向量圖,

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    <path
        android:fillColor="#ffffff"
        android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

當產生好了就新增一個menu檔案在menu資料夾內。

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        app:showAsAction="always"
        android:title="menu"
        android:icon="@drawable/ic_more_vert_black_24dp"
        android:id="@+id/menu_more"
        />
</menu>

如果要在Activity內產生Menu必須覆寫onCreateOptionsMenu方法,
而不是透過Toolbar去inflateMenu, 這點可能要注意一下。

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

這樣就可以看到我們的Menu被產生出來了。


接下來我們要做出自訂的Menu popupwindow,
可以先參考
如何自訂Dialog之二-客製化Menu
內的Dialog部分,
生成一個自訂的Dialog。


首先先來宣告一個Dialog,

public class OptionDialog extends Dialog{
    public OptionDialog(Context context) {
        super(context);
        getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.option_dialog);
    }
}

這邊將Dialog設定為背景透明且沒有title的Dialog,
然後在MainAcitivty內設定Menu按下去的事件。

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mToolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(mToolbar);
    mToolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
        @Override
        public boolean onMenuItemClick(MenuItem item) {
            new OptionDialog(MainActivity.this).show();
            return false;
        }
    });
}

記住這邊一定要寫在setSupportActionBar(mToolbar)之下,
否則事件會無效。
而我們的Dialog背景目前沒有任何東西

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

</RelativeLayout>

所以按下去只會出現暗暗的畫面。

如果不想讓畫面看起來暗暗的, 那麼就改變一下Dialog的Theme。

public class OptionDialog extends Dialog{
    public OptionDialog(Context context) {
        super(context, R.style.AppTheme);
        getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.option_dialog);
    }
}

讓它使用我們一開始定義好的AppTheme。


就算點了感覺也沒甚麼效果, 沒關係,
我們加一個icon就可以看到有東西呈現,
一樣使用Vector產生一個箭頭向上的小三角形。

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    <path
        android:fillColor="#ffffff"
        android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

在Dialog上面放一個ImageView, 並且指派src為這張向量圖。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:src="@drawable/ic_arrow_drop_up_black_24dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

讓這個小三角形位置為右上, 因此指派它在最外圈Parent的右邊跟上面,
當我們點Menu按鈕的時候, 就可以看到這個小三角形出現了。


不過位置好像怪怪的,
我們希望它出現在ToolBar的點點點下方,
所以調整一下位置。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:layout_marginRight="13dp"
        android:layout_marginTop="35dp"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:src="@drawable/ic_arrow_drop_up_black_24dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

透過margin來調整一下位置,

  • 注意:這邊會根據解析度大小不同, 而做不一樣的dp調整, 你應該根據各種解析度去調整dimens,
    找到對應的dp。


    接著我們建構下面那一塊List。
    建立一塊Layout讓它黏在箭頭的下方,
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/arrow"
        android:layout_marginRight="13dp"
        android:layout_marginTop="35dp"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:src="@drawable/ic_arrow_drop_up_black_24dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <LinearLayout
        android:layout_marginRight="13dp"
        android:layout_marginTop="49dp"
        android:layout_alignParentTop="true"
        android:layout_alignParentRight="true"
        android:layout_below="@id/arrow"
        android:background="#000000"
        android:layout_width="80dp"
        android:layout_height="80dp">

    </LinearLayout>
</RelativeLayout>
  • 注意:同樣的不同解析度, 所取的margin就要根據解析度做不同的dimens的調整。
    這個畫面調整出來就會長這樣。


    不過正方形可以再調整一下做成圓角會更好看,
    所以我們在drawable內新增了一個圓角的xml。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
    <corners android:radius="5dp" />
    <solid android:color="#49494b" />
</shape>



看起來好多了,
接著就是要加入List的內容了。
首先加入兩個TextView, 讓外面的LinearLayout設定權重,
接著將字的顏色設定為白色, 畢竟我們的背景是黑色的。

<LinearLayout
    android:layout_marginRight="13dp"
    android:layout_marginTop="49dp"
    android:layout_alignParentTop="true"
    android:layout_alignParentRight="true"
    android:layout_below="@id/arrow"
    android:background="@drawable/coner"
    android:layout_width="80dp"
    android:layout_height="80dp"
    android:weightSum="2"
    android:orientation="vertical">
    <TextView
        android:gravity="center"
        android:text="Row 1"
        android:layout_weight="1"
        android:textColor="@android:color/white"
        android:textSize="16sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:gravity="center"
        android:text="Row 2"
        android:layout_weight="1"
        android:textColor="@android:color/white"
        android:textSize="16sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

越來越有Menu的樣子了。


加一條分隔線好了。

<LinearLayout
    android:layout_marginRight="13dp"
    android:layout_marginTop="49dp"
    android:layout_alignParentTop="true"
    android:layout_alignParentRight="true"
    android:layout_below="@id/arrow"
    android:background="@drawable/coner"
    android:layout_width="80dp"
    android:layout_height="80dp"
    android:weightSum="2"
    android:orientation="vertical">
    <TextView
        android:gravity="center"
        android:text="Row 1"
        android:layout_weight="1"
        android:textColor="@android:color/white"
        android:textSize="16sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <View
        android:layout_marginRight="5dp"
        android:layout_marginLeft="5dp"
        android:layout_gravity="center"
        android:background="@android:color/white"
        android:layout_width="match_parent"
        android:layout_height="1px" />
    <TextView
        android:gravity="center"
        android:text="Row 2"
        android:layout_weight="1"
        android:textColor="@android:color/white"
        android:textSize="16sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>



你會發現當我們點擊menu以外的地方怎麼會沒有反應呢?
因為我們的Dialog事件並沒有寫這個處理,
所以我們給最外圍的Layout一個名稱並且增加關閉Dialog的事件給它。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:id="@+id/dialog_outside"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!--menu內的內容 -->
</RelativeLayout>

接著在ToolBar的setOnMenuItemClickListener內改寫一些東西。

 mToolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
    @Override
    public boolean onMenuItemClick(MenuItem item) {
        final OptionDialog mOptionDialog = new OptionDialog(MainActivity.this);
        mOptionDialog.findViewById(R.id.dialog_outside).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mOptionDialog.dismiss();
            }
        });
        mOptionDialog.show();
        return false;
    }
});

這時候你就會發現點擊menu外部以及menu內部都會讓menu消失,
為什麼會這樣?
因為我們還未對menu內的每一個item給予事件,
因此我們可以在Dialog內部寫入事件看看。

<LinearLayout
    android:layout_marginRight="13dp"
    android:layout_marginTop="49dp"
    android:layout_alignParentTop="true"
    android:layout_alignParentRight="true"
    android:layout_below="@id/arrow"
    android:background="@drawable/coner"
    android:layout_width="80dp"
    android:layout_height="80dp"
    android:weightSum="2"
    android:orientation="vertical">
    <TextView
        android:id="@+id/item1"
        android:gravity="center"
        android:text="Row 1"
        android:layout_weight="1"
        android:textColor="@android:color/white"
        android:textSize="16sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <View
        android:layout_marginRight="5dp"
        android:layout_marginLeft="5dp"
        android:layout_gravity="center"
        android:background="@android:color/white"
        android:layout_width="match_parent"
        android:layout_height="1px" />
    <TextView
        android:id="@+id/item2"
        android:gravity="center"
        android:text="Row 2"
        android:layout_weight="1"
        android:textColor="@android:color/white"
        android:textSize="16sp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

各自給TextView一個id, 分別是item1, item2,
接著在Dialog內加入事件。

public class OptionDialog extends Dialog{
    public OptionDialog(Context context) {
        super(context, R.style.AppTheme);
        getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.option_dialog);
        findViewById(R.id.item1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

            }
        });
        findViewById(R.id.item2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

            }
        });
    }
}

這時候你就再執行一次就會發現,
點擊每個item都不會讓Dialog消失了。
現在又出現一個問題了,
當我們點擊item的時候, 沒有一些特效, 沒有點下去的感覺,
這時候可以參考如何使用第三方達成Ripple的效果這篇文章,
來製造Ripple的效果,
首先先把lib import進來。

compile 'com.balysv:material-ripple:1.0.2'

然後把原本依附在TextView上面的屬性全部掛到第三方上面。

 <LinearLayout
    android:layout_marginRight="13dp"
    android:layout_marginTop="49dp"
    android:layout_alignParentTop="true"
    android:layout_alignParentRight="true"
    android:layout_below="@id/arrow"
    android:background="@drawable/coner"
    android:layout_width="80dp"
    android:layout_height="80dp"
    android:weightSum="2"
    android:orientation="vertical">
    <com.balysv.materialripple.MaterialRippleLayout
        android:layout_weight="1"
        app:mrl_rippleColor="@android:color/darker_gray"
        android:id="@+id/item1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:gravity="center"
            android:text="Row 1"
            android:textColor="@android:color/white"
            android:textSize="16sp"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </com.balysv.materialripple.MaterialRippleLayout>
    <View
        android:layout_marginRight="5dp"
        android:layout_marginLeft="5dp"
        android:layout_gravity="center"
        android:background="@android:color/white"
        android:layout_width="match_parent"
        android:layout_height="1px" />
    <com.balysv.materialripple.MaterialRippleLayout
        android:layout_weight="1"
        app:mrl_rippleColor="@android:color/darker_gray"
        android:id="@+id/item2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:gravity="center"
            android:text="Row 2"
            android:textColor="@android:color/white"
            android:textSize="16sp"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </com.balysv.materialripple.MaterialRippleLayout>
</LinearLayout>



如此一來menu就看起來很完整了。
等等, 那Activity那邊要怎麼知道我按下了哪一個,
其實這個很簡單, 只要寫好Listener就好,
對於Listener不熟悉的可以參考
如何寫一個Listener之一
如何寫一個Listener之二

一開始先宣告兩個變數, 分別是ITEM1跟ITEM2,
這邊是要通知哪一個item被按下去了。

public static final int ITEM1 = 0;
public static final int ITEM2 = 1;

接著就可以寫入我們宣告的Listener

private OnItemClickListener mOnItemClickListener;
public interface OnItemClickListener{
    void onItemClick(int pos);
}
public void setOnItemClickListener(OnItemClickListener listener){
    mOnItemClickListener = listener;
}

接著在事件內進行判斷。

findViewById(R.id.item1).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if(mOnItemClickListener != null){
            mOnItemClickListener.onItemClick(ITEM1);
        }
    }
});
findViewById(R.id.item2).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if(mOnItemClickListener != null){
            mOnItemClickListener.onItemClick(ITEM2);
        }
    }
});

如果item被按下去, 就通知已經註冊的Listener回傳對應的位置,
回到我們的MainActivity內接收callback。

mOptionDialog.setOnItemClickListener(new OptionDialog.OnItemClickListener() {
    @Override
    public void onItemClick(int pos) {
        Toast.makeText(MainActivity.this, "item " + (pos + 1) + " 被按下。", Toast.LENGTH_SHORT).show();
    }
});

pos是從0開始, 因此要+1。


按下item之後沒有把dialog關閉, 補一下。

mOptionDialog.setOnItemClickListener(new OptionDialog.OnItemClickListener() {
    @Override
    public void onItemClick(int pos) {
        Toast.makeText(MainActivity.this, "item " + (pos + 1) + " 被按下。", Toast.LENGTH_SHORT).show();
        mOptionDialog.dismiss();
    }
});


到目前為止, 我們客製化的Dialog menu就算完成了。

github