PopupMenu En Android

El PopupMenu en Android es un menú que se ancla a un View para que aparezca por debajo de este en caso de existir el espacio para ello. De lo contrario se mostrará por encima.

Apariencia de PopupMenu en Android

A diferencia del menú de opciones y el contextual, la aparición del PopupMenu es controlada por nosotros, basado en algún evento de interés sobre un view.

Ejemplo de PopupMenu En Android

En este tutorial aprenderás a crear, mostrar y procesar los eventos de un PopupMenu a partir del siguiente ejemplo de ilustración:

App de ejemplo de PopupMenu en Android

Como ves, se trata de una interacción simple donde haces click a un ImageButton y se despliega el menú con varias acciones relacionadas al contenido. Cuando seleccionas un ítem o cierras el menú, se despliega un mensaje indicando el evento.

Puedes descargar el proyecto Android Studio desde el siguiente enlace:

1. Definir View Ancla Del PopupMenu

Antes de nada, debes seleccionar al view que disparará la aparición del menú con las acciones relacionadas al contenido específico.

Diseño de layout para ejemplo de PopupMenu

Pongamos por caso nuestra App de ejemplo, donde el view que sirve de ancla es un ImageView. El archivo res/activity_main.xml contiene el diseño donde se encuentra este elemento:

<?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="wrap_content"
    android:background="@color/blue_50"
    android:padding="16dp"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/post_title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:text="@string/post_title"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
        app:layout_constraintEnd_toStartOf="@+id/more_actions_button"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintStart_toEndOf="@+id/post_featured_image"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/post_intro"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:text="@string/post_description"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/more_actions_button"
        app:layout_constraintStart_toEndOf="@+id/post_featured_image"
        app:layout_constraintTop_toBottomOf="@+id/post_title" />

    <ImageView
        android:id="@+id/post_featured_image"
        android:layout_width="44dp"
        android:layout_height="44dp"

        android:contentDescription="@string/post_image_desc"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageButton
        android:id="@+id/more_actions_button"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:background="?selectableItemBackgroundBorderless"
        android:contentDescription="@string/more_actions_desc"
        android:padding="0dp"
        android:src="@drawable/ic_more_actions"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Ahora bien, como el evento de click es el que muestra al menú, entonces añadimos una función lambda con setOnClickListener() para dejar expresado la creación del mismo:

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

        val moreButton: ImageView = findViewById(R.id.more_actions_button)
        moreButton.setOnClickListener { view ->
            showMoreActionsMenu(view)
        }
    }

    private fun showMoreActionsMenu(button: View) {

    }
}

Al interior de showMoreActionMenu() añadiremos la lógica de creación que veremos en los siguientes pasos.

2. Crear Recurso De Menú

Acto seguido, crea el recurso de menú que contendrá la definición XML de las acciones a presentar ante el usuario. Recuerda que en Android Studio esto se realiza con New > Android Resource File.

Crear recurso de menú para PopupMenu

Debido a que tendremos tres acciones, entonces añadimos una etiqueta <item> por cada una y asignamos los títulos correspondientes:

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

    <item
        android:id="@+id/add_favorites"
        android:title="@string/add_favorites" />
    <item
        android:id="@+id/share"
        android:title="@string/share" />
    <item
        android:id="@+id/move"
        android:title="@string/move" />
</menu>

3. Crear PopupMenu En Kotlin

Luego usa el constructor público de la clase PopupMenu para crear la instancia que será mostrada e infla el recurso de menú sobre ella con el método MenuInflater.inflate():

private fun showMoreActionsMenu(button: View) {
    val popupMenu = PopupMenu(this, button)
    menuInflater.inflate(R.menu.main_menu, popupMenu.menu)
    popupMenu.show()
}

Pasa a la propiedad PopupMenu.menu al inflado para convertir el recurso en una implementación de Menu, con el fin de que el menú modal haga uso de ella.

Al final del método, llama a show() desde para mostrarlo en pantalla. Verás que su aparición será por debajo del botón de overflow.

Crear PopupMenu Programáticamente

Por otro lado, si quisieras producir el mismo resultado anterior pero en tiempo de ejecución, entonces crea la instancia PopupMenu y luego añade los ítems con el método add():

private fun showMoreActionsMenu(button: View) {
    val popupMenu = PopupMenu(this, button)
    popupMenu.menu.add(R.string.add_favorites)
    popupMenu.menu.add(R.string.share)
    popupMenu.menu.add(R.string.move)
    popupMenu.show()
}

Recuerda que el objeto mostrado en pantalla se encuentra en la propiedad menu, por lo que es sobre ella que realizas la adición o cualquier otra operación de modificación.

4. Manejar Eventos De Click

Habíamos dicho al inicio que cuando se haga click sobre los ítems del menú mostraríamos un mensaje que evidencia la captura del evento:

Manejar eventos de click con OnMenuItemClickListener

¿Cómo logras este manejo?

A través de la escucha PopupMenu.OnMenuItemClickListener y su controlador onMenuItemClick(). Este método recibe la instancia MenuItem del elemento clickeado y retorna un booleano (true para representar evento consumido, false para lo contrario).

Asimismo debes asignar al callback al menú con el método setOnMenuItemClickListener(), la cual puede ser creada anónimamente, una referencia o implementarla sobre la actividad o fragmento donde vive el menú:

private fun showMoreActionsMenu(button: View) {
    val popupMenu = PopupMenu(this, button)
    menuInflater.inflate(R.menu.main_menu, popupMenu.menu)
    popupMenu.setOnMenuItemClickListener (::manageItemClick)
    popupMenu.show()
}

private fun manageItemClick(menuItem: MenuItem): Boolean {
    return when(menuItem.itemId){
        R.id.add_favorites, R.id.share, R.id.move-> {
            showMessage(menuItem.title)
            true
        }
        else -> false
    }
}

private fun showMessage(title: CharSequence) {
    Toast.makeText(this, title, Toast.LENGTH_SHORT).show()
}

El ejemplo anterior pasa la referencia del método manageItemClick() a setOnMenuItemClickListener() para manejar los clicks.

Su objetivo es exactamente el que hemos visto en todos los menús hasta ahora. Usar la expresión when para dirigir el flujo dependiendo del ID del ítem.

Debido a que estamos viendo un ejemplo simple, todos los ítems muestran un Toast con showMessage() a partir de su propiedad title.

5. Manejar Evento De Descarte

Y finalmente, veamos que también existe un observador para cuando el menú se cierra debido a un descarte o consumo de click en alguno de sus ítems.

Procesar eventos de cierre de PopupMenu con OnDismissListener

Su nombre es PopupMenu.OnDismissListener y tiene un solo controlador llamado onDismiss(), el cual recibe la instancia del menú modal:

private fun showMoreActionsMenu(button: View) {
    // ...
    popupMenu.setOnDismissListener(::manageDismiss)
    // ...
}

private fun manageDismiss(popupMenu: PopupMenu) {
    showMessage("Menú cerrado")
}

En el código anterior, usamos setOnDismissListener() para mostrar un Toast en el momento que se desvanece el menú. Al mismo tiempo usamos la referencia de manageDismiss() para entregarle el control del cierre.

¿Qué Sigue?

En este tutorial aprendiste a crear, inflar, mostrar y manejar eventos de un PopupMenu en Android. Aquí termina la sección de menús en Android. Ahora puedes avanzar al apartado de ajustes (todo) de usuario para aprender a crear la interfaz con la librería de preferencias.

Más Tutoriales Android

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!