Widgets Roláveis
Viewport (Área Visível)
No Flutter, o termo ViewPort (área visível), sem especificações diferentes, refere-se à área real de exibição de um Widget. Por exemplo, se uma ListView tiver uma área de exibição com altura de 800 pixels, embora a altura total dos itens da lista possa ser muito maior que 800 pixels, seu ViewPort ainda será de 800 pixels.
Eixo Principal e Eixo Secundário
Na descrição de coordenadas de widgets roláveis, geralmente a direção de rolamento é chamada de eixo principal, enquanto a direção não rolável é chamada de eixo secundário. Como a direção padrão dos widgets roláveis geralmente é vertical, por padrão o eixo principal se refere à direção vertical, e o mesmo se aplica à direção horizontal.
Scrollable
Quando o conteúdo excede a área visível (ViewPort), sem tratamento especial, o Flutter exibirá um erro de Overflow. Para isso, o Flutter oferece vários widgets roláveis para exibir listas e layouts longos. Todos os widgets roláveis contêm direta ou indiretamente um widget Scrollable:
Scrollable({
...
this.axisDirection = AxisDirection.down,
this.controller,
this.physics,
@required this.viewportBuilder, //será explicado mais tarde
})
- axisDirection: direção do rolamento.
- physics: esta propriedade acieta um objeto ScrollPhysics, que determina como o widget rolável responde às ações do usuário, como a continuação da animação após o usuário deslizar e levantar o dedo, ou como exibir quando chegar aos limites.
- controller: esta propriedade acieta um objeto ScrollController. O principal propósito do ScrollController é controlar a posição do rolamento e monitorar eventos de rolamento.
Scrollbar
Scrollbar é um indicador de rolagem no estilo Material. Para adicionar uma barra de rolagem a um widget rolável, basta tornar o Scrollbar o widget pai do widget rolável, como:
Scrollbar(
child: SingleChildScrollView(
...
),
);
SingleChildScrollView
SingleChildScrollView é semelhante ao ScrollView do Android, mas pode receber apenas um widget filho. Sua definição é:
SingleChildScrollView({
this.scrollDirection = Axis.vertical, //direção do rolamento, padrão é vertical
this.reverse = false,
this.padding,
bool primary,
this.physics,
this.controller,
this.child,
})
Além das propriedades gerais, vamos nos concentrar nas propriedades reverse e primary:
- reverse: esta propriedade, segundo a documentação da API, indica se o rolamento deve ocorrer na direção oposta à direção de leitura.
- primary: indica se deve ser usado o PrimaryScrollController padrão na árvore de widgets; quando a direção do rolamento é vertical (valor de scrollDirection é Axis.vertical) e nenhum controller é especificado, primary é verdadeiro por padrão.
ListView
ListView é o widget rolável mais comum, capaz de organizar linearmente todos os widgets filhos em uma direção. Vamos examinar a definição do construtor padrão do ListView:
ListView({
...
//parâmetros comuns para widgets roláveis
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
EdgeInsetsGeometry padding,
//parâmetros comuns a todos os construtores do ListView
double itemExtent,
bool shrinkWrap = false,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double cacheExtent,
//lista de widgets filhos
List<Widget> children = const <Widget>[],
})
- itemExtent: se não for nulo, força o "comprimento" dos children para o valor de itemExtent; aqui "comprimento" se refere ao comprimento do widget filho na direção do rolamento, ou seja, se a direção do rolamento for vertical, itemExtent representa a altura do widget filho, se a direção for horizontal, itemExtent representa o comprimento do widget filho.
- shrinkWrap: indica se o comprimento do ListView deve ser definido com base no comprimento total dos widgets filhos, o valor padrão é false. Por padrão, o ListView ocupa o máximo de espaço possível na direção do rolamento. Quando um ListView está em um contêiner sem limites (na direção do rolamento), shrinkWrap deve ser verdadeiro.
- addAutomaticKeepAlives: indica se os itens da lista (widgets filhos) devem ser envolvidos em um widget AutomaticKeepAlive; tipicamente, em uma lista preguiçosa, se os itens da lista forem envolvidos em AutomaticKeepAlive, o item não será coletado pelo lixo quando sair da área visível, ele usará KeepAliveNotification para salvar seu estado. Se o item da lista gerenciar seu próprio estado KeepAlive, este parâmetro deve ser definido como false.
- addRepaintBoundaries: indica se os itens da lista (widgets filhos) devem ser envolvidos em um RepaintBoundary. Quando um widget rolável rola, envolver os itens da lista em RepaintBoundary pode evitar a repintura dos itens, mas quando o custo de repintura de um item é muito pequeno (como um bloco de cor ou um texto curto), não adicionar RepaintBoundary pode ser mais eficiente. Como addAutomaticKeepAlive, se o item da lista gerenciar seu próprio estado KeepAlive, este parâmetro deve ser definido como false.
ListView.builder
ListView.builder é adequado para listas com muitos (ou infinitos) itens, pois os widgets filhos só são criados quando realmente precisam ser exibidos. Vamos ver a lista de parâmetros principais do ListView.builder:
ListView.builder({
// parâmetros comuns do ListView omitidos
...
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
...
})
- itemBuilder: é o construtor de itens da lista, do tipo IndexedWidgetBuilder, que retorna um widget. Quando a lista rola para uma posição de índice específica, este construtor é chamado para construir o item da lista.
- itemCount: o número de itens na lista, se for nulo, é uma lista infinita.
ListView.separated
ListView.separated pode gerar separadores entre os itens da lista. Além do ListView.builder, ele tem um parâmetro adicional separatorBuilder, que é um gerador de separadores.
class ListaComSeparadores extends StatelessWidget {
@override
Widget build(BuildContext context) {
//widget de sublinhado pré-definido para reutilização.
Widget separador1 = Divider(color: Colors.blue,);
Widget separador2 = Divider(color: Colors.green);
return ListView.separated(
itemCount: 100,
//construtor de itens da lista
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("Item ${index + 1}"));
},
//construtor de separadores
separatorBuilder: (BuildContext context, int index) {
return index % 2 == 0 ? separador1 : separador2;
},
);
}
}
GridView
GridView pode construir uma lista em grade bidimensional. Sua definição do construtor padrão é:
GridView({
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required SliverGridDelegate gridDelegate, //delegado que controla o layout dos widgets filhos
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
})
- O parâmetro gridDelegate, do tipo SliverGridDelegate, é responsável por controlar como os widgets filhos do GridView são dispostos (layout).
SliverGridDelegate é uma classe abstrata que define as interfaces relacionadas ao layout do GridView. As subclasses precisam implementar essas interfaces para implementar algoritmos de layout específicos. O Flutter oferece duas subclasses de SliverGridDelegate: SliverGridDelegateWithFixedCrossAxisCount e SliverGridDelegateWithMaxCrossAxisExtent:
SliverGridDelegateWithFixedCrossAxisCount
Esta subclasses implementa um algoritmo de layout com um número fixo de elementos no eixo transversal. Seu construtor é:
SliverGridDelegateWithFixedCrossAxisCount({
@required double crossAxisCount,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
})
- crossAxisCount: número de elementos filhos no eixo transversal. Após definir este valor, o comprimento dos elementos filhos no eixo transversal é determinado, ou seja, comprimento do ViewPort no eixo transversal / crossAxisCount.
- mainAxisSpacing: espaçamento na direção do eixo principal.
- crossAxisSpacing: espaçamento entre elementos filhos no eixo transversal.
- childAspectRatio: a proporção entre o comprimento no eixo transversal e o comprimento no eixo principal dos elementos filhos. Como crossAxisCount determina o comprimento do elemento filho no eixo transversal, o valor deste parâmetro pode determinar o comprimento do elemento filho no eixo principal.
Pode-se observar que o tamanho dos elementos filhos é determinado pelos parâmetros crossAxisCount e childAspectRatio.
GridView.count
O construtor GridView.count usa internamente SliverGridDelegateWithFixedCrossAxisCount, permitindo-nos criar rapidamente um GridView com um número fixo de elementos no eixo transversal:
GridView.count(
crossAxisCount: 3,
childAspectRatio: 1.0,
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
);
SliverGridDelegateWithMaxCrossAxisExtent
Esta subclasses implementa um algoritmo de layout em que os elementos filhos no eixo transversal têm um comprimento máximo fixo. Seu construtor é:
SliverGridDelegateWithMaxCrossAxisExtent({
double maxCrossAxisExtent,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
})
- maxCrossAxisExtent é o comprimento máximo dos elementos filhos no eixo transversal. É chamado de "máximo" porque o comprimento de cada elemento no eixo transversal ainda é igualmente dividido,
- childAspectRatio refere-se à proporção final entre o comprimento no eixo transversal e o comprimento no eixo principal dos elementos filhos. Outros parâmetros são iguais aos de SliverGridDelegateWithFixedCrossAxisCount.
GridView.extent
O construtor GridView.extent usa internamente SliverGridDelegateWithMaxCrossAxisExtent, permitindo-nos criar rapidamente um GridView com um comprimento máximo fixo para os elementos no eixo transversal:
GridView.extent(
maxCrossAxisExtent: 120.0,
childAspectRatio: 2.0,
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
);
GridView.builder
Os GridView que introduzimos anteriormente precisam de uma matriz de widgets como seus elementos filhos, e todos esses métodos constroem com antecedência todos os widgets filhos, então só são adequados quando há poucos widgets filhos. Quando há muitos widgets filhos, podemos usar GridView.builder para criar dinamicamente widgets filhos. GridView.builder deve ter dois parâmetros obrigatórios:
GridView.builder(
...
@required SliverGridDelegate gridDelegate,
@required IndexedWidgetBuilder itemBuilder,
)
Onde itemBuilder é o construtor de widgets filhos.
CustomScrollView
CustomScrollView é um widget que pode usar slivers para personalizar o modelo de rolamento (efeitos). Ele pode conter vários modelos de rolamento. Por exemplo, suponha que haja uma página que precise de um GridView na parte superior e um ListView na parte inferior, e que o efeito de rolagem de toda a página seja unificado, ou seja, eles parecem um todo. Se implementarmos usando GridView + ListView, não podemos garantir um efeito de rolagem consistente, porque seus efeitos de rolagem são separados. Portanto, precisamos de uma "cola" para "colar" esses widgets roláveis independentes (Slivers) juntos, e a função do CustomScrollView é equivalente a essa "cola".
Sliver
Sliver significa "fina fatia" ou "pequeno pedaço". No Flutter, Sliver geralmente se refere a um bloco rolável com um efeito de rolamento específico. Widgets roláveis, como ListView e GridView, têm implementações de Sliver correspondentes, como SliverList e SliverGrid. Para a maioria dos Slivers, a principal diferença em relação aos widgets roláveis é que eles não contêm o widget Scrollable, ou seja, o Sliver em si não contém o modelo de interação de rolagem. É por isso que o CustomScrollView pode "colar" vários Slivers juntos, esses Slivers compartilham o Scrollable do CustomScrollView, finalmente implementando um efeito de deslizamento unificado.
Exemplo
import 'package:flutter/material.dart';
class ExemploCustomScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
//Como esta rota não usa Scaffold, para permitir que widgets filhos (como Text) usem
//o estilo padrão do Material Design, usamos Material como raiz desta rota.
return Material(
child: CustomScrollView(
slivers: <Widget>[
//AppBar, contendo uma barra de navegação
SliverAppBar(
pinned: true,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Demonstração'),
background: Image.asset(
"./imagens/avatar.png", fit: BoxFit.cover,),
),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: new SliverGrid( //Grade
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, //Grade exibida em duas colunas
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
//Criando widget filho
return new Container(
alignment: Alignment.center,
color: Colors.cyan[100 * (index % 9)],
child: new Text('item da grade ${index + 1}'),
);
},
childCount: 20,
),
),
),
//Lista
new SliverFixedExtentList(
itemExtent: 50.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
//Criando item da lista
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('item da lista ${index + 1}'),
);
},
childCount: 50 //50 itens na lista
),
),
],
),
);
}
}
O código é dividido em três partes:
- SliverAppBar: SliverAppBar corresponde ao AppBar. A diferença é que o SliverAppBar pode ser integrado ao CustomScrollView. SliverAppBar pode combinar com FlexibleSpaceBar para implementar o modelo de cabeçalho expansível/contrátil do Material Design, como o efeito específico, os leitores podem executar este exemplo para ver.
- SliverGrid: é envolvido em SliverPadding para adicionar preenchimento ao SliverGrid. SliverGrid é uma grade de duas colunas com proporção largura/altura de 4, com 20 widgets filhos.
- SliverFixedExtentList: é uma lista em que todos os elementos filhos têm altura de 50 pixels.