Kurs Android

ActionBar - przykłady użycia

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

 

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.34 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:

ładowanie1 ł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...

Komentarze

Wojtek

Czekamy na akcje nawigacyjne! Mam nadzieję, że chodzi tu o tab m. in. :)

Daro

Gdzie można znaleźć obiecany na początku artykułu kod źródłowy dla tego przykładu, łatwiej jest analizować mając całość niż wyrywkowe fragmenty. Pozdrawiam

magnez

Witam.
Jak wyżej - gdzie znajdę kod źródłowy do omawianego przykładu?

Marcin Kunert

Mam na innym komputerze. Wrzucę jak będę miał dostęp.

Bartek4175

Kiedy skończysz to? Czekam na nowe kursy :D