Kurs Android (6)

Autor: Damian Chodorek • Opublikowany: 22 lutego 2015 • Ostatnia aktualizacja: 1 kwietnia 2015 • Kategoria: android, kursy

Wykorzystywanie fragmentów. Selektory zasobów.

Urządzenia, na których działa Android, mają różne rozmiary ekranów. Na dodatek mają możliwość zmiany orientacji. Powoduje to dynamiczne zmniejszenie lub zwiększenie przestrzeni, na której można wyświetlać elementy UI.

Akademickim przykładem jest aplikacja, która posiada listę elementów. Po kliknięciu na jeden z nich, pojawiają się bardziej szczegółowe informacje na jego temat. Typowo rozwiązuje się to na dwa sposoby.

Pierwszy sposób polega na wyświetleniu dwóch kolumn jednocześnie. Jedna posiada listę elementów, a druga posiada szczegółowe informacje na jego temat. Ten sposób stosuje się na urządzeniach o wystarczającej szerokości ekranu.

Drugi sposób, który wykorzystywany jest na wąskich ekranach, polega na pokazaniu tylko jednej kolumny z listą elementów. Po kliknięciu pojawia się nowy widok ze szczegółowymi informacjami.

Oto schematyczny rysunek pierwszego sposobu z dwoma kolumnami naraz.

Poniżej rysunek drugiego, jednokolumnowego sposobu.

1. Fragmenty (fragments)

Aby ułatwić projektowanie UI na urządzenia o różnych szerokościach ekranu, wprowadzono fragmenty. Fragment to komponent, który może być użyty przez aktywność. Jest kontenerem dla danych elementów UI oraz dla funkcjonalności, które są im przypisane.

Dzięki wykorzystaniu takich niezależnych kontenerów, nasza aplikacja staje się modularna. Możemy przykładowo w danej aktywności wykorzystać dwa fragmenty, które reprezentują dwie kolumny. Możemy także stworzyć dwie różne aktywności jednokolumnowe, które będą zawierać po jednym fragmencie.

Domyślasz się więc, że dzięki fragmentom możemy łatwo utworzyć układ zarówno jedno jak i wielokolumnowy dla różnych szerokości ekranu.

1.1. Cykl życia fragmentów

Fragment jest dzieckiem aktywności. Mimo, że posiada swój własny cykl życia, to jest on związany z cyklem rodzica. Jeśli aktywność zostaje zatrzymana lub zniszczona, fragment także.

Jak napisano na stronie http://developer.android.com/guide/components/fragments.html, w każdej aplikacji powinny być zaimplementowane przynajmniej trzy poniższe metody:

  • onCreate() – wywoływana podczas tworzenia fragmentu. Metoda zadziała po metodzie onCreate() aktywności, do której należy, ale przed onCreateView().
  • onCreateView() – metoda wywoływana jest kiedy należy narysować UI.
  • onPause() – wywoływana kiedy fragment nie jest już aktywny. Tutaj powinny być zachowane wszelkie dane pomiędzy sesjami użytkownika.

1.2. Więcej informacji o fragmentach

Przy pomocy metody getActivity() mamy dostęp do aktywności, która jest rodzicem danego fragmentu. Należy mieć także świadomość, że fragmenty można tworzyć zarówno statycznie jak i dynamicznie.

Z fragmentów można korzystać na dwa sposoby:

  • W aktywności definiujemy FrameLayout. Jest to kontener na fragmenty. W zależności co chce zobaczyć użytkownik, dynamicznie podmieniamy fragment, który jest aktualnie wyświetlany (zamiast tworzyć nową aktywność).
  • Tworzymy fragmenty statycznie. Przykładowo w jednej aktywności wyświetlamy dwa fragmenty obok siebie jeśli jest wystarczająco miejsca. Jeśli nie, tworzymy oddzielne aktywności dla każdego fragmentu.

Który sposób wybrać – statyczny czy dynamiczny? To zależy od konkretnego przypadku. Ogólnie przyjmuje się, że dynamiczny sposób jest trochę trudniejszy w implementacji, ale za to bardziej elastyczny. W tym oraz następnym artykule przećwiczymy obydwa.

1.3. Komunikacja z fragmentami

Fragmenty nie powinny komunikować się ze sobą bezpośrednio, ale pośrednio – przy pomocy aktywności nadrzędnej. Dzięki temu możemy korzystać z nich w sposób modularny, niezależnie od siebie.

Typowo robi się to tak, że w klasie fragmentu trzymamy referencję do aktywności, która implementuje interfejs zdefiniowany we fragmencie. Dlaczego? Chodzi o to, żeby fragment znał metody, które może wywołać na referencji do aktywności.

public class MyFragment extends Fragment {

    // referencja do aktywności nadrzędnej
    private MyActivityListener listener;

    // interfejs, który implementuje aktywność
    public interface MyActivityListener {
        public void sendMessage(String msg);
    }

    // zapisujemy referencję do aktywności
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        listener = (MyActivityListener) activity;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        listener=null;
    }
}
public class MyActivity extends Activity implements MyFragment.MyActivityListener {
    /*
        ...
    */
}

2. Ćwiczenie – fragmenty statyczne

Celem ćwiczenia będzie utworzenie aplikacji, która robi dokładnie to co przedstawiłem na rysunkach. W trybie portrait będzie pokazana tylko jedna kolumna (jeden fragment). W trybie landscape dwie naraz.

2.1. Tworzymy fragmenty

Utwórz nowy projekt o nazwie com.damianchodorek.kurs.android6.

Pierwszym krokiem będzie zdefiniowanie dwóch layoutów, dla dwóch fragmentów, z których będziemy korzystać. Przypominam, że jeden fragment będzie służył do wyświetlania wielu elementów. Po kliknięciu na jeden z nich, w drugim fragmencie pojawią się szczegółowe dane na jego temat.

W folderze res/layout utwórz plik fragment_detail.xml. Będzie zawierał layout dla fragmentu ze szczegółowymi informacjami o elemencie.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/detailsText"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:layout_marginTop="20dip"
        android:text="Domyślne informacje." />

</LinearLayout>

W tym samym folderze utwórz plik fragment_overview.xml. Jak się domyślasz, będzie on zawierał elementy.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Element 1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Element 2" />

</LinearLayout>

Mamy już layouty. Czas stworzyć klasy.

Utwórz klasę DetailFragment.

package com.damianchodorek.kurs.android6;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class DetailFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater
                .inflate(R.layout.fragment_detail, container, false);
        return view;
    }

    public void setText(String txt) {
        TextView view = (TextView) getView().findViewById(R.id.detailsText);
        view.setText(txt);
    }
}

Utwórz także klasę OverviewFragment.

package com.damianchodorek.kurs.android6;

import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;

public class OverviewFragment extends Fragment {

    private OverviewFragmentActivityListener listener;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {

        // przypisujemy layout do fragmentu
        View view = inflater.inflate(R.layout.fragment_overview, container,
                false);

        // definiujemy listener dla poszczególnych elementów (buttonów)
        OnClickListener clickListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                switch (v.getId()) {
                case R.id.button1:
                    updateDetail("Szczegółowe informacje o elemencie pierwszym.");
                    break;
                case R.id.button2:
                    updateDetail("Szczegółowe informacje o elemencie drugim.");
                    break;
                default:
                    break;
                }
            }
        };

        // przypisujemy elementom clickListener
        Button button1 = (Button) view.findViewById(R.id.button1);
        Button button2 = (Button) view.findViewById(R.id.button2);

        button1.setOnClickListener(clickListener);
        button2.setOnClickListener(clickListener);

        return view;
    }

    // interfejs, który będzie implementować aktywność
    public interface OverviewFragmentActivityListener {
        public void onItemSelected(String msg);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        if (activity instanceof OverviewFragmentActivityListener) {
            listener = (OverviewFragmentActivityListener) activity;
        } else {
            throw new ClassCastException( activity.toString() + " musi implementować interfejs:
                OverviewFragment.OverviewFragmentActivityListener");
        }
    }

    // metoda wysyła dane do aktywności
    public void updateDetail(String msg) {
        listener.onItemSelected(msg);
    }
}

2.2. Tworzymy aktywność

Czas na aktywność główną. Na początku utworzymy jej layout. Dokonaj zmian w pliku res/layout/activity_main.xml jak poniżej.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:baselineAligned="false"
    android:orientation="horizontal" >

    <fragment
        android:id="@+id/overviewFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        class="com.damianchodorek.kurs.android6.OverviewFragment" >
    </fragment>

    <fragment
        android:id="@+id/detailFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="2"
        class="com.damianchodorek.kurs.android6.DetailFragment" >
    </fragment>

</LinearLayout>

Pozostaje jeszcze zdefiniować ciało klasy MainActivity.

package com.damianchodorek.kurs.android6;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends Activity implements
        OverviewFragment.OverviewFragmentActivityListener {

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

    // ta metoda pochodzi z OverviewFragmentActivityListener
    @Override
    public void onItemSelected(String msg) {
        DetailFragment fragment = (DetailFragment) getFragmentManager()
                .findFragmentById(R.id.detailFragment);

        // sprawdzamy czy fragment istnieje w tej aktywności
        if (fragment != null && fragment.isInLayout()) {
            // ustawiamy teskt we fragmencie
            fragment.setText(msg);
        }
    }
}

2.3. Testujemy

Czas przetestować aplikację. Na tym etapie wszystko powinno działać. Przyciski powinny powodować pojawienie się tekstu na drugim fragmencie. Aplikacja powinna pokazywać dwa fragmenty jednocześnie zarówno w trybie portrait jak i landscape. Oczywiście nie chcemy tego. Czas to zmienić.

2.4. Tworzymy alternatywny layout

Chcemy, aby w trybie portrait był widoczny tylko jeden fragment oraz aby wciśnięcie przycisku powodowało utworzenie nowej aktywności. W tym celu skorzystamy z selektorów zasobów. Innymi słowy utworzymy folder res/layout-port – z tego folderu będą automatycznie pobierane zasoby, jeżeli telefon znajdzie się w trybie portrait. Więcej na ten temat napiszę pod koniec artykułu, na razie nie przejmuj się szczegółami.

W folderze res/layout-port utwórz plik activity_main.xml.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:baselineAligned="false"
    android:orientation="horizontal" >

    <fragment
        android:id="@+id/overviewFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        class="com.damianchodorek.kurs.android6.OverviewFragment" >
    </fragment>

</LinearLayout>

Jest to alternatywny layout dla aktywności głównej. Zawiera tylko jeden fragment.

2.5. Tworzymy drugą aktywność

Drugi fragment chcemy umieścić w nowej aktywności. Należy więc stworzyć dla niej layout. W tym samym folderze utwórz plik activity_detail.xml.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <fragment
        android:id="@+id/detailFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        class="com.damianchodorek.kurs.android6.DetailFragment" />

</LinearLayout> 

Należy teraz utworzyć klasę dla drugiej aktywności – DetailActivity.

package com.damianchodorek.kurs.android6;

import android.app.Activity;
import android.content.res.Configuration;
import android.os.Bundle;

public class DetailActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // jeżeli użytkownik będzie w orientacji landscape, należy zamknąć
        // aktywność
        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
            finish();
            return;
        }

        setContentView(R.layout.activity_detail);

        // pobieramy dane wysłane przez aktywność główną
        Bundle extras = getIntent().getExtras();
        if (extras != null) {
            String url = extras.getString("msg");
            DetailFragment detailFragment = (DetailFragment) getFragmentManager()
                    .findFragmentById(R.id.detailFragment);

            // ustawiamy tekst fragmentu w tej aktywności
            detailFragment.setText(url);
        }
    }
}

Trzeba jeszcze zadeklarować aktywność w pliku AndroidManifest.xml.

<activity
    android:name=".DetailActivity"
    android:label="@string/app_name" >
</activity>

2.6. Dostosowujemy główną aktywność

Mamy wszystko przygotowane. Dla przypomnienia, chcemy osiągnąć następujący efekt:

  • W trybie landscape widoczne są dwie aktywności naraz. Aktualnie tak działa nasza aplikacja.
  • W trybie portrait ma być widoczny tylko fragment z elementami. Po kliknięciu na jeden z nich, powinna pojawić się nowa aktywność zawierająca fragment, na którym wyświetlone są szczegółowe informacje. Nad tym pracujemy teraz.

Aby zrealizować drugi podpunkt powinniśmy dostosować aktywność główną. Uaktualnij metodę onItemSelected() w klasie MainActivity zgodnie z poniższym kodem.

    @Override
    public void onItemSelected(String msg) {
        DetailFragment fragment = (DetailFragment) getFragmentManager()
                .findFragmentById(R.id.detailFragment);
        // jeżeli fragment istnieje w tej aktywności,
        // znaczy, że jesteśmy w trybie landscape
        if (fragment != null && fragment.isInLayout()) {
            fragment.setText(msg);
        } else {
            // w trybie portrait wywołujemy drugą aktywność
            Intent intent = new Intent(getApplicationContext(),
                    DetailActivity.class);
            intent.putExtra("msg", msg);
            startActivity(intent);
        }
    }

Uruchom aplikację. Wszystko powinno działać. Przetestuj ją w trybie portrait i landscape.

3. Selektory zasobów

W metodzie onCreate(), w aktywności głównej, używamy identyfikatora R.layout.activity_main. Skąd aplikacja wie kiedy użyć pliku layout/activity_main.xml, a kiedy layout-port/activity_main.xml?

Odpowiedź: po nazwie folderu. Tworząc foldery, które są nazwane w odpowiedni sposób, Android sam będzie wybierał alternatywną wersję pliku xml. Dotyczy to nie tylko layoutów, ale np. folderu values lub drawable.

Oto przykładowe nazwy folderów dla layoutów:

  • layout-large/activity_layout.xml – layout dla dużych szerokości ekranów,
  • layout-xlarge/activity_layout.xml – layout dla bardzo dużych szerokości ekranów,
  • layout-port/activity_layout.xml – layout dla orientacji pionowej,
  • layout-land/activity_layout.xml – layout dla orientacji poziomej.

Twoim zadaniem jest zdefiniowanie tylko plików xml. Android dynamicznie je podmieni. Teraz już wiesz dlaczego mamy automatycznie tworzone foldery: drawable-hdpi, drawable-ldpi, drawable-mdpi, drawable-xhdpi. Selektorem jest gęstość pikseli. Dla niskich rozdzielczości Android wybierze mniejsze obrazki czy ikony, dla większych duże. Sprytne prawda?

Szerokość ekranu nie jest jedynym kryterium. Żeby zdefiniować plik values/strings.xml, w którym będą stałe tekstowe, dla wielu wersji językowych, można utworzyć odpowiedni folder.

  • values/strings.xml – domyślny plik ze stałymi tekstowymi, wersja angielska,
  • values-pl/strings.xml – wersja polska,
  • values-fr/strings.xml – wersja francuska.

W każdym z plików powinna znaleźć się ta sama stała, ale z inną wartością.

  • <string name="write_something">Write something</string>
  • <string name="write_something">Napisz coś</string>
  • <string name="write_something">Écrire quelque chose</string>

Teraz wystarczy w layoucie skorzystać z konstrukcji: @string/write_something. W zależności od tego jaki język jest aktualnie aktywny w systemie, taka stała zostanie użyta.

<EditText
        android:id="@+id/new_note_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:hint="@string/write_something" />

Selektory można ze sobą łączyć. Szczegółowo ten temat jest opisany na tej oraz tej stronie. Jeżeli chcesz poznać szczegóły, to zachęcam do przeczytania.

W tym artykule to wszystko. W kolejnej części przećwiczymy dynamiczne tworzenie fragmentów. Dowiesz się także co nieco na temat fragmentów, które nie posiadają własnego UI.

część 7