Android

11 mar, 2016

Views customizadas no Android – Parte 01

Publicidade

Para implementar uma aplicação típica, o Android disponibiliza uma grande variedade de widgets:

  • TextView
  • EditText
  • ImageView
  • Button
  • ImageButton
  • ProgressBar
  • CheckBox
  • RadioButton
  • e outros. . .

Entretanto, apesar dessa grande quantidade de widgets fornecidos pela plataforma, quase sempre nos deparamos com cenários nos quais o pessoal de UX/UI especificam componentes peculiares que o Android não possui por padrão.

Para construir esses componentes, a maioria dos desenvolvedores não se preocupa com o baixo acoplamento, reuso e alta coesão e acabam construindo e replicando código dentro de Activities ou até mesmo criando Fragments para uma simples View. Isso faz com que eles tenham que lidar com o ciclo de vida dos Fragments:

Ciclo de vida do fragment
Vamos supor que no aplicativo que estamos desenvolvendo teremos que implementar um botão que pisque toda vez que for clicado. Para esse exemplo, vamos usar o retrolambda, que possibilita a utilização de lambda do Java 8 em projetos Android.

@MainActivity.java


package com.tpinho.customviews.ui.activity;
 
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.Button;
 
import com.tpinho.customviews.R;
 
public class MainActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        final Button button = (Button) findViewById(R.id.button_main_click);
        button.setOnClickListener(view -> {
            blinkView(view);
            Snackbar.make(view, getString(R.string.clicked, button.getText()), Snackbar.LENGTH_LONG).show();
        });
    }
 
    private void blinkView(final View view) {
        Animation alphaAnimation = new AlphaAnimation(1f, 0f);
        alphaAnimation.setDuration(1000);
        view.startAnimation(alphaAnimation);
    }
 
}

@layout/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:gravity="center"
    android:orientation="vertical">
 
    <Button
        android:id="@+id/button_main_click"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/click" />
 
</LinearLayout>

A implementação funciona perfeitamente, mas se precisarmos implementar o efeito de piscar em outros botões teremos que replicar o código ou criar uma classe utilitária para isso. Além disso, estaremos misturando efeitos visuais com a regra de negócio, o que vai dificultar os testes e a evolução do código.

Então, o que fazer? Customizar o widget de botão do Android e implementar a funcionalidade de piscar o botão toda vez que for clicado:

@BlinkButton.java

package com.tpinho.customviews.ui.custom;
 
import android.content.Context;
import android.support.v7.widget.AppCompatButton;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
 
public class BlinkButton extends AppCompatButton {
 
    private int times = 0;
 
    private OnClickListener onClickListener;
 
    public BlinkButton(Context context) {
        super(context);
        init();
    }
 
    public BlinkButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
 
    public BlinkButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
 
    private void init() {
        super.setOnClickListener(view -> blink(view));
    }
 
    @Override
    public void setOnClickListener(OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }
 
    private void blink(final View view) {
        Animation alphaAnimation = new AlphaAnimation(1f, 0f);
        alphaAnimation.setDuration(1000);
        alphaAnimation.setRepeatCount(times);
        alphaAnimation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }
 
            @Override
            public void onAnimationRepeat(Animation animation) {
            }
 
            @Override
            public void onAnimationEnd(Animation animation) {
                if(onClickListener != null)
                    onClickListener.onClick(view);
            }
        });
        startAnimation(alphaAnimation);
    }
 
}

@MainActivity.java

public class MainActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        final Button button = (Button) findViewById(R.id.button_main_click);
        button.setOnClickListener(view -> {
            Snackbar.make(view, getString(R.string.clicked, button.getText()), Snackbar.LENGTH_LONG).show();
        });
    }
 
}

@layout/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:gravity="center"
    android:orientation="vertical">
 
    <com.tpinho.customviews.ui.custom.BlinkButton
        android:id="@+id/button_main_click"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/click" />
 
</LinearLayout>

Outra opção é criar um agrupamento de Views no Android utilizando o ViewGroup ou classes filhas, como LinearLayout, RelativeLayout, GridLayout e etc.

Para exemplificar, vamos criar uma view customizada para exibição de valores monetários, na qual o símbolo de moeda tem um tamanho diferente do tamanho definido para o valor e o valor já é inserido formatado (utilizei a biblioteca Canarinho) no TextView.

@layout/view_money.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
 
    <TextView
        android:id="@+id/text_money_currency"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        tools:text="Rquot; />
 
    <TextView
        android:id="@+id/text_money_value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp"
        android:layout_marginStart="5dp"
        android:textSize="20sp"
        tools:text="10.000,00" />
 
</LinearLayout>

@MoneyView.java

package com.tpinho.customviews.ui.custom;
 
import android.annotation.TargetApi;
import android.content.Context;
import android.support.annotation.StringRes;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.TextView;
 
import com.tpinho.customviews.R;
 
import br.com.concretesolutions.canarinho.formatador.FormatadorValor;
 
import static android.os.Build.VERSION_CODES.LOLLIPOP;
 
public class MoneyView extends LinearLayout {
 
    private TextView textMoneyCurrency;
    private TextView textMoneyValue;
 
    public MoneyView(Context context) {
        super(context);
        init();
    }
 
    public MoneyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
 
    public MoneyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
 
    @TargetApi(LOLLIPOP)
    public MoneyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }
 
    private void init() {
        inflate(getContext(), R.layout.view_money, this);
 
        textMoneyCurrency = (TextView) findViewById(R.id.text_money_currency);
        textMoneyValue = (TextView) findViewById(R.id.text_money_value);
 
        setCurrency(R.string.default_currency);
        setValue(R.string.default_value);
    }
 
    public void setCurrency(@StringRes int text) {
        textMoneyCurrency.setText(text);
    }
 
    public void setCurrency(String text) {
        textMoneyCurrency.setText(text);
    }
 
    public void setValue(@StringRes int value) {
        setValue(getResources().getString(value));
    }
 
    public void setValue(String value) {
        textMoneyValue.setText(FormatadorValor.VALOR.formata(value));
    }
 
}

Podemos incluir a MoneyView diretamente no xml correspondente ao layout:

@layout/activity_main.xml

...
 
<com.tpinho.customviews.ui.custom.MoneyView
    android:id="@+id/money_main_price"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
 
...

@MainActivity.java

...
 
final MoneyView moneyView = (MoneyView) findViewById(R.id.money_main_price);
moneyView.setValue("15000");
 
...

Ou podemos adicionar a MoneyView programaticamente:

MoneyView moneyView = new MoneyView(context);
view.addView(moneyView);

E é isso! O projeto de exemplo pode ser encontrado neste repositório do Github. Ficou alguma dúvida ou tem alguma sugestão? Aproveite os campos abaixo!

Na segunda parte desse artigo, vou mostrar como criar os atributos customizados para que seja possível definir valores para a view customizada direto do xml. Até lá!