Mostrando postagens com marcador Alert. Mostrar todas as postagens
Mostrando postagens com marcador Alert. Mostrar todas as postagens

sexta-feira, 11 de outubro de 2013

Adeus AlertDialog! Bem-vindo DialogFragment

Olá povo,

Até a versão 2.3 do Android (API Level 10), a forma padrão de exibir mensagens para os usuários era através da classes AlertDialog, mas partir da versão 3 do Android (Honeycomb API Level 11) uma nova abordagem foi adotada, a utilização da classe DialogFragment.
Apesar de ter sido lançada na versão 3, a API de compatibilidade do Android nos permite usar esse recurso em versões anteriores. Umas das vantagens dessa abordagem é o maior controle sobre o ciclo de vida do Dialog, uma vez que ele nada mais é do que um Fragment.
Apenas para relembrar, até o 2.3 fazíamos isso:

int x = 0;
 
@Override
protected Dialog onCreateDialog(int id) {
  AlertDialog dialog = new AlertDialog.Builder(this)
    .setTitle("Título")
    .setMessage("Deseja exibir "+ x++)
    .setPositiveButton("Sim", null) // Listener aqui
    .setNegativeButton("Não", null) // e aqui
    .create();
  return dialog;
}
 
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
  ((AlertDialog)dialog).setMessage("Mensagem "+ x++);
  super.onPrepareDialog(id, dialog);
}
 
public void abrirAlert(View v){
  showDialog(0);
}
A abordagem acima era a recomendada pois, se abríssimos um dialog e girássemos a tela, o mesmo era perdido. No método onCreateDialog, é criado o dialog (dããã), mas se precisássemos alterar o texto mudando apenas um detalhe (no exemplo acima, o valor de x) deveríamos usar o método onPrepareDialog conforme acima. Todos os dialogs da Acvity deviam ser criados no onCreateDialog e cada um tem o seu ID que é recebido por parâmetro. Para exibir o dito-cujo, era só chamar showDialog(ID).

A classe DialogFragment envolve (wraps) um AlertDialog, ou pode ter qualquer view. Podemos dizer que seria como um "Fragment Modal". No exemplo abaixo, temos um exemplo de um DialogFragment que recebe um título, uma mensagem e os (de 1 a 3) botões.

import android.app.AlertDialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentManager;

public class SimpleDialog extends DialogFragment 
  implements OnClickListener {
 
  private static final 
    String EXTRA_ID      = "id";
  private static final 
    String EXTRA_MESSAGE = "message";
  private static final 
    String EXTRA_TITLE   = "title";
  private static final 
    String EXTRA_BUTTONS = "buttons";
  private static final 
    String DIALOG_TAG    = "SimpleDialog";
 
  private int dialogId;
 
  public static SimpleDialog newDialog(int id, 
    String title, String message, int[] buttonTexts){
    // Usando o Bundle para salvar o estado
    Bundle bundle  = new Bundle();
    bundle.putInt(EXTRA_ID, id);
    bundle.putString(EXTRA_TITLE, title); 
    bundle.putString(EXTRA_MESSAGE, message);
    bundle.putIntArray(EXTRA_BUTTONS, buttonTexts);
  
    SimpleDialog dialog = new SimpleDialog();
    dialog.setArguments(bundle);
    return dialog; 
  }
 
  @Override
  public Dialog onCreateDialog(
    Bundle savedInstanceState) {

    String title = getArguments()
      .getString(EXTRA_TITLE);
    String message = getArguments()
      .getString(EXTRA_MESSAGE);
    int[] buttons = getArguments()
      .getIntArray(EXTRA_BUTTONS);
     
    AlertDialog.Builder alertDialogBuilder = 
      new AlertDialog.Builder(getActivity());
    alertDialogBuilder.setTitle(title);
    alertDialogBuilder.setMessage(message);
        
    switch (buttons.length) {
      case 3:
        alertDialogBuilder.setNeutralButton(
          buttons[2], this);

      case 2:
        alertDialogBuilder.setNegativeButton(
          buttons[1], this);
   
      case 1:
        alertDialogBuilder.setPositiveButton(
          buttons[0], this);
    }    
    return alertDialogBuilder.create();
  }
    
  @Override
  public void onClick(
    DialogInterface dialog, int which) {
    // Sua Activity deve implementar essa interface
    ((FragmentDialogInterface)getActivity())
      .onClick(dialogId, which);
  }

  public void openDialog(
    FragmentManager supportFragmentManager) {

    if (supportFragmentManager.findFragmentByTag(
      DIALOG_TAG) == null){

      show(supportFragmentManager, DIALOG_TAG);
    }  
  }
  // Interface que erá chamada ao clicar no bot"ao
  public interface FragmentDialogInterface {
    void onClick(int id, int which);
  }
}
Podemos notar no código acima que estamos herdando da classe DialogFragment. Criei um método estático para inicializar o Dialog, lembrando que essa é uma boa prática uma vez que sempre devemos ter o construtor padrão e o Bundle armazenará o estado do Fragment. No método onCreateDialog, inicializamos o AlertDialog com os dados passados no "método construtor".
Nossa Activity deve implementar essa interface, pois não podemos manter a referência dela, uma vez que ela pode ter sido destruída ao girar o aparelho. Para abrir o dialog, verificamos se ele já foi adicionado ao FragmentManager, caso contrário, exibimos.
Pra exibir o dialog, basta fazer conforme abaixo:
public void abrirSimpleDialog(View v) {
  SimpleDialog dialog = SimpleDialog.newDialog(
    0,              // Id do dialog
    "Alerta",       // título
    "Mensagem",     // mensagem
    new int[]{      // texto dos botões
      android.R.string.ok, 
      android.R.string.cancel });
    dialog.openDialog(getSupportFragmentManager());
  }

  @Override
  public void onClick(int id, int which) {
    Toast.makeText(MainActivity.this, 
      "Botão clicado "+ which, Toast.LENGTH_SHORT)
        .show();
  }
Agora se quisermos um Dialog customizado, para por exemplo, receber input do usuário, podemos criar um arquivo de layout e associá-lo ao DialogFragment.
 
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/edtName"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_gravity="center"
  android:orientation="vertical"
  android:padding="16dp" >

  <TextView
    android:id="@+id/lbl_your_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Your name" />

  <EditText
    android:id="@+id/txt_your_name"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:imeOptions="actionDone"
    android:inputType="text" />

</LinearLayout>
O Layout é simple e contém apenas um TextView e um EditText. Vamos ver como ficará o DialogFragment que usaria esse layout.
public class EditNameDialog 
  extends DialogFragment 
  implements OnEditorActionListener {

  private static final 
    String DIALOG_TAG = "editDialog";
  private static final 
    String EXTRA_INPUT_TEXT = "message";
  private static final 
    String EXTRA_TITLE = "inputText";

  private EditText mEditText;

  // Sua Activity deve implementar essa Interface
  public interface EditNameDialogListener {
    void onFinishEditDialog(String inputText);
  }

  public static EditNameDialog newInstance(
    String title, String inputText) {

    Bundle bundle = new Bundle();
    bundle.putString(EXTRA_TITLE, title);
    bundle.putString(EXTRA_INPUT_TEXT, inputText);

    EditNameDialog dialog = new EditNameDialog();
    dialog.setArguments(bundle);
    return dialog;
  }

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

    String title = getArguments()
      .getString(EXTRA_TITLE);
    String inputText = getArguments()
      .getString(EXTRA_INPUT_TEXT);

    View view = inflater.inflate(
      R.layout.layout_dialog, container);

    TextView txtView = (TextView)
      view.findViewById(R.id.lbl_your_name);
    txtView.setText(inputText);
    // Exibe o teclado virtual ao exibir o Dialog
    getDialog().getWindow().setSoftInputMode(
      WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);

    getDialog().setTitle(title);

    mEditText = (EditText)
      view.findViewById(R.id.txt_your_name);
    mEditText.requestFocus();
    // Listener para quando clicarmos 
    // em 'Done' no teclado
    mEditText.setOnEditorActionListener(this);

    return view;
  }

  @Override
  public boolean onEditorAction(TextView v, 
    int actionId, KeyEvent event) {
    // Se clicou em 'Done'
    if (EditorInfo.IME_ACTION_DONE == actionId) {
      // Notifique a Activity
      EditNameDialogListener activity = 
        (EditNameDialogListener) getActivity();
      activity.onFinishEditDialog(
        mEditText.getText().toString());
      // Feche o dialog
      dismiss();
      return true;
    }
    return false;
  }

  public void openDialog(FragmentManager fm) {
    if (fm.findFragmentByTag(DIALOG_TAG) == null) {
      show(fm, DIALOG_TAG);
    }
  }
}
Para exibir e tratar esse dialog usamos o código abaixo:
public void abrirEditDialog(View v){
  EditNameDialog editNameDialog = 
    EditNameDialog.newInstance(
      "Informação", "Digite seu nome");

  editNameDialog.openDialog(
    getSupportFragmentManager());
}
 
@Override
public void onFinishEditDialog(
  String inputText) {

  Toast.makeText(this, "Olá, " + inputText, 
    Toast.LENGTH_SHORT).show();  
}
O resultado ficará conforme a figura.


[EDITADO em (11/10/2013 às 11:20)]
Como você devem ter notado, em ambos os exemplo estamos usando a Activity para tratar o retorno do Dialog. Mas e se quisermos chamar o Dialog a partir de um Fragment?
Nesse caso podemos utilizar a propriedade Target Fragment. Ela funciona como o startActivityForResult, só que para Fragments.
// A partir de um Fragment
public void abrirSimpleDialog() {
  SimpleDialog dialog = SimpleDialog.newDialog(
    0, // Id do dialog
    "Alerta", // título
    "Mensagem", // mensagem
    new int[] { // texto dos botões
      android.R.string.ok, 
      android.R.string.cancel });
  // Segredo do sucesso! :)
  // 1 = RequestCode
  dialog.setTargetFragment(this, 1); 
  dialog.openDialog(
    getActivity().getSupportFragmentManager());
}

@Override
public void onActivityResult(int requestCode, 
  int resultCode, Intent data) {

  super.onActivityResult(
    requestCode, resultCode, data);

  int which = data.getIntExtra("which", -1);
  // Tratar dialog  
}
Com essa alteração, a interface FragmentDialogInterface pode ser eliminada (uma vez que a Activity não precisa mais implementá-la), e o código do onClick da classe SimpleDialog, ficaria dessa forma:
@Override
public void onClick(DialogInterface dialog, int which) {
  Intent it = new Intent();
  it.putExtra("which", which);
  // Chamando o onActivityResult do targetFragment
  getTargetFragment().onActivityResult(
    getTargetRequestCode(), Activity.RESULT_OK, it);
}

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

quarta-feira, 28 de setembro de 2011

AlertDialog Personalizado no Android

Olá povo,

Ultimamente estou escrevendo post curtos, com soluções para problemas que as equipes onde eu trabalho/trabalhei têm/tiveram, e que ao meu ver poderiam ter sido feitas de outra forma. A última foi como personalizar o AlertDialog do Android, dependendo da versão do Android (e até do fabricante) o aspecto visual das mensagens que são exibidas para o usuário tem um visual diferente. Por causa disso, o pessoal de UX (User eXperience) resolveu padronizar isso. Só que o pessoal estava usando uma Activity com o tema de diálogo.

Eu não acho essa uma boa abordagem, uma vez que você mexe com o ciclo de vida da activity (métodos onCreate, onStart, onPause, ...) para exibir um simples dialog. Esse tutorial do site do Android Developers mostra como criar um dialog com o conteúdo personalizado.

Sim, mas e se eu quiser mudar o background do título e dos botões? Nesse post vou mostrar como fazer isso. O código abaixo mostra como personalizar as 3 áreas de um AlertDialog: a barra de título, a área de conteúdo e parte inferior onde ficam os botões.
// O parâmetro temTitulo deve ser informado,
// pois não há como saber se o Dialog tem
// título para mudar a imagem de background
// do topo e do meio.
public void mostraDialog(
AlertDialog dialog, boolean temTitulo) {

dialog.show();
Resources res = dialog.getContext().getResources();

// Pega a View do AlertDialog toda
ViewGroup contentView = (ViewGroup)
dialog.findViewById(android.R.id.content);

// Pega a ViewGroup com os filhos
// (title, textView, customView e barra dos botões)
ViewGroup contentChilds =
(ViewGroup)contentView.getChildAt(0);

// Barra de título
if (temTitulo)
contentChilds.getChildAt(0).setBackgroundDrawable(
res.getDrawable(R.drawable.box_top));

// Layout onde fica o texto (mensagem) do dialog
contentChilds.getChildAt(1).setBackgroundDrawable(
res.getDrawable(
temTitulo?
R.drawable.box_middle :
R.drawable.box_top));

// Layout onde fica a View personalizada (quando usada)
contentChilds.getChildAt(2).setBackgroundDrawable(
res.getDrawable(
temTitulo?
R.drawable.box_middle :
R.drawable.box_top));

// Layout dos botões
// Se não colocar wrap_content, o botão fica cortado
contentChilds.getChildAt(3).setLayoutParams(
new LinearLayout.LayoutParams(
LayoutParams.FILL_PARENT,
LayoutParams.WRAP_CONTENT));

contentChilds.getChildAt(3).setBackgroundDrawable(
res.getDrawable(R.drawable.box_bottom));

// Alterando botões
Button btn = (Button)dialog.findViewById(
android.R.id.button1);
if (btn != null){
btn.setTextColor(Color.WHITE);
btn.setBackgroundResource(
R.drawable.selector_botao_verde);
}

btn = (Button)dialog.findViewById(
android.R.id.button2);
if (btn != null){
btn.setTextColor(Color.WHITE);
btn.setBackgroundResource(
R.drawable.selector_botao_vermelho);
}
btn = (Button)dialog.findViewById(
android.R.id.button3);
if (btn != null){
btn.setTextColor(Color.WHITE);
btn.setBackgroundResource(
R.drawable.selector_botao_vermelho);
}
}

Como todos devem/deveriam saber tudo que é exibido no Android contém objetos da classe View. A classe ViewGroup é uma implementação do padrão de projeto Composite, e como tal, herda de View e é composto de várias Views. Dessa forma, após exibir o Alert, pegamos a ViewGroup que representa o AlertDialog e obtemos cada um dos seus "filhos". O Alert é composto de 4 filhos: o primeiro contém a view com o título e o ícone; a segunda tem o texto do Alert; o terceiro pode conter uma view personalizada associada ao Alert; e o último contém os botões (no máximo 3: positivo, negativo e neutro). Nesse exemplo, pegamos cada filho e alteramos o seu background.

As imagens de background utilizadas estão no formato 9.png que falei nesse post aqui. Mas um detalhe interessante, é que o background do botão é definido utilizando os drawable selector_botao_verde e selector_botao_vermelho. Esses dois arquivos, apesar de ficarem na pasta res/drawable, não são imagens. Na verdade eles são arquivos XML que descrevem as imagens que serão utilizadas pelos botões dependendo do seu estado. Abaixo temos o XML que descreve o botão verde.
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/btn_verde"
android:state_pressed="false"/>
<item
android:drawable="@drawable/btn_verde_pressionado"
android:state_pressed="true" />
</selector>

Notem que devemos ter (por uma questão de usabilidade) duas imagens: uma imagem para o botão não pressionado, e outra para ele pressionado. Isso dará a sensação que o usuário estará pressionando no botão. Essa prática pode ser adotada para todos os botões da sua aplicação (inclusive para o botão vermelho usado nesse post).

Para utilizar o AlertDialog personalizado, basta criá-lo normalmente e passá-lo para o método que criamos anteriormente.
AlertDialog.Builder builder =
new AlertDialog.Builder(this)
.setTitle("Titulo")
.setMessage("Mensagem")
.setPositiveButton("Sim", null)
.setNegativeButton("Não", null);
AlertDialog d = builder.create();
mostraDialog(d, true);

Abaixo temos um exemplo do nosso Alert em funcionamento.

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber