Android 101: Usar fragmentos para lidar com diferentes orientações de ecrã

O Honeycomb introduziu o conceito de fragmentos. Os fragmentos são a resposta para lidar com ecrãs de diferentes tamanho, desde telemóveis até tablets, televisões e outros gadgets. Um fragmento representa uma porção de um interface de utilizador. Uma Acitvity pode ter diversos fragmentos e o mesmo fragmento pode ser usado em mais que uma Activity.

Um layout para uma listagem e detalhes, usando fragmentos

Vamos considerar o exemplo comum que demonstra a sua utilidade. Temos um painel esquerdo com uma listagem de itens e um painel direito que mostra os detalhes do item seleccionado. Este layout é usado na aplicação Honeybuzz. Um layout destes podem ser feito com dois fragmentos: um para a listagem dos itens e outro para os detalhes de um item. Dependendo da orientação do dispositivo, ou mostramos os fragmentos lado a lado ou, se o dispositivo estiver em modo portrait, mostramos apenas um dos fragmentos.

São necessárias diversas partes para construir um layout como o usado na aplicação Honeybuzz:

  • ListActivity: tem um layout diferente conforme a orientação do ecrã:
    • layout: layout no modo paisagem. Contém ListFragment e DetailsFragment.
    • layout-port: layout no modo portrait. Apenas contém ListFragment.
  • DetailsActivity: apenas usada no modo portrait. Contém DetailsFragment.
  • ListFragment: quando um item é seleccionado, dependendo da orientação do ecrã, vai actualizar o DetailsFragment ou então lançar a DetailsActivity.

No fundo temos duas estratégias diferentes e vamos usá-las conforme a orientação do ecrã.

Criar a ListActivity e o ListFragment

A ListActivity é bastante simples. É uma Activity normal, mas com um layout diferente dependendo da orientação do ecrã. Como expliquei anteriormente, o layout no modo paisagem contém tanto o ListFragment como o DetailsFragment e o layout no modo portrait contém apenas o ListFragment.

O Android é bastante esperto no que toca a escolher o ficheiro de recurso apropriado para um layout. Se o dispositivo estiver no modo paisagem, o Android vai primeiro procurar na pasta layout-land pelo ficheiro de recurso. Se não estiver disponível, só então irá tentar encontrá-lo na pasta layout. De igual forma para o modo portrait, mas procurando antes na pasta layout-port. Basicamente criam-se dois ficheiros de layout separados com o mesmo nome, colocando-os nas pastas apropriadas e o Android vai carregá-los apropriadamente de acordo com a orientação do ecrã.

O ficheiro de layout no modo paisagem é o seguinte:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="fill_parent">
    <fragment class="com.quasibit.honeybuzz.HoneybuzzListFragment"
            android:id="@+id/listFragment"
            android:layout_weight="1"
            android:layout_width="@dimen/list_item_size"
            android:layout_height="fill_parent" />
    <fragment class="com.quasibit.honeybuzz.HoneybuzzDetailsFragment"
            android:id="@+id/details"
            android:layout_weight="2"
            android:layout_width="match_parent"
            android:layout_height="fill_parent"
            android:background="?android:attr/detailsElementBackground" />
</LinearLayout>

E no modo portrait:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment class="com.quasibit.honeybuzz.HoneybuzzListFragment"
            android:id="@+id/listFragment"
            android:layout_weight="1"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
</LinearLayout>

Quanto ao ListFragment, este será o fragmento que irá mostrar a listagem dos itens. Pode-se usar um ficheiro de layout normal. O nosso layout só tem um LinearLayout, uma ListView e um TextView:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/listLayout"
      android:orientation="vertical" 
      android:layout_width="fill_parent"
      android:layout_height="wrap_content">
    <ListView
        android:id="@+id/listBuzzes" 
        android:drawSelectorOnTop="false" 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
    <TextView android:id="@android:id/empty"
               android:layout_height="fill_parent"
               android:text="@string/no_activities"
               android:layout_width="wrap_content"
               android:layout_gravity="left"
               android:layout_marginLeft="10dp"
               android:layout_marginTop="10dp" />
</LinearLayout>

A parte importante é a classe do fragmento. Fica aqui um excerto com as partes mais importantes:

public class HoneybuzzListFragment extends HoneybuzzFragment implements OnItemClickListener {
    protected String mCurrentId;
    protected ListView mListView;
    protected ArrayList<com.quasibit.honeybuzz.Buzz> mBuzzes;
    protected Boolean mDualPane;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.list_fragment, container, false);
        
        return view;
    }
        
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        
        // set list view properties
        this.mListView = (ListView) getActivity().findViewById(R.id.listBuzzes);
        
        if (this.mListView != null) {
            this.mListView.setOnItemClickListener(this);
            this.mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
        }

        // check if we are in dual pane mode
        HoneybuzzDetailsFragment details = (HoneybuzzDetailsFragment) getFragmentManager().findFragmentById(R.id.details);
        mDualPane = !(details == null || !details.isInLayout());
        
        // get current id from saved state or extras
        if (savedInstanceState != null) {
            // Restore last state for checked position.
            mCurrentId = savedInstanceState.getString(HoneybuzzListActivity.EXTRA_ID);
        }
        
        if (this.getActivity().getIntent() != null) {
            String id = this.getActivity().getIntent().getStringExtra(HoneybuzzListActivity.EXTRA_ID);
            
            if (id != null && !id.isEmpty()) {
                mCurrentId = id;
            }
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(HoneybuzzListActivity.EXTRA_ID, mCurrentId);
    }

    @Override
    public void onItemClick(AdapterView<?> l, View v, int position, long id) {
        if (position < mBuzzes.size()) {
            // get matching buzz id from position
            String buzzId = mBuzzes.get(position).id;
            showDetails(buzzId);
        }
    }

    public void showDetails(String id) {
        if (id != null && !id.isEmpty() && mBuzzes != null && !mBuzzes.isEmpty()) {
            // find buzz object
            com.quasibit.honeybuzz.Buzz buzz = getBuzz(id);
            
            if (buzz != null) {
                mCurrentId = id;
                
                // highlight selected item
                int index = mBuzzes.indexOf(buzz);
                
                if (index >= 0) {
                    mListView.setItemChecked(index, true);
                }
        
                // load item
                if (mDualPane) {
                    HoneybuzzDetailsFragment details = (HoneybuzzDetailsFragment) getFragmentManager().findFragmentById(R.id.details);
                    details.load(buzz);
                } else {
                    // launch new activity because we're in single mode pane
                    Intent showContent = new Intent(getActivity(), HoneybuzzDetailsActivity.class);
                    showContent.putExtra(HoneybuzzDetailsActivity.EXTRA_BUZZ, buzz);
                    startActivity(showContent);
                }
            }
        }
    }
}

Algumas notas:

  • A classe descende de HoneybuzzFragment, que é um fragmento com código Google Analytics e com outros métodos úteis.
  • onCreateView: usado para especificar o ficheiro de layout.
  • onActivityCreated: determina se o ecrã está em modo duplo e regista isso na variável mDualPane. Tentamos ler o ID actual tanto da instância gravada (quando a Activity está a ser recuperada) como da informação dos extras (quando outra parte da aplicação pede para ver os detalhes de um ID particular).
  • onSaveInstanceState: o ID actual é gravado antes da Activity ir para estado de background. Este ID é recuperado quando a Activity é recriada.
  • showDetails: carrega o objecto Buzz com a ID dada. O importante é o modo como os detalhes são mostrados. Se estivermos em modo duplo, vamos buscar o HoneybuzzDetailsFragment e passamos o objecto Buzz. De outra forma, lançamos a HoneybuzzDetailsActivity e passamos o objecto Buzz.

Construir o DetailsFragment e a DetailsActivity

A DetailsActivity só é usada em modo portrait. Neste modo precisamos de uma ListActivity e de uma DetailsActivity, porque temos dois ecrãs diferentes e só um é mostrado de cada vez.

A DetailsActivity é bastante simples, apenas reusamos o DetailsFragment (que também é usado na ListActivity):

public class HoneybuzzDetailsActivity extends HoneybuzzActivity {
    public static final String EXTRA_BUZZ = "com.quasibit.honeybuzz.HoneybuzzListActivity.EXTRA_BUZZ";

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

        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
            // If the screen is now in landscape mode, we can show the
            // dialog in-line with the list so we don't need this activity.
            finish();
            return;
        }

        if (savedInstanceState == null) {
            // During initial setup, plug in the details fragment.
            HoneybuzzDetailsFragment details = new HoneybuzzDetailsFragment();
            details.setArguments(getIntent().getExtras());
            getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
        }
    }
}

O DetailsFragment tem um método load que preenche o layout com a informação do objecto Buzz.

public class HoneybuzzDetailsFragment extends HoneybuzzFragment implements OnClickListener {
    protected com.quasibit.honeybuzz.Buzz mBuzz;
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.details, container, false);
        
        return view;
    }
        
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        
        if (savedInstanceState != null) {
            // Restore last state for checked position.
            mBuzz = (Buzz) savedInstanceState.getSerializable(HoneybuzzDetailsActivity.EXTRA_BUZZ);
        }
        
        if (this.getActivity().getIntent() != null) {
            com.quasibit.honeybuzz.Buzz buzz = (Buzz) this.getActivity().getIntent().getSerializableExtra(HoneybuzzDetailsActivity.EXTRA_BUZZ);
            
            if (buzz != null) {
                mBuzz = buzz;
            }
        }
        
        if (mBuzz != null) {
            load(mBuzz);
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putSerializable(HoneybuzzDetailsActivity.EXTRA_BUZZ, mBuzz);
    }
    
    public void load(com.quasibit.honeybuzz.Buzz buzz) {
        mBuzz = buzz;
        
        if (buzz != null) {
            // load views with Buzz object data
        }
    }
}

Em onActivityCreated tentamos encontrar o objecto Buzz a mostrar, tanto um gravado antes da Activity ir para o background, como um passado nos extras (chamado a partir de outra parte da aplicação).

More resources

Artigos relacionados