Flutter'da animasyonlar

1. Giriş

Animasyonlar, uygulamanızın kullanıcı deneyimini iyileştirmenin, kullanıcıya önemli bilgileri iletmenin ve uygulamanızı daha iyi hale getirip kullanımını daha keyifli hale getirmenin harika bir yoludur.

Flutter'ın animasyon çerçevesine genel bakış

Flutter, her karede widget ağacının bir bölümünü yeniden oluşturarak animasyon efektlerini gösterir. Animasyon oluşturmayı ve bestelemeyi kolaylaştırmak için önceden oluşturulmuş animasyon efektleri ve diğer API'ler sunar.

  • Örtülü animasyonlar, animasyonun tamamını otomatik olarak çalıştıran önceden oluşturulmuş animasyon efektleridir. Animasyonun hedef değeri değiştiğinde animasyonu geçerli değerden hedef değere kadar çalıştırır ve aradaki her değeri gösterir. Böylece widget sorunsuz bir şekilde animasyon yapar. Örtülü animasyonlara örnek olarak AnimatedSize, AnimatedScale ve AnimatedPositioned verilebilir.
  • Açık animasyonlar da önceden oluşturulmuş animasyon efektleridir ancak çalışmak için Animation nesnesi gerektirir. Örnek olarak SizeTransition, ScaleTransition veya PositionedTransition verilebilir.
  • Animation, çalışan veya durdurulan bir animasyonu temsil eden bir sınıftır. Animasyonun çalıştığı hedef değeri temsil eden bir value ve animasyonun herhangi bir zamanda ekranda gösterdiği mevcut değeri temsil eden status değerinden oluşur. Listenable alt sınıfıdır ve animasyon çalışırken durum değiştiğinde dinleyicilerini bilgilendirir.
  • AnimationController, animasyon oluşturmanın ve durumunu kontrol etmenin bir yoludur. forward(), reset(), stop() ve repeat() gibi yöntemleri, ölçek, boyut veya konum gibi görüntülenen animasyon efektini tanımlamaya gerek kalmadan animasyonu kontrol etmek için kullanılabilir.
  • Ara animasyonlar, başlangıç ve bitiş değerleri arasındaki değerleri enterpolasyon yapmak için kullanılır ve çift, Offset veya Color gibi herhangi bir türü temsil edebilir.
  • Eğriler, bir parametrenin zaman içindeki değişim hızını ayarlamak için kullanılır. Bir animasyon çalışırken, animasyonun başlangıcında veya sonunda değişim hızını daha hızlı ya da daha yavaş hale getirmek için genellikle yumuşak geçiş eğrisi uygulanır. Eğriler, 0,0 ile 1,0 arasında bir giriş değeri alır ve 0,0 ile 1,0 arasında bir çıkış değeri döndürür.

Ne oluşturacaksınız?

Bu codelab'de, çeşitli animasyon efektleri ve teknikleri içeren çoktan seçmeli bir bilgi yarışması oyunu oluşturacaksınız.

3026390ad413769c.gif

Şunları nasıl yapacağınızı öğreneceksiniz:

  • Boyutunu ve rengini canlandıran bir widget oluşturma
  • 3D kart çevirme efekti oluşturma
  • Animasyon paketindeki hazır animasyon efektlerini kullanma
  • Android'in en yeni sürümünde kullanılabilen tahmin edilen geri gitme hareketi desteğini ekleyin.

Neler öğreneceksiniz?

Bu codelab'de öğrenecekleriniz:

  • Çok fazla kod yazmadan harika görünümlü animasyonlar elde etmek için örtülü animasyonlu efektleri kullanma
  • AnimatedSwitcher veya AnimationController gibi önceden oluşturulmuş animasyonlu widget'ları kullanarak kendi efektlerinizi yapılandırmak için açıkça animasyonlu efektleri kullanma
  • 3D efekt gösteren kendi widget'ınızı tanımlamak için AnimationController nasıl kullanılır?
  • Minimal kurulumla süslü animasyon efektleri görüntülemek için animations paketinin nasıl kullanılacağı.

İhtiyacınız olanlar

  • Flutter SDK'sı
  • VSCode veya Android Studio / IntelliJ gibi bir IDE

2. Flutter geliştirme ortamınızı kurma

Bu laboratuvarı tamamlamak için iki yazılıma ihtiyacınız var: Flutter SDK ve bir düzenleyici.

Codelab'i aşağıdaki cihazlardan herhangi birini kullanarak çalıştırabilirsiniz:

  • Bilgisayarınıza bağlı ve Geliştirici moduna ayarlanmış fiziksel bir Android (7. adımda tahmin edilen geri gitme özelliğini uygulamak için önerilir) veya iOS cihaz.
  • iOS simülatörü (Xcode araçlarının yüklenmesi gerekir).
  • Android Emulator (Android Studio'da kurulum gerektirir).
  • Tarayıcı (hata ayıklama için Chrome gereklidir).
  • Windows, Linux veya macOS işletim sistemli bir masaüstü bilgisayar. Dağıtmayı planladığınız platformda geliştirme yapmanız gerekir. Bu nedenle, bir Windows masaüstü uygulaması geliştirmek istiyorsanız uygun derleme zincirine erişmek için Windows'ta geliştirme yapmanız gerekir. docs.flutter.dev/desktop adresinde ayrıntılı olarak ele alınan işletim sistemine özgü gereksinimler vardır.

Yüklemenizi doğrulama

Flutter SDK'nızın doğru şekilde yapılandırıldığını ve yukarıdaki hedef platformlardan en az birinin yüklendiğini doğrulamak için Flutter Doctor aracını kullanın:

$ flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale
    en)
[✓] Android toolchain - develop for Android devices
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio
[✓] IntelliJ IDEA Ultimate Edition
[✓] VS Code
[✓] Connected device (4 available)
[✓] Network resources

• No issues found!

3. Başlangıç uygulamasını çalıştırma

Başlangıç uygulamasını indirme

Başlangıç uygulamasını GitHub'daki flutter/samples deposundan klonlamak için git komutunu kullanın.

git clone https://github.com/flutter/codelabs.git
cd codelabs/animations/step_01/

Alternatif olarak, kaynak kodunu ZIP dosyası olarak indirebilirsiniz.

Uygulamayı çalıştırma

Uygulamayı çalıştırmak için flutter run komutunu kullanın ve hedef cihazı (ör. android, ios veya chrome) belirtin. Desteklenen platformların tam listesi için Desteklenen platformlar sayfasına bakın.

flutter run -d android

Uygulamayı, istediğiniz IDE'yi kullanarak da çalıştırabilir ve hatalarını ayıklayabilirsiniz. Daha fazla bilgi için resmi Flutter belgelerini inceleyin.

Kodu inceleme

Başlangıç uygulaması, model-görünüm-görünüm-model (MVVM) tasarım kalıbını izleyen iki ekrandan oluşan çoktan seçmeli bir bilgi yarışması oyunudur. QuestionScreen (Görünüm), QuizViewModel (Görünüm Modeli) sınıfını kullanarak kullanıcıya QuestionBank (Model) sınıfından çoktan seçmeli sorular sorar.

  • home_screen.dart: Yeni Oyun düğmesinin bulunduğu bir ekranı gösterir.
  • main.dart: MaterialApp öğesini Material 3'ü kullanacak ve ana ekranı gösterecek şekilde yapılandırır.
  • model.dart: Uygulamada kullanılan temel sınıfları tanımlar.
  • question_screen.dart: Bilgi yarışması oyununun kullanıcı arayüzünü gösterir.
  • view_model.dart: QuestionScreen tarafından görüntülenen, test oyununun durumunu ve mantığını saklar.

fbb1e1f7b6c91e21.png

Uygulama, kullanıcının Yeni Oyun düğmesine basması durumunda Flutter'ın Navigator sınıfı tarafından gösterilen varsayılan görünüm geçişi dışında henüz herhangi bir animasyonlu efekti desteklemiyor.

4. Örtülü animasyon efektlerini kullanma

Örtülü animasyonlar özel bir yapılandırma gerektirmediğinden birçok durumda iyi bir seçimdir. Bu bölümde, StatusBar widget'ını güncelleyerek animasyonlu bir skor tablosu göstermesini sağlayacaksınız. Sık kullanılan örtülü animasyon efektlerini bulmak için ImplicitlyAnimatedWidget API dokümanlarına göz atın.

206dd8d9c1fae95.gif

Animasyon içermeyen skor tablosu widget'ını oluşturma

Aşağıdaki kodu kullanarak yeni bir lib/scoreboard.dart dosyası oluşturun:

lib/scoreboard.dart

import 'package:flutter/material.dart';

class Scoreboard extends StatelessWidget {
  final int score;
  final int totalQuestions;

  const Scoreboard({
    super.key,
    required this.score,
    required this.totalQuestions,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          for (var i = 0; i < totalQuestions; i++)
            Icon(
              Icons.star,
              size: 50,
              color: score < i + 1
                  ? Colors.grey.shade400
                  : Colors.yellow.shade700,
            ),
        ],
      ),
    );
  }
}

Ardından, daha önce puanı ve toplam soru sayısını gösteren Text widget'larının yerine StatusBar widget'ının alt öğelerine Scoreboard widget'ını ekleyin. Düzenleyiciniz, dosyanın en üstüne gerekli import "scoreboard.dart" öğesini otomatik olarak eklemelidir.

lib/question_screen.dart

class StatusBar extends StatelessWidget {
  final QuizViewModel viewModel;

  const StatusBar({required this.viewModel, super.key});

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      child: Padding(
        padding: EdgeInsets.all(8.0),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            Scoreboard(                                        // NEW
              score: viewModel.score,                          // NEW
              totalQuestions: viewModel.totalQuestions,        // NEW
            ),
          ],
        ),
      ),
    );
  }
}

Bu widget, her soru için bir yıldız simgesi gösterir. Bir soru doğru yanıtlandığında başka bir yıldız animasyon olmadan anında yanar. Sonraki adımlarda, kullanıcının puanının değiştiğini bildirmek için puanın boyutunu ve rengini animasyonla değiştireceksiniz.

Örtülü animasyon efekti kullanma

Yıldız etkinleştiğinde scale tutarını 0.5 değerinden 1.0 değerine değiştirmek için AnimatedScale bileşenini kullanan AnimatedStar adlı yeni bir bileşen oluşturun:

lib/scoreboard.dart

import 'package:flutter/material.dart';

class Scoreboard extends StatelessWidget {
  final int score;
  final int totalQuestions;

  const Scoreboard({
    super.key,
    required this.score,
    required this.totalQuestions,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          for (var i = 0; i < totalQuestions; i++)
            AnimatedStar(isActive: score > i),                 // Edit this line.
        ],
      ),
    );
  }
}

class AnimatedStar extends StatelessWidget {                   // Add from here...
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      duration: _duration,
      child: Icon(
        Icons.star,
        size: 50,
        color: isActive ? _activatedColor : _deactivatedColor,
      ),
    );
  }
}                                                              // To here.

Artık kullanıcı bir soruyu doğru yanıtladığında AnimatedStar widget'ı, örtülü bir animasyon kullanarak boyutunu güncelliyor. Burada Icon'nın color animasyonu yok. Yalnızca AnimatedScale widget'ı tarafından yapılan scale animasyonu var.

84aec4776e70b870.gif

İki değer arasında interpolasyon yapmak için Tween kullanma

AnimatedStar widget'ının renginin, isActive alanı doğru olarak değiştirildikten hemen sonra değiştiğini fark edin.

Animasyonlu bir renk efekti elde etmek için AnimatedContainer widget'ını (ImplicitlyAnimatedWidget'ün başka bir alt sınıfı) kullanmayı deneyebilirsiniz. Bu widget, renk de dahil olmak üzere tüm özelliklerini otomatik olarak canlandırabilir. Maalesef widget'ımızda kapsayıcı değil simge gösterilmesi gerekiyor.

Ayrıca, simgelerin şekilleri arasında geçiş efektleri uygulayan AnimatedIcon'ı da deneyebilirsiniz. Ancak AnimatedIcons sınıfında yıldız simgesinin varsayılan bir uygulaması yoktur.

Bunun yerine, parametre olarak Tween alan TweenAnimationBuilder adlı başka bir ImplicitlyAnimatedWidget alt sınıfını kullanacağız. Tween, iki değer (begin ve end) alan ve aradaki değerleri hesaplayan bir sınıftır. Böylece animasyon bu değerleri gösterebilir. Bu örnekte, animasyon efektimizi oluşturmak için gereken Tween arayüzünü karşılayan bir ColorTween kullanacağız.

Icon widget'ını seçin ve IDE'nizdeki "Wrap with Builder" hızlı işlemini kullanarak adı TweenAnimationBuilder olarak değiştirin. Ardından süreyi ve ColorTween'ı girin.

lib/scoreboard.dart

class AnimatedStar extends StatelessWidget {
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      duration: _duration,
      child: TweenAnimationBuilder(                            // Add from here...
        duration: _duration,
        tween: ColorTween(
          begin: _deactivatedColor,
          end: isActive ? _activatedColor : _deactivatedColor,
        ),
        builder: (context, value, child) {                     // To here.
          return Icon(Icons.star, size: 50, color: value);     // And modify this line.
        },
      ),
    );
  }
}

Yeni animasyonu görmek için uygulamayı anında yeniden yükleyin.

8b0911f4af299a60.gif

ColorTween öğemizin end değerinin, isActive parametresinin değerine göre değiştiğini unutmayın. Bunun nedeni, Tween.end değerinin her değiştiğinde TweenAnimationBuilder animasyonunu yeniden çalıştırmasıdır. Bu durumda, yeni animasyon mevcut animasyon değerinden yeni bitiş değerine kadar çalışır. Bu sayede, animasyon çalışırken bile rengi istediğiniz zaman değiştirebilir ve doğru ara değerlerle sorunsuz bir animasyon efekti görüntüleyebilirsiniz.

Eğri uygulama

Bu animasyon efektlerinin her ikisi de sabit bir hızda çalışır ancak animasyonlar hızlandığında veya yavaşladığında genellikle daha ilgi çekici ve bilgilendirici olur.

Curve, bir parametrenin zaman içindeki değişim hızını tanımlayan bir yumuşatma işlevi uygular. Flutter, Curves sınıfında easeIn veya easeOut gibi önceden oluşturulmuş bir dizi yumuşatma eğrisiyle birlikte gelir.

5dabe68d1210b8a1.gif3a9e7490c594279a.gif

Curves API dokümanları sayfasında bulunan bu diyagramlar, eğrilerin nasıl çalıştığı hakkında fikir verir. Eğriler, 0,0 ile 1,0 arasındaki bir giriş değerini (x ekseninde gösterilir) 0,0 ile 1,0 arasındaki bir çıkış değerine (y ekseninde gösterilir) dönüştürür. Bu diyagramlarda, çeşitli animasyon efektlerinin yumuşatma eğrisi kullanıldığında nasıl göründüğüne dair bir önizleme de gösterilir.

AnimatedStar'da _curve adlı yeni bir alan oluşturun ve bunu AnimatedScale ile TweenAnimationBuilder widget'larına parametre olarak iletin.

lib/scoreboard.dart

class AnimatedStar extends StatelessWidget {
  final bool isActive;
  final Duration _duration = const Duration(milliseconds: 1000);
  final Color _deactivatedColor = Colors.grey.shade400;
  final Color _activatedColor = Colors.yellow.shade700;
  final Curve _curve = Curves.elasticOut;                       // NEW

  AnimatedStar({super.key, required this.isActive});

  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      scale: isActive ? 1.0 : 0.5,
      curve: _curve,                                           // NEW
      duration: _duration,
      child: TweenAnimationBuilder(
        curve: _curve,                                         // NEW
        duration: _duration,
        tween: ColorTween(
          begin: _deactivatedColor,
          end: isActive ? _activatedColor : _deactivatedColor,
        ),
        builder: (context, value, child) {
          return Icon(Icons.star, size: 50, color: value);
        },
      ),
    );
  }
}

Bu örnekte, elasticOut eğrisi, yay hareketiyle başlayıp sona doğru dengelenen abartılı bir yay efekti sağlar.

8f84142bff312373.gif

Bu eğrinin AnimatedSize ve TweenAnimationBuilder için uygulandığını görmek üzere uygulamayı anında yeniden yükleyin.

206dd8d9c1fae95.gif

Yavaş animasyonları etkinleştirmek için Geliştirici Araçları'nı kullanma

Flutter DevTools, animasyon efektlerinde hata ayıklamak için uygulamanızdaki tüm animasyonları yavaşlatma olanağı sunar. Böylece animasyonu daha net görebilirsiniz.

Geliştirici araçlarını açmak için uygulamanın hata ayıklama modunda çalıştığından emin olun ve VSCode'daki Debug toolbar'da (Hata ayıklama araç çubuğu) Widget Inspector'ı (Widget inceleyici) seçerek veya IntelliJ / Android Studio'daki Debug tool window'da (Hata ayıklama aracı penceresi) Open Flutter DevTools (Flutter geliştirici araçlarını aç) düğmesini seçerek Widget Inspector'ı açın.

3ce33dc01d096b14.png363ae0fbcd0c2395.png

Widget inceleyici açıldıktan sonra araç çubuğundaki Animasyonları yavaşlat düğmesini tıklayın.

adea0a16d01127ad.png

5. Açık animasyon efektleri kullanma

Açık animasyonlar, örtülü animasyonlar gibi önceden oluşturulmuş animasyon efektleridir ancak hedef değer almak yerine parametre olarak bir Animation nesnesi alırlar. Bu nedenle, animasyonun zaten bir gezinme geçişi (ör. AnimatedSwitcher veya AnimationController) ile tanımlandığı durumlarda kullanışlıdır.

Açık bir animasyon efekti kullanma

Açık bir animasyon efektiyle çalışmaya başlamak için Card widget'ını AnimatedSwitcher ile sarmalayın.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({required this.question, super.key});

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(                                 // NEW
      duration: const Duration(milliseconds: 300),           // NEW
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),                                                     // NEW
    );
  }
}

AnimatedSwitcher varsayılan olarak çapraz geçiş efektini kullanır ancak transitionBuilder parametresini kullanarak bunu geçersiz kılabilirsiniz. Geçiş oluşturucu, AnimatedSwitcher öğesine iletilen alt widget'ı ve bir Animation nesnesi sağlar. Bu, açık bir animasyon kullanmak için harika bir fırsattır.

Bu codelab'de kullanacağımız ilk açık animasyon SlideTransition, gelen ve giden widget'ların arasında hareket edeceği başlangıç ve bitiş uzaklığını tanımlayan bir Animation<Offset> alır.

Ara animasyonlarda, herhangi bir Animation öğesini ara animasyon uygulanmış başka bir Animation öğesine dönüştüren animate() yardımcı işlevi bulunur. Bu, AnimatedSwitcher tarafından sağlanan Animation değerinin Tween kullanılarak SlideTransition widget'ına sağlanacak bir Animation değerine dönüştürülebileceği anlamına gelir.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({required this.question, super.key});

  @override
  Widget build(BuildContext context) {
    return AnimatedSwitcher(
      transitionBuilder: (child, animation) {               // Add from here...
        final curveAnimation = CurveTween(
          curve: Curves.easeInCubic,
        ).animate(animation);
        final offsetAnimation = Tween<Offset>(
          begin: Offset(-0.1, 0.0),
          end: Offset.zero,
        ).animate(curveAnimation);
        return SlideTransition(position: offsetAnimation, child: child);
      },                                                    // To here.
      duration: const Duration(milliseconds: 300),
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),
    );
  }
}

Bu işlevin, Tween.animate kullanarak Animation öğesine Curve uyguladığını ve ardından bunu 0,0 ile 1,0 arasında değişen bir Tween değerinden x ekseninde -0,1 ile 0,0 arasında geçiş yapan bir Tween değerine dönüştürdüğünü unutmayın.

Alternatif olarak, Animation sınıfında herhangi bir drive() değerini alan ve bunu yeni bir Animation değerine dönüştüren bir Tween (veya Animatable) işlevi bulunur. Bu sayede ara değerler "zincirlenebilir" ve sonuçta elde edilen kod daha kısa olur:

lib/question_screen.dart

transitionBuilder: (child, animation) {
  var offsetAnimation = animation
      .drive(CurveTween(curve: Curves.easeInCubic))
      .drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
  return SlideTransition(position: offsetAnimation, child: child);
},

Açık animasyon kullanmanın bir diğer avantajı da bunların birlikte oluşturulabilmesidir. SlideTransition widget'ını sarmalayarak aynı eğri animasyonu kullanan başka bir açık animasyon FadeTransition ekleyin.

lib/question_screen.dart

return AnimatedSwitcher(
  transitionBuilder: (child, animation) {
    final curveAnimation = CurveTween(
      curve: Curves.easeInCubic,
    ).animate(animation);
    final offsetAnimation = Tween<Offset>(
      begin: Offset(-0.1, 0.0),
      end: Offset.zero,
    ).animate(curveAnimation);
    final fadeInAnimation = curveAnimation;                            // NEW
    return FadeTransition(                                             // NEW
      opacity: fadeInAnimation,                                        // NEW
      child: SlideTransition(position: offsetAnimation, child: child), // NEW
    );                                                                 // NEW
  },

layoutBuilder'ı özelleştirme

AnimationSwitcher ile ilgili küçük bir sorun fark edebilirsiniz. QuestionCard yeni bir soruya geçtiğinde, animasyon çalışırken soruyu mevcut alanın ortasına yerleştirir. Ancak animasyon durdurulduğunda widget ekranın üst kısmına sabitlenir. Bu durum, soru kartının son konumu animasyon çalışırkenki konumla eşleşmediği için animasyonun titremesine neden olur.

d77de181bdde58f7.gif

Bu sorunu düzeltmek için AnimatedSwitcher, düzeni tanımlamak üzere kullanılabilecek bir layoutBuilder parametresine de sahiptir. Kartı ekranın üst kısmına hizalamak için düzen oluşturucuyu yapılandırmak üzere bu işlevi kullanın:

lib/question_screen.dart

@override
Widget build(BuildContext context) {
  return AnimatedSwitcher(
    layoutBuilder: (currentChild, previousChildren) {
      return Stack(
        alignment: Alignment.topCenter,
        children: <Widget>[
          ...previousChildren,
          if (currentChild != null) currentChild,
        ],
      );
    },

Bu kod, AnimatedSwitcher sınıfındaki defaultLayoutBuilder'ın değiştirilmiş bir sürümüdür ancak Alignment.center yerine Alignment.topCenter kullanır.

Özet

  • Açık animasyonlar, bir Animation nesnesi alan animasyon efektleridir (ImplicitlyAnimatedWidgets ise hedef value ve duration alır).
  • Animation sınıfı, çalışan bir animasyonu temsil eder ancak belirli bir efekti tanımlamaz.
  • Tweens ve Curves'ü (CurveTween kullanılarak) animasyona uygulamak için Tween().animate veya Animation.drive()'ı kullanın.
  • Alt öğelerini nasıl yerleştireceğini ayarlamak için AnimatedSwitcher öğesinin layoutBuilder parametresini kullanın.

6. Animasyonun durumunu kontrol etme

Şimdiye kadar her animasyon, çerçeve tarafından otomatik olarak çalıştırıldı. Örtülü animasyonlar otomatik olarak çalışır ve açık animasyon efektlerinin doğru çalışması için Animation gerekir. Bu bölümde, AnimationController kullanarak kendi Animation nesnelerinizi nasıl oluşturacağınızı ve Tween'leri birleştirmek için TweenSequence'yi nasıl kullanacağınızı öğreneceksiniz.

AnimationController kullanarak animasyon çalıştırma

AnimationController kullanarak animasyon oluşturmak için şu adımları uygulamanız gerekir:

  1. StatefulWidget oluşturun
  2. AnimationController için Ticker sağlamak üzere State sınıfınızda SingleTickerProviderStateMixin mixin'ini kullanın
  3. AnimationController öğesini initState yaşam döngüsü yönteminde başlatın ve mevcut State nesnesini vsync (TickerProvider) parametresine sağlayın.
  4. AnimationController, dinleyicilerini her bilgilendirdiğinde widget'ınızın yeniden oluşturulduğundan emin olun. Bunu AnimatedBuilder kullanarak veya listen() ve setState'yi manuel olarak çağırarak yapabilirsiniz.

Yeni bir dosya (flip_effect.dart) oluşturun ve aşağıdaki kodu kopyalayıp yapıştırın:

lib/flip_effect.dart

import 'dart:math' as math;

import 'package:flutter/widgets.dart';

class CardFlipEffect extends StatefulWidget {
  final Widget child;
  final Duration duration;

  const CardFlipEffect({
    super.key,
    required this.child,
    required this.duration,
  });

  @override
  State<CardFlipEffect> createState() => _CardFlipEffectState();
}

class _CardFlipEffectState extends State<CardFlipEffect>
    with SingleTickerProviderStateMixin {
  late final AnimationController _animationController;
  Widget? _previousChild;

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
      vsync: this,
      duration: widget.duration,
    );

    _animationController.addListener(() {
      if (_animationController.value == 1) {
        _animationController.reset();
      }
    });
  }

  @override
  void didUpdateWidget(covariant CardFlipEffect oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (widget.child.key != oldWidget.child.key) {
      _handleChildChanged(widget.child, oldWidget.child);
    }
  }

  void _handleChildChanged(Widget newChild, Widget previousChild) {
    _previousChild = previousChild;
    _animationController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animationController,
      builder: (context, child) {
        return Transform(
          alignment: Alignment.center,
          transform: Matrix4.identity()
            ..rotateX(_animationController.value * math.pi),
          child: _animationController.isAnimating
              ? _animationController.value < 0.5
                    ? _previousChild
                    : Transform.flip(flipY: true, child: child)
              : child,
        );
      },
      child: widget.child,
    );
  }
}

Bu sınıf, AnimationController oluşturur ve widget yapılandırmasının değiştiğini ve yeni bir alt widget olabileceğini bildirmek için çerçeve didUpdateWidget işlevini her çağırdığında animasyonu yeniden çalıştırır.

AnimatedBuilder, AnimationController dinleyicilerini her bilgilendirdiğinde widget ağacının yeniden oluşturulmasını sağlar. Transform widget'ı ise kartın ters çevrilmesini simüle etmek için 3D döndürme efekti uygulamak üzere kullanılır.

Bu widget'ı kullanmak için her yanıt kartını CardFlipEffect widget'ı ile sarmalayın. Card widget'ına bir key sağladığınızdan emin olun:

lib/question_screen.dart

@override
Widget build(BuildContext context) {
  return GridView.count(
    shrinkWrap: true,
    crossAxisCount: 2,
    childAspectRatio: 5 / 2,
    children: List.generate(answers.length, (index) {
      var color = Theme.of(context).colorScheme.primaryContainer;
      if (correctAnswer == index) {
        color = Theme.of(context).colorScheme.tertiaryContainer;
      }
      return CardFlipEffect(                                    // NEW
        duration: const Duration(milliseconds: 300),            // NEW
        child: Card.filled(                                     // NEW
          key: ValueKey(answers[index]),                        // NEW
          color: color,
          elevation: 2,
          margin: EdgeInsets.all(8),
          clipBehavior: Clip.hardEdge,
          child: InkWell(
            onTap: () => onTapped(index),
            child: Padding(
              padding: EdgeInsets.all(16.0),
              child: Center(
                child: Text(
                  answers.length > index ? answers[index] : '',
                  style: Theme.of(context).textTheme.titleMedium,
                  overflow: TextOverflow.clip,
                ),
              ),
            ),
          ),
        ),                                                      // NEW
      );
    }),
  );
}

Şimdi, CardFlipEffect widget'ını kullanarak yanıt kartlarının çevrildiğini görmek için uygulamayı anında yeniden yükleyin.

5455def725b866f6.gif

Bu sınıfın, açık bir animasyon efektine çok benzediğini fark edebilirsiniz. Hatta kendi sürümünüzü uygulamak için genellikle AnimatedWidget sınıfını doğrudan genişletmeniz iyi bir fikirdir. Maalesef bu sınıfın önceki widget'ı State içinde saklaması gerektiğinden StatefulWidget kullanması gerekir. Kendi açık animasyon efektlerinizi oluşturma hakkında daha fazla bilgi edinmek için AnimatedWidget ile ilgili API dokümanlarına bakın.

TweenSequence kullanarak gecikme ekleme

Bu bölümde, her kartın tek tek çevrilmesi için CardFlipEffect widget'ına gecikme ekleyeceksiniz. Başlamak için delayAmount adlı yeni bir alan ekleyin.

lib/flip_effect.dart

class CardFlipEffect extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final double delayAmount;                      // NEW

  const CardFlipEffect({
    super.key,
    required this.child,
    required this.duration,
    required this.delayAmount,                   // NEW
  });

  @override
  State<CardFlipEffect> createState() => _CardFlipEffectState();
}

Ardından delayAmount öğesini AnswerCards oluşturma yöntemine ekleyin.

lib/question_screen.dart

@override
Widget build(BuildContext context) {
  return GridView.count(
    shrinkWrap: true,
    crossAxisCount: 2,
    childAspectRatio: 5 / 2,
    children: List.generate(answers.length, (index) {
      var color = Theme.of(context).colorScheme.primaryContainer;
      if (correctAnswer == index) {
        color = Theme.of(context).colorScheme.tertiaryContainer;
      }
      return CardFlipEffect(
        delayAmount: index.toDouble() / 2,                     // NEW
        duration: const Duration(milliseconds: 300),
        child: Card.filled(
          key: ValueKey(answers[index]),

Ardından _CardFlipEffectState içinde, TweenSequence kullanarak gecikmeyi uygulayan yeni bir Animation oluşturun. Bu kodun, dart:async kitaplığındaki Future.delayed gibi yardımcı programları kullanmadığını unutmayın. Bunun nedeni, gecikmenin AnimationController kullanılırken widget'ın açıkça kontrol ettiği bir şey olmaması ve animasyonun bir parçası olmasıdır. Bu, Geliştirici Araçları'nda yavaş animasyonlar etkinleştirildiğinde animasyon efektinin hata ayıklamasını kolaylaştırır.TickerProvider

TweenSequence kullanmak için iki TweenSequenceItem oluşturun. Bunlardan biri, animasyonu göreceli bir süre boyunca 0'da tutan bir ConstantTween, diğeri ise 0.0'den 1.0'a giden normal bir Tween içermelidir.

lib/flip_effect.dart

class _CardFlipEffectState extends State<CardFlipEffect>
    with SingleTickerProviderStateMixin {
  late final AnimationController _animationController;
  Widget? _previousChild;
  late final Animation<double> _animationWithDelay;            // NEW

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
      vsync: this,
      duration: widget.duration * (widget.delayAmount + 1),
    );

    _animationController.addListener(() {
      if (_animationController.value == 1) {
        _animationController.reset();
      }
    });

    _animationWithDelay = TweenSequence<double>([              // Add from here...
      if (widget.delayAmount > 0)
        TweenSequenceItem(
          tween: ConstantTween<double>(0.0),
          weight: widget.delayAmount,
        ),
      TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.0),
    ]).animate(_animationController);                          // To here.
  }

Son olarak, AnimationController'nın animasyonunu build yöntemindeki yeni gecikmeli animasyonla değiştirin.

lib/flip_effect.dart

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _animationWithDelay,                            // Modify this line
    builder: (context, child) {
      return Transform(
        alignment: Alignment.center,
        transform: Matrix4.identity()
          ..rotateX(_animationWithDelay.value * math.pi),      // And this line
        child: _animationController.isAnimating
            ? _animationWithDelay.value < 0.5                  // And this one.
                  ? _previousChild
                  : Transform.flip(flipY: true, child: child)
            : child,
      );
    },
    child: widget.child,
  );
}

Şimdi uygulamayı anında yeniden yükleyin ve kartların tek tek çevrilmesini izleyin. Bir meydan okuma için Transform widget'ının sağladığı 3D efektin perspektifini değiştirmeyi deneyin.

28b5291de9b3f55f.gif

7. Özel gezinme geçişlerini kullanma

Şimdiye kadar efektleri tek bir ekranda nasıl özelleştireceğimizi gördük. Animasyonları kullanmanın bir başka yolu da ekranlar arasında geçiş yapmak için kullanmaktır. Bu bölümde, animations paketinin pub.dev'de sunduğu yerleşik animasyon efektlerini ve önceden oluşturulmuş animasyon efektlerini kullanarak ekran geçişlerine animasyon efektleri uygulamayı öğreneceksiniz.

Gezinme geçişini canlandırma

PageRouteBuilder sınıfı, geçiş animasyonunu özelleştirmenize olanak tanıyan bir Route'dır. Bu yöntem, Navigator tarafından çalıştırılan gelen ve giden animasyonu temsil eden iki Animation nesnesi sağlayan transitionBuilder geri çağırma işlevini geçersiz kılmanıza olanak tanır.

Geçiş animasyonunu özelleştirmek için MaterialPageRoute yerine PageRouteBuilder, kullanıcı HomeScreen öğesinden QuestionScreen öğesine gittiğinde geçiş animasyonunu özelleştirmek için ise yerine yazın. Yeni ekranın önceki ekranın üzerinde görünmesini sağlamak için FadeTransition (açıkça animasyonlu bir widget) kullanın.

lib/home_screen.dart

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      PageRouteBuilder(                                         // Add from here...
        pageBuilder: (context, animation, secondaryAnimation) {
          return const QuestionScreen();
        },
        transitionsBuilder:
            (context, animation, secondaryAnimation, child) {
              return FadeTransition(
                opacity: animation,
                child: child,
              );
            },
      ),                                                        // To here.
    );
  },
  child: Text('New Game'),
),

Animasyon paketi, FadeThroughTransition gibi hazır animasyon efektleri sunar. Animasyon paketini içe aktarın ve FadeTransition öğesini FadeThroughTransition widget'ıyla değiştirin:

lib/home_screen.dart

import 'package;animations/animations.dart';

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      PageRouteBuilder(
        pageBuilder: (context, animation, secondaryAnimation) {
          return const QuestionScreen();
        },
        transitionsBuilder:
            (context, animation, secondaryAnimation, child) {
              return FadeThroughTransition(                     // Add from here...
                animation: animation,
                secondaryAnimation: secondaryAnimation,
                child: child,
              );                                                // To here.
            },
      ),
    );
  },
  child: Text('New Game'),
),

Tahmin edilen geri gitme animasyonunu özelleştirme

1c0558ffa3b76439.gif

Tahmin edilen geri gitme, kullanıcının gezinmeden önce mevcut rotanın veya uygulamanın arkasına bakarak ne olduğunu görmesini sağlayan yeni bir Android özelliğidir. Göz atma animasyonu, kullanıcının parmağını ekranda geriye doğru sürüklerken parmağının konumuna göre belirlenir.

Flutter, gezinme yığınında açılacak rota olmadığında veya başka bir deyişle geri tuşuna basıldığında uygulamadan çıkılacaksa özelliği sistem düzeyinde etkinleştirerek sistemin tahmin edilen geri gitme özelliğini destekler. Bu animasyon, Flutter'ın kendisi tarafından değil, sistem tarafından işlenir.

Flutter, bir Flutter uygulamasında rotalar arasında gezinirken tahmin edilen geri gitme özelliğini de destekler. PageTransitionsBuilder adlı özel bir PredictiveBackPageTransitionsBuilder, sistemin tahmin edilen geri gitme hareketlerini dinler ve sayfa geçişini hareketin ilerleme durumuyla yönlendirir.

Tahmin edilen geri gitme özelliği yalnızca Android U ve sonraki sürümlerde desteklenir. Ancak Flutter, orijinal geri hareketi davranışına ve ZoomPageTransitionBuilder'a sorunsuz bir şekilde geri döner. Kendi uygulamanızda nasıl ayarlayacağınızla ilgili bölüm de dahil olmak üzere daha fazla bilgi için blog yayınımıza göz atın.

Uygulamanızın ThemeData yapılandırmasında, PageTransitionsTheme öğesini Android'de PredictiveBack, diğer platformlarda ise animations paketindeki geçiş efekti kullanacak şekilde yapılandırın:

lib/main.dart

import 'package:animations/animations.dart';                                 // NEW
import 'package:flutter/material.dart';

import 'home_screen.dart';

void main() {
  runApp(MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        pageTransitionsTheme: PageTransitionsTheme(
          builders: {
            TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),  // NEW
            TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(),         // NEW
            TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(),       // NEW
            TargetPlatform.windows: FadeThroughPageTransitionsBuilder(),     // NEW
            TargetPlatform.linux: FadeThroughPageTransitionsBuilder(),       // NEW
          },
        ),
      ),
      home: HomeScreen(),
    );
  }
}

Artık Navigator.push() geri aramayı MaterialPageRoute olarak değiştirebilirsiniz.

lib/home_screen.dart

ElevatedButton(
  onPressed: () {
    // Show the question screen to start the game
    Navigator.push(
      context,
      MaterialPageRoute(                                        // Add from here...
        builder: (context) {
          return const QuestionScreen();
        },
      ),                                                        // To here.
    );
  },
  child: Text('New Game'),
),

Mevcut soruyu değiştirmek için FadeThroughTransition'ı kullanın.

AnimatedSwitcher widget'ı, oluşturucu geri çağırmasında yalnızca bir Animation sağlar. Bu sorunu çözmek için animations paketi PageTransitionSwitcher sağlar.

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({required this.question, super.key});

  @override
  Widget build(BuildContext context) {
    return PageTransitionSwitcher(                              // Add from here...
      layoutBuilder: (entries) {
        return Stack(alignment: Alignment.topCenter, children: entries);
      },
      transitionBuilder: (child, animation, secondaryAnimation) {
        return FadeThroughTransition(
          animation: animation,
          secondaryAnimation: secondaryAnimation,
          child: child,
        );
      },                                                        // To here.
      duration: const Duration(milliseconds: 300),
      child: Card(
        key: ValueKey(question),
        elevation: 4,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            question ?? '',
            style: Theme.of(context).textTheme.displaySmall,
          ),
        ),
      ),
    );
  }
}

OpenContainer'ı kullanma

77358e5776eb104c.png

animations paketindeki OpenContainer widget'ı, iki widget arasında görsel bağlantı oluşturmak için genişleyen bir kapsayıcı dönüşümü animasyon efekti sağlar.

Başlangıçta closedBuilder tarafından döndürülen widget gösterilir. Kapsayıcıya dokunulduğunda veya openContainer geri çağırma işlevi çağrıldığında openBuilder tarafından döndürülen widget'a genişler.

openContainer geri çağırmasını görünüm modeline bağlamak için viewModel öğesini QuestionCard widget'ına ileten yeni bir geçiş ekleyin ve "Oyun Bitti" ekranını göstermek için kullanılacak bir geri çağırmayı saklayın:

lib/question_screen.dart

class QuestionScreen extends StatefulWidget {
  const QuestionScreen({super.key});

  @override
  State<QuestionScreen> createState() => _QuestionScreenState();
}

class _QuestionScreenState extends State<QuestionScreen> {
  late final QuizViewModel viewModel = QuizViewModel(
    onGameOver: _handleGameOver,
  );
  VoidCallback? _showGameOverScreen;                                    // NEW

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: viewModel,
      builder: (context, child) {
        return Scaffold(
          appBar: AppBar(
            actions: [
              TextButton(
                onPressed:
                    viewModel.hasNextQuestion && viewModel.didAnswerQuestion
                    ? () {
                        viewModel.getNextQuestion();
                      }
                    : null,
                child: const Text('Next'),
              ),
            ],
          ),
          body: Center(
            child: Column(
              children: [
                QuestionCard(                                           // NEW
                  onChangeOpenContainer: _handleChangeOpenContainer,    // NEW
                  question: viewModel.currentQuestion?.question,        // NEW
                  viewModel: viewModel,                                 // NEW
                ),                                                      // NEW
                Spacer(),
                AnswerCards(
                  onTapped: (index) {
                    viewModel.checkAnswer(index);
                  },
                  answers: viewModel.currentQuestion?.possibleAnswers ?? [],
                  correctAnswer: viewModel.didAnswerQuestion
                      ? viewModel.currentQuestion?.correctAnswer
                      : null,
                ),
                StatusBar(viewModel: viewModel),
              ],
            ),
          ),
        );
      },
    );
  }

  void _handleChangeOpenContainer(VoidCallback openContainer) {        // NEW
    _showGameOverScreen = openContainer;                               // NEW
  }                                                                    // NEW

  void _handleGameOver() {                                             // NEW
    if (_showGameOverScreen != null) {                                 // NEW
      _showGameOverScreen!();                                          // NEW
    }                                                                  // NEW
  }                                                                    // NEW
}

Yeni bir widget ekleyin, GameOverScreen:

lib/question_screen.dart

class GameOverScreen extends StatelessWidget {
  final QuizViewModel viewModel;
  const GameOverScreen({required this.viewModel, super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(automaticallyImplyLeading: false),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Scoreboard(
              score: viewModel.score,
              totalQuestions: viewModel.totalQuestions,
            ),
            Text('You Win!', style: Theme.of(context).textTheme.displayLarge),
            Text(
              'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
              style: Theme.of(context).textTheme.displaySmall,
            ),
            ElevatedButton(
              child: Text('OK'),
              onPressed: () {
                Navigator.popUntil(context, (route) => route.isFirst);
              },
            ),
          ],
        ),
      ),
    );
  }
}

QuestionCard widget'ında, Card yerine animations paketinden bir OpenContainer widget'ı yerleştirin. viewModel ve açık kapsayıcı geri çağırması için iki yeni alan ekleyin:

lib/question_screen.dart

class QuestionCard extends StatelessWidget {
  final String? question;

  const QuestionCard({
    required this.onChangeOpenContainer,
    required this.question,
    required this.viewModel,
    super.key,
  });

  final ValueChanged<VoidCallback> onChangeOpenContainer;
  final QuizViewModel viewModel;

  static const _backgroundColor = Color(0xfff2f3fa);

  @override
  Widget build(BuildContext context) {
    return PageTransitionSwitcher(
      duration: const Duration(milliseconds: 200),
      transitionBuilder: (child, animation, secondaryAnimation) {
        return FadeThroughTransition(
          animation: animation,
          secondaryAnimation: secondaryAnimation,
          child: child,
        );
      },
      child: OpenContainer(                                         // NEW
        key: ValueKey(question),                                    // NEW
        tappable: false,                                            // NEW
        closedColor: _backgroundColor,                              // NEW
        closedShape: const RoundedRectangleBorder(                  // NEW
          borderRadius: BorderRadius.all(Radius.circular(12.0)),    // NEW
        ),                                                          // NEW
        closedElevation: 4,                                         // NEW
        closedBuilder: (context, openContainer) {                   // NEW
          onChangeOpenContainer(openContainer);                     // NEW
          return ColoredBox(                                        // NEW
            color: _backgroundColor,                                // NEW
            child: Padding(                                         // NEW
              padding: const EdgeInsets.all(16.0),                  // NEW
              child: Text(
                question ?? '',
                style: Theme.of(context).textTheme.displaySmall,
              ),
            ),
          );
        },
        openBuilder: (context, closeContainer) {                    // NEW
          return GameOverScreen(viewModel: viewModel);              // NEW
        },                                                          // NEW
      ),
    );
  }
}

4120f9395857d218.gif

8. Tebrikler

Tebrikler, Flutter uygulamasına animasyon efektleri eklemeyi ve Flutter'ın animasyon sisteminin temel bileşenlerini öğrendiniz. Özellikle şu konuları öğrendiniz:

  • ImplicitlyAnimatedWidget nasıl kullanılır?
  • ExplicitlyAnimatedWidget nasıl kullanılır?
  • Animasyona Curves ve Tweens efektlerini uygulama
  • AnimatedSwitcher veya PageRouteBuilder gibi önceden oluşturulmuş geçiş widget'larını kullanma
  • animations paketindeki FadeThroughTransition ve OpenContainer gibi hazır animasyon efektlerini kullanma
  • Android'de Tahmin Edilen Geri Gitme desteği ekleme de dahil olmak üzere varsayılan geçiş animasyonunu özelleştirme

3026390ad413769c.gif

Sırada ne var?

Aşağıdaki codelab'lere göz atın:

Alternatif olarak, çeşitli animasyon tekniklerini gösteren animasyon örnek uygulamasını indirebilirsiniz.

Daha fazla bilgi

Daha fazla animasyon kaynağını flutter.dev adresinde bulabilirsiniz:

Dilerseniz Medium'daki şu makalelere de göz atabilirsiniz:

Referans belgeleri