Android Native - How to create Contextual Menus

dimitrilc 1 Tallied Votes 216 Views Share

Introduction

The Android platform provides many different types of menus to be added to an Android app. In this tutorial, we will learn how to add Contextual Menus to our app.

Goals

At the end of the tutorial, you would have learned:

  1. How to add a floating contextual menu.
  2. How to add a contextual menu in action mode.

Tools Required

  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 3.

Prerequisite Knowledge

  1. Basic Android.
  2. Menu XML resource.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Give the default TextView an android:id of hello and android:textSize of 32sp.

  3. Your activity_main.xml should look like the code below.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <TextView
            android:id="@+id/hello"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/hello_world"
            android:textSize="32sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. Add the string resources below into your strings.xml file.

     <string name="shuffle">Shuffle</string>
     <string name="reset">Reset</string>
     <string name="insert_random">Insert Random</string>
     <string name="hello_world">Hello World!</string>
  5. Add a menu XML resource called contextual.xml under res/menu. This will represent a menu with 3 options, Shuffle, Reset, and Insert Random. Later on, these actions will be performed on the default TextView.

     <?xml version="1.0" encoding="utf-8"?>
     <menu xmlns:android="http://schemas.android.com/apk/res/android">
    
        <item
            android:id="@+id/shuffle"
            android:title="@string/shuffle" />
        <item
            android:id="@+id/reset"
            android:title="@string/reset" />
        <item
            android:id="@+id/insert_random"
            android:title="@string/insert_random" />
     </menu>

Contextual Menu modes

A Contextual Menu can operate in two different modes:

  1. floating contextual menu: a floating menu appears on the screen after long-clicking a View. The floating menu uses this View as an anchor.
    floating_menu_1.png

  2. contextual action mode menu: the menu appears in the Action Bar instead, showing menu items as actions.

contextual_action.png

Add a floating contextual menu

To add a floating contextual menu, follow the steps below:

  1. In MainActivity’s onCreate(), retrieve the default TextView.

     val textView = findViewById<TextView>(R.id.hello)
  2. Next, we will have to register this View for a contextual menu by calling registerForContextMenu() and passing in this same View.

     //Contextual floating menu
     registerForContextMenu(textView)
  3. Now, we need to override MainActivity’s onCreateContextMenu() method, and inflate the menu resource file contextual.xml here.

     override fun onCreateContextMenu(
        menu: ContextMenu?,
        v: View?,
        menuInfo: ContextMenu.ContextMenuInfo?
     ) {
        super.onCreateContextMenu(menu, v, menuInfo)
        menuInflater.inflate(R.menu.contextual, menu)
     }
  4. The floating contextual menu should be working if you run the app now, but let us add some behaviors to the menu items to make them useful. We can listen for the floating contextual menu’s item clicks in onContextItemSelected().

     override fun onContextItemSelected(item: MenuItem): Boolean {
        super.onContextItemSelected(item)
        return handleContextMenuItem(item)
     }
  5. Because we will reuse the logic to handle the menu items later on in the tutorial, we will extract this logic into a different function called handleContextMenuItem(). This function will shuffle, reset, or insert a random alphabet character at a random index to Textview.

     private fun handleContextMenuItem(item: MenuItem): Boolean {
        val textView = findViewById<TextView>(R.id.hello)
        val text = textView.text
    
        return when(item.itemId){
            R.id.shuffle -> {
                textView.text = text
                    .asSequence()
                    .shuffled()
                    .joinToString(separator = "") {
                        "$it"
                    }
                true
            }
            R.id.reset -> {
                textView.text = getString(R.string.hello_world)
                true
            }
            R.id.insert_random -> {
                textView.text = StringBuilder(text)
                    .insert(
                        text.indices.random(),
                        alphabet.random()
                    )
                true
            }
            else -> false
        }
     }
  6. Also, add the companion object below for the alphabet constant.

     companion object {
        //Don't want chars 91->96
        private val alphabet = CharRange('A', 'Z').plus(CharRange('a', 'z'))
     }

We are now done with the floating contextual menu. The app should behave similarly to the animation below.

ContextualMenu1.gif

Add a contextual menu in action mode

In this section, we will learn how to add a contextual menu in action mode.

  1. Comment out registerForContextMenu(textView) in onCreate() as we will not be using the floating action menu anymore.

  2. Implement the ActionMode.Callback interface. This callback allows you to inflate, listen to click and destroy events, etc.

     private val actionModeCallback = object : ActionMode.Callback {
        override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
            menuInflater.inflate(R.menu.contextual, menu)
            return true
        }
    
        override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
            return handleContextMenuItem(item)
        }
    
        override fun onPrepareActionMode(mode: ActionMode, menu: Menu) = false
        override fun onDestroyActionMode(mode: ActionMode) {}
     }
  3. In onCreateActionMode(), we inflate the menu XML resource as done previously in onCreateContextMenu. Because actionModeCallback is declared inside the MainActivity scope, we have access to the menuInflater in the callback as well. If your implementation of ActionMode.Callback is outside of MainActivity, then you can access a MenuInflater from the ActionMode parameter instead.

  4. Back in onCreate(), we can bind the TextView’s OnLongClickListener to invoke a function called startActionMode().

     //Contextual action menu
     textView.setOnLongClickListener {
        startActionMode(actionModeCallback)
        true
     }

When we run the app now, we should get a contextual menu in Action Bar instead of a floating contextual menu.

ContextualMenu2.gif

Solution Code

MainActivity.kt

class MainActivity : AppCompatActivity() {

   private val actionModeCallback = object : ActionMode.Callback {
       override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
           menuInflater.inflate(R.menu.contextual, menu)
           return true
       }

       override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
           return handleContextMenuItem(item)
       }

       override fun onPrepareActionMode(mode: ActionMode, menu: Menu) = false
       override fun onDestroyActionMode(mode: ActionMode) {}
   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val textView = findViewById<TextView>(R.id.hello)

       //Contextual floating menu
       //registerForContextMenu(textView)

       //Contextual action menu
       textView.setOnLongClickListener {
           startActionMode(actionModeCallback)
           true
       }
   }

   override fun onCreateContextMenu(
       menu: ContextMenu?,
       v: View?,
       menuInfo: ContextMenu.ContextMenuInfo?
   ) {
       super.onCreateContextMenu(menu, v, menuInfo)
       menuInflater.inflate(R.menu.contextual, menu)
   }

   override fun onContextItemSelected(item: MenuItem): Boolean {
       super.onContextItemSelected(item)
       return handleContextMenuItem(item)
   }

   private fun handleContextMenuItem(item: MenuItem): Boolean {
       val textView = findViewById<TextView>(R.id.hello)
       val text = textView.text

       return when(item.itemId){
           R.id.shuffle -> {
               textView.text = text
                   .asSequence()
                   .shuffled()
                   .joinToString(separator = "") {
                       "$it"
                   }
               true
           }
           R.id.reset -> {
               textView.text = getString(R.string.hello_world)
               true
           }
           R.id.insert_random -> {
               textView.text = StringBuilder(text)
                   .insert(
                       text.indices.random(),
                       alphabet.random()
                   )
               true
           }
           else -> false
       }
   }

   companion object {
       //Don't want chars 91->96
       private val alphabet = CharRange('A', 'Z').plus(CharRange('a', 'z'))
   }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <TextView
       android:id="@+id/hello"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/hello_world"
       android:textSize="32sp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

contextual.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

   <item
       android:id="@+id/shuffle"
       android:title="@string/shuffle" />
   <item
       android:id="@+id/reset"
       android:title="@string/reset" />
   <item
       android:id="@+id/insert_random"
       android:title="@string/insert_random" />
</menu>

**strings.xml**

<resources>
   <string name="app_name">Daniweb Android Contextual Menu</string>
   <string name="shuffle">Shuffle</string>
   <string name="reset">Reset</string>
   <string name="insert_random">Insert Random</string>
   <string name="hello_world">Hello World!</string>
</resources>

Summary

We have learned how to create a floating contextual menu as well as a contextual menu in action mode. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidContextualMenus.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.