Android: creare favolose animazioni con ConstraintLayout – Parte 1

A

Introduzione

Nei prossimi 2 articoli ci occuperemo di capire come creare delle semplici ma accattivanti animazioni da includere nella nostra app utilizzando come base un ConstraintLayout.

Per molti programmatori, o per lo meno per chi ama concentrarsi più sulle funzionalità che sull’estetica, le animazioni sono un qualcosa che tende più al superfluo che al necessario. Anche la mia visione si avvicina molto a questa filosofia ma diciamoci la verità, i veri utilizzatori della app saranno i nostri utenti ed è quindi molto meglio spendere qualche minuto in più sull’aspetto estetico per avere un prodotto che possa essere maggiormente apprezzato.

Potete come sempre trovare l’app di esempio sul mio account di GitHub per poter seguire passo passo tutto ciò che verrà analizzato e il codice riportato.

Primo esempio: immagine con zoom e dettaglio

In questa prima parte andremo a realizzare una semplicissima animazione ma di grande effetto.

L’idea base è quella di visualizzare un pianeta ( Marte ) e alcune informazioni generali, ad esempio il nome, alcune informazioni base e una sua descrizione sommaria. L’animazione verrà applicata toccando l’immagine del pianeta e consisterà nel far sparire tutte le informazioni per visualizzare solamente l’immagine ingrandita e viceversa.

Per capire bene come sarà l’effetto finale ecco qualche frame dell’applicazione di esempio

LAYOUTs

Cerchiamo ora di capire in dettaglio come si realizza questa animazione. Il concetto base dal quale partire è quello di costruire 2 layouts, il primo che rappresenta lo stato iniziale ( l’immagine rimpicciolita con le descrizioni ) e il secondo lo stato finale ( immagine ingrandita).

Per fare questo ho realizzato il primo layout chiamato activity_zd_planet_detail.xml che si presenta in questo modo:

Il secondo layout invece ( activity_zd_planet.xml ) è realizzato partendo da una copia del primo portando fuori dallo schermo tutti gli elementi che dovranno sparire.

Cercando di visualizzare un blueprint con gli elementi esterni allo schermo avremo qualcosa di simile

Nel secondo layout quindi tutti gli elementi rappresentati con sfondo bianco sono stati spostati in quella che risulterà essere la posizione assunta a fine animaizone.

Animazione

Ecco giunti al punto fondamentale di questo articolo. Potrà stupire molti ma per dar vita a questa animazione bastano solo poche righe di codice. Andiamo a vedere insieme quali.

Come prima cosa dobbiamo clonare le ConstraintSet del layout di destinazione

val constraintSet = ConstraintSet()
constraintSet.clone(this, R.layout.activity_zd_planet)

definiamo la transition da eseguire

val transition = ChangeBounds()
transition.interpolator = AnticipateOvershootInterpolator(1.0f)
transition.duration = 1000

e come ultima cosa creiamo una nuova scene e avviamo la transazione in questo modo

TransitionManager.beginDelayedTransition(rootLayout, transition)
constraintSet.applyTo(rootLayout)

Dove rootLayout rappresenta la View del ConstraintLayout che racchiude tutti gli elementi del layout. Come potete notare il tutto si realizza in modo veramente semplice e compatto, e tutta l’activity si riassume in sole 30 righe di codice!

@EActivity(R.layout.activity_zd_planet_detail)
open class ZoomDetailActivity : AppCompatActivity() {

    @ViewById
    internal lateinit var rootLayout: ConstraintLayout

    private var showZoomedImage = false

    @Click
    fun planetImageClicked() {
        if (showZoomedImage)
            startTransition(R.layout.activity_zd_planet_detail)
        else
            startTransition(R.layout.activity_zd_planet)

        showZoomedImage = !showZoomedImage
    }

    private fun startTransition(layoutResId: Int) {
        val constraintSet = ConstraintSet()
        constraintSet.clone(this, layoutResId)

        val transition = ChangeBounds()
        transition.interpolator = AnticipateOvershootInterpolator(1.0f)
        transition.duration = 1000

        TransitionManager.beginDelayedTransition(rootLayout, transition)
        constraintSet.applyTo(rootLayout)
    }
}

Secondo esempio: RecyclerView e Items animati

Nell’esempio precedente abbiamo potuto creare la nostra animazione sfruttando un layout di partenza e uno di arrivo con una transazione, ora invece andremo ad ottenere un effetto di parallax scrolling sfruttando delle semplici Guideline.

In questo caso alla base di tutto utilizzaremo una RecyclerView con swipe orizzontale dove in ogni scheda apparirà l’immagine di un pianeta, una sua descrizione e il nome. Le animazioni verranno attivate durante lo scrolling tra una scheda e l’altra e saranno le seguenti:

  1. Immagine pianeta: durante lo scrolling a sinistra o a destra verrà spostata nella corrispondente direzione per una variazione massima rispetto al centro di – o + 20%.
  2. Nome pianeta: durante l’animazione di entrata e di uscita della scheda comparirà con effetto slide sul lato inferiore dell’immagine.

Ecco il risultato finale

RecyclerView e Adapter

L’implementazione della RecyclerView e del suo adapter sono totalmente standard. Nel caso dell’adapter sarà quindi sufficiente estendere la classe RecyclerView.Adapter ed eseguire il bind del ValueHolder con tutti i dati necessari. Per capire come è stato realizzato il layout di ogni pianeta è possibile fare riferimento al file recycler_parallax_item_planet.xml dell’app di esempio mentre l’adapter verrà realizzato in questo modo

class PlanetsAdapter(var planets: Array<Planet>) : RecyclerView.Adapter<PlanetsAdapter.ViewHolder>() {

    override fun getItemCount(): Int = planets.size

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
            ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.recycler_parallax_item_planet, parent, false))

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val planet = planets[position]
        holder.nameTV.setText(planet.nameResId)
        holder.imageView.setImageResource(planet.imageResId)
        holder.descriptionTV.setText(planet.descriptionResId)
        holder.numberTV.text = planet.number.toString()
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var imageView = itemView.planetImageView!!
        var descriptionTV = itemView.descriptionTV!!
        var nameTV = itemView.nameTV!!
        var numberTV = itemView.numberTV!!
    }
}

La RecyclerView viene inserita in un ConstraintLayout come possiamo verificare dal layout activity_recycler_parallax.xml e nell’activity viene configurata in questo modo

// Creazione dell'adapter e assegnazione alla RecyclerView
recyclerViewPlanets.adapter = PlanetsAdapter(Planets.get().toTypedArray())

// Configurazione del layout a scorrimento orizzontale
recyclerViewPlanets.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)

PagerSnapHelper

Lo scorrimento della RecyclerView è continuo e quindi non è possibile, in modo semplice tramite swipe, riuscire a fermare l’oggetto che stà arrivando in modo che risulti centrato. Il PagerSnapHelper, introdotto con la versione 25.1.0, ci aiuta proprio a fare questo. La nostra RecyclerView riprodurrà l’effetto di una ViewPager, l’unico accorgimento da rispettare è che altezza e larghezza degli oggetti del RecyclerView.Adapter siano impostate a MATCH_PARENT. La configurazione richiede solo 2 linee di codice.

val snapHelper = PagerSnapHelper()
snapHelper.attachToRecyclerView(recyclerViewPlanets)

RecyclerView.OnScrollListener

Eccoci arrivati al punto saliente dell’esempio. Animare le nostre immagini e il testo. Prima di capire il come vediamo di capire bene il perchè questa animazione sia possibile.

Animazione Immagine

Possiamo notare che esiste una GuideLine verticale posizionata a una percentuale del 50% e l’immagine imposta il suo start e il suo end proprio su di essa

app:layout_constraintEnd_toEndOf="@+id/guidelineImage"
app:layout_constraintStart_toStartOf="@+id/guidelineImage"

mentre la width è impostata a wrap_content. In questo modo è facile intuire che spostando a sinistra o a destra la GuideLine, aumentando o diminuendo la percentuale, l’immagine si sposterà di conseguenza per rispettare le constraint impostate.

Animazione testo

In questo caso la GuideLine utilizzata è orizzontale e posizionata sul lato inferiore dell’immagine. Durante lo scrolling verrà spostata in alto per visualizzare il testo e in basso per nasconderlo sotto il componente della descrizione.

Listener

Il listener che andremo a impostare per eseguire lo spostamento delle GuideLine sarà questo

recyclerViewPlanets.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                val manager = recyclerView!!.layoutManager as LinearLayoutManager
                val firstPosition = manager.findFirstVisibleItemPosition()
                val lastPosition = manager.findLastVisibleItemPosition()
                val offSet = recyclerView.computeHorizontalScrollOffset()
                for (i in 0..lastPosition - firstPosition) {
                    val layout = manager.findViewByPosition(firstPosition + i) as ConstraintLayout
                    val guidelineImage = layout.findViewById<Guideline>(R.id.guidelineImage)
                    val paramsImage = guidelineImage.layoutParams as ConstraintLayout.LayoutParams
                    val w = recyclerView.width
                    val deltaPos = offSet - (firstPosition + i) * w
                    val percent = deltaPos / w.toFloat()
                    paramsImage.guidePercent = Math.max(0.3f, Math.min(0.7f, 0.5f - percent))
                    guidelineImage.layoutParams = paramsImage

                    val layoutName = layout.findViewById<ConstraintLayout>(R.id.nameLayout)
                    val guidelineTitle = layoutName.findViewById<Guideline>(R.id.guidelineTitle)
                    val paramsTitle = guidelineTitle.layoutParams as ConstraintLayout.LayoutParams
                    paramsTitle.guidePercent = 1f + percent.absoluteValue
                    guidelineTitle.layoutParams = paramsTitle
                }
            }
        })

Le linee più importanti sulle quali porre l’attenzione sono:

12-16: viene calcolata la percentuale di scroll orizzontale attuale per poterla applicare alla GuideLine risettandone i layoutParams. Possiamo notare la linea 15 in cui viene calcolata la parcentuale dando comunque uno spostamento massimo del 20% in entrambe le direzioni ( 0.3f min – 0.5f – 0.7f max )

21-22: In questo caso la percentuale di scoll calcolata viene sommata a quella di default della GuideLine del testo ( 1f = 100% ). Si esegue sempre una somma in quanto lo scorrimento avviene in positivo o in negativo a seconda della direzione.

Conclusioni

Abbiamo analizzato 2 semplici esempi di come sia possibile con poche righe di codice realizzare delle stupende animazioni da includere nelle nostre applicazioni.

Realizzarle in questo modo chiaramete porta dei vantaggi ( la semplicità appunto ) ma anche degli svantaggi. Basti pensare al primo esempio e potrebbe essere fastidioso dover pensare di trovarsi a gestire 2 layout. Nel caso di modifiche infatti ci si deve ricordare di eseguirle specularmente su entrambi. Altro svantaggio potrebbe essere quello di non poter gestire delle animazioni complesse in quanto aumenterebbe esponenzialmente il numero di layout impiegati e la loro gestione.

Nonostante queste ultime considerazioni credo che chi abbia intenzione di aggiungere in modo semplice e veloce delle simpatiche animazioni possa contare sull’uso dei ConstraintLayout, spesso sottovalutati o non considerati.

Certo, come spiegato a inizio articolo, le animazioni non portano nessuna funzionalità o aumento delle prestazioni ma sono spesso uno dei punti di forza che spinge un utente a utilizzare un’applicazione perchè è pur sempre vero che anche l’occhio vuole la sua parte!

Riferimenti

Potere trovare tutti punti analizzati nell’app di esempio da me realizzata sul mio account di GitHub.

Documentazione ufficiale di ConstraintLayout, ConstraintSet e GuideLine.

A proposito di me

Gianluca Fattarsi

Perito informatico, esperienza come programmatore nell'ambito lavorativo dal lontano 2002. La sua formazione in Java spazia dal Front-End al Back-End con esperienze in gestione database e realizzazione di App Android in Kotlin. Hobby preferiti: Linux, lettura e World of Warcraft.

Gli articoli più letti

Articoli recenti

Commenti recenti