ActionBar - przykłady użycia

Funkcje

Tym razem na nasz celownik trafia ActionBar. Pełni on aktualnie sporo funkcji:

  • informuje użytkownika gdzie aktualnie znajduje się w aplikacji
  • umożliwia przejście w górę (nie mylić z przejściem wstecz)
  • udostępnia akcje dostępne dla danego widoku, dla nowszych urządzeń bez przycisku "opcje" dostarcza overflow button, który pełni tę rolę
  • akcje dla wybranego elementu
  • informuje o ładowaniu, odświeżaniu danego widoku
  • udostępnia akcje nawigacyjne

Historia

ActionBar został wprowadzony razem z Androidem 3.0 (API 11), ale dzięki Support Library dostępne jest już od API 7. Jeśli planujemy wypuścić aplikację na urządzenia z API 11+, to powinniśmy korzystać z dostępnego w SDK ActionBara, a w przeciwnym wypadku tego z dodatkowej biblioteki.

Na cele tego artykułu będziemy zajmować się tylko API 11+. Ponieważ API 10 to podczas pisania tego artykułu 30% rynku (źródło) w przyszłości na pewno zajmiemy się kompatybilnością z poprzednimi wersjami.

Z góry informuję, że kod źródłowy jak i APK aplikacji będzie dostępne na końcu artykułu więc w razie czego istnieje możliwość ich pobrania.

Aktualne położenie w aplikacji

Tutaj szału nie ma. Po prostu ustawiamy tytuł.

getActionBar().setTitle("Ekran wyboru");

Efekt:

2013-09-14 13.45.55

Przejście w górę

Przejście w górę często pokrywa się z przejściem wstecz, ale jest między nimi zdecydowana różnica. Załóżmy sytuację, że mamy listę artykułów. Po wybraniu pierwszego uruchomiony zostaje widok pierwszego artykułu, który zawiera przycisk "następny". Naciskamy przycisk i otworzony zostaje artykuł drugi. Kolejne akcje zaznaczyłem i ponumerowałem kolorem zielonym.

Jaka akcja powinna zostać podjęta po naciśnięciu poszczególnych przycisków?

  • wstecz powinno przenieść nas do artykułu 1 (kolor zielony)
  • w górę powinno przenieść nas do listy artykułów (kolor czerwony)
flow

Dodatkowe założenia przycisku "w górę":

  • strzałka w lewo powinna być widoczna tylko gdy naciśnięcie przycisku spowoduje akcję
  • nie powinien doprowadzać do wyłączenia się aplikacji
  • nie powinien prowadzić do innej aplikacji
  • powinien zawsze prowadzić do jednego widoku, bez znaczenia jaką ścieżkę nawigacji wybrał użytkownik

Dobra, koniec teorii. Do dzieła!

Dla celów przykładu wszystkie dane umieszczę bezpośrednio w kodzie. Oczywiście w realnej aplikacji warto skorzystać z bazy danych.

Zacznijmy od listy wpisów:

public class EntryListActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_entry_list);
	getActionBar().setTitle("Lista artykułów");

	ListView listView = (ListView) findViewById(R.id.entry_list_view);
	List<String> articles = new ArrayList<String>();
	articles.add("Pierwszy artykuł");
	articles.add("Drugi artykuł");
	listView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, articles));
	listView.setOnItemClickListener(new OnItemClickListener() {

	    @Override
	    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
		Intent intent = new Intent(getApplicationContext(), EntryActivity.class);
		intent.putExtra(EntryActivity.EXTRA_ARTICLE_ID, position);
		startActivity(intent);
	    }
	});
    }
}

Do listy dodajemy dwa elementy i korzystając z ArrayAdaptera podpinamy całość pod widok listy.

Pojedynczy wpis:

public class EntryActivity extends Activity {

    protected static final String EXTRA_ARTICLE_ID = "extra_article_id";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_entry);
	getActionBar().setDisplayHomeAsUpEnabled(true);

	int id = getIntent().getExtras().getInt(EXTRA_ARTICLE_ID);

	Button btnPrevious = (Button) findViewById(R.id.btn_previous);
	Button btnNext = (Button) findViewById(R.id.btn_next);

	Button btnEnabled;
	Button btnDisabled;
	String title = "Artykuł ";
	final int extra;

	if (id == 0) {
	    btnDisabled = btnPrevious;
	    btnEnabled = btnNext;
	    title += " pierwszy";
	    extra = 1;
	} else {
	    btnDisabled = btnNext;
	    btnEnabled = btnPrevious;
	    title += " drugi";
	    extra = 0;
	}

	setTitle(title);

	btnDisabled.setEnabled(false);
	btnEnabled.setOnClickListener(new OnClickListener() {

	    @Override
	    public void onClick(View v) {
		Intent intent = new Intent(getApplicationContext(), EntryActivity.class);
		intent.putExtra(EntryActivity.EXTRA_ARTICLE_ID, extra);
		startActivity(intent);

	    }
	});
    }

    /**
     * Możliwość wykrycia kliknięcia i obsługi przycisku w górę w własny sposób
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
	if (item.getItemId() == android.R.id.home) {

	    Toast.makeText(getApplicationContext(), "Naciśnięto przycisk w górę", Toast.LENGTH_SHORT).show();

	    // Użycie finish() jest niezalecane
	    // finish();
	    return false;
	}
	return super.onOptionsItemSelected(item);
    }

    /**
     * Obsługa przycusku wstecz
     */
    @Override
    public void onBackPressed() {
	Toast.makeText(getApplicationContext(), "Naciśnięto przycisk wstecz", Toast.LENGTH_SHORT).show();
	super.onBackPressed();
    }
}

Tutaj nowością jest linia:

getActionBar().setDisplayHomeAsUpEnabled(true);

Dodaje ona strzałkę przy logo. Poprawne działanie aplikacji wymaga jeszcze odpowiednich wpisów w AndroidManifest.xml

        <activity
            android:name="pl.javastart.actionbartest.upbutton.EntryActivity"
            android:parentActivityName="pl.javastart.actionbartest.upbutton.EntryListActivity" >

            <!-- Wsparcie dla 4.0 i poprzednie -->
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="pl.javastart.actionbartest.upbutton.EntryListActivity" />
        </activity>

Jako bonus zamieszczam manualną obsługę przycisków w górę oraz wstecz:

    /**
     * Możliwość wykrycia kliknięcia i obsługi przycisku w górę w własny sposób
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
	if (item.getItemId() == android.R.id.home) {

	    Toast.makeText(getApplicationContext(), "Naciśnięto przycisk w górę", Toast.LENGTH_SHORT).show();

	    // Użycie finish() jest niezalecane
	    // finish();
	    return false;
	}
	return super.onOptionsItemSelected(item);
    }

    /**
     * Obsługa przycusku wstecz
     */
    @Override
    public void onBackPressed() {
	Toast.makeText(getApplicationContext(), "Naciśnięto przycisk wstecz", Toast.LENGTH_SHORT).show();
	super.onBackPressed();
    }

Dokładny opis i wskazówki użycia przycisków wstecz i w górę znajdziecie tutaj.

Akcje dla danego widoku

Akcje dla danego widoku definiujemy niemalże identycznie jak menu. Co ciekawe Android za nas obsługuje problem ograniczonego miejsca na pasku akcji, a także fakt, że niektóre urządzenia(nowsze) nie posiadają przycisku opcji.

public class ActionsActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_actions);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
	getMenuInflater().inflate(R.menu.menu_actions, menu);
	return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
	Toast.makeText(getApplicationContext(), "Naciśnięto: " + item.getTitle().toString(), Toast.LENGTH_SHORT).show();
	return super.onOptionsItemSelected(item);
    }
}

Menu:

<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/menu_delete"
        android:icon="@android:drawable/ic_menu_delete"
        android:orderInCategory="100"
        android:showAsAction="always"
        android:title="Usuń"/>
    <item
        android:id="@+id/menu_settings"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="Ustawienia"/>
    <item
        android:id="@+id/menu_help"
        android:icon="@android:drawable/ic_menu_help"
        android:orderInCategory="100"
        android:showAsAction="ifRoom|withText"
        android:title="Pomoc Pomoc Pomoc"/>

</menu>

Tak to wygląda na telefonie pionowo (po naciśnięciu przycisk opcji):

2013-09-14 16.24.13

Na telefonie poziomo (jak widać napis Pomoc Pomoc Pomoc) się już mieści:

2013-09-14 16.24.20

I na tablecie. Tutaj dodany został overflow button (te 3 kropki) który pełni rolę wcześniejszego przycisku opcji.

2013-09-14 16.22.00

Tryb akcji: Action mode

public class ActionModeActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_action_mode);

	findViewById(R.id.btn_action_mode).setOnClickListener(new View.OnClickListener() {

	    @Override
	    public void onClick(View v) {

		startActionMode(new ActionMode.Callback() {

		    @Override
		    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
			MenuInflater inflater = mode.getMenuInflater();
			inflater.inflate(R.menu.menu_action_mode, menu);
			return true;
		    }

		    @Override
		    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
			return false;
		    }

		    @Override
		    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
			switch (item.getItemId()) {

			default:
			    return false;
			}
		    }

		    @Override
		    public void onDestroyActionMode(ActionMode mode) {

		    }
		});
	    }
	});

    }
}

Menu:

<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/menu_settings"
        android:icon="@android:drawable/ic_menu_delete"
        android:orderInCategory="100"
        android:showAsAction="always"
        android:title="delete"/>

</menu>

Tutaj ciekawostka. Możliwość przejścia w tryb akcji, możemy go użyć np przy zaznaczaniu elementów. Po zastosowaniu paru powyższych linijek powstaje coś takiego (po lewej przed naciśnięciem przycisku, a po prawej po)

2013-09-14 16.03.40

Ładowanie / odświeżanie widoku

Przy okazji tworzenia pewnego projektu napotkałem potrzebę udostępnienia użytkownikowi możliwości odświeżenia widoku pobierając dane z internetu. Udało mi się znaleźć rozwiązanie korzystające z ActionBar. Tak to wygląda:

ładowanie2

Po zakończeniu akcji wraca do pierwszego widoku. Samo rozwiązanie wydaje mi się trochę "dookoła", ale działa bez zastrzeżeń. Jeśli znasz sposób jak zrobić to ładniej, to daj proszę znać :)

public class LoadingActivity extends Activity {

    private boolean mRefreshButtonEnabled = true;
    private Task mTask;

    @Override
    public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
	setContentView(R.layout.activity_blank);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {

	if (mRefreshButtonEnabled) {
	    getMenuInflater().inflate(R.menu.menu_loading, menu);
	    ((MenuItem) menu.findItem(R.id.refreshButton)).setOnMenuItemClickListener(new OnMenuItemClickListener() {

		@Override
		public boolean onMenuItemClick(MenuItem item) {
		    mTask = new Task();
		    mTask.execute();
		    return true;
		}
	    });
	} else {
	    getMenuInflater().inflate(R.menu.menu_no_loading, menu);
	}

	return true;
    }

    @Override
    public void onPause() {
	super.onPause();
	if (mTask != null) {
	    mTask.cancel(true);
	    mTask = null;
	}
	setRefreshButtonVisible(false);
    }

    private void setRefreshButtonVisible(boolean state) {
	setProgressBarIndeterminateVisibility(!state);
	mRefreshButtonEnabled = state;
	invalidateOptionsMenu();
    }

    private class Task extends AsyncTask<Void, Void, Void> {

	@Override
	protected void onPreExecute() {
	    setRefreshButtonVisible(false);
	}

	@Override
	protected void onPostExecute(Void param) {
	    setRefreshButtonVisible(true);
	}

	@Override
	protected Void doInBackground(Void... params) {
	    try {
		Thread.sleep(3000);
	    } catch (InterruptedException e) {
		e.printStackTrace();
	    }
	    return null;
	}
    }
}

Kluczowe elementy:

requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);

Linia musi zostać wywołana przed setContentView()

@Override
    public boolean onCreateOptionsMenu(Menu menu) {

	if (mRefreshButtonEnabled) {
	    getMenuInflater().inflate(R.menu.menu_loading, menu);
	    ((MenuItem) menu.findItem(R.id.refreshButton)).setOnMenuItemClickListener(new OnMenuItemClickListener() {

		@Override
		public boolean onMenuItemClick(MenuItem item) {
		    mTask = new Task();
		    mTask.execute();
		    return true;
		}
	    });
	} else {
	    getMenuInflater().inflate(R.menu.menu_no_loading, menu);
	}

	return true;
    }

Tutaj zależnie od zmiennej mRefreshButtonEnabled dodajemy odpowiednie menu. Jest tutaj pokazana kolejna możliwość obsługi naciśnięcia przycisku dodając do niego odpowiednio Listenera.

    private void setRefreshButtonVisible(boolean state) {
	setProgressBarIndeterminateVisibility(!state);
	mRefreshButtonEnabled = state;
	invalidateOptionsMenu();
    }

setProgressBarIndeterminateVisibility() pokazuje lub ukrywa widok ładowania

invalidateOptionsMenu - wymusza odświeżenie menu, czyli m.in. wywołanie metody onCreateOptionsMenu()

 private class Task extends AsyncTask<Void, Void, Void>

Tym zadaniem symulujemy dłuższe akcje wywoływane w tle. O AsyncTasku dostępny jest osobny artykuł.

Akcje nawigacyjne

Artykuł zostanie dokończony wkrótce...

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.