Flutter için Material Motion ile Güzel Geçişler Oluşturma

1. Giriş

Materyal Tasarım, cesur ve güzel dijital ürünler oluşturmaya yönelik bir sistemdir. Stil, markalama, etkileşim ve hareket tutarlı bir dizi ilke ve bileşen altında birleştirildiğinde ürün ekipleri en büyük tasarım potansiyellerini gerçekleştirebilir.

logo_components_color_2x_web_96dp.png

Material Components (MDC), geliştiricilerin Materyal Tasarım'ı uygulamasına yardımcı olur. Google'daki bir mühendis ve kullanıcı deneyimi tasarımcısı ekibi tarafından oluşturulan MDC, düzinelerce güzel ve işlevsel kullanıcı arayüzü bileşeni içerir. Android, iOS, web ve Flutter.material.io/develop için kullanılabilir.

Flutter için Material'ın hareket sistemi nedir?

Flutter için Materyal hareket sistemi, Materyal Tasarım yönergelerinde açıklandığı gibi, kullanıcıların bir uygulamayı anlamasına ve uygulamada gezinmesine yardımcı olabilecek animasyonlar paketindeki bir dizi geçiş desenidir.

Dört ana Materyal geçişi deseni şunlardır:

  • Kapsayıcı Dönüşümü: Kapsayıcı içeren kullanıcı arayüzü öğeleri arasındaki geçişler. Bir öğeyi sorunsuz bir şekilde başka bir öğeye dönüştürerek iki farklı kullanıcı arayüzü öğesi arasında görünür bir bağlantı oluşturur.

11807bdf36c66657.gif

  • Paylaşılan Eksen: Uzamsal veya gezinme ilişkisi olan kullanıcı arayüzü öğeleri arasındaki geçişler için kullanılır. Öğeler arasındaki ilişkiyi güçlendirmek için x, y veya z eksenlerinde paylaşılan bir dönüşüm kullanılır.

71218f390abae07e.gif

  • Şeffaflaşarak Geçiş: Birbiriyle güçlü bir ilişkisi olmayan kullanıcı arayüzü öğeleri arasındaki geçişler için kullanılır. Gelen öğenin ölçeğiyle birlikte sıralı bir şekilde şeffaflaşarak kaybolma ve şeffaflaşarak görünme efekti kullanılır.

385ba37b8da68969.gif

  • Şeffaflaşma: Ekran sınırları içinde giren veya çıkan kullanıcı arayüzü öğeleri için kullanılır.

cfc40fd6e27753b6.gif

Animations paketi, hem Flutter animations kitaplığı (flutter/animation.dart) hem de Flutter material kitaplığı (flutter/material.dart) üzerine kurulu bu kalıplar için geçiş widget'ları sunar:

Bu codelab'de, Flutter çerçevesi ve Material kitaplığı üzerine kurulu Material geçişlerini kullanacaksınız. Bu da widget'larla çalışacağınız anlamına gelir. :)

Ne oluşturacaksınız?

Bu codelab'de, animasyonlar paketindeki geçişleri kullanarak uygulamanızın görünümünü ve tarzını nasıl özelleştirebileceğinizi göstermek için Dart'ı kullanarak Reply adlı örnek bir Flutter e-posta uygulamasına bazı geçişler ekleme konusunda size yol gösterilecektir.

Yanıt uygulamasının başlangıç kodu sağlanacak ve aşağıdaki Material geçişlerini uygulamaya dahil edeceksiniz. Bu geçişler, tamamlanmış codelab'in aşağıdaki GIF'inde görülebilir:

  • E-posta listesinden e-posta ayrıntıları sayfasına Kapsayıcı Dönüşümü geçişi
  • FAB'den e-posta oluşturma sayfasına Kapsayıcı Dönüşümü geçişi
  • Arama simgesinden arama görünümü sayfasına ortak Z ekseni geçişi
  • Posta kutusu sayfaları arasında şeffaflaşma geçişi
  • Oluşturma ve yanıtlama düğmesi arasında şeffaflaşma geçişi
  • Kaybolan posta kutusu başlığı arasında Şeffaflaşarak Geçiş
  • Alt uygulama çubuğu işlemleri arasında Fade Through (Yavaşça Geçiş) geçişi

b26fe84fed12d17d.gif

Gerekenler

  • Flutter geliştirme ve Dart hakkında temel bilgi
  • Kod düzenleyici
  • Android/iOS emülatörü veya cihaz
  • Örnek kod (sonraki adıma bakın)

Flutter uygulamaları geliştirme deneyiminizi nasıl değerlendirirsiniz?

Yeni Başlayan Orta Düzey Uzman

Bu codelab'den ne öğrenmek istiyorsunuz?

Konuya yeni başlıyorum ve iyi bir genel bakış istiyorum. Bu konu hakkında bilgi sahibiyim ancak bilgilerimi tazelemek istiyorum. Projemde kullanmak için örnek kod arıyorum. Belirli bir şeyin açıklamasını arıyorum.

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

Bu laboratuvarı tamamlamak için iki yazılım gerekir: 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 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 masaüstü uygulaması olarak. 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.

3. Codelab başlangıç uygulamasını indirin

1. seçenek: Başlangıç codelab uygulamasını GitHub'dan kopyalayın

Bu codelab'i GitHub'dan klonlamak için aşağıdaki komutları çalıştırın:

git clone https://github.com/material-components/material-components-flutter-motion-codelab.git
cd material-components-flutter-motion-codelab

2. seçenek: Başlangıç codelab uygulaması zip dosyasını indirin.

Başlangıç uygulaması material-components-flutter-motion-codelab-starter dizininde bulunur.

Proje bağımlılıklarını doğrulama

Proje, animasyon paketine bağlıdır. pubspec.yaml bölümünde, dependencies bölümünün şunları içerdiğini unutmayın:

animations: ^2.0.0

Projeyi açıp uygulamayı çalıştırın.

  1. Projeyi tercih ettiğiniz düzenleyicide açın.
  2. Seçtiğiniz düzenleyici için Başlarken: Test sürüşü bölümündeki "Uygulamayı çalıştırma" talimatlarını uygulayın.

Başarılı aktarım Yanıtla'nın ana sayfası için başlangıç kodu, cihazınızda/emülatörünüzde çalışmalıdır. E-posta listesini içeren gelen kutusunu görürsünüz.

Reply ana sayfası

İsteğe bağlı: Cihaz animasyonlarını yavaşlatma

Bu codelab'de hızlı ancak kusursuz geçişler yer aldığından, geçişlerin bazı ince ayrıntılarını gözlemlemek için cihazın animasyonlarını yavaşlatmak faydalı olabilir. Bu işlem, alt çekmece açıkken ayarlar simgesine dokunarak erişilebilen bir uygulama içi ayar aracılığıyla gerçekleştirilebilir. Endişelenmeyin, cihaz animasyonlarını yavaşlatma yöntemi, cihazdaki Reply uygulaması dışındaki animasyonları etkilemez.

d23a7bfacffac509.gif

İsteğe bağlı: Koyu Mod

Yanıtla uygulamasının parlak teması gözlerinizi yoruyorsa doğru yerdesiniz. Uygulama temasını gözlerinize daha uygun olacak şekilde koyu moda değiştirmenize olanak tanıyan bir uygulama içi ayar bulunur. Bu ayara, alt çekmece açıkken ayarlar simgesine dokunarak erişebilirsiniz.

87618d8418eee19e.gif

4. Örnek uygulama kodu hakkında bilgi edinme

Koda göz atalım. Uygulamada farklı ekranlar arasında geçiş yapmak için animations paketini kullanan bir uygulama sağladık.

  • Ana Sayfa: Seçilen posta kutusunu gösterir.
  • InboxPage: E-postaların listesini gösterir.
  • MailPreviewCard: E-postanın önizlemesini gösterir.
  • MailViewPage: Tek bir e-postanın tamamını gösterir.
  • ComposePage: Yeni bir e-posta oluşturulmasına olanak tanır.
  • SearchPage: Arama görünümünü gösterir.

router.dart

Öncelikle uygulamanın kök gezinme özelliğinin nasıl ayarlandığını anlamak için lib dizininde router.dart dosyasını açın:

class ReplyRouterDelegate extends RouterDelegate<ReplyRoutePath>
   with ChangeNotifier, PopNavigatorRouterDelegateMixin<ReplyRoutePath> {
 ReplyRouterDelegate({required this.replyState})
     : navigatorKey = GlobalObjectKey<NavigatorState>(replyState) {
   replyState.addListener(() {
     notifyListeners();
   });
 }

 @override
 final GlobalKey<NavigatorState> navigatorKey;

 RouterProvider replyState;

 @override
 void dispose() {
   replyState.removeListener(notifyListeners);
   super.dispose();
 }

 @override
 ReplyRoutePath get currentConfiguration => replyState.routePath!;

 @override
 Widget build(BuildContext context) {
   return MultiProvider(
     providers: [
       ChangeNotifierProvider<RouterProvider>.value(value: replyState),
     ],
     child: Selector<RouterProvider, ReplyRoutePath?>(
       selector: (context, routerProvider) => routerProvider.routePath,
       builder: (context, routePath, child) {
         return Navigator(
           key: navigatorKey,
           onPopPage: _handlePopPage,
           pages: [
             // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
             const CustomTransitionPage(
               transitionKey: ValueKey('Home'),
               screen: HomePage(),
             ),
             if (routePath is ReplySearchPath)
               const CustomTransitionPage(
                 transitionKey: ValueKey('Search'),
                 screen: SearchPage(),
               ),
           ],
         );
       },
     ),
   );
 }

 bool _handlePopPage(Route<dynamic> route, dynamic result) {
   // _handlePopPage should not be called on the home page because the
   // PopNavigatorRouterDelegateMixin will bubble up the pop to the
   // SystemNavigator if there is only one route in the navigator.
   assert(route.willHandlePopInternally ||
       replyState.routePath is ReplySearchPath);

   final bool didPop = route.didPop(result);
   if (didPop) replyState.routePath = const ReplyHomePath();
   return didPop;
 }

 @override
 Future<void> setNewRoutePath(ReplyRoutePath configuration) {
   replyState.routePath = configuration;
   return SynchronousFuture<void>(null);
 }
}

Bu, kök gezinme yöneticimizdir ve HomePage ile SearchPage gibi tuvali tamamen kaplayan uygulama ekranlarımızı yönetir. Rotayı ReplySearchPath olarak ayarlayıp ayarlamadığımızı kontrol etmek için uygulamamızın durumunu dinler. Bu durumda, yığınımızın en üstünde SearchPage ile gezinme aracımızı yeniden oluşturur. Ekranlarımızın, geçiş tanımlanmamış bir CustomTransitionPage ile sarmalandığını fark edin. Bu örnekte, ekranlar arasında özel geçiş olmadan gezinmenin bir yolu gösterilmektedir.

home.dart

home.dart içindeki _BottomAppBarActionItems'de aşağıdakileri yaparak uygulamamızın durumunda rotamızı ReplySearchPath olarak ayarlıyoruz:

Align(
 alignment: AlignmentDirectional.bottomEnd,
 child: IconButton(
   icon: const Icon(Icons.search),
   color: ReplyColors.white50,
   onPressed: () {
     Provider.of<RouterProvider>(
       context,
       listen: false,
     ).routePath = const ReplySearchPath();
   },
 ),
);

onPressed parametremizde RouterProvider öğemize erişip routePath özelliğini ReplySearchPath olarak ayarlıyoruz. RouterProvider, temel gezinme durumumuzu takip eder.

mail_view_router.dart

Şimdi uygulamamızın iç navigasyonunun nasıl ayarlandığını görelim. mail_view_router.dart dizininde lib dosyasını açın. Yukarıdakine benzer bir gezinme çubuğu görürsünüz:

class MailViewRouterDelegate extends RouterDelegate<void>
   with ChangeNotifier, PopNavigatorRouterDelegateMixin {
 MailViewRouterDelegate({required this.drawerController});

 final AnimationController drawerController;

 @override
 Widget build(BuildContext context) {
   bool _handlePopPage(Route<dynamic> route, dynamic result) {
     return false;
   }

   return Selector<EmailStore, String>(
     selector: (context, emailStore) => emailStore.currentlySelectedInbox,
     builder: (context, currentlySelectedInbox, child) {
       return Navigator(
         key: navigatorKey,
         onPopPage: _handlePopPage,
         pages: [
           // TODO: Add Fade through transition between mailbox pages (Motion)
           CustomTransitionPage(
             transitionKey: ValueKey(currentlySelectedInbox),
             screen: InboxPage(
               destination: currentlySelectedInbox,
             ),
           )
         ],
       );
     },
   );
 }
...
}

Bu, içimizdeki navigatördür. Bu işlev, tuvalin yalnızca gövdesini kullanan uygulamamızın iç ekranlarını (ör. InboxPage) yönetir. InboxPage, uygulamamızın mevcut posta kutusu durumuna bağlı olarak bir e-posta listesi gösterir. Uygulamamızın durumunun currentlySelectedInbox özelliğinde bir değişiklik olduğunda, gezinme çubuğu yığının en üstünde doğru InboxPage ile yeniden oluşturulur.

home.dart

home.dart içindeki _HomePageState içinde aşağıdakileri yaparak mevcut posta kutumuzu uygulamamızın durumuna ayarlıyoruz:

void _onDestinationSelected(String destination) {
 var emailStore = Provider.of<EmailStore>(
   context,
   listen: false,
 );

 if (emailStore.onMailView) {
   emailStore.currentlySelectedEmailId = -1;
 }

 if (emailStore.currentlySelectedInbox != destination) {
   emailStore.currentlySelectedInbox = destination;
 }

 setState(() {});
}

_onDestinationSelected işlevimizde EmailStore öğemize erişip currentlySelectedInbox öğesini seçilen hedefe ayarlıyoruz. EmailStore, iç gezinme durumumuzu takip eder.

home.dart

Son olarak, kullanılan bir navigasyon yönlendirmesi örneğini görmek için lib dizininde home.dart dosyasını açın. _ReplyFabState sınıfını, InkWell widget'ının onTap özelliği içinde bulun. Bu özellik aşağıdaki gibi görünmelidir:

onTap: () {
 Provider.of<EmailStore>(
   context,
   listen: false,
 ).onCompose = true;
 Navigator.of(context).push(
   PageRouteBuilder(
     pageBuilder: (
       BuildContext context,
       Animation<double> animation,
       Animation<double> secondaryAnimation,
     ) {
       return const ComposePage();
     },
   ),
 );
},

Bu örnekte, özel geçiş olmadan e-posta oluşturma sayfasına nasıl gidebileceğiniz gösterilmektedir. Bu codelab sırasında, uygulama genelindeki çeşitli gezinme işlemleriyle birlikte çalışan Material geçişlerini ayarlamak için Reply'ın kodunu inceleyeceksiniz.

Başlangıç kodunu öğrendiğinize göre şimdi ilk geçişimizi uygulayalım.

5. E-posta listesinden e-posta ayrıntıları sayfasına Kapsayıcı Dönüşümü geçişi ekleme

Başlamak için bir e-postayı tıkladığınızda geçiş ekleyeceksiniz. Bu gezinme değişikliği için kapsayıcı dönüşüm deseni, kapsayıcı içeren kullanıcı arayüzü öğeleri arasındaki geçişler için tasarlandığından oldukça uygundur. Bu desen, iki kullanıcı arayüzü öğesi arasında görünür bir bağlantı oluşturur.

Kod eklemeden önce Yanıtla uygulamasını çalıştırmayı ve bir e-postayı tıklamayı deneyin. Basit bir kesme işlemi yapılmalıdır. Bu durumda, ekranda geçiş olmadan değişiklik yapılır:

Önce

48b00600f73c7778.gif

Aşağıdaki snippet'te gösterildiği gibi, mail_card_preview.dart dosyasının en üstüne animasyon paketi için bir içe aktarma ekleyerek başlayın:

mail_card_preview.dart

import 'package:animations/animations.dart';

Animasyon paketi için içe aktarma işlemine sahip olduğunuza göre artık uygulamanıza güzel geçişler eklemeye başlayabiliriz. Öncelikle OpenContainer widget'ımızı barındıracak bir StatelessWidget sınıfı oluşturarak başlayalım.

mail_card_preview.dart içinde, mail_card_preview.dart sınıf tanımından sonra aşağıdaki kod snippet'ini ekleyin:MailPreviewCard

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
class _OpenContainerWrapper extends StatelessWidget {
 const _OpenContainerWrapper({
   required this.id,
   required this.email,
   required this.closedChild,
 });

 final int id;
 final Email email;
 final Widget closedChild;

 @override
 Widget build(BuildContext context) {
   final theme = Theme.of(context);
   return OpenContainer(
     openBuilder: (context, closedContainer) {
       return MailViewPage(id: id, email: email);
     },
     openColor: theme.cardColor,
     closedShape: const RoundedRectangleBorder(
       borderRadius: BorderRadius.all(Radius.circular(0)),
     ),
     closedElevation: 0,
     closedColor: theme.cardColor,
     closedBuilder: (context, openContainer) {
       return InkWell(
         onTap: () {
           Provider.of<EmailStore>(
             context,
             listen: false,
           ).currentlySelectedEmailId = id;
           openContainer();
         },
         child: closedChild,
       );
     },
   );
 }
}

Şimdi yeni sarmalayıcımızı kullanalım. MailPreviewCard sınıf tanımının içinde, build() işlevimizdeki Material widget'ını yeni _OpenContainerWrapper ile sarmalayacağız:

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
 id: id,
 email: email,
 closedChild: Material(
...

_OpenContainerWrapper öğemizde bir InkWell widget'ı var ve OpenContainer öğesinin renk özellikleri, kapsadığı kapsayıcının rengini tanımlıyor. Bu nedenle, Material ve Inkwell widget'larını kaldırabiliriz. Bu işlem sonucunda elde edilen kod aşağıdaki gibi görünür:

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
 id: id,
 email: email,
 closedChild: Dismissible(
   key: ObjectKey(email),
   dismissThresholds: const {
     DismissDirection.startToEnd: 0.8,
     DismissDirection.endToStart: 0.4,
   },
   onDismissed: (direction) {
     switch (direction) {
       case DismissDirection.endToStart:
         if (onStarredInbox) {
           onStar();
         }
         break;
       case DismissDirection.startToEnd:
         onDelete();
         break;
       default:
     }
   },
   background: _DismissibleContainer(
     icon: 'twotone_delete',
     backgroundColor: colorScheme.primary,
     iconColor: ReplyColors.blue50,
     alignment: Alignment.centerLeft,
     padding: const EdgeInsetsDirectional.only(start: 20),
   ),
   confirmDismiss: (direction) async {
     if (direction == DismissDirection.endToStart) {
       if (onStarredInbox) {
         return true;
       }
       onStar();
       return false;
     } else {
       return true;
     }
   },
   secondaryBackground: _DismissibleContainer(
     icon: 'twotone_star',
     backgroundColor: currentEmailStarred
         ? colorScheme.secondary
         : theme.scaffoldBackgroundColor,
     iconColor: currentEmailStarred
         ? colorScheme.onSecondary
         : colorScheme.onBackground,
     alignment: Alignment.centerRight,
     padding: const EdgeInsetsDirectional.only(end: 20),
   ),
   child: mailPreview,
 ),
);

Bu aşamada, tamamen çalışan bir kapsayıcı dönüşümünüz olmalıdır. Bir e-postayı tıkladığınızda liste öğesi ayrıntılar ekranına genişlerken e-posta listesi geri çekilir. Geri düğmesine basıldığında, e-posta ayrıntıları ekranı e-posta listesinde ölçek büyütülürken liste öğesine geri daraltılır.

Sonra

663e8594319bdee3.gif

6. FAB'den e-posta oluşturma sayfasına Container Transform geçişi ekleme

Konteyner dönüşümüne devam edelim ve kayan işlem düğmesinden ComposePage düğmeyi genişleterek kullanıcı tarafından yazılacak yeni bir e-postaya geçiş ekleyelim. Öncelikle uygulamayı yeniden çalıştırın ve e-posta oluşturma ekranı başlatılırken geçiş olmadığını görmek için FAB'ı tıklayın.

Önce

4aa2befdc5170c60.gif

Bu geçişi yapılandırma şeklimiz, aynı widget sınıfı olan OpenContainer kullandığımız için son adımdakiyle çok benzer olacaktır.

home.dart içinde, dosyanın üst kısmındaki package:animations/animations.dart öğesini içe aktaralım ve _ReplyFabState build() yöntemini değiştirelim. Döndürülen Material widget'ını OpenContainer widget'ı ile sarmalayalım:

home.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
 openBuilder: (context, closedContainer) {
   return const ComposePage();
 },
 openColor: theme.cardColor,
 onClosed: (success) {
   Provider.of<EmailStore>(
     context,
     listen: false,
   ).onCompose = false;
 },
 closedShape: circleFabBorder,
 closedColor: theme.colorScheme.secondary,
 closedElevation: 6,
 closedBuilder: (context, openContainer) {
   return Material(
     color: theme.colorScheme.secondary,
     ...

Önceki OpenContainer widget'ımızı yapılandırmak için kullanılan parametrelere ek olarak artık onClosed de ayarlanıyor. onClosed, OpenContainer rotası kaldırıldığında veya kapalı duruma döndüğünde çağrılan bir ClosedCallback'dir. Bu işlemin dönüş değeri, bu işleve bağımsız değişken olarak iletilir. Bu Callback, uygulama sağlayıcımızı ComposePage rotasından ayrıldığımız konusunda bilgilendirmek için kullanılır. Böylece sağlayıcı, tüm dinleyicileri bilgilendirebilir.

Son adımda yaptığımıza benzer şekilde, Material widget'ını kaldıracağız. Bunun nedeni, OpenContainer widget'ının closedBuilder tarafından closedColor ile döndürülen widget'ın rengini işlemesidir. Ayrıca, Navigator.push() widget'ı artık kendi yönlendirmesini yaptığından InkWell widget'ımızın onTap içindeki Navigator.push() çağrımızı kaldırıp OpenContainer widget'ının closedBuilder tarafından verilen openContainer() Callback ile değiştireceğiz.OpenContainer

Bu işlem sonucunda elde edilen kod aşağıdaki gibidir:

home.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
 openBuilder: (context, closedContainer) {
   return const ComposePage();
 },
 openColor: theme.cardColor,
 onClosed: (success) {
   Provider.of<EmailStore>(
     context,
     listen: false,
   ).onCompose = false;
 },
 closedShape: circleFabBorder,
 closedColor: theme.colorScheme.secondary,
 closedElevation: 6,
 closedBuilder: (context, openContainer) {
   return Tooltip(
     message: tooltip,
     child: InkWell(
       customBorder: circleFabBorder,
       onTap: () {
         Provider.of<EmailStore>(
           context,
           listen: false,
         ).onCompose = true;
         openContainer();
       },
       child: SizedBox(
         height: _mobileFabDimension,
         width: _mobileFabDimension,
         child: Center(
           child: fabSwitcher,
         ),
       ),
     ),
   );
 },
);

Şimdi de eski kodları temizleyelim. OpenContainer widget'ımız artık onClosed ClosedCallback üzerinden uygulamamızın sağlayıcısına ComposePage üzerinde olmadığımızı bildirme işlemini yaptığından mail_view_router.dart'deki önceki uygulamamızı kaldırabiliriz:

mail_view_router.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
emailStore.onCompose = false; /// delete this line
return SynchronousFuture<bool>(true);

Bu adım tamamlandı. FAB'den oluşturma ekranına aşağıdaki gibi bir geçiş olmalıdır:

Sonra

5c7ad1b4b40f9f0c.gif

7. Arama simgesinden arama görünümü sayfasına geçiş için paylaşılan z ekseni geçişi ekleyin

Bu adımda, arama simgesinden tam ekran arama görünümüne geçiş ekleyeceğiz. Bu gezinme değişikliğinde kalıcı bir kapsayıcı yer almadığından, iki ekran arasındaki mekansal ilişkiyi güçlendirmek ve uygulamanın hiyerarşisinde bir düzey yukarı çıkıldığını belirtmek için paylaşılan Z ekseni geçişini kullanabiliriz.

Ek kod eklemeden önce uygulamayı çalıştırmayı ve ekranın sağ alt köşesindeki arama simgesine dokunmayı deneyin. Bu işlem, geçiş olmadan arama görünümü ekranını açmalıdır.

Önce

df7683a8ad7b920e.gif

Başlamak için router.dart dosyamıza gidelim. ReplySearchPath sınıf tanımımızdan sonra aşağıdaki snippet'i ekleyin:

router.dart

// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
class SharedAxisTransitionPageWrapper extends Page {
 const SharedAxisTransitionPageWrapper(
     {required this.screen, required this.transitionKey})
     : super(key: transitionKey);

 final Widget screen;
 final ValueKey transitionKey;

 @override
 Route createRoute(BuildContext context) {
   return PageRouteBuilder(
       settings: this,
       transitionsBuilder: (context, animation, secondaryAnimation, child) {
         return SharedAxisTransition(
           fillColor: Theme.of(context).cardColor,
           animation: animation,
           secondaryAnimation: secondaryAnimation,
           transitionType: SharedAxisTransitionType.scaled,
           child: child,
         );
       },
       pageBuilder: (context, animation, secondaryAnimation) {
         return screen;
       });
 }
}

Şimdi, istediğimiz geçişi elde etmek için yeni SharedAxisTransitionPageWrapper özelliğimizi kullanalım. ReplyRouterDelegate sınıf tanımımızın içinde, pages özelliği altında, arama ekranımızı CustomTransitionPage yerine SharedAxisTransitionPageWrapper ile sarmalayalım:

router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
   const CustomTransitionPage(
     transitionKey: ValueKey('Home'),
     screen: HomePage(),
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('Search'),
       screen: SearchPage(),
     ),
 ],
);

Şimdi uygulamayı yeniden çalıştırmayı deneyin.

81b3ea098926931.gif

Her şey yolunda görünüyor. Alt uygulama çubuğundaki arama simgesini tıkladığınızda, paylaşılan eksen geçişiyle arama sayfası ölçeklenerek görünür hale gelir. Ancak ana sayfanın ölçeği genişletmediğini, bunun yerine arama sayfası ölçeği daraltırken statik kaldığını fark edin. Ayrıca, geri düğmesine basıldığında ana sayfa görünüm için ölçeklenmez. Bunun yerine, arama sayfası görünümden ölçeklenirken statik kalır. Yani henüz işimiz bitmedi.

HomePage öğesini CustomTransitionPage yerine SharedAxisTransitionWrapper ile sarmalayarak her iki sorunu da düzeltelim:

router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
   const SharedAxisTransitionPageWrapper(
     transitionKey: ValueKey('home'),
     screen: HomePage(),
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('search'),
       screen: SearchPage(),
     ),
 ],
);

İşte bu kadar. Şimdi uygulamayı yeniden çalıştırmayı ve arama simgesine dokunmayı deneyin. Ana ekran ve arama görünümü ekranları, Z ekseni boyunca derinlikte aynı anda solup ölçeklenerek iki ekran arasında sorunsuz bir efekt oluşturmalıdır.

Sonra

462d890086a3d18a.gif

8. Posta kutusu sayfaları arasına şeffaflaşma geçişi ekleme

Bu adımda, farklı posta kutuları arasında geçiş ekleyeceğiz. Mekansal veya hiyerarşik bir ilişkiyi vurgulamak istemediğimiz için e-posta listeleri arasında basit bir "değişim" gerçekleştirmek üzere geçiş efekti kullanacağız.

Başka kod eklemeden önce uygulamayı çalıştırmayı, alt uygulama çubuğundaki Yanıtla logosuna dokunmayı ve posta kutularını değiştirmeyi deneyin. E-posta listesi geçiş olmadan değişmelidir.

Önce

89033988ce26b92e.gif

Başlamak için mail_view_router.dart dosyamıza gidelim. MailViewRouterDelegate sınıf tanımımızdan sonra aşağıdaki snippet'i ekleyin:

mail_view_router.dart

// TODO: Add Fade through transition between mailbox pages (Motion)
class FadeThroughTransitionPageWrapper extends Page {
 const FadeThroughTransitionPageWrapper({
   required this.mailbox,
   required this.transitionKey,
 })  : super(key: transitionKey);

 final Widget mailbox;
 final ValueKey transitionKey;

 @override
 Route createRoute(BuildContext context) {
   return PageRouteBuilder(
       settings: this,
       transitionsBuilder: (context, animation, secondaryAnimation, child) {
         return FadeThroughTransition(
           fillColor: Theme.of(context).scaffoldBackgroundColor,
           animation: animation,
           secondaryAnimation: secondaryAnimation,
           child: child,
         );
       },
       pageBuilder: (context, animation, secondaryAnimation) {
         return mailbox;
       });
 }
}

Son adımımıza benzer şekilde, istediğimiz geçişi gerçekleştirmek için yeni FadeThroughTransitionPageWrapper özelliğimizi kullanalım. MailViewRouterDelegate sınıf tanımımızda, pages özelliği altında, posta kutusu ekranımızı CustomTransitionPage ile sarmak yerine FadeThroughTransitionPageWrapper kullanın:

mail_view_router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Fade through transition between mailbox pages (Motion)
   FadeThroughTransitionPageWrapper(
     mailbox: InboxPage(destination: currentlySelectedInbox),
     transitionKey: ValueKey(currentlySelectedInbox),
   ),
 ],
);

Uygulamayı yeniden çalıştırın. Alt gezinme çekmecesini açıp posta kutularını değiştirdiğinizde, mevcut e-posta listesi solup küçülürken yeni liste solup büyümeli. Güzel!

Sonra

8186940082b630d.gif

9. Yazma ve yanıtlama FAB'si arasına şeffaflaşma geçişi ekleme

Bu adımda, farklı FAB simgeleri arasında geçiş ekleyeceğiz. Uzamsal veya hiyerarşik bir ilişkiyi vurgulamak istemediğimiz için, FAB'deki simgeler arasında basit bir "değişim" gerçekleştirmek üzere solma geçişi kullanacağız.

Başka kod eklemeden önce uygulamayı çalıştırmayı, bir e-postaya dokunmayı ve e-posta görünümünü açmayı deneyin. FAB simgesi geçiş olmadan değişmelidir.

Önce

d8e3afa0447cfc20.gif

Bu codelab'in geri kalanında home.dart üzerinde çalışacağız. Bu nedenle, animasyon paketi için içe aktarma ekleme konusunda endişelenmeyin. home.dart için bunu 2. adımda zaten yaptık.

Bir sonraki birkaç geçişi yapılandırma şeklimiz çok benzer olacak. Çünkü bunların tümünde yeniden kullanılabilir bir sınıf olan _FadeThroughTransitionSwitcher kullanılacak.

home.dart bölümünde, _ReplyFabState altına aşağıdaki snippet'i ekleyelim:

home.dart

// TODO: Add Fade through transition between compose and reply FAB (Motion)
class _FadeThroughTransitionSwitcher extends StatelessWidget {
 const _FadeThroughTransitionSwitcher({
   required this.fillColor,
   required this.child,
 });

 final Widget child;
 final Color fillColor;

 @override
 Widget build(BuildContext context) {
   return PageTransitionSwitcher(
     transitionBuilder: (child, animation, secondaryAnimation) {
       return FadeThroughTransition(
         fillColor: fillColor,
         child: child,
         animation: animation,
         secondaryAnimation: secondaryAnimation,
       );
     },
     child: child,
   );
 }
}

_ReplyFabState bölümünde fabSwitcher widget'ını bulun. fabSwitcher, e-posta görünümünde olup olmamasına bağlı olarak farklı bir simge döndürür. _FadeThroughTransitionSwitcher ile özetleyelim:

home.dart

// TODO: Add Fade through transition between compose and reply FAB (Motion)
static final fabKey = UniqueKey();
static const double _mobileFabDimension = 56;

@override
Widget build(BuildContext context) {
 final theme = Theme.of(context);
 final circleFabBorder = const CircleBorder();

 return Selector<EmailStore, bool>(
   selector: (context, emailStore) => emailStore.onMailView,
   builder: (context, onMailView, child) {
     // TODO: Add Fade through transition between compose and reply FAB (Motion)
     final fabSwitcher = _FadeThroughTransitionSwitcher(
       fillColor: Colors.transparent,
       child: onMailView
           ? Icon(
               Icons.reply_all,
               key: fabKey,
               color: Colors.black,
             )
           : const Icon(
               Icons.create,
               color: Colors.black,
             ),
     );
...

_FadeThroughTransitionSwitcher öğelerimize şeffaf bir fillColor veriyoruz. Böylece geçiş sırasında öğeler arasında arka plan olmuyor. Ayrıca bir UniqueKey oluşturup simgelerden birine atarız.

Bu adımda, tam olarak animasyonlu bir bağlamsal kayan işlem düğmesi elde etmiş olmanız gerekir. E-posta görünümüne geçildiğinde eski FAB simgesi şeffaflaşarak küçülürken yeni simge şeffaflaşarak büyüyor.

Sonra

c55bacd9a144ec69.gif

10. Kaybolan posta kutusu başlığı arasına şeffaflaşma geçişi ekleme

Bu adımda, e-posta görünümündeyken posta kutusu başlığını görünür ve görünmez durum arasında geçirmek için bir geçiş efekti ekleyeceğiz. Mekansal veya hiyerarşik bir ilişkiyi vurgulamak istemediğimiz için, posta kutusu başlığını kapsayan Text widget'ı ile boş bir SizedBox arasında basit bir "değiştirme" işlemi gerçekleştirmek için geçiş efekti kullanacağız.

Başka kod eklemeden önce uygulamayı çalıştırmayı, bir e-postaya dokunmayı ve e-posta görünümünü açmayı deneyin. Posta kutusu başlığı geçiş olmadan kaybolmalıdır.

Önce

59eb57a6c71725c0.gif

Bu codelab'in geri kalanı hızlı ilerleyecek. Çünkü son adımda _FadeThroughTransitionSwitcher ile ilgili işlerin çoğunu zaten yaptık.

Şimdi geçişimizi eklemek için home.dart'teki _AnimatedBottomAppBar sınıfımıza gidelim. Son adımımızdaki _FadeThroughTransitionSwitcher öğesini yeniden kullanacağız ve onMailView koşullu ifadesini sarmalayacağız. Bu ifade, boş bir SizedBox döndürür veya alt çekmeceyle senkronize olarak solan bir posta kutusu başlığı döndürür:

home.dart

...
const _ReplyLogo(),
const SizedBox(width: 10),
// TODO: Add Fade through transition between disappearing mailbox title (Motion)
_FadeThroughTransitionSwitcher(
 fillColor: Colors.transparent,
 child: onMailView
     ? const SizedBox(width: 48)
     : FadeTransition(
         opacity: fadeOut,
         child: Selector<EmailStore, String>(
           selector: (context, emailStore) =>
               emailStore.currentlySelectedInbox,
           builder: (
             context,
             currentlySelectedInbox,
             child,
           ) {
             return Text(
               currentlySelectedInbox,
               style: Theme.of(context)
                   .textTheme
                   .bodyMedium!
                   .copyWith(
                     color: ReplyColors.white50,
                   ),
             );
           },
         ),
       ),
),

Bu adımı tamamladık.

Uygulamayı yeniden çalıştırın. Bir e-postayı açıp e-posta görünümüne yönlendirildiğinizde, alt uygulama çubuğundaki posta kutusu başlığı solup küçülmelidir. Mükemmel!

Sonra

3f1a3db01a481124.gif

11. Alt uygulama çubuğu işlemleri arasına geçiş efekti ekleme

Bu adımda, uygulama bağlamına göre alttaki uygulama çubuğu işlemlerini geçişli olarak göstermek için geçişli silme işlemi ekleyeceğiz. Mekansal veya hiyerarşik bir ilişkiyi vurgulamak istemediğimiz için, uygulama ana sayfadayken, alt çekmece görünürken ve e-posta görünümündeyken alt uygulama çubuğu işlemleri arasında basit bir "değiştirme" işlemi gerçekleştirmek için geçiş efektini kullanacağız.

Başka kod eklemeden önce uygulamayı çalıştırmayı, bir e-postaya dokunmayı ve e-posta görünümünü açmayı deneyin. Yanıtla logosuna dokunmayı da deneyebilirsiniz. Alt uygulama çubuğu işlemleri geçiş olmadan değişmelidir.

Önce

5f662eac19fce3ed.gif

Son adıma benzer şekilde, _FadeThroughTransitionSwitcher öğesini tekrar kullanacağız. İstediğiniz geçişi elde etmek için _BottomAppBarActionItems sınıf tanımımıza gidin ve build() işlevimizin döndürülen widget'ını _FadeThroughTransitionSwitcher ile sarmalayın:

home.dart

// TODO: Add Fade through transition between bottom app bar actions (Motion)
return _FadeThroughTransitionSwitcher(
 fillColor: Colors.transparent,
 child: drawerVisible
     ? Align(
         key: UniqueKey(),
         alignment: AlignmentDirectional.bottomEnd,
         child: IconButton(
           icon: const Icon(Icons.settings),
           color: ReplyColors.white50,
           onPressed: () async {
             drawerController.reverse();
             showModalBottomSheet(
               context: context,
               shape: RoundedRectangleBorder(
                 borderRadius: modalBorder,
               ),
               builder: (context) => const SettingsBottomSheet(),
             );
           },
         ),
       )
     : onMailView
...

Şimdi deneyelim. Bir e-postayı açıp e-posta görünümüne yönlendirildiğinizde, eski alt uygulama çubuğu işlemleri solup küçülürken yeni işlemler solup büyümeli. Tebrikler!

Sonra

cff0fa2afa1c5a7f.gif

12. Tebrikler!

100 satırdan az Dart kodu kullanan animasyonlar paketi, Materyal Tasarım yönergelerine uygun ve tüm cihazlarda tutarlı bir şekilde görünen ve çalışan mevcut bir uygulamada güzel geçişler oluşturmanıza yardımcı oldu.

d5637de49eb64d8a.gif

Sonraki adımlar

Material hareket sistemi hakkında daha fazla bilgi edinmek için yönergeleri ve geliştirici dokümanlarının tamamını inceleyin. Ayrıca, uygulamanıza bazı Material geçişleri eklemeyi deneyin.

Material Motion'ı denediğiniz için teşekkür ederiz. Bu codelab'i beğendiğinizi umuyoruz.

Bu codelab'i makul bir süre ve çabayla tamamlayabildim.

Kesinlikle katılıyorum Katılıyorum Ne katılıyorum ne katılmıyorum Katılmıyorum Kesinlikle katılmıyorum

Gelecekte Material hareket sistemini kullanmaya devam etmek istiyorum

Kesinlikle katılıyorum Katılıyorum Ne katılıyorum ne katılmıyorum Katılmıyorum Kesinlikle katılmıyorum

Material Flutter kitaplığı ve Flutter çerçevesi tarafından sağlanan widget'ların nasıl kullanılacağıyla ilgili daha fazla demo için Flutter Gallery'yi ziyaret edin.

46ba920f17198998.png

6ae8ae284bf4f9fa.png