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. Ürün ekipleri stil, marka, etkileşim ve hareketi tutarlı bir ilke ve bileşen kümesi altında bir araya getirerek en büyük tasarım potansiyellerini gerçekleştirebilir.

logo_components_color_2x_web_96dp.png

Materyal Bileşenleri (MDC), geliştiricilerin Materyal Tasarım'ı uygulamasına yardımcı olur. Google'da mühendislerden ve kullanıcı deneyimi tasarımcılarından oluşan bir ekip tarafından oluşturulan MDC, onlarca güzel ve işlevsel kullanıcı arayüzü bileşeni içerir. Ayrıca Android, iOS, web ve Flutter.material.io/develop'da 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ı olabilen, animasyon paketinde yer alan bir dizi geçiş kalıbıdır.

Dört temel Materyal geçiş kalıbı ş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 diğerine 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: Mekansal veya gezinme ilişkisi olan kullanıcı arayüzü öğeleri arasındaki geçişler; öğeler arasındaki ilişkiyi güçlendirmek için x, y veya z ekseninde paylaşılan bir dönüşüm kullanır.

71218f390abae07e.gif

  • Karartma: Birbiriyle güçlü bir ilişkisi olmayan kullanıcı arayüzü öğeleri arasındaki geçişler; ardışık bir belirme ve belirme kullanır.

385ba37b8da68969.gif

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

cfc40fd6e27753b6.gif

Animasyon paketi, bu kalıplar için hem Flutter animasyon kitaplığının (flutter/animation.dart) hem de Flutter materyal kitaplığının (flutter/material.dart) üzerine kurulan geçiş widget'ları sunar:

Bu codelab'de Flutter çerçevesi ve Material kitaplığı temel alınarak oluşturulan Material geçişlerini kullanacaksınız. Bu sayede widget'larla ilgileneceksiniz. :)

Oluşturacaklarınız

Bu codelab'de, uygulamanızın görünümünü ve tarzını özelleştirmek için animasyon paketindeki geçişleri nasıl kullanabileceğinizi göstermek üzere Dart kullanarak Reply adlı örnek bir Flutter e-posta uygulamasına bazı geçişler yapabilirsiniz.

Yanıtla uygulaması için başlangıç kodu sağlanacak. Ardından, uygulamaya aşağıdaki Material geçişlerini dahil edeceksiniz. Bunları, tamamlanan codelab'in GIF'inde görebilirsiniz:

  • Kapsayıcı Dönüşümü'nün e-posta listesinden e-posta ayrıntıları sayfasına geçiş
  • FAB'dan e-posta oluşturma sayfasına Kapsayıcı Dönüşümü geçişi
  • Arama simgesinden arama görünümü sayfasına paylaşılan Z ekseni geçişi
  • Posta kutusu sayfaları arasında Kararma geçişi
  • Oluşturma ve yanıtlama FAB'si arasında Kararma geçişi
  • Kaybolan posta kutusu başlıkları arasında Kararma geçişi
  • Alt uygulama çubuğu işlemleri arasında Kararma geçişi

b26fe84fed12d17d.gif

Gerekenler

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

Flutter uygulamaları geliştirme konusundaki deneyim düzeyinizi nasıl değerlendirirsiniz?

Acemi Orta Yeterli

Bu codelab'den ne öğrenmek istersiniz?

Konuyla yeni tanıştım ve iyi bir genel bakış istiyorum. Bu konuyla ilgili bilgim var ancak bilgilerinizi tazelemek istiyorum. Projemde kullanmak için örnek kod arıyorum. Belirli bir konuyla ilgili açıklama arıyorum.

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

Bu laboratuvarı tamamlamak için iki yazılıma ihtiyacınız vardır: Flutter SDK'sı ve düzenleyici.

Codelab'i aşağıdaki cihazlardan 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üklenmesini gerektirir).
  • Android Emülatör (Android Studio'da kurulum gerektirir).
  • Tarayıcı (hata ayıklama için Chrome gereklidir).
  • Windows, Linux veya macOS masaüstü uygulaması olarak Uygulamayı dağıtmayı planladığınız platformda gerçekleştirmeniz gerekir. Bu nedenle, bir Windows masaüstü uygulaması geliştirmek istiyorsanız uygun derleme zincirine erişmek için Windows'da geliştirme yapmanız gerekir. İşletim sistemine özgü gereksinimler docs.flutter.dev/desktop sayfasında ayrıntılı olarak açıklanmıştır.

3. codelab başlangıç uygulamasını indirme

1. seçenek: GitHub'dan başlangıç codelab uygulamasını klonlama

Bu codelab'i GitHub'dan klonlamak için şu 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ının posta dosyasını indirin

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

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

Proje, animasyon paketine bağlıdır. pubspec.yaml dokümanındaki dependencies bölümünde aşağıdakilerin bulunduğuna dikkat edin:

animations: ^2.0.0

Projeyi açın ve uygulamayı çalıştırın

  1. Projeyi istediğiniz düzenleyicide açın.
  2. "Uygulamayı çalıştırma" talimatlarını izleyin Başlarken: Seçtiğiniz düzenleyici için uygulamayı test edin.

Başarıyla gerçekleştirildi. Yanıt'ın ana sayfası için başlangıç kodu, cihazınızda/emülatörünüzde çalışmalıdır. E-posta listesinin yer aldığı Gelen Kutusu gösterilir.

Yanıtla ana sayfası

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

Bu codelab'de hızlı ve gösterişli geçişler yer aldığından, uyguladığınız geçişlerin daha ince ayrıntılarını gözlemlemek için cihazın animasyonlarını yavaşlatmanız yararlı olabilir. Bunu uygulama içi bir ayar kullanarak yapabilirsiniz. Alt çekmece açıkken ayarlar simgesine dokunarak erişebilirsiniz. Endişelenmeyin, cihaz animasyonlarını yavaşlatmaya yönelik bu yöntem, Yanıtla uygulamasının dışında cihazdaki animasyonları etkilemez.

d23a7bfacffac509.gif

İsteğe bağlı: Koyu Mod

Yanıt özelliğinin parlak teması gözlerinizi acıyorsa başka yere yardım etmeyin. Uygulama içinde bulunan bir ayar sayesinde uygulama temasını koyu moda çevirerek gözlerinize daha uygun hale getirebilirsiniz. Bu ayara, alt çekmece açıkken ayarlar simgesine dokunarak erişebilirsiniz.

87618d8418eee19e.gif

4. Örnek uygulama kodu hakkında bilgi edinin

Koda bakalım. Uygulamadaki farklı ekranlar arasında geçiş yapmak için animasyon paketini kullanan bir uygulama sağladık.

  • HomePage: Seçilen posta kutusunu görüntüler
  • InboxPage: E-postaların listesini gösterir
  • MailPreviewCard: Bir e-postanın önizlemesini gösterir
  • MailViewPage: Tek bir tam e-posta gösterir
  • ComposePage: Yeni bir e-postanın oluşturulmasına olanak tanır
  • SearchPage: Bir arama görünümünü görüntüler

router.dart

İlk olarak, uygulamanın kök gezinme işlevinin 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 aracımızdır ve uygulamamızın tuvalin tamamını kaplayan HomePage ve SearchPage gibi ekranlarını yönetir. Araç, uygulamamızın durumunu dinleyerek ReplySearchPath rotasını belirleyip belirlemediğimizi kontrol eder. Varsa, yığınımızın en üstündeki SearchPage ile gezginimizi yeniden oluşturur. Ekranlarımızın, hiçbir geçiş tanımlanmadan bir CustomTransitionPage içine yerleştirildiğine dikkat edin. Bu, herhangi bir özel geçiş olmadan ekranlar arasında gezinebileceğiniz bir yol gösterir.

home.dart

home.dart içinde _BottomAppBarActionItems içinde şunu yaparak rotamızı uygulamamızın durumunda ReplySearchPath olarak ayarladık:

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 değerine erişiyor ve routePath değerini ReplySearchPath olarak ayarlıyoruz. RouterProvider uygulamamız, kök gezginlerin durumunu izler.

mail_view_router.dart

Şimdi, uygulamamızın iç gezinme menüsünün nasıl ayarlandığına bakalım. lib dizininde mail_view_router.dart uygulamasını açın. Yukarıdakine benzer bir kılavuz 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 bizim içimizdeki gezgin. Uygulamamızın, InboxPage gibi yalnızca kanvasın gövdesini kaplayan iç ekranlarını işler. InboxPage, uygulamamızın durumunda bulunan posta kutusunun ne olduğuna bağlı olarak bir e-posta listesi görüntüler. Uygulamamızın durumunun currentlySelectedInbox özelliğinde bir değişiklik olduğunda, gezgin, yığının üst kısmında doğru InboxPage olacak şekilde yeniden oluşturulur.

home.dart

home.dart içinde _HomePageState içinde aşağıdakileri yaparak mevcut posta kutumuzu uygulamamızın durumuna göre ayarladık:

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 fonksiyonumuzda, EmailStore işlevimize erişiyor ve currentlySelectedInbox değerini seçilen hedefe ayarlıyoruz. EmailStore aracımız, içindeki gezginlerin durumunu takip eder.

home.dart

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

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 görsel, herhangi bir özel geçiş olmadan e-posta oluşturma sayfasına nasıl gidebileceğinizi gösterir. Bu codelab'de, uygulama genelinde çeşitli gezinme işlemleriyle birlikte çalışan Materyal geçişleri ayarlamak için Yanıt'ın kodunu ayrıntılı olarak inceleyeceksiniz.

Artık başlangıç kodunu öğrendiğinize göre 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 ekleyin

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

Herhangi bir kod eklemeden önce, Yanıtla uygulamasını çalıştırmayı ve bir e-postayı tıklamayı deneyin. Sıçramalı kesme sayesinde ekran, geçiş olmadan değiştirilir:

Önce

48b00600f73c7778.gif

Aşağıdaki snippet'te gösterildiği gibi mail_card_preview.dart üst kısmındaki animasyon paketi için içe aktarma işlemi ekleyerek başlayın:

mail_card_preview.dart

import 'package:animations/animations.dart';

Artık animasyon paketinizi içe aktardığınıza göre uygulamanıza güzel geçişler eklemeye başlayabiliriz. OpenContainer widget'ımızı barındıracak bir StatelessWidget sınıfı oluşturarak başlayalım.

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

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 öğesimizin bir InkWell widget'ı vardır ve OpenContainer öğesinin renk özellikleri, içinde bulunduğu kapsayıcının rengini tanımlar. 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, tam olarak çalışan bir container dönüşümünüz olmalıdır. Bir e-postayı tıkladığınızda, e-posta listesi çıkartılırken liste öğesi bir ayrıntılar ekranına genişletilir. Geri tuşuna bastığınızda e-posta ayrıntıları ekranı tekrar bir liste öğesi haline getirilirken e-posta listesinde yukarı ölçeklenir.

Sonra

663e8594319bdee3.gif

6. E-posta oluşturma sayfasına, FAB'dan Kapsayıcı Dönüşümü geçişi ekleyin

Şimdi, container dönüşümüne devam edelim ve Kayan İşlem Düğmesi'nden ComposePage öğesine bir geçiş ekleyerek FAB'yi kullanıcı tarafından yazılacak yeni bir e-posta adresine genişletelim. Öncelikle uygulamayı yeniden çalıştırın ve e-posta oluşturma ekranını başlatırken herhangi bir geçiş olmadığını görmek için FAB'yi tıklayın.

Önce

4aa2befdc5170c60.gif

Aynı widget sınıfını (OpenContainer) kullandığımızdan, bu geçişi yapılandırma yöntemimiz son adımda yaptığımız yönteme çok benzeyecektir.

home.dart ürününde, dosyanın en üstündeki package:animations/animations.dart öğesini içe aktaralım ve _ReplyFabState build() yöntemini değiştirelim. Döndürülen Material widget'ını bir 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, onClosed de ayarlanıyor. onClosed, OpenContainer rotası açıldığında veya kapalı duruma döndüğünde çağrılan bir ClosedCallback. Söz konusu işlemin döndürülen değeri bu işleve bağımsız değişken olarak iletilir. Bu Callback numarasını, uygulama sağlayıcımızı ComposePage rotasından ayrıldığımızı bildirmek için kullanırız. Bu sayede tüm dinleyiciler bilgilendirilir.

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

Elde edilen kod aşağıdaki gibi olur:

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 biraz eski kodları temizleyin. OpenContainer widget'ımız artık onClosed ClosedCallback aracılığıyla uygulama sağlayıcımızı ComposePage uygulamasında olmadığımızı bildirme işlemini gerçekleştirdiğinden, mail_view_router.dart üzerindeki eski 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ın sonuna geldik! FAB'dan oluşturma ekranına geçişiniz aşağıdaki gibi görünür:

Sonra

5c7ad1b4b40f9f0c.gif

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

Bu adımda, arama simgesinden tam ekran arama görünümüne bir geçiş ekleyeceğiz. Bu gezinme değişikliğine dahil olan kalıcı bir kapsayıcı olmadığından, iki ekran arasındaki uzamsal ilişkiyi güçlendirmek ve uygulamanın hiyerarşisinde bir seviye yukarı hareket ettiğini göstermek 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. Bunu yaptığınızda, geçiş olmadan arama görünümü ekranı görüntülenir.

Ö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 gerçekleştirmek için yeni SharedAxisTransitionPageWrapper aracımızı kullanalım. ReplyRouterDelegate sınıf tanımımızda, pages özelliğinin 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 gitmeye başlıyor. Alt uygulama çubuğundaki arama simgesini tıkladığınızda, paylaşılan bir eksen geçişi arama sayfasını ölçeklendirir. Ancak arama sayfası genişledikçe ana sayfanın nasıl ölçeklendirilmediğine ve sabit kaldığına dikkat edin. Ayrıca, geri düğmesine basıldığında ana sayfa görünüm olarak ölçeklenmez. Bunun yerine, arama sayfası görünümden çıktığında statik kalır. 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 sayfa ve arama görünümü ekranları aynı anda Z ekseninde ayrıntılı olarak kararma ve ölçeklenmeli, böylece iki ekran arasında kesintisiz bir efekt sağlanmalıdır.

Sonra

462d890086a3d18a.gif

8. Posta kutusu sayfaları arasında Şeffaflaştırma geçişi ekleme

Bu adımda, farklı posta kutuları arasında bir geçiş ekleyeceğiz. Uzamsal veya hiyerarşik bir ilişkiyi vurgulamak istemediğimizden, basit bir "değiştirme" işlemi yapmak için e-posta listeleri arasında geçiş yapın.

Başka kod eklemeden önce uygulamayı çalıştırmayı, Alt Uygulama Çubuğu'ndaki Yanıtla logosuna dokunmayı ve posta kutularını değiştirmeyi deneyin. E-posta listesi geçiş işlemi olmadan değişir.

Ö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 aracımızı kullanalım. MailViewRouterDelegate sınıf tanımımızda, pages özelliği altında, posta kutusu ekranımızı CustomTransitionPage ile sarmalamak yerine FadeThroughTransitionPageWrapper kodunu 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. Alttaki gezinme çekmecesini açıp posta kutularını değiştirdiğinizde, mevcut e-posta listesi kaybolarak genişler ve yeni liste genişler ve genişler. Güzel!

Sonra

8186940082b630d.gif

9. Oluşturma ve yanıtlama FAB'si arasında Şeffaflaştırma geçişi ekle

Bu adımda farklı FAB simgeleri arasında bir geçiş ekleyeceğiz. Uzamsal veya hiyerarşik bir ilişkiyi vurgulamak istemediğimizden, basit bir "değiştirme" işlemi yapmak için simgelerinin arasına yerleştirin.

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

Önce

d8e3afa0447cfc20.gif

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

Sonraki birkaç geçişin tümü yeniden kullanılabilir bir sınıftan (_FadeThroughTransitionSwitcher) yararlanacağı için yapılandırma biçimimiz çok benzer olacaktır.

home.dart için _ReplyFabState altına şu 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,
   );
 }
}

Şimdi, _ReplyFabState içinde fabSwitcher widget'ını arayın. fabSwitcher, e-posta görünümünde olup olmadığına bağlı olarak farklı bir simge döndürür. _FadeThroughTransitionSwitcher ile toparlayalım:

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 için şeffaf bir fillColor sunuyoruz. Böylece geçiş sırasında öğeler arasında arka plan olmayacak. Ayrıca, bir UniqueKey oluşturup simgelerden birine atarız.

Şimdi bu adımda, tamamen animasyonlu bir bağlama dayalı FAB'nizin olması gerekir. Bir e-posta görünümüne geçtiğinizde, yeni simge belirip genişledikçe eski FAB simgesi kaybolup genişleyecektir.

Sonra

c55bacd9a144ec69.gif

10. Kaybolan posta kutusu başlığı arasındaki Şeffaflaştırma geçişi ekleme

Bu adımda, e-posta görünümündeyken posta kutusu başlığının görünür ve görünmez durumlar arasında geçiş yapması için bir geçiş geçişi ekleyeceğiz. Uzamsal veya hiyerarşik bir ilişkiyi vurgulamak istemediğimizden, basit bir "değiştirme" işlemi yapmak için posta kutusu başlığını kapsayan Text widget'ı ile boş bir SizedBox arasında.

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

Önce

59eb57a6c71725c0.gif

Son adımımızda _FadeThroughTransitionSwitcher özelliğindeki işin çoğunu zaten gerçekleştirdiğimiz için bu codelab'in geri kalanı hızlı olacak.

Şimdi, geçişimizi eklemek için home.dart uygulamasındaki _AnimatedBottomAppBar dersimize gidelim. Son adımımızdan itibaren _FadeThroughTransitionSwitcher yeniden kullanılacak ve boş bir SizedBox döndüren onMailView koşulumuzu sarmalayacağız veya alt çekmeceyle senkronize olarak kaybolan bir posta kutusu başlığı döndüreceğiz:

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,
                   ),
             );
           },
         ),
       ),
),

Hepsi bu kadar, bu adımı da 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ığı soluklanır ve genişler. Mükemmel!

Sonra

3f1a3db01a481124.gif

11. Alt uygulama çubuğu işlemleri arasında Şeffaflaştırma geçişi ekle

Bu adımda, uygulama bağlamına göre alt uygulama çubuğu işlemlerinde şeffaf bir geçiş için şeffaf geçiş ekleyeceğiz. Uzamsal veya hiyerarşik bir ilişkiyi vurgulamak istemediğimizden, basit bir "değiştirme" işlemi yapmak için ve e-posta görünümündeyken alt uygulama çubuğu işlemleri arasında görünür.

Başka kod eklemeden önce bir e-postaya dokunup e-posta görünümünü açarak uygulamayı çalıştırmayı 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 aracımızı tekrar kullanacağız. İstenen geçişi elde etmek için _BottomAppBarActionItems sınıf tanımımıza gidin ve build() işlevimizin dönüş widget'ını bir _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
...

Haydi deneyelim! Bir e-postayı açıp e-posta görünümüne yönlendirildiğinizde eski alt uygulama çubuğu işlemleri, yeni işlemler yavaş yavaş çoğalarak artarken kaybolmalıdır. Tebrikler!

Sonra

cff0fa2afa1c5a7f.gif

12. Tebrikler!

100 satırdan daha az Dart kodu kullanan animasyonlar paketi, mevcut bir uygulamada Materyal Tasarım yönergelerine uyan ve tüm cihazlarda tutarlı bir görünüm ve davranışa sahip harika geçişler oluşturmanıza yardımcı oldu.

d5637de49eb64d8a.gif

Sonraki adımlar

Materyal hareket sistemi hakkında daha fazla bilgi edinmek için yönergeleri ve geliştirici dokümanlarının tamamını incelediğinizden emin olun ve uygulamanıza bazı Materyal geçişler eklemeyi deneyin!

Material motion'u denediğiniz için teşekkürler. Bu codelab'den memnun kaldığınızı umuyoruz.

Bu codelab'i makul bir zaman ve çabayla tamamlayabildim

Kesinlikle katılıyorum Katılıyorum Ne memnunum ne değilim Katılmıyorum Kesinlikle katılmıyorum

Gelecekte Materyal hareket sistemini kullanmaya devam etmek istiyorum

Kesinlikle katılıyorum Katılıyorum Ne memnunum ne değilim Katılmıyorum Kesinlikle katılmıyorum

Material Flutter kitaplığı ve Flutter çerçevesi tarafından sağlanan widget'ları nasıl kullanacağınızla ilgili daha fazla demo için Flutter Gallery'yi ziyaret etmeyi unutmayın.

46ba920f17198998.png

6ae8ae284bf4f9fa.png