Przejdź do treści
School IT / Przestrzeń do nauki

school-it.pl // android + kotlin // rozdział 9 // lekcja 35

Menu na pasku aplikacji

Przyciski zajmują miejsce na ekranie. Czynności, z których korzysta się rzadziej — ustawienia, udostępnianie, „O aplikacji” — chowa się w menu na górnym pasku. Najważniejsze dostają ikonę na pasku, reszta trafia pod trzy kropki ⋮. Dodasz do aplikacji pasek (MaterialToolbar), opiszesz menu w pliku XML i obsłużysz wybór pozycji. Zbudujesz notatnik ze zmianą wielkości czcionki, trybem nocnym i udostępnianiem.

Kotlin XML MaterialToolbar res/menu onCreateOptionsMenu onOptionsItemSelected 2 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Dodasz pasek aplikacji MaterialToolbar i podłączysz go jako pasek akcji.
  • Utworzysz plik menu w res/menu z ikonami i pozycjami ukrytymi pod ⋮.
  • Pokażesz menu w onCreateOptionsMenu.
  • Obsłużysz wybór pozycji w onOptionsItemSelected.
  • Użyjesz pozycji z zaznaczeniem (włącz / wyłącz) i podtytułu paska.
TEORIA

Pasek aplikacji

Szablon Empty Views Activity używa motywu bez paska (…NoActionBar) — dlatego dotąd na górze ekranu nic nie było. Pasek wstawiamy sami jako zwykłą kontrolkę w układzie i mówimy aktywności: „to jest twój pasek akcji”.

pasek w układzie
<com.google.android.material.appbar.MaterialToolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="?attr/colorPrimaryContainer"
    app:title="Notatnik" />
w onCreate, po initUI()
setSupportActionBar(toolbar)          // od teraz menu i tytuł trafiają na ten pasek
ZapisZnaczenie
?attr/actionBarSizeStandardowa wysokość paska z motywu (zwykle 56–64dp).
?attr/colorPrimaryContainerKolor z motywu Material 3 — pasek pasuje do reszty aplikacji.
app:titleTytuł na pasku. Atrybut z przestrzeni app: — w głównym elemencie układu musi być xmlns:app="http://schemas.android.com/apk/res-auto".
supportActionBar?.subtitle = "…"Podtytuł pod tytułem — zmieniany z kodu.
TEORIA

Plik menu

Pozycje menu opisuje się w XML, w osobnym folderze res/menu. Tworzysz go: prawy klik na res → New → Android Resource File → Resource type: Menu, nazwa pliku menu_main.

res/menu/menu_main.xml — budowa
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <!-- ikona na pasku, jeśli jest miejsce -->
    <item
        android:id="@+id/akcja_powieksz"
        android:icon="@drawable/ic_powieksz"
        android:title="Powiększ"
        app:showAsAction="ifRoom" />

    <!-- zawsze pod ⋮ -->
    <item
        android:id="@+id/akcja_o_aplikacji"
        android:title="O aplikacji"
        app:showAsAction="never" />
</menu>
showAsActionGdzie pojawi się pozycja
ifRoomIkona na pasku, gdy jest miejsce; gdy brak — pod ⋮. Najczęstszy wybór.
neverZawsze w rozwijanym menu pod ⋮ (jako tekst).
alwaysZawsze na pasku — używaj oszczędnie, bo na wąskim ekranie zabraknie miejsca na tytuł.

android:title jest potrzebny także przy ikonie: po przytrzymaniu ikony pojawia się jako podpowiedź i czyta go czytnik ekranu. Zwróć uwagę na app:showAsAction, a nie android:showAsAction — w aplikacjach z AppCompatActivity obowiązuje wersja app:.

TEORIA

Pokazanie menu

Android sam pyta aktywność, czy ma menu. Odpowiadasz, nadpisując metodę onCreateOptionsMenu — wczytujesz w niej plik XML (podobnie jak LayoutInflater w lekcji 28):

MainActivity — tworzenie menu
override fun onCreateOptionsMenu(menu: Menu): Boolean {
    menuInflater.inflate(R.menu.menu_main, menu)     // XML → pozycje menu
    return true                                      // true = pokaż menu
}

Nie wywołujesz tej metody sam — Android robi to po onCreate. Wystarczy ją napisać w klasie aktywności (wpisz onCreateOp i wybierz podpowiedź — Android Studio wstawi szkielet).

TEORIA

Reakcja na wybór

Wszystkie kliknięcia w menu trafiają do jednej metody. Rozpoznajesz pozycję po itemId — to android:id z pliku menu:

MainActivity — wybór pozycji
override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
        R.id.akcja_powieksz -> zmienRozmiar(2f)
        R.id.akcja_o_aplikacji -> pokazOAplikacji()
        else -> return super.onOptionsItemSelected(item)   // nie nasza pozycja
    }
    return true                                          // obsłużone
}
dotknięcie pozycji
ikona na pasku
lub tekst pod ⋮
→
onOptionsItemSelected(item)
when (item.itemId)
→
nasza metoda
zmienRozmiar(), udostepnij()…
return true
TEORIA

Pozycja z zaznaczeniem

Pozycja z android:checkable="true" ma w menu pole wyboru — dobre dla ustawień typu włącz / wyłącz. Uwaga: Android nie przełącza zaznaczenia sam. Robisz to w kodzie:

przełączenie zaznaczenia
R.id.akcja_tryb_nocny -> {
    trybNocny = !trybNocny
    item.isChecked = trybNocny       // pole wyboru w menu
    pokazWyglad()
}
PRZYKŁAD

Notatnik z menu

Duże pole na notatkę i pasek z menu. Na pasku dwie ikony: powiększ i pomniejsz czcionkę (12–32 sp, krok 2). Podtytuł paska pokazuje bieżący rozmiar. Pod ⋮: Tryb nocny (z zaznaczeniem), Udostępnij (lekcja 34), Wyczyść (z pytaniem, lekcja 22) i O aplikacji.

19:30
NotatnikCzcionka: 18 spA⁺A⁻⋮
Na piątek:
– oddać projekt z Androida
– powtórzyć intencje
– kupić zeszyt w kratkę
dwie ikony z ifRoom i ⋮
→dotknij ⋮
19:30
NotatnikCzcionka: 18 spA⁺A⁻⋮
Na piątek:
– oddać projekt z Androida
– powtórzyć intencje
– kupić zeszyt w kratkę
Tryb nocny
Udostępnij
Wyczyść
O aplikacji
pozycje z showAsAction=”never”
19:31
NotatnikCzcionka: 24 spA⁺A⁻⋮
Na piątek:
– oddać projekt z Androida
– powtórzyć intencje
– kupić zeszyt w kratkę
tryb nocny zmienia pole notatki, czcionka 24 sp

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Notatnik, Kotlin, API 24.
  2. Ikony: prawy klik na res → New → Vector Asset → Clip art: ic_powieksz (text increase) i ic_pomniejsz (text decrease).
  3. Plik menu: prawy klik na res → New → Android Resource File, nazwa menu_main, typ Menu.
  4. Podmień activity_main.xml, menu_main.xml i MainActivity.kt.
app/src/main/
├── java/…/MainActivity.kt
└── res/
    ├── drawable/ic_powieksz.xml, ic_pomniejsz.xml
    ├── layout/activity_main.xmlpasek + pole notatki
    └── menu/menu_main.xmlnowy folder i plik
res/layout/activity_main.xml — pełny plik
<?xml version="1.0" encoding="utf-8"?>
<!-- bez marginesu: pasek ma sięgać krawędzi ekranu -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:fitsSystemWindows="true">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimaryContainer"
        app:title="Notatnik" />

    <EditText
        android:id="@+id/etNotatka"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="@null"
        android:gravity="top"
        android:hint="Pisz tutaj…"
        android:inputType="textMultiLine|textCapSentences"
        android:padding="16dp" />

</LinearLayout>
res/menu/menu_main.xml — pełny plik
<?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/akcja_powieksz"
        android:icon="@drawable/ic_powieksz"
        android:title="Powiększ czcionkę"
        app:showAsAction="ifRoom" />

    <item
        android:id="@+id/akcja_pomniejsz"
        android:icon="@drawable/ic_pomniejsz"
        android:title="Pomniejsz czcionkę"
        app:showAsAction="ifRoom" />

    <item
        android:id="@+id/akcja_tryb_nocny"
        android:checkable="true"
        android:title="Tryb nocny"
        app:showAsAction="never" />

    <item
        android:id="@+id/akcja_udostepnij"
        android:title="Udostępnij"
        app:showAsAction="never" />

    <item
        android:id="@+id/akcja_wyczysc"
        android:title="Wyczyść"
        app:showAsAction="never" />

    <item
        android:id="@+id/akcja_o_aplikacji"
        android:title="O aplikacji"
        app:showAsAction="never" />
</menu>
MainActivity.kt — pełny plik
package pl.schoolit.notatnik

import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.appbar.MaterialToolbar

class MainActivity : AppCompatActivity() {

    // ---------- kontrolki ----------
    private lateinit var toolbar: MaterialToolbar
    private lateinit var etNotatka: EditText

    // ---------- stan ----------
    private val MIN_ROZMIAR = 12f
    private val MAX_ROZMIAR = 32f
    private var rozmiar = 18f
    private var trybNocny = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initUI()
        setSupportActionBar(toolbar)          // pasek z układu staje się paskiem akcji
        pokazWyglad()
    }

    private fun initUI() {
        toolbar = findViewById(R.id.toolbar)
        etNotatka = findViewById(R.id.etNotatka)
    }

    // ---------- menu ----------
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.akcja_powieksz -> zmienRozmiar(2f)
            R.id.akcja_pomniejsz -> zmienRozmiar(-2f)
            R.id.akcja_tryb_nocny -> {
                trybNocny = !trybNocny
                item.isChecked = trybNocny        // Android sam tego nie zrobi
                pokazWyglad()
            }
            R.id.akcja_udostepnij -> udostepnij()
            R.id.akcja_wyczysc -> zapytajOWyczyszczenie()
            R.id.akcja_o_aplikacji -> pokazOAplikacji()
            else -> return super.onOptionsItemSelected(item)
        }
        return true
    }

    // ---------- akcje ----------
    private fun zmienRozmiar(zmiana: Float) {
        val nowy = rozmiar + zmiana
        if (nowy < MIN_ROZMIAR || nowy > MAX_ROZMIAR) {
            Toast.makeText(this, "Rozmiar od 12 do 32 sp", Toast.LENGTH_SHORT).show()
            return
        }
        rozmiar = nowy
        pokazWyglad()
    }

    private fun udostepnij() {
        val tekst = etNotatka.text.toString().trim()
        if (tekst.isEmpty()) {
            Toast.makeText(this, "Notatka jest pusta", Toast.LENGTH_SHORT).show()
            return
        }
        val intencja = Intent(Intent.ACTION_SEND)          // lekcja 34
        intencja.type = "text/plain"
        intencja.putExtra(Intent.EXTRA_TEXT, tekst)
        startActivity(Intent.createChooser(intencja, "Udostępnij notatkę"))
    }

    private fun zapytajOWyczyszczenie() {
        if (etNotatka.text.isEmpty()) return
        AlertDialog.Builder(this)
            .setTitle("Wyczyścić notatkę?")
            .setMessage("Tekst zostanie usunięty.")
            .setPositiveButton("Wyczyść") { _, _ -> etNotatka.text.clear() }
            .setNegativeButton("Anuluj", null)
            .show()
    }

    private fun pokazOAplikacji() {
        AlertDialog.Builder(this)
            .setTitle("Notatnik")
            .setMessage("Wersja 1.0\nPrzykład z lekcji 35 kursu Android + Kotlin.")
            .setPositiveButton("OK", null)
            .show()
    }

    // ---------- stan → ekran ----------
    private fun pokazWyglad() {
        etNotatka.textSize = rozmiar                       // w sp
        supportActionBar?.subtitle = "Czcionka: ${rozmiar.toInt()} sp"

        if (trybNocny) {
            etNotatka.setBackgroundColor(Color.parseColor("#1C1B1F"))
            etNotatka.setTextColor(Color.parseColor("#E6E1E5"))
        } else {
            etNotatka.setBackgroundColor(Color.WHITE)
            etNotatka.setTextColor(Color.BLACK)
        }
    }
}

Co dzieje się po kolei

  • Układ zaczyna się od paska MaterialToolbar. setSupportActionBar(toolbar) mówi aktywności, żeby tam pokazywała tytuł i menu.
  • Po onCreate Android wywołuje onCreateOptionsMenu: plik menu_main.xml zamienia się w dwie ikony i cztery pozycje pod ⋮.
  • Każde dotknięcie pozycji trafia do onOptionsItemSelected; when po itemId wywołuje odpowiednią metodę.
  • zmienRozmiar pilnuje zakresu 12–32 sp. Poza nim pokazuje Toast i kończy metodę przez return.
  • Tryb nocny przełącza pole w kodzie i ręcznie ustawia item.isChecked.
  • pokazWyglad() ustawia rozmiar, podtytuł i kolory — wszystko w jednym miejscu.

Sprawdź trzy rzeczy

  1. Przytrzymaj ikonę A+ na pasku. Pojawi się dymek z tekstem „Powiększ czcionkę” — to android:title.
  2. Obróć ekran (emulator: ikona obrotu na pasku bocznym). W poziomie jest więcej miejsca — sprawdź, czy któraś pozycja nie przeskoczyła na pasek. Zauważ też, że rozmiar czcionki i tryb nocny wróciły do początkowych. Dlaczego — w lekcji 36.
  3. Usuń z onOptionsItemSelected linię item.isChecked = trybNocny. Tryb nocny działa, ale pole wyboru w menu się nie zmienia.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
MaterialToolbarkontrolkaGórny pasek aplikacji.
setSupportActionBar(toolbar)metodaCzyni pasek z układu paskiem akcji aktywności.
supportActionBar?.title / subtitlewłaściwościTytuł i podtytuł paska z kodu.
res/menu/*.xml, <item>zasóbOpis pozycji menu.
app:showAsAction="ifRoom | never | always"atrybutIkona na pasku albo pozycja pod ⋮.
android:checkable="true"atrybutPozycja z polem wyboru.
override fun onCreateOptionsMenu(menu: Menu): BooleanmetodaTworzy menu; menuInflater.inflate(…).
override fun onOptionsItemSelected(item: MenuItem): BooleanmetodaReakcja na wybór; item.itemId.
item.isCheckedBooleanZaznaczenie pozycji.
onPrepareOptionsMenu(menu) + invalidateOptionsMenu()metodyZmiana menu w trakcie działania (np. wyłączenie pozycji) — zadanie 5.
editText.textSize = 18fwłaściwośćRozmiar tekstu w sp.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
menu w ogóle się nie pokazujeBrak setSupportActionBar(toolbar) albo onCreateOptionsMenu zwraca false.
wszystkie pozycje są pod ⋮, mimo showAsActionUżyto android:showAsAction. W AppCompat musi być app:showAsAction (i xmlns:app w <menu>).
Unresolved reference: menu przy R.menu.menu_mainPlik nie leży w folderze res/menu albo ma inną nazwę.
pole wyboru „Tryb nocny” się nie zmieniaBrak item.isChecked = … — Android nie przełącza zaznaczenia sam.
ikona na pasku ma kolor tła — niewidocznaKolor ikony ustawiony na stałe w pliku wektora. Zostaw android:tint="?attr/colorControlNormal" z Vector Asset.
pasek nachodzi na pasek stanuBrak android:fitsSystemWindows="true" w głównym układzie.
onOptionsItemSelected bez else -> return super…Działa w tym przykładzie, ale później zepsuje strzałkę „wstecz” na pasku (android.R.id.home) — zostawiaj super.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Empty Views Activity z paskiem MaterialToolbar. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Licznik z menu★☆☆

Duża liczba na środku ekranu. Na pasku ikony + i − (add, remove), pod ⋮ pozycja Zeruj. Tytuł paska: „Licznik”, podtytuł: „Kliknięć: 12” (wszystkie kliknięcia, także −).

pokaż wskazówkę
  • Dwa pola: wartosc i klikniecia; jedna metoda pokazStan() ustawia napis i supportActionBar?.subtitle.
ZAD 2Kolor tła z menu★☆☆

Pod ⋮ cztery pozycje kolorów: Biały, Błękitny, Miętowy, Różowy. Wybór zmienia tło ekranu i podtytuł paska na nazwę koloru.

pokaż wskazówkę
  • Nazwę koloru masz w item.title. Tło: findViewById<View>(R.id.main).setBackgroundColor(…).
ZAD 3Wybór jednej opcji w menu★★☆

Lista uczniów (ListView). Pod ⋮ grupa „Sortuj”: Alfabetycznie, Od końca, Według długości — zaznaczona może być tylko jedna (kółko, nie kwadrat).

pokaż wskazówkę
  • W XML: <group android:checkableBehavior="single"> wokół trzech <item>. W kodzie: item.isChecked = true — grupa sama odznaczy pozostałe.
ZAD 4Drugi ekran ze strzałką wstecz★★☆

Rozbuduj przykład: O aplikacji otwiera osobną aktywność (lekcja 30) z własnym paskiem i strzałką ← po lewej. Strzałka zamyka ekran.

pokaż wskazówkę
  • W drugiej aktywności: setSupportActionBar(toolbar), supportActionBar?.setDisplayHomeAsUpEnabled(true), a w onOptionsItemSelected: android.R.id.home -> finish().
ZAD 5Wyłączane pozycje★★☆

Rozbuduj przykład: zamiast Toastu na granicy zakresu pozycja A+ jest wyłączona przy 32 sp, a A− przy 12 sp. Wyczyść i Udostępnij są wyłączone, gdy notatka jest pusta.

pokaż wskazówkę
  • Nadpisz onPrepareOptionsMenu(menu): menu.findItem(R.id.akcja_powieksz).isEnabled = rozmiar < MAX_ROZMIAR. Po każdej zmianie wywołaj invalidateOptionsMenu() — Android przygotuje menu od nowa. Zmianę tekstu wykryjesz przez etNotatka.addTextChangedListener { invalidateOptionsMenu() }.
ZAD 6Lista zakupów z menu★★★

Przebuduj przykład z lekcji 26: przyciski Sortuj i Usuń kupione przenieś do menu (ikona sort na pasku, „Usuń kupione” pod ⋮). Dodaj pod ⋮ Udostępnij listę (niekupione produkty, każdy w nowej linii) i Wyczyść wszystko z pytaniem. Licznik produktów pokaż w podtytule paska.

pokaż wskazówkę
  • Tekst do udostępnienia: zakupy.filter { !it.startsWith(KUPIONE) }.joinToString("\n"). W pokazListe() ustaw supportActionBar?.subtitle.
PODSUMOWANIE

Co trzeba zapamiętać

  • Pasek: MaterialToolbar w układzie + setSupportActionBar(toolbar).
  • Menu: plik w res/menu, pozycje <item> z id, title, opcjonalnie icon i app:showAsAction.
  • onCreateOptionsMenu wczytuje plik, onOptionsItemSelected reaguje na wybór po itemId.
  • Pozycje z zaznaczeniem przełączasz sam: item.isChecked = ….
  • Rzadziej używane czynności — pod ⋮; najważniejsze — ikoną na pasku.

Dokumentacja: Menu, Konfiguracja paska aplikacji.

Postęp zapisuje się w tej przeglądarce.