Как стать автором
Поиск
Написать публикацию
Обновить

Каскадное меню в Jetpack Compose Kotlin

Время на прочтение3 мин
Количество просмотров1.7K

В Jetpack Compose легко реализуется меню первого уровня. Но реализация вложенного (каскадного) меню не очевидна, поскольку в лямбде onClick пункта меню DropdownMenuItem() невозможно вызвать @Composable функцию.

Мне пришлось потратить значительное время на поиски решения. Для того, чтобы помочь ищущим предлагаю разработанный мной вариант.

Тест меню реализован на основе проекта Empty Activity Android Studio. Для тестирования предлагается создать этот пустой проект и добавить в него последовательно кусочки кода из статьи.

Для отображения иконок вложенных меню необходимо импортировать

import androidx.compose.material.icons.automirrored.filled.ArrowRight

для чего следует включить в зависимости файла build.gradle.kts

implementation(libs.androidx.material.icons.extended)

и файла libs.versions.toml

androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }

Строка меню представлена объектами:

data class Menu(var title: String = "",
 var onClick: String = "",
 var menu: ListMenu = ListMenu()
)

Меню представляет собой объект-список:

data class ListMenu ( private var menu: MutableList<Menu> = mutableListOf<Menu>(),
var Count: Int = 0 )
{  // Добавить элемент ListMenu
  fun add(menu: Menu) {
    menu.add(menu)
    Count++
  }
  // Получить элемент Menu по индексу i
  fun getItem(i: Int): Menu {return _menu.get(i)}
}

На базе данных объектов реализуем простое трехуровневое меню:

fun testList(): ListMenu {
    var mListMenu1: ListMenu = ListMenu()
 // Меню первого уровня
    mListMenu1.add(Menu("Пункт 1"))
    var mListMenu2 = ListMenu() // Меню второго уровня
    var sListMenu3 = ListMenu() // Меню третьего уровня
    sListMenu3.add(Menu("Пункт 6"))
    sListMenu3.add(Menu("Пункт 7"))
    mListMenu2.add(Menu("Пункт 3", "", sListMenu3))
    mListMenu2.add(Menu("Пункт 4"))
    mListMenu1.add(Menu("Пункт 2", "", mListMenu2))
    mListMenu1.add(Menu("Пункт 5"))
    return mListMenu1
}

Для реализации потребуется простейший стек для сохранения списков меню:

class RStack {
  companion object {
    private var globlist: MutableList<ListMenu> = mutableListOf<ListMenu>()        
    fun push(listMenu: ListMenu) {
      globlist.add(0,listMenu)
    }
    fun pop(i:Int = 0): ListMenu {
      if (i<Count())
        return globlist[i]
      else
        return ListMenu()
    }
    fun clear() { globlist.clear() }
    // Глубина стека
    fun Count(): Int { return _globlist.count() }
  }
}

Меню состоит из двух @Compasable функций:

меню первого уровня:

@Composable
fun GlobalMenu( listMenu: ListMenu,) {
  var expanded by remember { mutableStateOf(false) }
  var showSubMenu by remember { mutableStateOf(false) }
  Box( modifier = Modifier.fillMaxWidth().padding(16.dp) ) {
    IconButton(onClick = { expanded = !expanded }) {}
    DropdownMenu( expanded = expanded,
      onDismissRequest = { expanded = false } ) {
        RStack.clear()
        for (i in 0..listMenu.Count-1) {
          DropdownMenuItem(
          onClick =  { expanded = false 
            if (listMenu.getItem(i).menu.Count>0) {
              RStack.push(listMenu.getItem(i).menu)
              showSubMenu = true
            }
          },
          text =  { Text(listMenu.getItem(i).title) },
          trailingIcon = {
            if (listMenu.getItem(i).menu.Count>0)
              Icon(Icons.AutoMirrored.Filled.ArrowRight,
              contentDescription = "Показать подменю")
          }
        )
      }
    }
  }
  if(showSubMenu) SubMenu(RStack.pop(),expanded)
}

в котором определен контейнер Box для размещения всех меню и меню следующих уровней:

@Composable
fun SubMenu(listMenu: ListMenu, key: Boolean) {
  var expanded by remember(key) {mutableStateOf(true)}
  var showSubMenu by remember {mutableStateOf(false)}
  DropdownMenu(
    expanded = expanded,
    onDismissRequest = {
      expanded = false
    }
  ) {
    RStack.clear()
    for (i in 0..listMenu.Count-1) {
      DropdownMenuItem(
        onClick =  {
          expanded = !expanded
          if (listMenu.getItem(i).menu.Count>0) {
            RStack.push(listMenu.getItem(i).menu)
            showSubMenu = true
          }
        },
        text =  { Text(listMenu.getItem(i).title) },
        trailingIcon = {
        if (listMenu.getItem(i).menu.Count>0)
          Icon(Icons.AutoMirrored.Filled.ArrowRight,
        contentDescription = "Показать подменю"
        }
      )
    }
  }
  if(showSubMenu)   SubMenu(RStack.pop(),expanded)
}

Как можно видеть в этом меню используется рекурсивный вызов.

При формировании соответствующего уровня меню производится проверка наличия подменю у пунктов меню и сохранение в стек подменю, если оно имеется.

Для проверки работы меню достаточно в проекте Empty Activiti заменить onCreate на следующий код:

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  enableEdgeToEdge()
  setContent {
    TestCascadeMenuTheme {
      Scaffold(
        topBar = { // Верхняя панель
          TopAppBar(
            colors = TopAppBarDefaults.topAppBarColors(
              containerColor = MaterialTheme.colorScheme.primaryContainer,
              titleContentColor = MaterialTheme.colorScheme.primary,
            ),
            title = {
              Text("Test menu")
            },
            actions = {
              IconButton(onClick = { }) {
                Icon(
                  imageVector = Icons.Filled.Menu,
                  contentDescription = "Localized description"
                )
                val listMenu = testList()
                GlobalMenu(listMenu)
              }
            }
          )
        },
        modifier = Modifier.fillMaxSize()
      ) { innerPadding ->
        Greeting(
          name = "Android",
          modifier = Modifier.padding(innerPadding)
        )
      }
    }
  }
}

В эмуляторе должно получиться что-то на вроде:

Теги:
Хабы:
-2
Комментарии12

Публикации

Ближайшие события