Android
Activity
Fragment
menu
onOptionsItemSelected

onOptionsItemSelectedの戻り値の使い道

こんにちは。

かねてより気になっていた「onOptionsItemSelectedの戻り値としてのtrue/falseの意味」を考察してみました。

APIドキュメントがのたまうには

Activity#onOptionsItemSelectedではこう書かれています。

boolean Return false to allow normal menu processing to proceed, true to consume it here.

和訳すれば、メニューをクリックしたことで「処理を続行するならfalseを返せ、費やすならtrueを返せ」といったかんじでしょうか。

実はこのメソッド、Fragment#onOptionsItemSelectedにもあるんです。

boolean Return false to allow normal menu processing to proceed, true to consume it here.

説明文は、一字一句同じですね。

画面を、Activityだけで実現しているのであれば、当件はtrueでもfalseでもどちらでもかまわないのでしょうが、Fragmentも使って画面とメニューを実現している 場合には注意しなければならないポイントとなります。

サンプルアプリ

Activity1個とFragment2個も使って画面とメニューを実現するアプリを使って検証してみます。

Activityとそのレイアウトリソースファイル

Activityは1個っきりのアプリです。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FF0000"
    android:orientation="horizontal">
</LinearLayout>

背景色は、真っ赤です。中身は空っぽ。プログラムの方で2つのFragmentを追加していきます。

MainActivity.java
package jp.co.casareal.onoptionsitemselected;

import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.MenuItem;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Transactionを利用してFragmentを追加
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.add(R.id.container, new MainFragment());
        transaction.add(R.id.container, new SubFragment());
        transaction.commit();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        Toast toast = Toast.makeText(getApplicationContext(), "MainActivity", Toast.LENGTH_SHORT);
        toast.setGravity(Gravity.CENTER, 0, 0);
        toast.show();
        return false;
    }
}

とりあえず、onOptionsItemSelectedメソッドの戻り値はfalseにしています。あとでtrueとかで試してみましょう。

メインの方のFragment

「メインの方」とか言いましたけど、以下の画像の「左の方」とか「緑の方」と言ってもいいです。このMainFragmentは、SubFragmentより先に追加しました。この順番も、ポイントですよ。

device-2017-12-07-153708.png

色見の強い画面にしてしまったので、凝視し続けないでください。残像錯視のような面白みはないですので。

fragment_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_margin="20dp"
    android:layout_weight="1"
    android:background="#00FF00"
    android:padding="20dp">

    <TextView
        android:id="@+id/text_menu"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="メイン"
        android:textColor="@android:color/black"
        android:textSize="50sp" />

</LinearLayout>
MainFragment.java
package jp.co.casareal.onoptionsitemselected;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

public class MainFragment extends Fragment {
    private TextView mTextView;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.main_menu, menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        Toast toast=Toast.makeText(getContext(), "MainFragment", Toast.LENGTH_SHORT);
        toast.setGravity(Gravity.LEFT, 0, 0);
        toast.show();
        return false;
    }
}

res/menuフォルダに、メニューリソースを置いておきます。

main_menu.xml
<?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
        android:id="@+id/menu_main"
        android:icon="@drawable/menu_one"
        android:title="Mainメニュー"
        app:showAsAction="always" />
</menu>

menu_oneという画像は、この画像です。menu_one.png

ポイントは、

  • onCreateOptionsMenuとonOptionsItemSelectedメソッドを実装している。
  • なので、setHasOptionsMenu(true);しておかなければならない。
  • ここでもonOptionsItemSelectedメソッドの戻り値はfalseにしています。あとでtrueとかで試してみましょう。

サブの方のFragment

「右の方」とか「青の方」です。

aaa
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_margin="20dp"
    android:layout_weight="1"
    android:background="#0000FF"
    android:padding="20dp">

    <TextView
        android:id="@+id/text_sub"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="サブ"
        android:textColor="@android:color/white"
        android:textSize="50sp" />

</LinearLayout>
SubFragment.java
package jp.co.casareal.onoptionsitemselected;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

public class SubFragment extends Fragment {

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        return inflater.inflate(R.layout.fragment_sub, container, false);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.sub_menu, menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        Toast toast = Toast.makeText(getContext(), "SubFragment", Toast.LENGTH_SHORT);
        toast.setGravity(Gravity.RIGHT, 0, 0);
        toast.show();
        return false;
    }
}

Toastを出す位置に注目してください。CENTERLEFTRIGHTと三者三様です。

sub_menu.xml
<?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
        android:id="@+id/menu_sub"
        android:icon="@drawable/menu_two"
        android:title="Subメニュー"
        app:showAsAction="always" />
</menu>

こちらのメニューアイテムのアイコンはこれです。menu_two.png

ActionBar上の右にメニューアイコンが2つ仲良く並ぶことになります。

動作確認

さあ、メニューアイテムをクリックしてみましょう(どちらでもいいです)!なお、3つのonOptionsItemSelectedメソッドはすべてfalseを返しています。

クリックすると...

device-2017-12-07-153739.png

まず、CENTERにMainActivityが出すToastが出たあと...

device-2017-12-07-153750.png

入れ替わりにMainFragmentがLEFTに出すToastが出たあと...

device-2017-12-07-153807.png

最後にお出ましは、SubFragmentがRIGHTにToastを出します。

ぜひ、3か所のreturn文をtrueにしたりfalseにしたりして試してみてください。

onOptionsItemSelectedがfalseをリターンと云うは、伝播する事と見つけたり

葉隠聞書はがくれききがきの一節「武士道と云うは死ぬ事と見付けたり」をもじってカッコつけた気分に浸っています。

閑話休題。

表示されたアプリの画面とは裏腹に、1個のActivityと2個のFragmentの重なり具合は、こういうことだったんですね!画面の見た目にだまされてうっかりActivityが最下層に位置していると勘違いさせられましたよ。

denpa.png

で、onOptionsItemSelectedメソッドでfalseをリターンする、ということはすなわち、consume(消費)せずにproceed(続行)するとな!

まるで、JavaScriptのaddEventListenerにおける「捕捉フェーズ」と似てますね。

今回はMainActivityにて、2つのFragmentをFragmentTransactionを用いて追加(add)したわけですが、その追加順に注意してください。MainFragmentのonOptionsItemSelectedでtrueをリターンしてしまうと、どーしたってSubFragmentには通知が届きません。つくづく画面の見た目(2つのFragmentが並列に並んでいる)に騙されないようにしないといけませんね。

ここから学んだこと

テキトーに「falseをリターンしとけばいい」とするのではなく、 メニュークリック時の処理の責務を明確 にして、 無駄な伝播はオーバーヘッドのもとになるだけ というこを、肝に銘じて今後の武士道を歩む所存に候。

以上です。