Ver mensagens sem resposta | Ver tópicos ativos Hoje é 18 Nov 2017, 00:11



Responder Tópico  [ 1 Mensagem ] 
 Animação Acordion / Sanfona / Expandir-Recolher 
Autor Mensagem
Google employee
Google employee

Data de registro: 28 Jan 2011, 11:42
Mensagens: 1037
Localização: Rio Grande do Sul
Mensagem Animação Acordion / Sanfona / Expandir-Recolher
Pessoal, como já vi várias pessoas perguntando como fazer uma animação em forma de acordeon (expandir - recolher / expand - collapse, efeito sanfona, enfim, como quiserem chamá-lo), resolvi criar esse simples tutorial mostrando uma maneira de se fazer isso.

Existem algumas bibliotecas que fazem isso dentro de uma lista ( https://github.com/tjerkw/Android-Slide ... leListView ), porém este tutorial irá demonstrar como expandir/recolher um container (no exemplo, um linear layout).

O Resultado do tutorial será esse: http://www.youtube.com/watch?v=6eOIexvK2SU

Como é de conhecimento de vocês, API inferior a 10 (Android 3.0) é bastante limitada, não tendo diversos recursos (Animation API por exemplo), então para maior compatibilidade, iremos realizar as animações utilizando uma biblioteca que dá suporte à versões antigas do android. A biblioteca é a Nine Old Androids. http://nineoldandroids.com/

Baixe o projeto do NineOldAndroids e importe a pasta "library". Versão da biblioteca utilizada nesse tutorial (2.4.0).

Com a biblioteca já importada, agora vamos criar o nosso projeto. Após criar o projeto, adicione a biblioteca recém importada como biblioteca (botão direito na pasta do projeto e vá em propriedades). Vá na aba Android e adicione o projeto importado (nine old) como biblioteca do nosso projeto.

Feito isso, a biblioteca está pronta para ser utilizada.

Vamos criar nosso layout. O layout que criaremos é como se fosse um título de uma notícia, e um botão de expandir, para mostrar toda a notícia, e recolher para mostrar novamente somente o título. Neste layout que criei, as notícias de forma estática, para facilitar o entendimento do tutorial, mas nada impede que vocês façam (e eu até recomendo) isso de forma dinâmica.

activity_main.xml


<ScrollView 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:fillViewport="true"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >


    <!-- News Container -->

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:orientation="vertical" >


        <!-- News 1 -->

        <LinearLayout
            android:id="@+id/firstParentLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:orientation="vertical" >


            <!-- Image / Title 1 -->

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:src="@drawable/ic_launcher" />


            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/title" />


            <!-- Content 1 -->

            <LinearLayout
                android:id="@+id/firstContentLayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:visibility="gone" >


                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/content_text"
                    android:textColor="@color/blue" />


                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:src="@drawable/ic_launcher" />


                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/content_text"
                    android:textColor="@color/red" />

            </LinearLayout>

            <!-- Expand / Collapse Button 1 -->

            <Button
                android:id="@+id/firstExpandButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:text="@string/expand_collapse" />

        </LinearLayout>

        <!-- News 2 -->

        <LinearLayout
            android:id="@+id/secondParentLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:background="@color/gray"
            android:orientation="vertical" >


            <!-- Image / Title 2 -->

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:src="@drawable/ic_launcher" />


            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/title" />


            <!-- Coontent 2 -->

            <LinearLayout
                android:id="@+id/secondContentLayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:visibility="gone" >


                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/content_text"
                    android:textColor="@color/blue" />


                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:src="@drawable/ic_launcher" />


                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/content_text"
                    android:textColor="@color/red" />

            </LinearLayout>

            <!-- Expand / Collapse Button 2 -->

            <Button
                android:id="@+id/secondExpandButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:text="@string/expand_collapse" />

        </LinearLayout>
    </LinearLayout>

</ScrollView>
 


Visualizando o layout, acredito que tenham percebido que o que vai expandir/recolher é o LinearLayout do conteúdo. Para fazer uma coisa mais genérica, irei criar uma classe que recebera a view (LinearLayout) do conteúdo que irá expandir/recolher. No exemplo, irei chamar essa classe de ExpandCollapseAnimator.java.

ExpandCollapseAnimator.java

package com.example.br.com.rafaeldecker.accordion;

import android.graphics.Point;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.IntEvaluator;
import com.nineoldandroids.animation.ValueAnimator;

public class ExpandCollapseAnimator {

    /**
     * Default value to animation duration
     */

    public static final int DEFAULT_ANIMATION_DURATION = 300;

    /**
     * Layout that will be animated
     */

    private LinearLayout mLayout;

    /**
     * Controller boolean to lock a new animation until the first one is not
     * ended
     */

    private boolean mIsAnimationBlocked;

    /**
     * Animation objects
     */

    private ValueAnimator mExpandAnimation;
    private ValueAnimator mCollapseAnimation;

    public ExpandCollapseAnimator(LinearLayout layoutToBeExpandedCollapsed, Point offsetLimit) {
        mLayout = layoutToBeExpandedCollapsed;
        mIsAnimationBlocked = false;

        if (mLayout == null || offsetLimit == null) {
            throw new IllegalArgumentException("The arguments \"layoutToBeExpandedCollapsed\" and \"offsetLimit\" must not be null");
        }

        mExpandAnimation = ValueAnimator.ofObject(new HeightEvaluator(mLayout), offsetLimit.x, offsetLimit.y);
        mExpandAnimation.addListener(new Animator.AnimatorListener() {

            @Override
            public void onAnimationStart(Animator animation) {
                mIsAnimationBlocked = true;
                mLayout.setVisibility(View.VISIBLE);
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                mIsAnimationBlocked = false;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }
        });

        mCollapseAnimation = ValueAnimator.ofObject(new HeightEvaluator(mLayout), offsetLimit.y, offsetLimit.x);
        mCollapseAnimation.addListener(new Animator.AnimatorListener() {

            @Override
            public void onAnimationStart(Animator animation) {
                mIsAnimationBlocked = true;
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                mIsAnimationBlocked = false;
                mLayout.setVisibility(View.GONE);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }
        });
    }

    /**
     * Start the layout animation
     *
     * @param int duration (DEFAULT VALUE =
     *        ContentAnimator.DEFAULT_ANIMATION_DURATION)
     */

    public void animate(int duration) {
        if (!mIsAnimationBlocked) {
            int animDuration = (duration > 0) ? duration : DEFAULT_ANIMATION_DURATION;
            boolean isExpanded = (mLayout.getVisibility() == View.VISIBLE) ? true : false;
            if (isExpanded) {
               
                // Do collapse animation
                mCollapseAnimation.setDuration(animDuration);
                mCollapseAnimation.start();
            } else {
                // Do expand animation
                mExpandAnimation.setDuration(animDuration);
                mExpandAnimation.start();
            }
        }
    }

    /**
     * Inner class used to do the animation on Height of the parent layout
     *
     * @author rafaeldecker
     *
     */

    private class HeightEvaluator extends IntEvaluator {

        private View v;

        public HeightEvaluator(View v) {
            this.v = v;
        }

        @Override
        public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
            Integer num = super.evaluate(fraction, startValue, endValue);
            ViewGroup.LayoutParams params = v.getLayoutParams();
            params.height = num;
            v.setLayoutParams(params);
            return num;
        }
    }

}

 


Agora vamos a uma breve explicação desta classe. Começando pelos atributos.

DEFAULT_ANIMATION_DURATION: representa a duração default da animação

mLayout: Instância do conteúdo que será animado.

mIsAnimationBlocked: boolean que controla se uma animação está bloqueada, ou seja, se o usuário clicou para expandir e essa animação ainda não terminou e ele clica novamente no botão expandir/fechar, nada acontecerá, pois a animação está bloqueada até que a animação em questão termine.

mExpandAnimation e mCollapseAnimation: Objetos que farão a animação de expandir e recolher.

No construtor da classe, recebemos dois argumentos: LinearLayout e Point.
O LinearLayout representa o layout que iremos animar.
O point representa os limites da view, ou seja, de onde até onde esta animação irá acontecer, no nosso caso, como a animação é na altura, a animação de expandir se dará da altura 0 até a altura da view. Utilizei a classe Point pois ela já me tras uma estrutura com 2 inteiros, que é o que precisamos, desta maneira o Point.x representa a altura da view com ela recolhida e o Point.y representa a altura da view com ela expandida.

Coloquei uma exception para caso algum dos parâmetros não seja passado, pois ambos são obrigatórios.

Próximo passo é instanciarmos nossas duas animações. Veja que temos uma inner class (classe interna) no final do arquivo chamada HeightEvaluator. Esta classe será utilizada na nossa animação. Ela seta a altura da view. Veja que passamos uma instância desta inner class como argumento da instanciação (não sei se existe essa palavra haha) do nosso objeto de animação, note também que passamos os limites da view [altura mínima (estado recolhido) , altura máxima (estado espandido)].

Adicionaremos um listener na nossa animação, para que sejamos comunicados quando a animação inicia e termina. Quando a animação inicia, devemos setar o boolean de bloqueio para true, e quando termina, para false.
Na animação de expande, precisamos setar o layout para VISIBLE no início da animação, pois caso contrário, a animação não será vísivel, pois quando o conteúdo está recolhido, a visibilidade da view está GONE.
Na animação de recolher, devemos setar para GONE a visibilidade da view, pois quando a animação termina, devemos "retirar da tela" o conteúdo.

Finalizamos o "setup" da view no construtor, agora precisamos de um método que faça a animação. Quem realiza a animação é o método animate, este que tem como parâmetro a duração da animação. Note que antes de fazer a animação, ele verifica se a animação não está bloqueada.
É verificada a duração da animação, caso a animação passada no argumento não seja maior que 0, será utilizada a duração default da animação.
Próximo passo, verifica qual animação deverá ser feita (expandir ou recolher).

Esta é nossa classe de animação. Agora vamos ver a nossa activity e como iremos interagir com essa nossa classe de animação.

MainActivity.java

package com.example.br.com.rafaeldecker.accordion;

import android.app.Activity;
import android.graphics.Point;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;

public class MainActivity extends Activity implements OnClickListener {

    /**
     * Views that represents the first block of content
     */

    private LinearLayout mFirstContentLayout;
    private Button mFirstExpandButton;
    private ExpandCollapseAnimator mFirstAnimator;

    /**
     * Views that represents the second block of content
     */

    private LinearLayout mSecondContentLayout;
    private Button mSecondExpandButton;
    private ExpandCollapseAnimator mSecondAnimator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Getting views reference
        mFirstContentLayout = (LinearLayout) findViewById(R.id.firstContentLayout);
        mFirstExpandButton = (Button) findViewById(R.id.firstExpandButton);
        mSecondContentLayout = (LinearLayout) findViewById(R.id.secondContentLayout);
        mSecondExpandButton = (Button) findViewById(R.id.secondExpandButton);

        // Set click listeners on buttons
        mFirstExpandButton.setOnClickListener(this);
        mSecondExpandButton.setOnClickListener(this);

        // Set the content visible to make sure we will get the right height on onWindowFocusChanged
        mFirstContentLayout.setVisibility(View.INVISIBLE);
        mSecondContentLayout.setVisibility(View.INVISIBLE);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        // Ready to get views size and now it's possible to create animators
        // objects
        mFirstAnimator = new ExpandCollapseAnimator(mFirstContentLayout, new Point(0, mFirstContentLayout.getHeight()));
        mSecondAnimator = new ExpandCollapseAnimator(mSecondContentLayout, new Point(0, mSecondContentLayout.getHeight()));

        // To ensure the content layout will start on hidden state
        mFirstContentLayout.setVisibility(View.GONE);
        mSecondContentLayout.setVisibility(View.GONE);
    }

    @Override
    public void onClick(View v) {

        switch (v.getId()) {
        case R.id.firstExpandButton: {
            if (mFirstAnimator != null) {
                mFirstAnimator.animate(ExpandCollapseAnimator.DEFAULT_ANIMATION_DURATION);
            }
            break;
        }

        case R.id.secondExpandButton: {
            if (mSecondAnimator != null) {
                mSecondAnimator.animate(ExpandCollapseAnimator.DEFAULT_ANIMATION_DURATION);
            }
            break;
        }
        }

    }

}

 


Os atributos são referencias para as views conteúdo e botão de expandir/recolher e também um objeto do tipo ExpandCollapseAnimator de cada notícia.

No método onCreate da activity, buscamos as referências das views do XML e setamos o click listener dos botões. Note que em nenhum momento pegamos o tamanho das views do conteúdo, não fizemos ainda, porque se pegarmos o tamanho da view no onCreate isso retornará sempre 0, pois a view ainda não está carregada. O método certo para pegarmos os tamanhos das views é no método onWindowFocusChanged.
Note também que estamos setando a visibilidade dos conteúdos para INVISIBLE no onCreate. Por que estamos fazendo isso? Para ter certeza que a view está na tela quando formos pegar o tamanho dela... Se notarmos no XML, o conteúdo está setado como GONE, ou seja, a view não está na tela, sendo assim, isso iria retornar o tamanho do conteúdo como 0. Mas por que INVISIBLE e não VISIBLE? Para que não haja a possibilidade de o usuário ver o conteúdo na tela. (mesmo setando para VISIBLE, eu não consegui ver o conteúdo na tela, mas acredito que em dispositivos bem lento, haja a possiblidade de vermos isso, então, resolvi iniciar a view com INVISIBLE).

No método onWindowFocusChanged, iremos pegar os tamanhos das views e instanciar os objetos de animação. E logo apoós instanciarmos estes objetos, devemos setar o estado inicial do nosso conteúdo, neste caso, fechado (representado por GONE, não está na tela).

Agora basta tratarmos os eventos de click no botão de expandir/recolher e chamarmos o método animate da nossa animação.

Código/Source : https://github.com/rafaeldecker/AccordionAnimation

Bom pessoal, era isso. Espero que seja útil para vocês.

Abraço


29 Nov 2013, 11:52
Perfil
Mostrar mensagens anteriores:  Organizar por  
Responder Tópico   [ 1 Mensagem ] 

Quem está online

Usuários vendo este fórum: ac0502, adautox, adilson.afl, adolfo, alansousa, albinoneto, Alessandro Tavares, Alex Marcelo Burnett, Alexandrercarvalho, andrelom, antoniodourado, Aparec, Beru Lars, bigr ecreio, Bitetti, Blackstorm, BornSlip, burujo, caciara, carlos rodrigues, cehills, cenatech, Chassot, CKorneLL, Corneta, criscmaia, czambroni, danilosouza, Dassi, David, dbispo, Demerval, diemesleno, difrene, dmd, du_sr, Dudi_FC, edervieira, estratec, euguns, eusobacana, Everton Moreira, eXagon, fabielp, felipedsilva, Fernando Cardia, ferrodecaju, freak, free_w3000, Gooooogle, Gui Pereira, Guilherme, gustavo, heliopassos, henrique.cardoso, icarodavi, ICCrawler - ICjobs, IgorBrum, italoraony, j-menezes, jackdaniel, jackstuard, JairoCN, johnnyjx, Juninhooooo, juniorfranca, Ki-Adi-Mundi, klassmann, lanlan, leofernandesmo, lfirpo, Lincoln, luciocamilo, maolveira, MarceloMC, marcelosv, MauNunes, mcroft, memnoch, mravel, mvoto, Newton Barbosa, nino, nullPointer, Nute Gunray, obitow, Orivalde, Padawan, paulanegreiros, paulo.esantos, paulosantos, pemam.com.br, rananfu, Renan, renatodondoni, ricardo_listadelphi, rmendes, robertofonte, Rodrigo, Rubens Prates, skcratch, soulmachine, Thiago, tiagoxv, ttaranto, vieira, voliverio, xa:=zin, Yuri, zeantonio e 2 visitantes


Você não pode criar novos tópicos neste fórum
Você não pode responder tópicos neste fórum
Você não pode editar suas mensagens neste fórum
Você não pode excluir suas mensagens neste fórum
Você não pode enviar anexos neste fórum

Procurar por:
cron

© 2007 - 2016 Portal Android - Comunidade de Desenvolvedores Android

Estamos no Linkedin    Siga-nos no twitter


Powered by phpBB - Hospedado por Bemobi