2015.01.01 15:13
jak działa robiona przez eklipsa androidowa antywność aktywność typu "Navigation Drawer Activity"
Jak się w eklipsie robi projekt typu "android application", to można kazać, żeby eklips sam stworzył aktywność. I wtedy jest do wyboru kilka rodzajów aktywności. Jedną z nich jest aktywność typu "Navigation Drawer Activity". To jest dobry rodzaj aktywności, bo on ma od razu zrobionego wysuwanego z lewej strony drawera z menu. I to zrobionego tak, jak twórcy Androida zalecają, żeby był zrobiony. Oto opis, jak ta aktywność jest zrobiona i jak działa.
Ta aktywność pokazuje pewien ekran. Po lewej stronie tego ekranu jest drawer. Ten drawer można wysuwać i wsuwać klikając przycisk znajdujący się z lewej strony action bara. Ten przycisk swoim wyglądem pokazuje, czy drawer jest ukryty czy pokazany - kiedy drawer się wysuwa, przycisk przesuwa się lekko w bok.
Ściślej mówiąc, ten drawer nie jest zawsze koniecznie po stronie lewej - on jest po tej stronie, od której się w danej kulturze pisze. Więc jeśli przełączymy w telefonie ustawienia lokalne na arabskie, ten drawer będzie wysuwał się od prawej.
Na drawerze jest lista z trzema napisami. Klikając na te trzy przyciski powodujemy, że na ekranie pojawia się co innego, i tytuł na action barze zmienia się na inny.
Kiedy uruchamiam aplikację po raz pierwszy, drawer jest wysunięty (widoczny). Kiedy ukryję drawera, a potem go z powrotem wysunę, aplikacja wnioskuje, że już wiem, jak się dostać do drawera, i zapamiętuje, że od teraz przy uruchomieniu aplikacji drawer ma być schowany (jak zechcę go zobaczyć, to go sobie wysunę).
Oto, jak jest zrobione, że jest drawer i że się on wysuwa, jak się kliknie na ten przycisk na action barze. Layout aktywności (jest on w pliku activity_main.xml) za korzeń ma element android.support.v4.widget.DrawerLayout. Ten DrawerLayout to taki layout, że jak się w nim umieści widok, który w layout_gravity ma "start" albo "left" albo "end" albo "right" to ten widok jest traktowany jako drawer. Co oznacza, że można go wysunąć przez zawołanie na DrawerLayoucie metody .openDrawer(Gravity.START) albo .openDrawer(Gravity.LEFT) (co znaczy: "wysuń tego drawera, który jest z lewej" (bo w DrawerLayoucie mogą być dwa drawery)). Z tym "start" i "left" chodzi o to, że kiedyś po prostu pisało się "lewy drawer", ale to jest lewodoprawocentryczne, więc teraz zamiast pisać w takich miejscach "left" (co znaczy "lewy") pisze się "start" (co znaczy "umieszczony z tej strony, z której w danej kulturze zaczyna się pisać"). A jak już drawer jest wysunięty, to można go schować wołając metodę .closeDrawer(Gravity.START) albo .closeDrawer(Gravity.LEFT) albo .closeDrawers(). Poza tym użytkownik może schować takiego wysuniętego drawera klikając na DrawerLayout poza drawerem. Zauważmy przy tym, że sam DrawerLayout nie daje użytkownikowi możliwości wysuwania drawera, a daje możliwość wsuwania go.
Tego DrawerLayouta można na przykład użyć tak, że zrobić taki layout:
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="${relativePackage}.${activityClass}" >
<TextView
android:id="@+id/tresc"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f00"
android:text="treść główna" />
<TextView
android:layout_gravity="start"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0f0"
android:text="panel" />
</android.support.v4.widget.DrawerLayout>
I taką aktywność:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View v = getLayoutInflater().inflate(R.layout.activity_main, null);
setContentView(v);
v.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
((TextView) findViewById(R.id.tresc)).setText("!!!!!!!!!!!");
((DrawerLayout) findViewById(R.id.drawer)).openDrawer(Gravity.LEFT);
return false;
}
});
}
}
No więc mówiłem, że w tej aktywności stworzonej przez eklipsa korzeniem layoutu jest DrawerLayout. Ale sam DrawerLayout nie daje użytkownikowi żadnego guzika do wysuwania drawera - to jak jest zrobione, że my tak przycisk mamy (na actionbarze)? W pliku activity_main.xml możemy zobaczyć, że DrawerLayout ma dwójkę dzieci. Pierwsze to FrameLayout. Ten FrameLayout nie ma gravity, a layout_width i layout_height ma ustawione na match_parent - więc on będzie główną zawartością tego ekranu. A drugie dziecko to fragment. Któremu layout_gravity ustawiono na start - więc jest on lewym drawerem. Któremu name ustawiono na pl.test.NavigationDrawerFragment - więc przy nadmuchiwaniu tego layoutu instancjonowany będzie fragment pl.test.NavigationDrawerFragment i to ten fragment będzie pokazywany w drawerze.
Ten NavigationDrawerFragment w metodzie setUp (to nie jest standardowa metoda - zauważmy, że nie ma nad nią @Override - więc zaraz przyjrzymy się, kto wywołuje tę metodę) tworzy obiekt anonimowej klasy dziedziczącej po ActionBarDrawerToggle. Ta ActionBarToggle to klasa, która umie dodawać na actionbarze przycisk otwierający drawera. Używa się jej na przykład tak:
package pl.test;
import android.os.Bundle;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.ActionBarDrawerToggle;
import android.view.MenuItem;
import android.view.View;
public class MainActivity extends ActionBarActivity {
private ActionBarDrawerToggle przyciskSzuflady;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View v = getLayoutInflater().inflate(R.layout.activity_main, null);
setContentView(v);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
przyciskSzuflady = new ActionBarDrawerToggle(this, (DrawerLayout) findViewById(R.id.drawer), R.string.otworz, R.string.zamknij);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return przyciskSzuflady.onOptionsItemSelected(item);
}
}
Jak widać, trzeba włączyć na actionbarze przycisk home (robi to fragment getSupportActionBar().setDisplayHomeAsUpEnabled(true)), stworzyć obiekt klasy ActionBarDrawerToggle, a potem jak na naszym fragmencie albo aktywności zostanie zawołane onOptionsItemSelected (że użytkownik kliknął coś na actionbarze), zadelegować to na ten obiekt klasy ActionBarDrawerToggle.
No i właśnie w tym fragmencie NavigationDrawerFragment w metodzie setUp włączamy przycisk home na actionbarze, tworzymy obiekt anonimowej klasy dziedziczącej po ActionBarDrawerToggle (tylko używamy trochę innego konstruktora niż tego, który użyłem w tym prostym przykładzie powyżej - przekazujemy jeszcze drawable, który ma być wyświetlony na tym przycisku na actionbarze). A potem - nadal w metodzie setUp - dzieje się coś dziwnego:
mDrawerLayout.post(new Runnable() {
@Override
public void run() {
mDrawerToggle.syncState();
}
});
Jak widać, tu na obiekcie ActionBarDrawerToggle wołamy metodę syncState - to jest metoda, która uzgadnia wygląd przycisku na actionbarze z tym, czy drawer jest aktualnie wysunięty czy wsunięty, i oprócz tego wyświetla na przycisku na actionbarze ten drawable, który przedtem przekazaliśmy konstruktorowi. Ale dlaczego jest to opakowane w runnable'a przekazanego mDrawerLayout.post - nie wiem. Spróbowałem zmienić ten fragment na po prostu taki:
mDrawerToggle.syncState();
i działało bez zmian. No a potem w onOptionsItemSelected delegujemy tę metodę na obiekt ActionBarDrawerToggle.
Ale żeby w ogóle onOptionsItemSelected było wywoływane, jest robione jeszcze coś. W onActivityCreated tego fragmentu fragment pl.test.NavigationDrawerFragment robimy:
setHasOptionsMenu(true);
W ten sposób mówimy, że ten fragment chce uczestniczyć w Actionbarze. Dokładniej, to dokumentacja mówi, że wołając tę metodę mówimy, że chcemy, żeby na tym fragmencie była zawołana metoda onCreateOptionsMenu. Ale moje eksperymenty pokazują, że onCreateOptionsMenu byłoby zawołane i tak, tylko że potem jak użytkownik kliknie na coś na action barze, to na fragmencie nie zostanie zawołane onOptionsItemSelected. Swoją drogą sprawdziłem eksperymentalnie, czy można by wywołanie tej metody przenieść do onCreate fragmentu i okazało się, że tak.
Zauważmy przy okazji, że w jeszcze jednym miejscu jest użyty ten obiekt. Ten fragment, zauważmy, przykrywa też metodę onConfigurationChanged i deleguje ją na obiekt ActionBarDrawerToggle. To dlatego, że przy zmianie konfiguracji fragmenty nie są restartowane, tylko jest na nich wołana metoda onConfigurationChanged - więc trzeba dać przyciskowi na actionbarze szansę, żeby na przykład jak się zmieniła konfiguracja z dziennej na nocną, to żeby przycisk zmienił swój wygląd.
A jeszcze przyjrzyjmy się, czym ta anonimowa klasa dziedzicząca po ActionBarDrawerToggle różni się od klasy ActionBarDrawerToggle. Są w niej przykryte dwie metody: onDrawer{Opened,Closed} - metody wołane przy każdym odpowiednio wysunięciu i wsunięciu drawera. A w tych metodach jedna się dzieje rzecz naprawdę ważna: kiedy użytkownik wysunie szufladę (czyli w onDrawerOpened), w preferencjach zapisujemy, że użytkownik już się nauczył, jak się obsługuje szufladę:
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
sp.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).commit();
A ta metoda setUp fragmentu jest wywoływana przez aktywność w jej onCreate:
mNavigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout));
No i tak jest zrobione, żeby dało się wysuwać drawera.
A teraz zobaczmy, gdzie jest ustawione, co ma być wyświetlane na drawerze. Decyduje o tym onCreateView fragmentu wyświetlanego na drawerze, czyli klasy NavigationDrawerFragment. A ta metoda nadmuchuje layout fragment_navigation_drawer:
mDrawerListView = (ListView) inflater.inflate(R.layout.fragment_navigation_drawer, container, false);
Ten layout jest ListView.
Po nadmuchaniu ustawiamy, co ma się dziać po kliknięciu na pozycje tego ListView:
mDrawerListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
selectItem(position);
}
});
A co ma być na tej liście, jest ustawione kawałek poniżej:
mDrawerListView.setAdapter(new ArrayAdapter<String>(
getActionBar().getThemedContext(),
android.R.layout.simple_list_item_1,
android.R.id.text1,
new String[]{
getString(R.string.title_section1),
getString(R.string.title_section2),
getString(R.string.title_section3),
}));
I tak oto jest zrobione, że na drawerze jest lista z kilkoma napisami, a kliknięcie na napis powoduje wywołanie metody selectItem. A ta metoda robi to.
Zaznacza na liście, że ten kliknięty punkt jest aktualny:
if (mDrawerListView != null) {
mDrawerListView.setItemChecked(position, true);
}
Chowa drawera:
if (mDrawerLayout != null) {
mDrawerLayout.closeDrawer(mFragmentContainerView);
}
I na pewnym obiekcie wywołuje metodę onNavigationDrawerItemSelected:
if (mCallbacks != null) {
mCallbacks.onNavigationDrawerItemSelected(position);
}
Ten obiekt z atrybutu mCallbacks to obiekt, który chcemy, żeby był powiadamiany o kliknięciu na napis na szufladzie. A zobaczmy, co to jest za obiekt:
private NavigationDrawerCallbacks mCallbacks;
(...)
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mCallbacks = (NavigationDrawerCallbacks) activity;
} catch (ClassCastException e) {
throw new ClassCastException("Activity must implement NavigationDrawerCallbacks.");
}
}
Jak widać, jest tam referencja do aktywności. A to spójrzmy, co robi w aktywności ta metoda onNavigationDrawerItemSelected:
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.container, PlaceholderFragment.newInstance(position + 1))
.commit();
O - jak widać, to tu jest zrobione, że po kliknięciu na napis na drawerze zmienia się treść wyświetlana w głównej części.
komentarze:
powrót na stronę główną
RSS