Este artigo faz parte de um Guia de Desenvolvimento para Android. O guia fala de vários temas necessários para construir uma aplicação simples na sua totalidade, abordando várias funcionalidades introduzidas no Honeycomb e que ainda são válidas para o Ice Cream Sandwich. Usamos um cliente do Google Buzz chamado Honeybuzz como exemplo para cada tópico. Na introdução encontra-se uma listagem completa de todos os artigos.
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).