Desenvolvimento

28 nov, 2016

Lista de rolagem dinâmica

Publicidade

No artigo anterior sobre a lista de rolagem básica, eu listei alguns problemas de performance que ela envolvia. Hoje, eu vou mostrar uma solução muito mais robusta que nós criamos na Tabasco. Nós estávamos utilizando felizes a versão base da lista até que começamos a trabalhar na campanha para o Kickerinho World. A campanha consiste em muitos, muitos níveis diferentes (mais de 600 quando o artigo foi escrito) que são exibidos em forma de uma lista de rolagem. O container de rolagem acabou se tornando muito grande e cheio de elementos, e causou um grande impacto na interface do usuário do jogo. A queda da taxa de FPS era inaceitável, então nós tivemos que pensar em uma solução mais sofisticada.

Conceito da mecânica

A ideia é simples – nós não queremos renderizar todos os elementos a cada atualização, nós queremos somente os visíveis. Então, conforme o usuário rola por nossa lista, nós precisamos esconder os elementos no topo e subir os de baixo. Para poder fazer isso, nós temos que calcular quantos itens cabem na tela e então reagir às mudanças sinalizadas pelo ScrollRect. O gif a seguir mostra nosso script em ação:

concept

A base

Não se preocupe, você não precisa jogar fora a lista que criamos antes, a nossa nova vai herdar dela. Os métodos AddElement e ClearPanel terão que ser sobrescritos, porque eles precisam operar somente nos elementos visíveis, sem modificar os dados originais.

protected override void AddElement(TData element)
{
    _elements.Add(element);
    UpdateContainerSize();
}

public override void ClearPanel()
{
    RemoveAllChildren();
    _elements.Clear();
}

Nós também precisamos de um método que redimensionará o container conforme o contador de elementos aumente.

protected void UpdateContainerSize()
{
    float height = _elements.Count * _childHeigth;
    RectTransform rt = _container.GetComponent<RectTransform>();
    rt.sizeDelta = new Vector2(rt.sizeDelta.x, height);
    UpdateChildren();
}

Método Init

No inicio da vida da nossa lista, nós teremos que fazer algumas preparações. Em primeiro lugar, chamaremos o método Init da classe base, que nos permitirá verificar se o container supre nossas expectativas. Ele também tem que ter o crescimento vertical desligado, pois, do contrário, nós não poderemos determinar corretamente o tamanho dos elementos. Então, nós precisamos realizar algumas capturas – lembre que tudo que requer a procura pelo componente (utilizando o método GetComponent) deve ser tratado com cuidado. Nós vamos cachear os elementos e o tamanho do container, o que nos permite calcular o número de itens que deverá caber na tela. Adicionalmente, a lista _elements será criada – ela representará a porção de dados que estará visível na tela. Nós também adicionamos um “listener” ao evento onValueChanged do método ScrollRect – que nos informará que é hora de jogar com os elementos.

public override void Init()
{
    if (!IsInitialized) {
        base.Init();
        Debug.Assert(_containerTransform.anchorMax.y == _containerTransform.anchorMin.y, "ScrollListPooled: Vertical stretching must be turned off! " + GetType());
        _elements = new List<TData>();
        var childTrans = _listElementCache.GetComponent<RectTransform>();
        _childWidth = childTrans.rect.width;
        _childHeigth = childTrans.rect.height;
        if (_transformCache.childCount > 0 && _childHeigth > 0) {
            RectTransform rt = _transformCache.GetChild(0).GetComponent<RectTransform>();
            _height = rt.rect.height;
            _count = Mathf.CeilToInt(_height / _childHeigth);
        }
        GetComponent<ScrollRect>().onValueChanged.AddListener((v) => UpdateChildren());
    }
}

Atualizando as filhas

O método que reage à ação de rolagem é chamado de UpdateChilden. Para determinar os índices dos elementos que deveriam estar visíveis, nós utilizamos as coordenadas de Y de uma propriedade anchoredPosition. Essa propriedade nos diz a posição do pivot do RectTransform do container relativo ao ponto de ancoragem.

public void UpdateChildren()
{
    UpdateChildren(_containerTransform.anchoredPosition.y);
}

public void UpdateChildren(float scrolledY)
{
    int newFirst = Mathf.Clamp(Mathf.CeilToInt(scrolledY / _childHeigth) - 1, 0, _elements.Count - 1);
    int newLast = Mathf.Clamp(Mathf.CeilToInt(scrolledY / _childHeigth) + _count, 0, _elements.Count);
    ...

Se existirem mudanças após calcular os novos índices do primeiro e último elementos visíveis, nós precisamos fazer uma limpeza. Se nós obtivermos valores ridículos, como o índice do novo primeiro elemento sendo maior que o do último elemento visível, nós assumimos que os elementos mudaram e construímos isso do zero, respeitando os índices do primeiro e último elementos recentemente calculados.

if (newFirst > _last || newLast < _first) {
    RemoveAllChildren();
    for (int i = _first; i < _last; i++) {
        AddChild(i, -1);
    }
}

Apesar de não ser necessário destruir todos os itens visíveis, nós só precisamos cuidar de alguns no topo e no fim da janela. Por exemplo – se rolarmos para baixo, temos que esconder os elementos de cima com os índices caindo entre o último elemento e o novo primeiro elemento. Nós também temos que adicionar alguns novos elementos ao fim, para que possamos criar itens com índices caindo entre e novo e o antigo índice do elemento. Então, nós obviamente temos que salvar o conjunto mais novo de valores.

else {
    if (_first != newFirst) {
        for (int i = _first - 1; i >= newFirst; i--) {
            AddChild(i, 0);
        }
        for (int i = _first; i < newFirst; i++) {
            RemoveChildFirst();
        }
    }
    if (_last != newLast) {
        for (int i = _last; i < newLast; i++) {
            AddChild(i, -1);
        }
        for (int i = newLast; i < _last; i++) {
            RemoveChildLast();
        }
    }
}
_first = newFirst;
_last = newLast;

Tratando as filhas

Você certamente notou que alguns novos métodos apareceram – todos eles envolvem modificar a conjunto de dados que é mostrado na tela atualmente. O “principal” deles é o AddChild, que cuida de instanciar o elemento e posicioná-lo dentro do container.

protected void AddChild(int dataIndex, int childIndex = -1)
{
    Debug.Assert(dataIndex >= 0 && dataIndex < _elements.Count);
    var _prefabTransform = _listElementCache.transform;
    if (dataIndex >= 0 && dataIndex < _elements.Count) {
        var newElement = SetUpChild(_elements[dataIndex]);
        if (childIndex == -1) {
            _children.Add(newElement);
        }
        else {
            _children.Insert(childIndex, newElement);
        }
        Vector3 position = _prefabTransform.localPosition;
        position.y = -dataIndex * _childHeigth;
        newElement.GetComponent<RectTransform>().localPosition = position;
    }
}

Note que a posição dos itens no container, na verdade, não muda – nós reposicionamos o container, mas somente exibimos e ocultamos os elementos.

comparison

O resto dos métodos desse grupo é bem direto:

protected void RemoveChildFirst()
{
    RemoveChildAt(0);
}

protected void RemoveChildLast()
{
    RemoveChildAt(_children.Count - 1);
}

protected void RemoveChildAt(int i)
{
    if (_children.Count > 0) {
        Destroy(_children[i]);
        _children.RemoveAt(i);
    }
}

protected void RemoveAllChildren()
{
    _children.ForEach(Destroy);
    _children.Clear();
    _first = 0;
    _last = 0;
}

Bônus – Rolando automaticamente para o item

Às vezes existe a necessidade de exibir as listas a partir de um índice específico, não do início. Nosso caso de uso foi centralizado no nível mais alto que estivesse disponível para o jogador. Esse comportamento pode ser alcançado utilizando este pequeno trecho de código.

public void ScrollTo(int id, float align = 0f)
{
    Debug.Assert(IsInitialized);
    Vector2 pos = _containerTransform.anchoredPosition;
    pos.y = Mathf.Max(0f, Mathf.Lerp((id - 1) * _childHeigth, id * _childHeigth - _height, align));
    _containerTransform.anchoredPosition = pos;
    UpdateChildren();
}

Resumo

É isso! O script completo e pronto para utilização pode ser encontrado aqui. Melhorias futuras podem envolver habilitar a rolagem horizontal e, com certeza,a utilização de objetos criados de antemão, mas isso é assunto para um novo artigo.

***

Emilia Szymanska faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://www.gamasutra.com/blogs/EmiliaSzymanska/20161027/284284/Dynamic_Scrolled_List.php.