Vi introduserer Flutters nye animasjonspakke

Flutter-teamet sendte nylig en ny stabil versjon av det fantastiske mobilrammeverket på tvers av plattformer. Denne nye versjonen inkluderer mange nye oppgraderinger, inkludert forbedret mobilytelse, reduserte appstørrelser, metallstøtte på iOS-enheter, nye materialmoduler og så videre.

Blant disse nye funksjonene var den nye animasjonspakken den som virkelig fanget meg. Basert på Googles nye Material motion-spesifikasjon, lar denne pakken utviklere implementere animasjonsmønstre i mobilapputvikling.

I følge dokumentasjonen, "Denne pakken inneholder forhåndsinnstilte animasjoner for vanlige ønskede effekter. Animasjonene kan tilpasses med innholdet ditt og legges inn i applikasjonen din for å glede brukerne dine.»

I denne artikkelen vil jeg diskutere hva som er i den nye animasjonspakken og hvordan du bruker den i appen din for å skape vakrere UI-interaksjoner. En grunnleggende kunnskap om Flutter og Dart bør være nok til å følge denne artikkelen – med alt det sagt, la oss komme i gang!

Hva er Material Designs bevegelsessystem?

I følge Material Design-nettstedet er "Bevegelsessystemet et sett med overgangsmønstre som kan hjelpe brukere med å forstå og navigere i en app." I utgangspunktet består Materials bevegelsesspesifikasjon av vanlige overgangsmønstre som gir meningsfulle og vakre brukergrensesnitt-interaksjoner.

På tidspunktet for skriving av denne artikkelen er Material motion-pakker/-biblioteker tilgjengelige for bruk i opprinnelig Android-utvikling og Flutter-utvikling. I Flutter kommer dette i form av animasjonspakken.

Det er for øyeblikket fire overgangsmønstre tilgjengelig i pakken:

  1. Beholdertransformasjon
  2. Delt akseovergang
  3. Fadese gjennom overgangen
  4. Fade overgang

Vi skal nå se på hvordan du implementerer disse overgangsmønstrene med Flutter og animasjonspakken.

Sett opp et nytt Flutter-prosjekt

Først må du lage en ny Flutter-app. Jeg pleier å gjøre dette med VSCode Flutter-utvidelsen. Når du har opprettet Flutter-prosjektet, legger du til animasjonspakken som en avhengighet i pubspec.yaml fil:

dependencies:
  flutter:
    sdk: flutter
  animations: ^1.0.0+5

Kjør nå følgende kommando for å få de nødvendige pakkene:

flutter pub get

Med vår nye Flutter-app satt opp, la oss begynne å skrive litt kode.

Beholdertransformasjonen

I følge Material motion-spesifikasjonen, "Beholdertransformasjonsmønsteret er designet for overganger mellom UI-elementer som inkluderer en beholder. Dette mønsteret skaper en synlig forbindelse mellom to UI-elementer." Beholderen fungerer som et vedvarende element gjennom hele overgangen.

Du kan se noen eksempler på containertransformasjonen i aksjon i animasjonspakkens dokumenter. Som du kan se, under overgangen, er det et felles element:beholderen, som holder det utgående og innkommende elementet og hvis dimensjoner og posisjon endres.

For å implementere containertransformasjonen kan vi bruke OpenContainer widget levert av animasjonspakken. OpenContainer lar oss definere innholdet i beholderen når den er lukket (startinnholdet) og innholdet i beholderen når den åpnes. Vi kan også definere andre egenskaper, for eksempel farge og høyden på beholderen i både lukket og åpnet tilstand.

Koden for implementering av beholdertransformasjonen ser slik ut:

void main() {
  runApp(
    MaterialApp(
      home:TestingContainer(),
    ),
  );
}

class TestingContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(),
      floatingActionButton: OpenContainer(
        closedBuilder: (_, openContainer){
          return FloatingActionButton(
            elevation: 0.0,
            onPressed: openContainer,
            backgroundColor: Colors.blue,
            child: Icon(Icons.add, color: Colors.white),
          );
        },
        openColor: Colors.blue,
        closedElevation: 5.0,
        closedShape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(100)
        ),
        closedColor: Colors.blue,
        openBuilder: (_, closeContainer){
          return Scaffold(
            appBar: AppBar(
              backgroundColor: Colors.blue,
              title: Text("Details"),
              leading: IconButton(
                onPressed: closeContainer,
                icon: Icon(Icons.arrow_back, color: Colors.white),
              ),
            ),
            body: (
              ListView.builder(
                itemCount: 10,
                itemBuilder: (_,index){
                  return ListTile(
                    title: Text(index.toString()),
                  );
                }
              )
            ),
          );
        }
      ),
    );
  }
}

Som du kan se, vår OpenContainer har to navngitte parametere (blant andre) kalt closedBuilder og openBuilder . Begge disse parameterne tar en funksjon som returnerer en widget.

Funksjonen tar inn et objekt av typen BuildContext og en funksjon som enten åpner beholderen (i tilfelle closedBuilder ) eller som lukker beholderen (i tilfelle openBuilder). ). Widgeten returnerte i closedBuilder er innholdet i beholderen i lukket tilstand, og widgeten returnert i openBuilder er innholdet i åpnet tilstand. Resultatet skal være:

Flere flotte artikler fra LogRocket:

  • Ikke gå glipp av et øyeblikk med The Replay, et kuratert nyhetsbrev fra LogRocket
  • Bruk Reacts useEffect for å optimalisere applikasjonens ytelse
  • Bytt mellom flere versjoner av Node
  • Finn ut hvordan du animerer React-appen din med AnimXYZ
  • Utforsk Tauri, et nytt rammeverk for å bygge binærfiler
  • Sammenlign NestJS vs. Express.js
  • Oppdag populære ORM-er som brukes i TypeScript-landskapet

Det delte akseovergangsmønsteret

I følge dokumentene, "Det delte aksemønsteret brukes for overganger mellom UI-elementer som har et romlig eller navigasjonsforhold. Dette mønsteret bruker en delt transformasjon på x-, y- eller z-aksen for å forsterke forholdet mellom elementene." Så hvis du trenger å animere navigasjonen langs en bestemt akse, er overgangsmønsteret for delt akse det for deg.

Du kan få en bedre ide om hva jeg mener ved å se animasjonen i aksjon på pakkens dokumentside. For implementering av overgangsmønsteret for delt akse gir animasjonspakken oss PageTransitionSwitcher og SharedAxisTransition widgets.

PageTransitionSwitcher widgeten går ganske enkelt over fra et gammelt barn til et nytt barn når barnet endres. Du bør alltid gi hvert barn av PageTransitionSwitcher en unik nøkkel slik at Flutter vet at widgeten nå har et nytt barn. Dette kan enkelt gjøres med en UniqueKey objekt.

Bortsett fra underordnet parameter, PageTransitionSwitcher har også andre navngitte parametere:duration , for å angi varigheten av overgangen; reverse , som tar en boolsk verdi og bestemmer om overgangen skal "spilles baklengs" eller ikke; og transitionBuilder , som tar en funksjon som vil returnere en widget.

I vårt tilfelle returnerer vi en SharedAxisTransition widget. I SharedAxisTransition widget, kan vi angi transitionType (om vi ønsker å gå langs x-aksen, y-aksen eller z-aksen). Vi har også animation og secondaryAnimation parametere, som definerer henholdsvis animasjonen som driver barnets inngang og utgang og animasjonen som driver overgangen til et nytt barn på toppen av det gamle.

Koden for implementering av SharedAxisTransition ser slik ut:

void main() {
  runApp(
    MaterialApp(
     home: TestingSharedAxis(),
    ),
  );
}

class TestingSharedAxis extends StatefulWidget {
  @override
  _TestingSharedAxisState createState() => _TestingSharedAxisState();
}
class _TestingSharedAxisState extends State<TestingSharedAxis> {
  bool _onFirstPage = true;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      body: SafeArea(
        child: Column(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: <Widget>[
                  FlatButton(
                      onPressed: _onFirstPage == true
                          ? null
                          : () {
                              setState(() {
                                _onFirstPage = true;
                              });
                            },
                      child: Text(
                        "First Page",
                        style: TextStyle(
                            color: _onFirstPage == true
                                ? Colors.blue.withOpacity(0.5)
                                : Colors.blue),
                      )),
                  FlatButton(
                      onPressed: _onFirstPage == false
                          ? null
                          : () {
                              setState(() {
                                _onFirstPage = false;
                              });
                            },
                      child: Text(
                        "Second Page",
                        style: TextStyle(
                            color: _onFirstPage == false
                                ? Colors.red.withOpacity(0.5)
                                : Colors.red),
                      ))
                ],
              ),
            ),
            Expanded(
              child: PageTransitionSwitcher(
                duration: const Duration(milliseconds: 300),
                reverse: !_onFirstPage,
                transitionBuilder: (Widget child, Animation<double> animation,
                    Animation<double> secondaryAnimation) {
                  return SharedAxisTransition(
                    child: child,
                    animation: animation,
                    secondaryAnimation: secondaryAnimation,
                    transitionType: SharedAxisTransitionType.horizontal,
                  );
                },
                child: _onFirstPage
                    ? Container(
                        key: UniqueKey(),
                        color: Colors.blue,
                        child: Align(
                          alignment: Alignment.topCenter,
                          child: Text("FIRST PAGE"),
                        ),
                      )
                    : Container(
                        key: UniqueKey(),
                        color: Colors.red,
                        child: Align(
                          alignment: Alignment.topCenter,
                          child: Text("SECOND PAGE"),
                        ),
                      ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

I kodeblokken ovenfor definerte vi en privat boolsk variabel kalt _onFirstPage , som er sant hvis vi er på første side og usann ellers. Vi brukte også verdien _onFirstPage for å definere verdien for den omvendte parameteren til PageTransitionSwitcher . Dette tillater PageTransitionSwitcher for å "sprette" den andre siden av når du bytter tilbake til den første siden.

Resultatet skal se omtrent slik ut:

Fade-through-overgangsmønsteret

Fade through-overgangsmønsteret brukes til å gå mellom UI-elementer som ikke er sterkt relatert til hverandre. Ta en titt på dokumentsiden for å se hvordan dette overgangsmønsteret ser ut.

Implementeringen av overgangsmønsteret for gjennomtoning er veldig likt det for overgangsmønsteret for delt akse. Her, FadeThroughTransition brukes i stedet for SharedAxisTransition . Her er koden for en enkel implementering av fade through-mønsteret i Flutter med animasjonspakken:

void main() {
  runApp(
    MaterialApp(
     home: TestingFadeThrough(),
    ),
  );
}

class TestingFadeThrough extends StatefulWidget {
  @override
  _TestingFadeThroughState createState() => _TestingFadeThroughState();
}
class _TestingFadeThroughState extends State<TestingFadeThrough> {
  int pageIndex = 0;
  List<Widget> pageList = <Widget>[
    Container(key: UniqueKey(),color:Colors.red),
    Container(key: UniqueKey(),color: Colors.blue),
    Container(key: UniqueKey(),color:Colors.green)
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Testing Fade Through')),
      body: PageTransitionSwitcher(
        transitionBuilder: (
          Widget child,
          Animation<double> animation,
          Animation<double> secondaryAnimation
        ){
          return FadeThroughTransition(
            animation: animation,
            secondaryAnimation: secondaryAnimation,
            child: child,
          );
        },
        child: pageList[pageIndex],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: pageIndex,
        onTap: (int newValue) {
          setState(() {
            pageIndex = newValue;
          });
        },
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.looks_one),
            title: Text('First Page'),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.looks_two),
            title: Text('Second Page'),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.looks_3),
            title: Text('Third Page'),
          ),
        ],
      ),

    );
  }
}

Det vi gjør her er ganske grunnleggende; vi gjengir et nytt barn avhengig av indeksen til BottomNavigationBarItem som er valgt for øyeblikket. Legg merke til at hvert barn har en unik nøkkel. Som jeg sa tidligere, gjør dette at Flutter kan skille mellom de forskjellige barna. Slik skal resultatet se ut:

Fade-overgangsmønsteret

Dette overgangsmønsteret brukes når et element må gå inn (enter) eller gå ut (ut) av skjermen, for eksempel i tilfelle av en modal eller dialog.

For å implementere dette i Flutter, må vi bruke FadeScaleTransition og en AnimationController å kontrollere inngangen og utgangen til overgangens barn. Vi vil bruke vår AnimationController status for å bestemme om den underordnede widgeten skal vises eller skjules.

Slik ser en implementering av fade-overgangen ut i kode:

void main() {
  runApp(
    MaterialApp(
     home: TestingFadeScale(),
    ),
  );
}

class TestingFadeScale extends StatefulWidget {
  @override
  _TestingFadeScaleState createState() => _TestingFadeScaleState();
}
class _TestingFadeScaleState extends State<TestingFadeScale>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  @override
  void initState() {
    _controller = AnimationController(
        value: 0.0,
        duration: const Duration(milliseconds: 500),
        reverseDuration: const Duration(milliseconds: 250),
        vsync: this)
      ..addStatusListener((status) {
        setState(() {});
      });
    super.initState();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  bool get _isAnimationRunningForwardsOrComplete {
    switch (_controller.status) {
      case AnimationStatus.forward:
      case AnimationStatus.completed:
        return true;
      case AnimationStatus.reverse:
      case AnimationStatus.dismissed:
        return false;
    }
    return null;
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Testing FadeScale Transition'),
      ),
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                RaisedButton(
                  onPressed: () {
                    if (_isAnimationRunningForwardsOrComplete) {
                      _controller.reverse();
                    } else {
                      _controller.forward();
                    }
                  },
                  color: Colors.blue,
                  child: Text(_isAnimationRunningForwardsOrComplete
                      ? 'Hide Box'
                      : 'Show Box'),
                )
              ],
            ),
          ),
          AnimatedBuilder(
            animation: _controller,
            builder: (context, child) {
              return FadeScaleTransition(animation: _controller, child: child);
            },
            child: Container(
              height: 200,
              width: 200,
              color: Colors.blue,
            ),
          ),
        ],
      ),
    );
  }
}

Som du kan se, er FadeScaleTransition widgeten har en navngitt parameter kalt animation , som tar inn en AnimationController . Resultatet skal se slik ut:

showModal funksjon

Animasjonspakken kommer også med en passende navngitt funksjon kalt showModal , som (som navnet antyder) brukes til å vise en modal.

showModal tar inn ulike argumenter, hvorav noen inkluderer:context , som brukes til å finne Navigator for modalen; builder , som er en funksjon som returnerer innholdet i modalen; og configuration .

configuration parameteren tar inn en widget som utvider ModalConfiguration klasse, og den brukes til å definere egenskapene til modalen, for eksempel fargen på barrieren (deler av skjermen som ikke dekkes av modalen), varighet, inn- og utgangsoverganger, og så videre.

Her er hva showModal funksjonen ser ut som i kode:

void main() {
  runApp(
    MaterialApp(
      home: TestingShowModal(),
    ),
  );
}


class TestingShowModal extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    timeDilation = 20;
    return Scaffold(
      body: Center(
        child: RaisedButton(
          color: Colors.blue,
          child: Text(
            "Show Modal",
            style: TextStyle(
              color: Colors.white      
            ),
          ),
          onPressed: (){
            showModal(
              context: context,
              configuration: FadeScaleTransitionConfiguration(),
              builder: (context){
                return AlertDialog(
                  title: Text("Modal title"),
                  content: Text("This is the modal content"),
                );
              }
            );
          }
        ),
      ),
    );
  }
}

I kodeblokken ovenfor brukte vi FadeScaleTransitionConfiguration som vårt konfigurasjonsargument. FadeScaleTransitionConfiguration er en forhåndsdefinert klasse som utvider ModalConfiguration og brukes til å legge til egenskapene til en fade-overgang til vår modal.

Overstyre standard sideruteovergang

Med SharedAxisPageTransitionsBuilder , FadeThroughPageTransitionsBuilder , og pageTransitionsTheme parameteren til vår MaterialApp tema, kan vi overstyre standard overgangsanimasjonen som oppstår når vi bytter fra én rute til en annen i Flutter-appen vår.

For å gjøre dette med SharedAxisPageTransitionsBuilder :

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(
        pageTransitionsTheme: const PageTransitionsTheme(
          builders: <TargetPlatform, PageTransitionsBuilder>{
            TargetPlatform.android: SharedAxisPageTransitionsBuilder(
                transitionType: SharedAxisTransitionType.horizontal),
          },
        ),
      ),
      home: HomePage(),
    ),
  );
}

Og for å gjøre dette med FadeThroughPageTransitionsBuilder :

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(
        pageTransitionsTheme: const PageTransitionsTheme(
          builders: <TargetPlatform, PageTransitionsBuilder>{
            TargetPlatform.android: FadeThroughPageTransitionsBuilder()
          },
        ),
      ),
      home: HomePage(),
    ),
  );
}

Konklusjon

Som jeg har vist deg, er animasjonspakken flott for å legge til nyttige UI-interaksjoner og overganger til Flutter-appen din. Du kan få hele kildekoden til eksemplene vist her.