среда, 25 июля 2012 г.

Используем фрагменты в приложении для Android 2.х

Разработчики Android основательно поработали над тем, чтобы наше приложение нормально выглядело как на десятидюймовом планшете, так и на смартфоне с экраном в 3,5 дюйма. Мы можем использовать несколько наборов графики, разные варианты разметки интерфейса в зависимости от ориентации экрана. И компоновать эти интерфейсы мы можем из произвольного набора автономных фрагментов. Конечно, фрагменты поддерживаются начиная с четвёртой версии Android, но с помощью android 4 support library и небольшого танца с бубном, можно реализовать эту технологию и под Android 2.х.
Давайте сделаем небольшое приложение "на фрагментах".

Что мы видим?

На первый взгляд может показаться, что на экране мы видим два Activity при горизонтальной ориентации экрана и одно - при вертикальной. Это, конечно же, не так. В обоих случаях мы видим одно и то же Activity, но layout-ы для него система выбирает разные. Как этого добиться? Очень просто. Нужно положить один main.xml res/layout, а второй (для "портретной", т.е. вертикальной ориентации) в res/layout-port. Вот текст наших layout-ов:

<?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="horizontal" >
    <fragment
            android:id="@+id/listFragment"
            android:layout_width="150dip"
            android:layout_height="match_parent"
            class="net.multipi.fragtest.ListFragment" ></fragment>
    <fragment
            android:id="@+id/detailFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            class="net.multipi.fragtest.DetailFragment" >
    </fragment>
</LinearLayout>

И для вертикальной ориентации:

<?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="horizontal" >
    <fragment
            android:id="@+id/listFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            class="net.multipi.fragtest.ListFragment" />
</LinearLayout>

Добиваемся кажущегося "склеивания" двух Activity при горизонтальной ориентации мы использованием двух элементов fragment. При вертикальной ориентации мы используем только один из них. А где же второй? Он при вертикальной ориентации будет принадлежать уже другому Activity и значит описывается в другом layout-е -  details_activity_layout.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="net.multipi.fragtest.DetailFragment" />
</LinearLayout> 

Фрагменты!

В разметке с нашими фрагментами атрибутом class мы связываем два класса, наследующих android.support.v4.app.Fragment. Это ListFragment:

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
 
public class ListFragment extends android.support.v4.app.ListFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
 
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String[] values = new String[] { "Android", "iPhone", "WindowsMobile",
                "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X",
                "Linux", "OS/2" };
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(),
                android.R.layout.simple_list_item_1, values);
        setListAdapter(adapter);
    }
 
    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        String item = (String) getListAdapter().getItem(position);
        DetailFragment fragment = (DetailFragment) getFragmentManager()
                .findFragmentById(R.id.detailFragment);
        if (fragment != null && fragment.isInLayout()) {
            fragment.setText(item);
        } else {
            Intent intent = new Intent(getActivity().getApplicationContext(),
                    DetailActivity.class);
            intent.putExtra("value", item);
            startActivity(intent);
        }
    }
}

и DetailsFragment:

import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
 
public class DetailFragment extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
 
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.details, container, false);
        return view;
    }
 
    public void setText(String item) {
        TextView view = (TextView) getView().findViewById(R.id.detailsText);
        view.setText(item);
    }

Обратите внимание на реализацию метода onItemListClick первого фрагмента. Тут мы пытаемся найти в layout-е второй фрагмент, и если находим - устанавливаем ему содержимое. А если его нет (вертикальная ориентация) - передаём это содержимое второму Activity через intent.putExtra(). Вообще, вся схема работы с фрагментами держится на таких вот "некрасивых" проверках.
Кстати, для второго фрагмента делаем отдельный layout - details.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" >
    <TextView
            android:id="@+id/detailsText"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="center_horizontal|center_vertical"
            android:layout_marginTop="20dip"
            android:text="Large Text"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:textSize="30dip" />
</LinearLayout> 


Activity: обязательное и не обязательное

Оба наши фрагмента замечательно уживаются в главном Activity при горизонтальной ориентации. Его код предельно прост, поэтому привожу его только для полноты картины:

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
 
public class MyActivity extends FragmentActivity {
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

А вот второе Activity будет вызвано только при клике на элементе списка при вертикальном положении экрана. Вот его код:

import android.content.res.Configuration;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.widget.TextView;
 
public class DetailActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getResources().getConfiguration().orientation ==
                Configuration.ORIENTATION_LANDSCAPE) {
            finish();
            return;
        }
 
        setContentView(R.layout.details_activity_layout);
        Bundle extras = getIntent().getExtras();
        if (extras != null) {
            String s = extras.getString("value");
            TextView view = (TextView) findViewById(R.id.detailsText);
            view.setText(s);
        }
    }
}

Тут снова ставим костыль: проверяем в onCreate ориентацию экрана и останавливаем Activity, чтобы вернуться на предыдущее, "двухстворчатое".
Вот такое у нас получилось приложение. Исходники его взяты отсюда.
В целом впечатление от технологии создаётся двойственное: с одной стороны есть возможность собирать интерфейсы из автономных "кубиков" в каждом из которых своя логика. Это здорово. С другой стороны мы получаем достаточно запутанную смесь xml и java, которая к тому же держится на нескольких костылях. Впрочем при реализации более сложной задачи преимущества технологии вероятно перевесят её недостатки.  


4 комментария:

  1. Фрагменты доступны еще со времен honeycomb.

    ОтветитьУдалить
  2. фрагменты появились в третьей версии (API Level 11)

    ОтветитьУдалить
  3. Ты не раздуваешь фрагмент DetailFragment в новом актитиви, а тупо разворачиваешь обычное view из xml. d(R.id.detailsText.

    ОтветитьУдалить