Seray Uzgur
19.12.2018

Flutter: Animasyonlara Naif Bir Giriş

Yıllardan 2007, Nokia’nın çok güzel modeller çıkardığı telefon üretiminde sektöre yön verdiği yıllar. O zamana kadar 2 tane dokunmatik telefon kullanmıştım (kalemle). Haziran ayında Apple yeni ürününü tanıttı, iPhone. Uzun bir süre hem performansı hem de yaşattığı deneyim ile açık ara önde olan bir telefon oldu. Sırf akıyor diye rehberinde bir aşağı bir yukarı kaydırma yapıp yağ gibi animasyonları, liste sonuna gelindiğinde verilen yay gibi tepkileri izledik. Ana ekran üzerinde bir sağa bir sola geçerken hiç takılma olmamasına, üzerine tıklanan simgelerin animasyon ile büyüyüp tam ekran olmasına hayran kaldık. Aynı deneyimi verebilen çoğu uygulama kullanıcıların eline yapıştı, alışkanlığa dönüştü. Güzel görsel tasarımları ve doğal bir hissiyat yaratması ile birlikte sadece iş adamlarının cebinde olan kişisel asistanlar, gücü yeten herkesin elinde, cebinde yerini almaya başladı. Yıl 2018, kullandığımız cihazlar inanılmaz gelişti ve yazılımları birçok yetenek kazandı. Firmalar UX ekipleri ile birlikte kullanıcıları kendilerine bağlayabilmek için birçok araştırma yaptı, yenilikler getirdi. Peki bu makalede ne yapıyoruz, kendi uygulamalarımıza benzer bir hissiyat verebilmek için uğraşıyoruz. Flutter ile basit bir uygulama geliştireceğiz ve içerisinde güzel birkaç animasyon ve ekran geçişi olacak.

İsterseniz kodların son haline buradan ulaşabilirsiniz.

Bir önceki makalemde Flutter’a bir giriş yapmıştık. Elimden geldiğince yaşadıklarımı ve edindiğim fikirleri aktarmaya çalışmıştım. Şimdi ise iki ekrandan oluşan bir örnekle, geçişleri, kod yazımı ve animasyonlar hakkında bahsedeceğim.

Hazırlık

İlk olarak projeyi yaratalım, sonrasında yavaş yavaş ekranları oluşturacağız. SDK yükleme işi için başlangıç dokümanlarını takip edebilirsiniz. Aşağıdaki komutu kullanarak dilediğimiz klasörün içerisine Flutter’ın bir uygulama iskeleti oluşturmasını isteyeceğiz.

flutter create sampleanim

Her şey başarılı ilerlemişse yukarıdaki gibi bir terminal çıktısına ulaşmanız gerekli.

Bundan sonrasında tercih edeceğimiz bir editör ile projeyi açıp çalıştırabiliriz. Hem eklentilerinden hem de alışkanlıklarımdan dolayı ben VSCode ile devam ediyorum.

Uygulamanın çalışabilmesi için ilk önce bir cihaz ile bağlanabilmesi gerekli. Flutter bize üç opsiyon tanıyor, bu bir telefon, simulator ya da emulator olabilir. Ben iOS simulatörü ile devam edeceğim ama bahsederken cihaz olarak bahsedeceğim, siz diğer alternatifleri uygulayabilirsiniz.

Aşağıdaki komutlar ile ilk önce iOS simülatörünü açıp sonrasında Flutter’ın çalışmasını sağlayabilirsiniz.

open -a Simulator.app 
flutter run

Demo uygulamasının başarılı bir şekilde başlatıldığını umuyoruz :) Her şey sırası ile yapılırsa ve bir aksilik yaşanmaz ise bağlandığımız cihazın ekranında örnek uygulamanın açıldığını görebilirsiniz.

Gözünüz terminalde olsun , hot reload için sizden bir girdi yapmanızı isteyecektir.

Başlangıç

Şimdi yapacağımız şey uygulamayı kendi ihtiyaçlarımıza göre değiştirmek olacak. Sizinle aklımdakini paylaşmak istiyorum. Yazılıma başlamadan önce mock çizmek vizyon oturtmak için yararlı oluyor. Genelde bunun için Balsamiq gibi wireframe araçları kullanırdım ama son zamanlarda değişen ihtiyaçlar ve çıkan ürünler beni ücretsiz bir Adobe ürününe Adobe XD’ye yönlendirdi. Bu programı kullanarak çizdiğim ekranlar aşağıdaki gibi.

Göründüğü üzerine beklentim ara geçişleri ve animasyonları ile birkaç ekran hazırlamak. Tekrar hatırlatayım bu yazıda sadece ilk 2 ekran hakkında yazacağım.

İlk olarak ekran arkaplanları, fontlar gibi dosyaları saklamak için assets diye bir klasör oluşturuyorum. İçerisine images klasörü oluşturup onu içinde kullanacağım görselleri saklıyorum.

Sonrasında bu dosyaların yollarını pubspec.yaml içerisinde belirtmem gerekiyor ki Flutter tarafından kullanılabilir olsun.

Artık bu imajları kullanabilirim. Şimdi ilk yapacağım şey bir açılış ekranı yapmak olacak. Bunun için alışık olduğum bir klasör isimlendirmesi kullanacağım. Bütün ekranlarım lib/pages/xxx.dart şeklinde konumlanacak. Böylece ihtiyacım olduğunda ekranların nerede olduğunu aramak yerine direkt müdehale edebilirim.

Splash

Uygulama açıldığında kullanıcının ilk olarak karşılaşacağı ekran. Genelde;

v.b. kontrolleri yaptığım basit bir görsel ve bir sürü kontrol oluşturacak ilk ekran. Bu örnek içerisinde çok fazla karmaşıklık yaratmamak adına Splash sadece bir zamanlayıcı içerecek ve 3 sn bekledikten sonra kullanıcıyı sıradaki ekrana yönlendirecek.

Sayaç olacağı için yazacağım ekran StatefulWidget olacak. Detayları için buradan dokümana ulaşabilirsiniz. Dosyayı lib/pages/splash.dart yoluna yaratıyorum ve içerisine background alabilecek bir container koyuyorum.

import 'package:flutter/material.dart';

class Splash extends StatefulWidget {
Splash({Key key}) : super(key: key);
@override
_State createState() => new _State();
}

class _State extends State<Splash> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage("assets/images/splash_bg.png"),
fit: BoxFit.cover,
)),
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
),
),
));
}
dispose() {
super.dispose();
}
}

Böylelikle Splash ekranı şu an için bitti, geriye sadece main.dart içerisinden Splash'ı açmak kalıyor.

import 'package:flutter/material.dart';
import 'pages/splash.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: new Splash(),
);
}
}

Gördüğünüz gibi arkaplanı olan bir ekran kodladık. Şimdi bir ekran daha oluşturup Splash’ın 3 sn sonra geçiş yapmasını sağlayan kodları ekleyeceğim.

Info

İçi boş ekranımı lib/pages/info.dart yolu altında yaratacağım. Sonra içini dolduracağız ama şimdilik sadece Splash içerisindeki geçiş için kullanacağım, aşağıdaki gibi "Hello World" yazsa yeterli.

import 'package:flutter/material.dart';
class Info extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(color: Colors.white),
child: Center(
child: Text('Hello World',
textDirection: TextDirection.ltr,
style: TextStyle(fontSize: 40.0, color: Colors.black87)),
),
);
}
}

Ekran Geçişi

İki ekran arasındaki geçişi yaparken bir animasyon kullanmak istiyorum. Bunun yapılabilmesi için Flutter route sınıflarını kullanıyor ve şimdi animasyonlu bir route tanımlayacağım. Yaratacağım widget'i lib/routes/fadeInFadeOutRoute.dart olarak kaydedeceğim ve içeriği aşağıdaki gibi olacak.

import 'package:flutter/material.dart';

class FadeInFadeOutRoute<T> extends MaterialPageRoute<T> {
FadeInFadeOutRoute({WidgetBuilder builder, RouteSettings settings})
: super(builder: builder, settings: settings);

@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(opacity: animation, child: child);
}
}

Artık bu route’u kullanarak Splash’dan Info’ya geçiş yapabilirim. Splash’ın son halini aşağıda paylaşıyorum ama sonrasında yaptığım değişiklikleri açıklayarak anlatacağım.

import 'package:flutter/material.dart';
import 'dart:async';

import '../routes/fadeInFadeOutRoute.dart';
import 'info.dart';

class Splash extends StatefulWidget {
Splash({Key key}) : super(key: key);
@override
_State createState() => new _State();
}

class _State extends State<Splash> {
Duration duration = const Duration(seconds: 3);

startTimeout() {
return new Timer(duration, handleTimeout);
}

void handleTimeout() {
Navigator.pop(context);
Navigator.push(
context, FadeInFadeOutRoute(builder: (context) => new Info()));
}

initState() {
super.initState();
this.startTimeout();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage("assets/images/splash_bg.png"),
fit: BoxFit.cover,
)),
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
),
),
));
}

dispose() {
super.dispose();
}
}

3 yeni method ve bir const ekledik. initState widget açıldığında çağrılacak ve sayacımızı başlatacak. startTimeout 3sn sayacak ve sonrasında handleTimeout methodunu çağıracak. handleTimeout yeni ekrana geçiş yapmakla sorumlu.

Kodu bu yeni hali ile çalıştırdığınızda cihazınızın ekranında ilk Splash ekranını ve 3sn sonra hazırladığımız Info ekranını göreceksiniz.

Animasyon

Artık uygulamamızın tek bir eksiği kaldı, o da Info ekranı içerisindeki muhteşem animasyonumuz. Kullanacağım animasyonu başka ekranlarda da kullanabilmek adına direkt ekrana değil bir widget olarak ayrı bir dosyada yazacağım. Kodun derli toplu olması adına tekrar kullanılacağına inandığım bütün widget’leri oluşturduğum lib/widgets klasörünün altına alacağım. Şimdi yazacağım animasyon widget'i bir container olacak ve içerisinde yazdığımız text'in etrafında bir su dalgasına benzetebileceğimiz daireler çizecek.

Water Ripple , Kaynak: https://vignette.wikia.nocookie.net/animal-jam-clans-1/images/a/a2/Anime_water.gif/revision/latest?cb=20170726030745

Flutter ile güzel bir daire oluşturmakla başlayalım. Bu daireyi farklı yerlede tekrar kullanabileceğimi düşünerek widgets altına shapes klasörünü oluşturuyorum.

Circle widget’ini lib/widgets/shapes/circle.dart yoluna oluşturuyorum

import 'package:flutter/material.dart';

@immutable
class Circle extends StatelessWidget {
Circle({this.diameter, this.color, this.child});
final double diameter;
final Color color;
final Widget child;

@override
Widget build(BuildContext context) {
return new Container(
alignment: Alignment.center,
width: this.diameter,
height: this.diameter,
decoration: new BoxDecoration(shape: BoxShape.circle, color: this.color),
child: this.child,
);
}
}

Basit bir widget ama biraz açıklamak istiyorum. Kendisi StatelessWidget yani sadece çizimle alakalı kodları içeriyor ve içerisinde herhangi bir durum bilgisi yok. İçerisine 3 adet bilgi alıyor (diameter, color ve child).

Bu widget’e daire özelliğini veren decoration satırıdır.

Şimdi gelelim bu daireleri kontrol edecek ve animasyonumuzu hesaplayacak olan widget’e, lib/widgets/animated/ripple.dart . Kendisi iç içe 3 daireden oluşacak ve bu dairelerin boyutlarını belli oranlarda değiştirerek doğal bir yayılma animasyonu oluşturacak.

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
import '../shapes/Circle.dart';

class Ripple extends StatefulWidget {
Ripple(this.diameter, {this.child});
final double diameter;
final Widget child;
_State createState() => new _State(diameter, child);
}

class _State extends State<Ripple> with SingleTickerProviderStateMixin {
_State(this.diameter, this.child);
Animation<double> animation;
AnimationController controller;
final double diameter;
final Widget child;

initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 5000), vsync: this);
animation = new Tween(begin: 0.0, end: diameter * 1.7).animate(controller);
animation.addListener(() {
setState(() {});
});
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.repeat();
}
});
controller.forward();
}

Widget build(BuildContext context) {
double d1 = (animation.value).clamp(00, diameter).toDouble();
double d2 =
(animation.value - diameter * 0.33).clamp(0, diameter).toDouble();
double d3 =
(animation.value - diameter * 0.7).clamp(0, diameter).toDouble();
int dInt = diameter.toInt();
int a1 = (dInt - d1.toInt()).clamp(0, 50);
int a2 = (dInt - d2.toInt()).clamp(0, 50);
int a3 = (dInt - d3.toInt()).clamp(0, 50);
return Container(
width: diameter,
height: diameter,
child: Stack(children: [
Center(
child: Circle(
diameter: d1,
color: Colors.white.withAlpha(a1),
child: Circle(
diameter: d2,
color: Colors.white.withAlpha(a2),
child: Circle(
diameter: d3,
color: Colors.white.withAlpha(a3),
)))),
Center(child: child)
]));
}

dispose() {
controller.dispose();
super.dispose();
}
}

Yazdığımız bu widget bir StatefulWidget, yani kendi içerisinde durum bilgisini tutacak. Durum bilgisini tamamen animasyon için gerekli olduğundan tutuyoruz. Doğru bir şekilde zamanlanabilmek adına animasyonlar saat sinyaline ihtiyaç duyar, burada _State sınıfımızı SingleTickerProviderStateMixin'den extend ederek aslında kendi içerisinde tekli bir ticker yani saat sinyali bulunduran bir state oluşturuyoruz.

class _State extends State<Ripple> with SingleTickerProviderStateMixin {

Artık state sürekli tick etmekte ve her tik de elimize sayısal bir değer vermektedir. Bu tiklerden yararlanabilmesi için animasyonumuzu kontrol edecek ve state ile kendini senkronize edecek AnimationController tanımlamamız gerekmektedir. Şimdi initState içerisinde animasyon için gerekli gördüğümüz kuralları tanımlayabiliriz. Aşağıdaki satır ile 5 saniyelik bir animasyonu kontrol edecek controller oluşturuyoruz.

controller = new AnimationController( duration: const Duration(milliseconds: 5000), vsync: this);

Şimdi animasyonu oluşturup onu kontrol edecek controller ile bağlamalıyım.

animation = new Tween(begin: 0.0, end: diameter * 1.7).animate(controller);

Animasyon her işlediğinde ekranda gerekli değişiklikleri tetiklemesi için boş state atayan bir listener tanımlıyorum. Burada isterseniz animasyonun değerini de verebilirim ama gerekli değil.

animation.addListener(() {
setState(() {});
});

Animasyonun bitmesini istemediğimden tekrar başlamasını sağlayacak bir kod daha yazmalıyım ve sonrasında controller’imi başlatarak gerekli tüm initState işlerini bitirmiş oluyorum.

animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.repeat();
}
});
controller.forward();

Buraya kadar sıkılmadan okuduysanız tebrik ederim :D. Şimdi ikinci ve son kısmı olan build methodunu anlatacağım.

Başlangıç olarak animation.value'yu kullanarak 3 adet çap değeri hesaplıyorum. Artık oluşacak olan dairelerin o anki animasyon değerine göre çaplarını hesaplayabiliyorum.

double d1 = (animation.value).clamp(00, diameter).toDouble();
double d2 = (animation.value - diameter * 0.33).clamp(0, diameter).toDouble();
double d3 = (animation.value - diameter * 0.7).clamp(0, diameter).toDouble();

Şimdi genişleyen dairelerin renginin silikleşmesini sağlayabilmek adına yine bir hesaplama ile saydamlık yani alpha değerlerini hesaplıyorum.

int dInt = diameter.toInt();
int a1 = (dInt - d1.toInt()).clamp(0, 50);
int a2 = (dInt - d2.toInt()).clamp(0, 50);
int a3 = (dInt - d3.toInt()).clamp(0, 50);

Artık istediğim daireleri çizebilirim. Hesapladığım değerleri iç içe kullanarak üç daireyi kullanıyorum ve son olarak hepsinin üstüne gelecek şekilde dışarıdan verilen TextWidget’imi yerleştiriyorum.

return Container(
width: diameter,
height: diameter,
child: Stack(children: [
Center(
child: Circle(
diameter: d1,
color: Colors.white.withAlpha(a1),
child: Circle(
diameter: d2,
color: Colors.white.withAlpha(a2),
child: Circle(
diameter: d3,
color: Colors.white.withAlpha(a3),
)))),
Center(child: child)
]));

Bitti, artık Info ekranına animasyon widget’imizi ekleyerek uygulamanın son haline ulaşabiliriz.

import 'package:flutter/material.dart';
import '../widgets/animated/ripple.dart';

class Info extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage("assets/images/info_bg.png"),
fit: BoxFit.cover,
)),
child: Center(
child: Ripple(
300,
child: Text('Info',
textDirection: TextDirection.ltr,
style: TextStyle(fontSize: 50.0, color: Colors.white)),
)),
));
}
}

Tekrar hatırlatayım, isterseniz kodların son haline buradan ulaşabilirsiniz.

Aslında yazının başına animasyonu koyup koymamak konusunda çok kararsız kaldım. Belki eski kafalılık belki okuyucuyu merak içerisinde bırakmak için bilmiyorum ama sona koymaya karar verdim. Sıkılmadan okuyup bu noktaya kadar geldiyseniz ne güzel. Umarım açıklayıcı bir şekilde yazabilmişimdir.

Esen Kalın

info@fill-labs.com clutch Clutch.co Twitter Twitter Instagram Instagram Linkedin Linkedin
©Fill-labs 2024 Legal Notice - Data protection - Use of cookies