Utilizando BottomSheet Material Design no Android
(10567) (4)
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Opa, blz?
Nesse post é apresentada a BottomSheet Material Design que veio junto a support library 23.2 para Android. Serão apresentadas também duas outras variações, são elas: BottomSheetDialog e BottomSheetDialogFragment sendo respectivamente opções as versões AppCompatDialog e AppCompatDialogFragment.
O BottomSheet tem como parte do objetivo apresentar o conteúdo complementar ao conteúdo atual na tela ou conteúdo de outra entidade (não complementar ao que já está sendo apresentado) de maneira simples e rápida sem perder o design do Material Design.
Devido ao recém lançamento da library de suporte alguns bugs vieram com o release, o principal deles é a apresentação errônea do BottomSheetDialog e BottomSheetDialogFragment, mas como comentado em vídeo, a experiência que temos com a support library indica que os bugs, ao menos esse principal, serão corrigidos em pouco tempo, até mesmo pela quantidade de stackoverflow e report na Web sobre o problema.
Abaixo vamos seguir com uma simples implementação que será o suficiente para a utilização das BottomSheets.
O primeiro passo é criar um projeto "Basic Activity" no Android Studio, pois vamos utilizar o Float Action Button nesse exemplo:
O segundo passo é declarar no build.gradle (Module: app) a referência a support library 23.2:
...
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.2.0'
compile 'com.android.support:design:23.2.0'
}
...
O terceiro passo é criar nosso domíno do problema. Primeiro as classes Item e Action:
import android.os.Parcel;
import android.os.Parcelable;
public class Item implements Parcelable {
public static final String ITEMS_KEY = "br.com.thiengo.bottomsheetexample.domain.Items";
private int iconId;
private String label;
public Item(int iconId, String label ){
this.iconId = iconId;
this.label = label;
}
public int getIconId(){
return( this.iconId );
}
public String getLabel() {
return label;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.iconId);
dest.writeString(this.label);
}
protected Item(Parcel in) {
this.iconId = in.readInt();
this.label = in.readString();
}
public static final Creator<Item> CREATOR = new Creator<Item>() {
public Item createFromParcel(Parcel source) {
return new Item(source);
}
public Item[] newArray(int size) {
return new Item[size];
}
};
}
Agora a Action:
import android.os.Parcel;
public class Action extends Item {
public Action(int iconId, String label) {
super(iconId, label);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
}
protected Action(Parcel in) {
super(in);
}
public static final Creator<Action> CREATOR = new Creator<Action>() {
public Action createFromParcel(Parcel source) {
return new Action(source);
}
public Action[] newArray(int size) {
return new Action[size];
}
};
}
Note que na classe Item há a implementação da interface Parcelable, isso é necessário, pois parte do exemplo será enviar a lista de itens (ArrayList<Item>) para o BottomSheetDialogFragment por meio do Bundle.
Porém como explicado no vídeo, não foi necessária a implementação na "unha" desse código do Parcelable, apenas acesse as preferências de seu Android Studio, clique em "Plugins", logo depois busque por "Parcelable". Provaelmente somente um pequeno texto e um link serão apresentados a ti, clique nesse link, dessa forma o primeiro plugin apresentado é o que está buscando, instale ele e reinicie o Android Studio.
O próximo passo é continuar com o domínio do problema, dessa vez implementando as classes adapters, começando pela classe abstrata Adapter:
import android.view.LayoutInflater;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
public abstract class Adapter extends BaseAdapter {
protected List<?> items;
protected LayoutInflater inflater;
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
Item item = (Item) items.get(position);
return item.getIconId();
}
static class ViewHolder{
ImageView icon;
TextView label;
}
}
A classe Adapter não implementa o método getView() para que as outras duas implemnetacões concretas de classes tenham de fazer isso. A vantagem da classe abstrata nesse contexto é a diminuição da repetição de código. A primeira classe adapter concreta é a ItemAdapter:
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
import br.com.thiengo.bottomsheetexample.R;
public class ItemAdapter extends Adapter {
public ItemAdapter(Context context, List<Item> items){
this.items = items;
this.inflater = LayoutInflater.from(context);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
Item item = (Item) items.get(position);
if( convertView == null ){
holder = new ViewHolder();
convertView = inflater.inflate(R.layout.item_grid, parent, false);
convertView.setTag( holder );
holder.icon = (ImageView) convertView.findViewById(R.id.iv_icon);
holder.label = (TextView) convertView.findViewById(R.id.tv_label);
}
else{
holder = (ViewHolder) convertView.getTag();
}
holder.icon.setImageResource( item.getIconId() );
holder.label.setText( item.getLabel() );
return convertView;
}
}
Acima a implementação do construtor tb é necessária para preencher as variaveis de instancia, pois a Adapter class não pode ter construtor. Abaixo o layout "item_grid.xml" é apresentado:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_centerHorizontal="true"
android:layout_alignParentTop="true"
android:id="@+id/iv_icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:adjustViewBounds="true" />
<TextView
android:id="@+id/tv_label"
android:gravity="center"
android:layout_below="@+id/iv_icon"
android:textSize="12sp"
android:maxLines="1"
android:ellipsize="end"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
Agora seguimos com a classe ActionAdapter e respectivamente o layout de item utilizado, "item_action.xml":
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
import br.com.thiengo.bottomsheetexample.R;
public class ActionAdapter extends Adapter {
public ActionAdapter(Context context, List<?> actions){
this.items = actions;
this.inflater = LayoutInflater.from(context);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
Action action = (Action) items.get(position);
if( convertView == null ){
holder = new ViewHolder();
convertView = inflater.inflate(R.layout.item_action, parent, false);
convertView.setTag( holder );
holder.icon = (ImageView) convertView.findViewById(R.id.iv_icon);
holder.label = (TextView) convertView.findViewById(R.id.tv_label);
}
else{
holder = (ViewHolder) convertView.getTag();
}
holder.icon.setImageResource( action.getIconId() );
holder.label.setText( action.getLabel() );
return convertView;
}
}
Então o layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:layout_width="match_parent"
android:layout_height="48dp">
<ImageView
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:id="@+id/iv_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="16dp" />
<TextView
android:id="@+id/tv_label"
android:layout_centerVertical="true"
android:textSize="16sp"
android:layout_toRightOf="@+id/iv_icon"
android:layout_toEndOf="@+id/iv_icon"
android:paddingLeft="32dp"
android:paddingStart="32dp"
android:paddingRight="16dp"
android:paddingEnd="16dp"
android:maxLines="1"
android:ellipsize="end"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
Note que em ambos os layouts muito são utilizados atributos de margem e padding, isso é necessário para manter o layout dos itens do BottomSheet de acordo com as especificações indicadas no site do Android Material Design: BottomSheet Material Design Behavior
O próximo passo é a implementação do padrão "Class Library" que vai permitir que utilizemos alguns métodos geradores de dados fake para testarmos as BottomSheets. Note que esse padrão necessita que somente métodos que não se encaixem em nenhuma das calsses de nosso domínimo do problema, somente essses, sejam vinculados a classe que implementa o Class Library, tendo em mente que todos os métodos também devem ser staticos. Segue implmentação da classe FakeCollection:
import java.util.ArrayList;
import br.com.thiengo.bottomsheetexample.R;
import br.com.thiengo.bottomsheetexample.domain.Action;
import br.com.thiengo.bottomsheetexample.domain.Item;
public final class FakeCollection {
static private ArrayList<Action> actions;
static private ArrayList<Item> items;
private FakeCollection(){}
static public ArrayList<Action> getActions(){
if( actions == null ){
actions = new ArrayList<>();
actions.add( new Action(R.drawable.ic_copy, "Copy") );
actions.add( new Action(R.drawable.ic_share, "Share") );
actions.add( new Action(R.drawable.ic_cut, "Cut") );
actions.add( new Action(R.drawable.ic_remove, "Remove") );
}
return( actions );
}
static public ArrayList<Item> getItems(){
if( items == null ){
items = new ArrayList<>();
items.add( new Item(R.drawable.ic_sign_up, "Cadastrar") );
items.add( new Item(R.drawable.ic_login, "Login") );
items.add( new Item(R.drawable.ic_contact, "Contato") );
items.add( new Item(R.drawable.ic_inspector, "Inspetor Device") );
items.add( new Item(R.drawable.ic_department, "Departamentos") );
items.add( new Item(R.drawable.ic_deep_link, "Deep link") );
items.add( new Item(R.drawable.ic_event, "Criar Evento") );
items.add( new Item(R.drawable.ic_news, "Criar Notícia") );
items.add( new Item(R.drawable.ic_wall, "Mural") );
}
return( items );
}
}
Note que foi colocado o construtor como private para evitar instanciação e a classe é final para evitar que ela seja herdada. Essa classe vai fornecer os dados que serão utilizados como itens no ListView e GridView que serão implementados posteriormente.
O próximo passo é criar um layout chamado "bottom_sheet.xml", esse será a representação de nosso Persistente BottomSheet, onde não há fade e foco somente no BottomSheet. Esse layout fica em /res/layout. Segue:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:elevation="3dp"
android:id="@+id/rv_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:layout_gravity="bottom|center"
android:background="@android:color/white"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<ListView
android:layout_alignParentTop="true"
android:id="@+id/lv_actions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:dividerHeight="0dp"
android:divider="@null"/>
</RelativeLayout>
Novamente vários paddings para padronizar com as especificações do Material Design. Mas a parte importante desse layout é o atributo app:layout_behavior="android.support.design.widget.BottomSheetBehavior" que será o responsável por permitir que nosso RelativeLayout (poderia ser qualquer outro ViewGroup) trabalhe como um BottomSheet.
Note que para o BottomSHeet existir ele deve ser um elemento filho de um CoordinatorLayout. Logo no layout da MainActivity, mais precisamente no final dele, antes da tag de fechamento do CoordinatorLayout, coloque a seguinte linha (include):
...
<include layout="@layout/bottom_sheet"/>
...
Dessa forma colocamos nosso BottomSheet no layout principal da APP. Isso já é o suficiente para ter o comportamento de BottomSheet, porém ainda temos de preencher o ListView do layout além de acrescentar um listener ao BottomSheet (esse último é opcional).
Abaixo segue o código do método init() que deve ser chamdo dentro do método de ciclo de vida onResume() da MainActivity. Segue trecho do código:
...
private void init(){
final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
View bottomSheet = findViewById(R.id.rv_bottom_sheet);
BottomSheetBehavior behavior = BottomSheetBehavior.from( bottomSheet );
behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
if( offsetVertical < slideOffset ){
fab.hide();
}
else if ( offsetVertical > slideOffset ){
fab.show();
}
offsetVertical = slideOffset;
}
});
ArrayList<Action> actions = FakeCollection.getActions();
ActionAdapter adapter = new ActionAdapter( this, actions );
ListView lv = (ListView) findViewById(R.id.lv_actions);
lv.setAdapter( adapter );
lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(MainActivity.this, "Pos: " + position, Toast.LENGTH_SHORT).show();
}
});
}
...
Antes de prosseguir com o primeiro teste, acesse o layout "content_main.xml" qe foi criado junto ao projeto, pelo Android Studio. Nele, acerte para ficar como o layout abaixo:
...
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="56dp"
tools:context="br.com.thiengo.bottomsheetexample.MainActivity"
tools:showIn="@layout/activity_main">
<TextView
android:id="@+id/tv_title"
android:layout_alignParentTop="true"
android:text="BottomSheet"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:layout_below="@+id/tv_title"
android:id="@+id/bt_call_bottom_sheet_dialog"
android:onClick="callBottomSheetDialog"
android:text="Show Bottom Sheet Dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:layout_below="@+id/bt_call_bottom_sheet_dialog"
android:onClick="callBottomSheetDialogFragment"
android:text="Show Bottom Sheet Dialog Fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
...
Agora devemos voltar a MainActivity para acrescentar dois novos métodos vazios, os mesmos indicados nos atributos android:onClick de ambos os buttons do layout acima. Segue implementação dos métodos:
...
public void callBottomSheetDialog( @NonNull View view ){
// TODO
}
public void callBottomSheetDialogFragment( @NonNull View view ){
// TODO
}
...
Agora já é possível realizar o primeiro teste, no caso o teste para o BottomSheet. Assim que criar o emulador e executar terá algo similar a:
E então, dando um Swipe Up na tela terá algo como:
Agora é partir para a implementação das entidades BottomSheetDialog e BottomSheetDialogFragment.
Logo o próximo passo é criarmos os layout que serão utilizado por ambas as implementações, o "bottom_sheet_dialog.xml":
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:elevation="3dp"
android:id="@+id/rv_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingBottom="16dp"
android:paddingTop="8dp"
android:layout_gravity="bottom|center"
android:background="@android:color/white">
<GridView
android:id="@+id/gv_items"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</FrameLayout>
Dessa vez com o GridView.
Agora é criar a classe CustomBottomSheetDialog que herdará de BottomSheetDialog, segue implementação:
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.BottomSheetDialog;
import android.view.View;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.Toast;
import java.util.ArrayList;
import br.com.thiengo.bottomsheetexample.R;
import br.com.thiengo.bottomsheetexample.util.FakeCollection;
public class CustomBottomSheetDialog extends BottomSheetDialog {
private Context context;
public CustomBottomSheetDialog(@NonNull Context context) {
super(context);
this.context = context;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View layout = getLayoutInflater().inflate(R.layout.bottom_sheet_dialog, null);
setContentView( layout );
ArrayList<Item> items = FakeCollection.getItems();
ItemAdapter adapter = new ItemAdapter( this.context, items );
GridView gv = (GridView) layout.findViewById(R.id.gv_items);
gv.setAdapter( adapter );
gv.setNumColumns( 3 );
gv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText( context, "Pos: " + position, Toast.LENGTH_SHORT).show();
}
});
}
}
Note que o número de colunas foi definido como três (gv.setNumColumns( 3 )), pois foi o suficiente para manter os paddings e tamanhos indicados nas especificações do BottomSheet, mas esse valor depende de sua implementação, não é padrão. Veja a classe FakeCollection trabalhando novamente, lembrando que na implementação dela ainda temos o padrão "Singleton" sendo utilizado, mais precisamente nas variaveis actions e items.
Agora partimos para a implamentação da classe CustomBottomSheetDialogFragment que herda de BottomSheetDialogFragment:
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.BottomSheetDialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.Toast;
import java.util.ArrayList;
import br.com.thiengo.bottomsheetexample.R;
public class CustomBottomSheetDialogFragment extends BottomSheetDialogFragment {
public static final String FRAGMENT_KEY = "br.com.thiengo.bottomsheetexample.domain.CustomBottomSheetDialogFragment";
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.bottom_sheet_dialog, container);
ArrayList<Item> items = getArguments().getParcelableArrayList( Item.ITEMS_KEY );
ItemAdapter adapter = new ItemAdapter( getActivity(), items );
GridView gv = (GridView) view.findViewById(R.id.gv_items);
gv.setAdapter( adapter );
gv.setNumColumns( 3 );
gv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText( getActivity(), "Pos: " + position, Toast.LENGTH_SHORT).show();
}
});
return(view);
}
}
Note que dessa vez não estamos obtendo os dados diretamente da FakeCollection. Os dados vão vir do método callBottomSheetDialogFragment(), mais precisamente de uma bundle que será vinculado ao fragment via método setArguments().
Agora devemos voltar a MainActivity para colocar código nos método callBottomSheetDialog() e callBottomSheetDialogFragment(). Segue ambas implementações:
...
public void callBottomSheetDialog( @NonNull View view ){
CustomBottomSheetDialog dialog = new CustomBottomSheetDialog(this);
dialog.show();
}
public void callBottomSheetDialogFragment( @NonNull View view ){
Bundle b = new Bundle();
b.putParcelableArrayList(Item.ITEMS_KEY, FakeCollection.getItems() );
CustomBottomSheetDialogFragment fragment = new CustomBottomSheetDialogFragment();
fragment.setArguments( b );
fragment.show( getSupportFragmentManager(), CustomBottomSheetDialogFragment.FRAGMENT_KEY );
}
...
Note a FRAGMENT_KEY sendo utilizado no método allBottomSheetDialogFragment(), isso para podermos recuperar no fragment a collection gerada pela FakeCollection calss.
Agora é testar. Primeiro execute o projeto e então clique no button "SHOW BOTTOM SHEET DIALOG". O resultado deve ser algo similar a:
Agora cliando no button "SHOW BOTTOM SHEET DIALOG FRAGMENT" temos algo como:
É isso mesmo, ambas as implementações não estão coerentes com o Bottom Sheet Material Design, esses são os pricipais bugs que comentei sobre logo no inicio do post. Agora é aguardar a correção ou utilizar libraries alternativas caso esteja em uma implementação critica e com o tempo curto. Provavelmente a correção virá logo devido a quantidade de reports na Web sobre esse problema.
A implementação completa e os icons estão no GitHub do projeto: https://github.com/viniciusthiengo/BottomSheetExample
Caso não tenha compreendido muito bem o vídeo e o conteúdo do post, veja também os conteúdos abaixo:
ListView: Entendendo e Utilizando no Android
GridView no Android, Entendendo e Utilizando
Utilizando BaseAdapter Para Personalização Completa da ListView
Fragments no Android, Trabalhando com Múltiplas Activities
Parcelable no Android, Entendendo e Utilizando
Segue links das páginas referências utilizadas para construir o post:
Libraries BottomSheet alternativas:
BottomSheetExample de Rúbem Sousa
Para icons no Android:
Abaixo o vídeo com a implementação completa do post acima:
Abraço
Comentários Facebook