Saturday, August 15, 2015

Custom options menu in Android application.

First of all I need to say, that I was cunning with title. This post is not about menu customization. It's about creating pseudo-menu.
So, requirement was to create screen-wide menu with icons. Also from image you can see, that menu appears below toolbar and doesn't overlay it.

Of course I googled this question. And appears, that Google want us to keep menu grey, flat and simple. Here is interesting article about this problem:
Android: How can you Implement a Custom Menu class.
I can't resist to quote it:
My question is to Google engineers.  Why?  Why is this so?  Why can’t I pass in a custom Theme to my Menus?  Why can’t I choose how to Style them?  Yes, I can understand you not wanting some devious developer taking control of a phone by disabling the actions of all the phone’s buttons but the Menu button?
My idea is pretty simple. Instead of menu I use Fragment, that I show in every activity on press on menu icon. I use Relative layout to place this MenuFragment below Toolbar. And yes, I use Toolbar from support library instead of action bar. This gives more flexibility.

But let's explore some code.

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent">  
   <include  
     android:id="@+id/app_bar"  
     layout="@layout/app_bar" />  
   <fragment xmlns:tools="http://schemas.android.com/tools"  
     android:id="@+id/fragment"  
     android:name="com.shakenbeer.customoptionsmenu.MainActivityFragment"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     android:layout_below="@id/app_bar"  
     tools:layout="@layout/fragment_main" />  
   <FrameLayout  
     android:id="@+id/fragment_menu_container"  
     android:layout_width="match_parent"  
     android:layout_height="wrap_content"  
     android:layout_below="@id/app_bar"></FrameLayout>  
 </RelativeLayout>  

This is how activity could look like. I used the same layout for every activity in my project. FrameLayout is a container for menu.
Also here is BaseActivity classm that contains logic for calling menu and choosing menu item:
public class BaseActivity extends AppCompatActivity implements MenuFragment.MenuListener {
    protected Toolbar toolbar;

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

        toolbar = (Toolbar) findViewById(R.id.app_bar);
        setSupportActionBar(toolbar);
        getSupportActionBar().setDisplayShowTitleEnabled(true);
        getSupportActionBar().setDisplayShowHomeEnabled(true);
        getSupportActionBar().setElevation(0f);
    }

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

    @Override    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_menu: {
                triggerFragmentMenu();
                return true;
            }
        }
        return super.onOptionsItemSelected(item);
    }

    private void triggerFragmentMenu() {
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        Fragment menuFragment = fragmentManager.findFragmentById(R.id.fragment_menu_container);
        if (menuFragment == null) {
            MenuFragment fragment = MenuFragment.newInstance();
            fragmentTransaction.replace(R.id.fragment_menu_container, fragment);
        } else {
            fragmentTransaction.remove(menuFragment);
        }
        fragmentTransaction.commit();
    }

    @Override    public void onMenuItemSelected(int menuPosition) {
        removeMenuFragment();
        MenuContent.MenuItem menuItem = MenuContent.ITEMS.get(menuPosition);
        Intent intent = new Intent(this, menuItem.activityClass);
        startActivity(intent);
    }

    @Override    public void onBackPressed() {
        if (!removeMenuFragment())
            super.onBackPressed();
    }

    private boolean removeMenuFragment() {
        FragmentManager fragmentManager = getFragmentManager();
        Fragment menuFragment = fragmentManager.findFragmentById(R.id.fragment_menu_container);
        if (menuFragment != null) {
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.remove(menuFragment);
            fragmentTransaction.commit();
            return true;
        } else {
            return false;
        }
    }}

Menu contains only one item and this item is always shown as action. This simple trick removes standard overflow menu icon from toolbar.

When we click on menu icon - we add MenuFragment to activity or remove it if it's already here.

When we click on menu item - we remove MenuFragment and start corresponding activity.
Here is sources of MenuFragment:
 public class MenuFragment extends Fragment {

    public interface MenuListener {
        public void onMenuItemSelected(int menuPosition);
    }

    private MenuListener listener;

    public static MenuFragment newInstance() {
        MenuFragment fragment = new MenuFragment();
        return fragment;
    }

    public MenuFragment() {
        // Required empty public constructor    }

    @Override    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_menu, container, false);

        ListView menuListView = (ListView) view.findViewById(R.id.fragment_menu_listview);

        menuListView.setAdapter(new MenuAdapter(getActivity()));

        menuListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override 
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                listener.onMenuItemSelected(position);
            }
        });

        return view;
    }

    @Override    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            listener = (MenuListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString()
                    + " must implement MenuListener");
        }
    }
} 



MenuFrament contains MenuListener, any activity should implements it and could override BaseActivity implementation do perform whatever you want. Starting new activity from menu - it's just an example.

Full sources: https://github.com/Shakenbeer/CustomOptionsMenu

No comments:

Post a Comment