Home » Android » Android ActionBar menu with vertical (rotated) text items based on Custom Action Provider

Android ActionBar menu with vertical (rotated) text items based on Custom Action Provider

Posted by: admin June 15, 2020 Leave a comment

Questions:

I am adding a menu to an action bar item. The menu will contain vertical text for each item. What the menu contains is not important. I basically just want to create my own view that will pop up when I press an action bar item. So for the purposes of this question, you could imagine my view as a big black box.

enter image description here

The image on the right was made with Gimp. It is what I am trying to do, not what I have accomplished yet.

What I have tried

In order to update an old app with the Material Design theme, I’ve been going through all the lessons in the Android documentation for adding the app bar. Since my vertical text menu doesn’t fit the common cases, I have to make a custom Action Provider. The documentation does not provide a full example for a custom action provider, though. The best I could find was this Stack Overflow answer.

The best I have been able to do (with a black View representing my future menu) is shown in the following image:

enter image description here

The star in the image above currently has the action provider. However, the custom view gets clipped off within the action bar. How do I make it float over everything? Also, I don’t want it appearing until I click on the action bar item. Currently, though, it just shows right away.

Code

MainActivity.java

public class MainActivity extends AppCompatActivity  {

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

        // setup toolbar
        Toolbar myToolbar = (Toolbar) findViewById(R.id.my_toolbar);
        setSupportActionBar(myToolbar);

        ...
    }

    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_settings:
                // User chose the "Settings" item, show the app settings UI...
                return true;

            case R.id.action_favorite:
                // User chose the "Favorite" action, mark the current item
                // as a favorite...
                return true;

            default:
                // If we got here, the user's action was not recognized.
                // Invoke the superclass to handle it.
                return super.onOptionsItemSelected(item);

        }
    }

    ...
}

activity_main.xml

<?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">

    <android.support.v7.widget.Toolbar
        android:id="@+id/my_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:elevation="4dp"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

...

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/action_favorite"
        android:icon="@drawable/ic_star_black_24dp"
        android:title="@string/menu_favorites"
        app:actionProviderClass="com.example.chimee.MyActionProvider"
        app:showAsAction="ifRoom"/>

    <item android:id="@+id/action_settings"
        android:title="@string/menu_item_settings"
        app:showAsAction="never"/>

</menu>

MyActionProvider.java

import android.content.Context;
import android.support.v4.view.ActionProvider;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;

public class MyActionProvider extends ActionProvider {

    private Context mContext;

    public MyActionProvider(Context context) {
        super(context);

        mContext = context;
    }

    // for versions older than api 16
    @Override
    public View onCreateActionView() {
        // Inflate the action provider to be shown on the action bar.
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        View providerView =
                layoutInflater.inflate(R.layout.my_action_provider, null);
        View myView =
                (View) providerView.findViewById(R.id.blackView);
        myView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("myTag", "black view was clicked");
            }
        });
        return providerView;
    }

    @Override
    public View onCreateActionView(MenuItem forItem) {
        // TODO: don't just repeat all this code here from above.
        // Inflate the action provider to be shown on the action bar.
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        View providerView =
                layoutInflater.inflate(R.layout.my_action_provider, null);
        View myView =
                (View) providerView.findViewById(R.id.blackView);
        myView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("myTag", "black view was clicked");
            }
        });
        return providerView;
    }
}

my_action_provider.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    style="?attr/actionButtonStyle"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:background="?attr/actionBarItemBackground"
    android:focusable="true" >

    <View
        android:id="@+id/blackView"
        android:layout_width="200dp"
        android:layout_height="150dp"
        android:background="#000000" />

</LinearLayout>

I would be glad to see an example of any fully functioning custom action provider that shows a view outside of the action bar frame.

How to&Answers:

If You didn’t find Custom Action Provider based solution, may be You want use Custom Toolbar & PopupWindow-based workaround which means:

1) create custom Toolbar with ImageButton as menu button and replace ActionBar with it (like in that post of Machado);

2) create PopupWindow with custom layout for menu items with vertical text;

3) add onClickListener to ImageButton from p.1 which show PopupWindow from p.2.

Layout of custom Toolbar (action_bar.xml) may be something like:

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

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:background="@color/colorPrimary"
        android:elevation="4dp"
        android:layout_height="?attr/actionBarSize">
    </android.support.v7.widget.Toolbar>
</RelativeLayout>

Layout of MainActivity (activity_main.xml) which use it:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    android:id="@+id/activity_main"
    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"
    android:padding="0dp"
    tools:context="<your_package_name>.MainActivity">

    <include
        android:id="@+id/tool_bar"
        layout="@layout/action_bar"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:layout_marginStart="31dp"
        android:layout_below="@+id/tool_bar"
        android:layout_alignParentStart="true"
        android:layout_marginTop="31dp"/>

</RelativeLayout>

ImageButton as “main popup menu” button described in main_menu.xml file this way (more in this post of ASH):

<?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_button"
          android:icon="@drawable/ic_more_vert"
          android:title=""
          app:showAsAction="always"
          app:actionViewClass="android.widget.ImageButton"/>
</menu>

For vertical text of menu items You can use, for example, custom View like VerticalLabelView from this of kostmo:

public class VerticalLabelView extends View {
    private TextPaint mTextPaint;
    private String mText;
    private int mAscent;
    private Rect text_bounds = new Rect();

    final static int DEFAULT_TEXT_SIZE = 15;

    public VerticalLabelView(Context context) {
        super(context);
        initLabelView();
    }

    public VerticalLabelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initLabelView();

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VerticalLabelView);

        CharSequence s = a.getString(R.styleable.VerticalLabelView_text);
        if (s != null) setText(s.toString());

        setTextColor(a.getColor(R.styleable.VerticalLabelView_textColor, 0xFF000000));

        int textSize = a.getDimensionPixelOffset(R.styleable.VerticalLabelView_textSize, 0);
        if (textSize > 0) setTextSize(textSize);

        a.recycle();
    }

    private final void initLabelView() {
        mTextPaint = new TextPaint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextSize(DEFAULT_TEXT_SIZE);
        mTextPaint.setColor(0xFF000000);
        mTextPaint.setTextAlign(Align.CENTER);
        setPadding(3, 3, 3, 3);
    }

    public void setText(String text) {
        mText = text;
        requestLayout();
        invalidate();
    }

    public void setTextSize(int size) {
        mTextPaint.setTextSize(size);
        requestLayout();
        invalidate();
    }

    public void setTextColor(int color) {
        mTextPaint.setColor(color);
        invalidate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        mTextPaint.getTextBounds(mText, 0, mText.length(), text_bounds);
        setMeasuredDimension(
                measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
            result = text_bounds.height() + getPaddingLeft() + getPaddingRight();

            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    private int measureHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        mAscent = (int) mTextPaint.ascent();
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
            result = text_bounds.width() + getPaddingTop() + getPaddingBottom();

            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        float text_horizontally_centered_origin_x = getPaddingLeft() + text_bounds.width()/2f;
        float text_horizontally_centered_origin_y = getPaddingTop() - mAscent;

        canvas.translate(text_horizontally_centered_origin_y, text_horizontally_centered_origin_x);
        canvas.rotate(-90);
        canvas.drawText(mText, 0, 0, mTextPaint);
    }
}

(NB: may be You need to customize paddings of VerticalLabelView: on line
result = text_bounds.height() + getPaddingLeft() + getPaddingRight() + 16;
add “+16” for better padding)

and attrs.xml for VerticalLabelView class:

<resources>
     <declare-styleable name="VerticalLabelView">
        <attr name="text" format="string" />
        <attr name="textColor" format="color" />
        <attr name="textSize" format="dimension" />
    </declare-styleable>
</resources>

Layout for PopupWindow (menu_layout.xml) in this case might be like:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/menu_root"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:layout_margin="@dimen/activity_horizontal_margin">

    <<your_package_name>.VerticalLabelView
        android:id="@+id/menu_item1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        android:layout_margin="16dp"
        android:padding="4dp"
        android:text="Vertical menu item 1"/>

    <<your_package_name>.VerticalLabelView
        android:id="@+id/menu_item2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        android:layout_margin="16dp"
        android:padding="4dp"
        android:text="Vertical menu item 2"/>

    <<your_package_name>.VerticalLabelView
        android:id="@+id/menu_item3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        android:layout_margin="16dp"
        android:padding="4dp"
        android:text="Vertical menu item 3"/>

    <<your_package_name>.VerticalLabelView
        android:id="@+id/menu_item4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:textSize="18sp"
        android:layout_margin="16dp"
        android:padding="4dp"
        android:text="Vertical menu item 4"/>

</LinearLayout>

And MainActivity class can be like:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    private Toolbar mToolbar;
    private int mToolbarTitleColor;
    private ImageButton mMainMenuButton;
    private int mActionBarSize;
    private PopupWindow mPopupMenu;
    private int mTextSize = 48;

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

        TypedValue tv = new TypedValue();
        if (getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
            mActionBarSize = TypedValue.complexToDimensionPixelSize(tv.data,getResources().getDisplayMetrics());
        }

        mToolbarTitleColor = Color.WHITE;
        mToolbar = (Toolbar) findViewById(R.id.toolbar);
        mToolbar.setTitleTextColor(mToolbarTitleColor);

        setSupportActionBar(mToolbar);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

        Drawable menuIcon = ContextCompat.getDrawable(this, R.drawable.ic_more_vert);
        menuIcon.setColorFilter(mToolbarTitleColor, PorterDuff.Mode.SRC_ATOP);

        getMenuInflater().inflate(R.menu.main_menu, menu);
        mMainMenuButton = (ImageButton) menu.findItem(R.id.menu_button).getActionView();
        mMainMenuButton.setBackground(null);
        mMainMenuButton.setImageDrawable(menuIcon);
        mMainMenuButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mPopupMenu != null && mPopupMenu.isShowing()) {
                    mPopupMenu.dismiss();
                }
                mPopupMenu = createPopupMenu();
                mPopupMenu.showAtLocation(v, Gravity.TOP | Gravity.RIGHT, 0, mActionBarSize);
            }
        });
        return true;
    }

    public PopupWindow createPopupMenu() {
        final PopupWindow popupWindow = new PopupWindow(this);
        LayoutInflater inflater = getLayoutInflater();

        View popupView = inflater.inflate(R.layout.menu_layout, null);

        VerticalLabelView menuItem1 = (VerticalLabelView)popupView.findViewById(R.id.menu_item1);
        menuItem1.setOnClickListener(mOnMenuItemClickListener);
        menuItem1.setText("Vertical menu item 1");
        menuItem1.setTextColor(Color.WHITE);
        menuItem1.setTextSize(mTextSize);

        VerticalLabelView menuItem2 = (VerticalLabelView)popupView.findViewById(R.id.menu_item2);
        menuItem2.setOnClickListener(mOnMenuItemClickListener);
        menuItem2.setText("Vertical menu item 2");
        menuItem2.setTextColor(Color.WHITE);
        menuItem2.setTextSize(mTextSize);

        VerticalLabelView menuItem3 = (VerticalLabelView)popupView.findViewById(R.id.menu_item3);
        menuItem3.setOnClickListener(mOnMenuItemClickListener);
        menuItem3.setText("Vertical menu item 3");
        menuItem3.setTextColor(Color.WHITE);
        menuItem3.setTextSize(mTextSize);

        VerticalLabelView menuItem4 = (VerticalLabelView)popupView.findViewById(R.id.menu_item4);
        menuItem4.setOnClickListener(mOnMenuItemClickListener);
        menuItem4.setText("Vertical menu item 4");
        menuItem4.setTextColor(Color.WHITE);
        menuItem4.setTextSize(mTextSize);

        popupWindow.setFocusable(true);
        popupWindow.setWidth(WindowManager.LayoutParams.WRAP_CONTENT);
        popupWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
        popupWindow.setContentView(popupView);

        return popupWindow;
    }

    private View.OnClickListener mOnMenuItemClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            switch (view.getId()) {
                case R.id.menu_item1: {
                    Log.d(TAG, "menu_item1");
                }
                break;
                case R.id.menu_item2: {
                    Log.d(TAG, "menu_item2");
                }
                break;
                case R.id.menu_item3: {
                    Log.d(TAG, "menu_item3");
                }
                case R.id.menu_item4: {
                    Log.d(TAG, "menu_item4");
                }
                break;
                default: {
                }
            }
            if (mPopupMenu != null && mPopupMenu.isShowing()) {
                mPopupMenu.dismiss();
            }
        }
    };
}

Ultimatly, as result You should receive something like that:

Vertical menu items screenshot

P.S. Of course You need more elegant solution for createPopupMenu().