Aşamalı bir animasyon sıralı veya örtüşen animasyonlardan oluşur. Animasyon, bir değişikliğin diğerinden sonra meydana geldiği tamamen sıralı olabilir veya kısmen veya tamamen örtüşebilir. Animasyon sıralı olduğunda öğeler, her öğenin başlangıç zamanları arasında hafif bir gecikmeyle sırayla canlandırılır. Bu, animasyonun öğeler arasında bir kerede değil, aşamalar halinde hareket ediyor gibi göründüğü basamaklı veya dalgalanma efekti yaratır.
Kademeli animasyonlar, bir tür mikro etkileşim olarak kabul edilir çünkü kullanıcıya bir arayüz aracılığıyla rehberlik eden incelikli, etkileşimli geri bildirim sağlayarak kullanıcı deneyimini geliştirir. Flutter'da, örtülü veya açık animasyonu kullanarak incelikli animasyonlar hazırlayarak mikro etkileşimler oluşturabilirsiniz.
Bağlam açısından, örtülü animasyonlar basit ve kullanımı kolay olacak şekilde tasarlanmıştır çünkü animasyon ayrıntıları soyutlanmıştır, açık animasyonlar ise karmaşık animasyonlar için daha uygundur çünkü animasyon süreci üzerinde tam kontrol sağlarlar. Bu, özellikle animasyon üzerinde daha ayrıntılı kontrole sahip olmanız gerektiğinde kullanılır.
Bu makalede, mikro etkileşimler kavramını derinlemesine inceleyeceğiz, ardından bir mikro etkileşim kullanım örneği için, bir sütun widget'ında bulunan çocukları canlandıran kademeli bir animasyon oluşturmak için açık animasyon kullanacağız.
Mükemmel ürünler, hem özellikleri hem de ayrıntıları iyi bir şekilde sunan ürünlerdir. Özellikler insanları ürünlerinize getirir, ancak ayrıntılar onları korur. Bu ayrıntılar uygulamanızı diğerlerinden farklı kılan şeylerdir. Mikro etkileşimler ile kullanıcılarınıza keyifli geri bildirimler sağlayarak bu detayları oluşturabilirsiniz.
Mikro etkileşim kavramı, küçük ayrıntıların genel kullanıcı deneyimi üzerinde büyük bir etkiye sahip olabileceği fikrine dayanmaktadır. Mikro etkileşimler, geri bildirimin veya bir eylemin sonucunun iletilmesi gibi temel işlevlere hizmet etmek için kullanılabilir. Mikro etkileşim örnekleri şunları içerir:
Aşağıda, mikro etkileşimlerin gerçek hayattaki uygulamalarını görebiliriz; bu ince animasyonlar, kullanıcı deneyimini geliştirmek için Flutter'da oluşturulmuştur. 👇🏽
Bu uygulamaların tasarım referansları Dribbble'dan alınmıştır.
Bu örnekte, sayfa kaydırıldığında bir sütun widget'ındaki alt öğeleri canlandıran kademeli bir animasyon oluşturacağız. Bu, açık animasyon yaklaşımı kullanılarak oluşturulacaktır çünkü bu durumda, animasyonun nasıl çalışmasını istediğimiz konusunda tam kontrole sahip olmamız gerekecektir.
İşte tamamladığımızda animasyon nasıl görünecek 👇🏽
Bu eğitimden en iyi şekilde yararlanmak için aşağıdakilere sahip olmalısınız:
Projenin tüm önkoşullarını kontrol ettikten sonra hemen konuya girelim.
Öncelikle bu iki sayfayı içeren ana ekranı oluşturacağız. Bu iki sayfa, kaydırma sırasında hangi sayfanın görüntüleneceğini kontrol eden bir sayfa görüntüleme widget'ına sarılacaktır. Ayrıca ana ekranın alt kısmında bize o anda bulunduğumuz sayfayı gösteren bir göstergemiz var.
class MainScreen extends StatefulWidget { const MainScreen({super.key}); @override State<MainScreen> createState() => _MainScreenState(); } class _MainScreenState extends State<MainScreen>{ final controller = PageController(keepPage: true); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: PageView( controller: controller, children: [ Page1(pageController: controller,), Page2(pageController: controller,), ], ), ), bottomNavigationBar: Container( alignment: Alignment.topCenter, padding: const EdgeInsets.only(top: 10), height: 30, child: SmoothPageIndicator( controller: controller, count: 2, effect: const JumpingDotEffect( dotHeight: 10, dotWidth: 10, activeDotColor: Colors.grey, dotColor: Colors.black12, ), ), ) ); } }
Yukarıdaki kod parçacığında kullanılan SmoothPageIndicator
pub.dev adresinde bulabilirsiniz. Burada, SmoothPageIndicator
geçerli sayfayı görünümde göstermek için kullanılır. Paketi pubspec.yaml
bu şekilde ekleyebilirsiniz 👇🏽
dependencies: flutter: sdk: flutter smooth_page_indicator: ^1.1.0
İki sayfada, birkaç boş kart widget'ı içeren bir sütun widget'ımız olacak. Bu boş kart widget'ları, kademeli animasyonu tam olarak görebilmemiz ve takdir edebilmemiz için sütunu doldurmak için kullanılacaktır. Boş kart widget'ını yeniden kullanılabilir bir bileşen olarak oluşturacağız, böylece onu kullandığımız her yerde sıfırdan oluşturmak zorunda kalmayacağız.
class EmptyCard extends StatelessWidget { const EmptyCard({super.key, required this.width, required this.height}); final double width; final double height; @override Widget build(BuildContext context) { return Container( height: height, width: width, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(10)), color: Colors.blue.shade200, boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset(0,4), ) ] ), ); } }
Tüm bu ortak kodlar ortadan kalktıktan sonra artık animasyonu oluşturmaya başlayacağız. Öncelikle AnimateWidget
adını vereceğimiz yeni bir durum bilgisi olan widget oluşturacağız. Animasyonun nasıl çalışacağını kontrol etmek için çeşitli parametrelere sahip olacaktır.
class AnimateWidget extends StatefulWidget { const AnimateWidget({super.key, required this.duration, required this.position, required this.horizontalOffset, required this.child, required this.controller, }); final Duration duration; final int position; final double? horizontalOffset; final Widget child; final PageController controller; @override State<AnimateWidget> createState() => _AnimateWidgetState(); }
Yukarıdaki kod parçasında görüldüğü gibi AnimateWidget
parametreleriyle başarılı bir şekilde kontrol edebiliriz:
pageController
, sayfa kaydırıldığında animasyonu tetiklemesine neden olur.
Daha sonra AnimateWiget
aşağıdakileri tanımlayacağız:
AnimationController
: Animasyon sırasını kontrol etmek için kullanılır.
Geçerli sayfa: geçerli sayfa verilerini tutmak için kullanılacaktır.
Zamanlayıcı: Animasyonu bir miktar gecikmeden sonra tetiklemek için kullanılacaktır; Kademeli animasyon basamaklı efektini ortaya çıkaracak olan şey budur.
class _AnimateWidgetState extends State<AnimateWidget> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin{ @override bool get wantKeepAlive => true; late AnimationController animationController; int currentPage = 0; Timer? _timer; @override void initState() { super.initState(); animationController = AnimationController(vsync: this, duration: widget.duration); _timer = Timer(getDelay(), animationController.forward); widget.controller.addListener(() { currentPage = widget.controller.page!.round(); if(currentPage == widget.controller.page){ _timer = Timer(getDelay(), animationController.forward); } }); } @override void dispose() { _timer?.cancel(); animationController.dispose(); super.dispose(); } Duration getDelay(){ var delayInMilliSec = widget.duration.inMilliseconds ~/ 6; int getStaggeredListDuration(){ return widget.position * delayInMilliSec; } return Duration(milliseconds: getStaggeredListDuration()); }
Yukarıdaki kod parçacığını incelediğinizde şunu fark edeceksiniz:
AnimationController
kullandık, bu nedenle SingleTickerProviderStateMixin
tanıttık.
init state
yönteminde, AnimationController
başlattık, ardından animationController.forward
kullanarak, sayfa girişinde ve sayfa kaydırmayı kullanarak animasyonu tetikledik.
dispose
yönteminde kaynakları temizledik.
AutomaticKeepAliveClientMixin
kullandık. Bunun amacı, bir sayfa kaydırıldığında ve artık görünür olmadığında AnimationController
öğesinin atılmasını önlemektir.
getDelay
fonksiyonunda her bir element için animasyonun tetiklenmesinden önceki gecikmeyi hesapladık. Bunu başarmak için milisaniye cinsinden süreyi 6'ya bölüp sonuçları yaklaşıklaştırdık ve ardından konumla çarptık. Bu, öğenin konumudur (bu durumda dizin).
Build yönteminde bir AnimatedBuilder
döndürüyoruz. Bu animasyonlu oluşturucuda, Transform.translate
widget'ını döndüren _slideAnimation
adında bir widget işlevi döndüreceğiz. _slideAnimation
fonksiyonunda offsetAnimation
fonksiyonumuz var.
offsetAnimation
işlevi, Transform.translate
widget'ında kullanılan Animation özelliğini döndürür. Transform.translate
widget'ı, animasyondaki değeri kullanarak alt widget'ı canlandırır.
@override Widget build(BuildContext context) { super.build(context); return AnimatedBuilder( animation: animationController, builder: (context, child){ return _slideAnimation(animationController); } ); } Widget _slideAnimation(Animation<double> animationController){ Animation<double> offsetAnimation(double offset, Animation<double> animationController) { return Tween<double>(begin: offset, end: 0.0).animate( CurvedAnimation( parent: animationController, curve: const Interval(0.0, 1.0, curve: Curves.ease), ), ); } return Transform.translate( offset: Offset( widget.horizontalOffset == 0.0 ? 0.0 : offsetAnimation(widget.horizontalOffset!, animationController).value, 0.0, ), child: widget.child, ); }
Bu, AnimateWidget
Sınıfının tam kodudur 👇🏽
class AnimateWidget extends StatefulWidget { const AnimateWidget({super.key, required this.duration, required this.position, required this.horizontalOffset, required this.child, required this.controller, }); final Duration duration; final int position; final double? horizontalOffset; final Widget child; final PageController controller; @override State<AnimateWidget> createState() => _AnimateWidgetState(); } class _AnimateWidgetState extends State<AnimateWidget> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin{ @override bool get wantKeepAlive => true; late AnimationController animationController; int currentPage = 0; Timer? _timer; @override void initState() { super.initState(); animationController = AnimationController(vsync: this, duration: widget.duration); _timer = Timer(getDelay(), animationController.forward); widget.controller.addListener(() { currentPage = widget.controller.page!.round(); if(currentPage == widget.controller.page){ _timer = Timer(getDelay(), animationController.forward); } }); } @override void dispose() { _timer?.cancel(); animationController.dispose(); super.dispose(); } Duration getDelay(){ var delayInMilliSec = widget.duration.inMilliseconds ~/ 6; int getStaggeredListDuration(){ return widget.position * delayInMilliSec; } return Duration(milliseconds: getStaggeredListDuration()); } @override Widget build(BuildContext context) { super.build(context); return AnimatedBuilder( animation: animationController, builder: (context, child){ return _slideAnimation(animationController); } ); } Widget _slideAnimation(Animation<double> animationController){ Animation<double> offsetAnimation(double offset, Animation<double> animationController) { return Tween<double>(begin: offset, end: 0.0).animate( CurvedAnimation( parent: animationController, curve: const Interval(0.0, 1.0, curve: Curves.ease), ), ); } return Transform.translate( offset: Offset( widget.horizontalOffset == 0.0 ? 0.0 : offsetAnimation(widget.horizontalOffset!, animationController).value, 0.0, ), child: widget.child, ); } }
Daha sonra, bu AnimateWidget
sınıfını bir sütun widget'ında kullanmak için, bir liste döndüren toStaggeredList
adında statik bir yönteme sahip bir sınıf oluşturacağız. Bu yöntemde, bir children
listesi de dahil olmak üzere gerekli tüm parametreleri iletiyoruz. children
parametresi, canlandıracağımız öğelerin listesini ileteceğimiz yerdir.
Daha sonra, her çocuğu AnimateWidget
ile sararak çocukları haritalandıracağız.
class AnimateList{ static List<Widget>toStaggeredList({ required Duration duration, double? horizontalOffset, required PageController controller, required List<Widget>children, })=> children .asMap() .map((index, widget){ return MapEntry( index, AnimateWidget( duration: duration, position: index, horizontalOffset: horizontalOffset, controller: controller, child: widget, ) ); }) .values .toList(); }
AnimateWidget
, listedeki her alt öğeyi başarılı bir şekilde canlandırmak için gerekli parametreleri iletiyoruz. AnimateList.toStaggeredList
yöntemini kullanarak artık üzerinde çalıştığımız iki sayfaya uygulayabiliriz.
class Page1 extends StatelessWidget { const Page1({super.key, required this.pageController}); final PageController pageController; @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: AnimateList.toStaggeredList( duration: const Duration(milliseconds: 375), controller: pageController, horizontalOffset: MediaQuery.of(context).size.width / 2, children: [ const EmptyCard(width: 250, height: 50,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 180, height: 80,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 270, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), ], ), ), const EmptyCard(width: 250, height: 50,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 180, height: 80,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 270, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), ], ), ), ], ), ), ); } } class Page2 extends StatelessWidget { const Page2({super.key, required this.pageController}); final PageController pageController; @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: AnimateList.toStaggeredList( duration: const Duration(milliseconds: 375), controller: pageController, horizontalOffset: MediaQuery.of(context).size.width / 2, children: [ const EmptyCard(width: 220, height: 70,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 300, height: 70,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 200, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 70, width: 70), EmptyCard(height: 70, width: 70), ], ), ), const EmptyCard(width: 220, height: 70,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 300, height: 70,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 70, width: 70), EmptyCard(height: 70, width: 70), ], ), ), ], ), ), ); } }
Sütun widget'ının alt öğelerinde AnimateList.toStaggeredList
ileteceğiz ve sütunda görüntülenecek widget'lar da dahil olmak üzere gerekli parametreleri ileteceğiz. Bununla, kaydırmayla tetiklenen kademeli bir animasyonu başarıyla oluşturduk. Kodun tamamını buradan kontrol edebilirsiniz.
Bu bizim nihai sonucumuz:
Bu eğitimimizin sonuna geldik. Bu noktada mikro etkileşim kavramına ve kullanıcı deneyimine etkisine değindik. Ayrıca, sayfa kaydırıldığında tetiklenen bir sütun widget'ındaki öğelerin aşamalı animasyonunu oluşturma sürecinden de geçtik.
Projenizin kullanıcı deneyimini geliştirmek için uygulamanıza ekleyebileceğiniz pek çok türde mikro etkileşim vardır; Flutter'da daha etkileşimli animasyonlar oluşturarak denemeler yapabilirsiniz.
Bu makaleyi faydalı bulduysanız beğenerek veya yorum yaparak destek olabilirsiniz. Daha fazla ilgili yazılarım için beni takip edebilirsiniz.