Ders 13 / 19
Animasyonlar
Bir animasyon düzeni her karede yeniden koşturur, ama her karede genişliğin sahibi değişmeyebilir. Ders bir panelin genişliğini 80'den 240'a beş karede ilerletir ve her karenin boyutlanmasını ölçer.
Yayın Güncelleme
Bu derste
- Animasyon denetleyicisini, eğriyi ve ara değeri birbirinden ayırır
- Örtük ve açık animasyon arasındaki farkı söyler
- Beş karenin her birinde panelin genişliğini ve boyutlanmasını ölçer
- Eğri değişse de genişliğin sahibinin değişmediğini gösterir
İçindekiler
Önceki ders, yazı tipi ölçeğinin bir düğmenin isteğini büyüttüğünü ve büyüyen isteğin aralığa sığıp sığmadığını ölçtü. Bu derste istek zamanla değişir: bir panelin genişliği her karede biraz daha büyür. Animasyon düzeni tek seferlik bir hesap olmaktan çıkarır ve her kare için yeniden koşturur. Kursun sorusu yine aynıdır: bu bileşenin genişliğini kim belirliyor? Ders yanıtı, kareler boyunca büyüyen bir panelin üzerinden arar.
Denetleyici, Eğri ve Ara Değer
Bir animasyonun üç parçası vardır. Animasyon denetleyicisi (controller) ilerlemeyi 0’dan 1’e taşır; kimin ne zaman başlattığı ve durdurduğu ondadır. Eğri (curve) ilerlemeyi biçimlendirir: doğrusal bir eğri eşit adımlarla, ivmelenen bir eğri önce yavaş sonra hızlı ilerler. Ara değer (tween) ilerlemeyi bir başlangıçla bir bitiş arasındaki değere çevirir. Bu derste ara değer 80’den 240’a genişliktir.
Örtük (implicit) animasyonda bileşen yeni değeri alır ve geçişi kendisi kurar; denetleyiciyi çerçeve yönetir. Açık (explicit) animasyonda denetleyici kodda tutulur ve ilerleme elle sürülür.
Çekirdek
Her ders aynı çekirdeği kullanır. measure ağacı ekran genişliğinde sıkı bir kısıtla dolaşır, her düğüm için bir kayıt üretir ve kayıttaki boyutlanmayı bir kural verir: kısıt sıkıysa dayatılan, isteğin aralık içinde kaldığı durumda seçilen, istek aralığın dışındaysa ya da yoksa ve üst sınır yoksa kırpılan, istek yok ve üst sınır sonluysa esnetilen. Çıktıda dört ad ASCII yazılır: dayatilan, secilen, kirpilan, esnetilen. ebeveyn: satırı dayatılan, kırpılan ve esnetilenin toplamıdır; genişliği bileşenin kendi isteği olmayan kayıtları sayar.
// sizing.dart — bir bilesenin genisligini kimin belirledigini siniflandirir
const sizings = ['dayatilan', 'secilen', 'kirpilan', 'esnetilen'];
class Limit {
final int min;
final int? max; // null: sinirsiz
const Limit(this.min, this.max);
bool get tight => max == min;
Limit loosen() => Limit(0, max);
Limit deflate(int by) => Limit(_floor(min - by), max == null ? null : _floor(max! - by));
String bounds() => '$min..${max ?? 'sonsuz'}';
}
int _floor(int value) => value < 0 ? 0 : value;
class Part {
final String kind;
final String name;
final int? width; // null: alabildigi kadar genis
final List<Part> children;
final int factor;
final bool tightFlex;
const Part(this.kind, this.name, this.width, this.children,
{this.factor = 0, this.tightFlex = true});
}
Part leaf(String name, int width) => Part('leaf', name, width, const []);
Part fill(String name) => Part('leaf', name, null, const []);
Part pad(String name, int padding, Part child) => Part('pad', name, padding, [child]);
Part center(String name, Part child) => Part('center', name, null, [child]);
Part fixed(String name, int width, Part child) => Part('fixed', name, width, [child]);
Part column(String name, List<Part> children) => Part('column', name, null, children);
Part row(String name, List<Part> children) => Part('row', name, null, children);
Part flex(String name, int factor, Part child, {bool tight = true}) =>
Part('flex', name, null, [child], factor: factor, tightFlex: tight);
class Sizing {
final String sizing;
final String path;
final int width;
final Limit limit;
const Sizing(this.sizing, this.path, this.width, this.limit);
}
Sizing classify(String path, int? desire, Limit limit) {
if (limit.tight) return Sizing('dayatilan', path, limit.min, limit);
if (desire == null) {
return limit.max == null
? Sizing('kirpilan', path, limit.min, limit)
: Sizing('esnetilen', path, limit.max!, limit);
}
if (desire < limit.min) return Sizing('kirpilan', path, limit.min, limit);
if (limit.max != null && desire > limit.max!) {
return Sizing('kirpilan', path, limit.max!, limit);
}
return Sizing('secilen', path, desire, limit);
}
int layout(Part part, Limit limit, List<Sizing> log, [String path = '']) {
final here = '$path/${part.name}';
final below = <Sizing>[];
if (part.kind == 'fixed') {
final own = classify(here, part.width, limit);
layout(part.children.first, Limit(own.width, own.width), below, here);
log..add(own)..addAll(below);
return own.width;
}
int? desire;
switch (part.kind) {
case 'leaf':
desire = part.width;
case 'pad':
final space = 2 * part.width!;
desire = layout(part.children.first, limit.deflate(space), below, here) + space;
case 'center':
final inner = layout(part.children.first, limit.loosen(), below, here);
desire = limit.max == null ? inner : null;
case 'flex':
desire = layout(part.children.first, limit, below, here);
case 'column':
var widest = 0;
for (final child in part.children) {
final width = layout(child, limit.loosen(), below, here);
if (width > widest) widest = width;
}
desire = widest;
case 'row':
final logs = [for (final _ in part.children) <Sizing>[]];
final bounded = limit.max != null;
var used = 0;
var factors = 0;
for (var i = 0; i < part.children.length; i++) {
final child = part.children[i];
if (child.kind == 'flex' && bounded) {
factors += child.factor;
} else {
used += layout(child, const Limit(0, null), logs[i], here);
}
}
final left = bounded ? _floor(limit.max! - used) : 0;
var given = 0;
var seen = 0;
for (var i = 0; i < part.children.length; i++) {
final child = part.children[i];
if (child.kind != 'flex' || !bounded) continue;
seen += child.factor;
final share = (seen == factors ? left : left * seen ~/ factors) - given;
given += share;
final room = child.tightFlex ? Limit(share, share) : Limit(0, share);
used += layout(child, room, logs[i], here);
}
for (final entries in logs) {
below.addAll(entries);
}
desire = bounded ? null : used;
}
final own = classify(here, desire, limit);
log..add(own)..addAll(below);
return own.width;
}
List<Sizing> measure(Part root, int screen) {
final log = <Sizing>[];
layout(root, Limit(screen, screen), log);
return log;
}
void printSizes(List<Sizing> log) {
for (final entry in log) {
print('${entry.sizing}: ${entry.path} genislik=${entry.width} kisit=${entry.limit.bounds()}');
}
}
void countSizings(List<Sizing> log) {
for (final name in sizings) {
print('$name: ${log.where((entry) => entry.sizing == name).length}');
}
print('ebeveyn: ${log.where((entry) => entry.sizing != 'secilen').length}');
}
Çerçeve Tarafı
Aşağıdaki parça açık animasyonun çerçeve tarafını gösterir. Parça ölçülmez; model çerçevenin kodunu çalıştırmaz.
// Platform kodu: acik animasyon
class _PanelState extends State<PanelView> with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(vsync: this);
late final Animation<double> _width = Tween<double>(begin: 80, end: 240)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn));
void open() => _controller.animateTo(1.0, duration: widget.span);
}
Denetleyici ilerlemeyi sürer, CurvedAnimation eğriyi uygular ve Tween genişliği üretir. Her kare yeni bir genişlik demektir ve yeni genişlik düzenin yeniden koşması demektir. Bu dersin sorusu o koşunun sonucunda genişliğin sahibidir.
Kareden Ağaca
Modelde zaman yoktur; yalnız kare sırası vardır. Beş kare, 0’dan 4’e numaralanır ve ara değer her karede bir tamsayı üretir. Yardımcı dosya iki eğri ve bir kare sayacı taşır. Doğrusal eğri eşit adım atar; ivmelenen eğri adımı karesiyle ölçekler. Sayaç ders-yereldir ve kareleri sayar. Yardımcı dosya çıktıya hiçbir şey basmaz.
// tween.dart — ara degerler ve kare sayaci
int linearWidth(int from, int to, int step, int steps) =>
from + (to - from) * step ~/ steps;
int easedWidth(int from, int to, int step, int steps) =>
from + (to - from) * step * step ~/ (steps * steps);
class FrameCounter {
int frames = 0;
void tick() => frames++;
}
buildPanel panelin genişliğini alır ve ağacı kurar: ekranın içinde bir sütun, sütunun içinde başlık ile panel. İlk ölçüm animasyonun başlangıç karesidir.
// main.dart
import 'sizing.dart';
import 'tween.dart';
Part buildPanel(int width) {
return pad('Screen', 16, column('Body', [
leaf('Title', 120),
leaf('Panel', width),
]));
}
void main() {
final log = measure(buildPanel(linearWidth(80, 240, 0, 4)), 360);
printSizes(log);
countSizings(log);
}
dayatilan: /Screen genislik=360 kisit=360..360 dayatilan: /Screen/Body genislik=328 kisit=328..328 secilen: /Screen/Body/Title genislik=120 kisit=0..328 secilen: /Screen/Body/Panel genislik=80 kisit=0..328 dayatilan: 2 secilen: 2 kirpilan: 0 esnetilen: 0 ebeveyn: 2
Başlangıç karesinde panel 80 ister, aralık 0..328‘dir. /Screen/Body/Panel seçilendir ve genişliği 80’dir. Başlık da seçilendir. Animasyon henüz hiçbir şeyi değiştirmemiştir; kare sıfırdaki ağaç, animasyon olmasaydı da aynı olurdu.
Beş Kare
Şimdi iki eğriyle beş kareyi sırayla ölçelim. Her kare ayrı bir measure çağrısıdır ve her kareden yalnız panelin kaydı basılır.
// main.dart
import 'sizing.dart';
import 'tween.dart';
Part buildPanel(int width) {
return pad('Screen', 16, column('Body', [
leaf('Title', 120),
leaf('Panel', width),
]));
}
void main() {
final curves = {'dogrusal': linearWidth, 'ivmelenen': easedWidth};
curves.forEach((name, curve) {
for (var step = 0; step <= 4; step++) {
final width = curve(80, 240, step, 4);
final log = measure(buildPanel(width), 360);
final panel = log.firstWhere((entry) => entry.path == '/Screen/Body/Panel');
print('$name kare=$step istek=$width ${panel.sizing} genislik=${panel.width}');
}
});
}
dogrusal kare=0 istek=80 secilen genislik=80 dogrusal kare=1 istek=120 secilen genislik=120 dogrusal kare=2 istek=160 secilen genislik=160 dogrusal kare=3 istek=200 secilen genislik=200 dogrusal kare=4 istek=240 secilen genislik=240 ivmelenen kare=0 istek=80 secilen genislik=80 ivmelenen kare=1 istek=90 secilen genislik=90 ivmelenen kare=2 istek=120 secilen genislik=120 ivmelenen kare=3 istek=170 secilen genislik=170 ivmelenen kare=4 istek=240 secilen genislik=240
Doğrusal eğri 80, 120, 160, 200 ve 240 üretir; ivmelenen eğri 80, 90, 120, 170 ve 240. İki eğri arada farklı genişlikler verir ve ikisinin de sonu 240’tır. Ama on satırın on tanesinde de boyutlanma secilendir ve genişlik isteğe eşittir. Eğri isteğin değerini biçimlendirir; isteğin aralıkla ilişkisini değiştirmez.
Kaç kez koştu sorusu bir işin kaç kez çalıştığını sayar. Bir animasyon düzeni her karede yeniden koşturur ve koşum sayısı gerçekten artar; bu ders onu bir ölçü olarak almaz. Her karede ölçülen, o karedeki genişliğin sahibidir.
Sınırlayıcı Ölçüm
Kareleri sayaçla sayalım ve her karenin boyutlanmasını bir toplama işleyelim. Beş karenin hepsi aynı boyutlanmayı veriyorsa toplam tek bir satıra iner. Odak olarak son karedeki panelin satırını da basalım.
// main.dart
import 'sizing.dart';
import 'tween.dart';
Part buildPanel(int width) {
return pad('Screen', 16, column('Body', [
leaf('Title', 120),
leaf('Panel', width),
]));
}
void main() {
final counter = FrameCounter();
final tally = <String, int>{};
String last = '';
for (var step = 0; step <= 4; step++) {
counter.tick();
final log = measure(buildPanel(linearWidth(80, 240, step, 4)), 360);
final panel = log.firstWhere((entry) => entry.path == '/Screen/Body/Panel');
tally[panel.sizing] = (tally[panel.sizing] ?? 0) + 1;
last = '${panel.path} ${panel.sizing} genislik=${panel.width} kisit=${panel.limit.bounds()}';
}
print('kareler=${counter.frames}');
tally.forEach((sizing, count) => print('$sizing=$count'));
print('son kare: $last');
}
kareler=5 secilen=5 son kare: /Screen/Body/Panel secilen genislik=240 kisit=0..328
Sayaç 5 basar ve toplam tek bir satırdır: beş karenin beşi de secilen. Odak düğüm son karedeki /Screen/Body/Panel‘dır. Genişliği 240, kısıtı 0..328‘dir ve panel kendi isteğini seçmiştir. Panel 80’den 240’a büyürken ağaç beş kez yeniden ölçüldü; ama genişliği her karede panel belirledi. Büyüyen bir genişlik, genişliğin sahibini değiştirmez.
UY1. Model yalnız genişliği ölçer. Karenin süresi, eğrinin gerçek zamandaki ilerleyişi, animasyonun başlatılıp iptal edilmesi ve panelin yüksekliği modelde yoktur. Model, bu boyutlanmanın kuralını kurar; model çerçevenin kendisi değildir. Beş karelik tamsayı ilerlemesi dersin varsayımıdır; gerçek bir animasyon kesirli değerler üretir.
Sonucun bir sınırı vardır ve bu derste ölçülmemiştir: kuralın tanımı gereği istek 328’i aşan bir kareden sonra panel kırpılan olur. Bitiş değerinin ekrana sığması ara değerin değil, aralığı bilen tarafın işidir.
Özet
- Bu dersin boyutlanması: seçilen. Odak düğüm son karedeki
/Screen/Body/Panel’dır: 240 istek,0..328aralığının içinde kalır ve panel kendi isteğini seçer. - Denetleyici ilerlemeyi sürer, eğri ilerlemeyi biçimlendirir ve ara değer genişliği üretir; örtük animasyonda denetleyiciyi çerçeve yönetir.
- Doğrusal ve ivmelenen eğriler ara karelerde farklı genişlikler (kare 2’de 160 ve 120) verdi; on ölçümün onunda da boyutlanma seçilen çıktı.
- Beş kare ayrı ayrı ölçüldü, sayaç 5 bastı ve beşi de seçilen oldu.
- Kural koşum sayısını değil, o karedeki genişliğin sahibini görür.
Sonraki Adım
Bu derste istek her karede değişti ve sahibi değişmedi. Sıradaki ders, Platform Kanalları, isteğin çerçevenin dışından, platformun kendi tarafından geldiği durumu ölçer: gömülü bir platform görünümünün istediği genişlik kaç ileti gelirse gelsin, yuvanın verdiği genişliğe nasıl çekildiğine bakar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.