İçeriğe geç
academia.sh

Ders 13 / 20

Hareketler ve Animasyonlar

Bir hareketin konumu ağaca özellik olarak yazılırsa her kare bir güncelleme komutu doğurur. Hareket yerel tarafta sürülürse aradaki karelerde ağaç korunur. Ders iki yolu aynı hareket üzerinde ölçer.

Yayın Güncelleme

Bu derste

  • Hareketin konumunu ağaca yazmakla yerel tarafa sürdürmek arasındaki farkı açıklar
  • Konum ağaca özellik olarak yazılınca her karenin bir güncelleme komutu doğurduğunu ölçer
  • Yerel sürülen harekette aradaki karelerde kartın korunan olduğunu ölçer
  • Hareket bitince konumu bir kez ağaca yazmanın tek bir güncelleme olduğunu gösterir
İçindekiler

Önceki ders dışarıdan gelen değişikliği ele aldı. Bir değişiklik de parmaktan gelir: bir kart sürüklenir, bir panel kayarak açılır. Hareket tek bir olay değil, kare kare ilerleyen bir değişimdir. Kartın konumu her karede değişir ve soru şudur: bu değişimin her adımı görünüm ağacından geçer mi, yoksa ağaç yalnızca başlangıcı ve bitişi mi görür?

Kursun sorusu değişmez: bu güncelleme görünüm ağacındaki düğüme ne yapıyor? Akıbet korunan, güncellenen, kurulan ya da sökülendir; güncellenen, kurulan ve sökülen düğümlerin toplamı komuttur. Akıbeti elle atamayız, kural verir:

// fate.mjs — bir guncellemenin gorunum agacindaki her dugume ne yaptigini siniflandirir
export const FATES = ["korunan", "guncellenen", "kurulan", "sokulen"];

export function el(type, props = {}, children = [], key = null) {
  return { type, props, children, key };
}

function segment(node) {
  return node.key === null ? node.type : `${node.type}[${node.key}]`;
}

function identity(node, index) {
  return node.key === null ? `#${index}` : `key:${node.key}`;
}

function sameProps(a, b) {
  const names = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
  return names.every((n) => JSON.stringify(a[n]) === JSON.stringify(b[n]));
}

function whole(node, path, fate, ops) {
  const here = `${path}/${segment(node)}`;
  ops.push({ fate, path: here });
  node.children.forEach((child) => whole(child, here, fate, ops));
}

export function reconcile(prev, next, path = "", ops = []) {
  if (prev === null) {
    whole(next, path, "kurulan", ops);
    return ops;
  }
  if (next === null) {
    whole(prev, path, "sokulen", ops);
    return ops;
  }
  if (prev.type !== next.type || prev.key !== next.key) {
    whole(prev, path, "sokulen", ops);
    whole(next, path, "kurulan", ops);
    return ops;
  }
  const here = `${path}/${segment(next)}`;
  ops.push({ fate: sameProps(prev.props, next.props) ? "korunan" : "guncellenen", path: here });
  const before = new Map(prev.children.map((child, i) => [identity(child, i), child]));
  const matched = new Set();
  next.children.forEach((child, i) => {
    const id = identity(child, i);
    matched.add(id);
    reconcile(before.has(id) ? before.get(id) : null, child, here, ops);
  });
  prev.children.forEach((child, i) => {
    if (!matched.has(identity(child, i))) reconcile(child, null, here, ops);
  });
  return ops;
}

export function printOps(ops) {
  ops.forEach((op) => console.log(`${op.fate}: ${op.path}`));
}

export function countFates(ops) {
  for (const name of FATES) {
    console.log(`${name}: ${ops.filter((op) => op.fate === name).length}`);
  }
  console.log(`komut: ${ops.filter((op) => op.fate !== "korunan").length}`);
}

Model, bu akıbetin kuralını kurar; model çerçevenin kendisi değildir. Kural kareleri değil ağaç çiftlerini karşılaştırır; bir hareketin kaç kare sürdüğünü, ağaca kaç kez uğradığından anlarız.

Konum Ağaca Yazılırsa

Hareketi sürmenin ilk yolu konumu uygulamanın durumunda tutmaktır. Parmak hareket ettikçe durum değişir, ağaç yeniden kurulur ve kartın konum özelliği yeni değeri taşır. Her kare için iki ağaç karşılaştırılır ve fark yerel tarafa komut olarak gider.

UK1. Model kartı Deck içindeki tek bir Card olarak kurar; Card, konumu bir özellik olarak taşır ve altında bir Text vardır. Konum 0, 40, 80 gibi sayılarla verilir; sayılar karenin kimliğidir, ölçü değildir.

// deck.mjs — kart destesinin gorunum agacini iki surus yolu icin kurar
import { el } from "./fate.mjs";

export function renderDeck(position) {
  return el("Deck", {}, [
    el("Card", { konum: position }, [el("Text", { metin: "Siparis" })]),
  ]);
}

export function renderDriven() {
  return el("Deck", {}, [
    el("Card", { surucu: "yerel" }, [el("Text", { metin: "Siparis" })]),
  ]);
}

renderDeck konumu özellik olarak yazar. renderDriven ise konumu ağaca yazmaz; kart yalnız hareketin yerel tarafta sürüldüğünü bildirir. İkinci yol çerçevede, hareketin yerel sürücüye devredilmesiyle kurulur.

// Platform kodu: SlideCard
function SlideCard({ x }) {
  useEffect(() => {
    Animated.timing(x, { toValue: 160, useNativeDriver: true }).start();
  }, []);
  return <Animated.View style={{ transform: [{ translateX: x }] }} />;
}

Bu parça ölçülmez; model yerel sürücüyü çalıştırmaz. Parçadaki kanca konumu durumdan geçirmez: hareketin başlangıç ve bitiş değerleri yerel tarafa bir kez verilir, aradaki kareleri yerel taraf üretir.

İlk ölçüm konumun ağaca yazıldığı tek bir kare geçişidir: kart 0 konumundan 40 konumuna gidiyor.

// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderDeck } from "./deck.mjs";

const ops = reconcile(renderDeck(0), renderDeck(40));
printOps(ops);
countFates(ops);
korunan: /Deck
guncellenen: /Deck/Card
korunan: /Deck/Card/Text
korunan: 2
guncellenen: 1
kurulan: 0
sokulen: 0
komut: 1

/Deck/Card güncellenendir, çünkü konum özelliği değişti. Deck ve Text korunandır; kartın altındaki metin hareketten etkilenmez. Komut sayısı 1’dir: tek bir kare tek bir güncelleme taşıdı.

Her Kare Bir Komut

Bir hareket tek karede bitmez. Aynı geçiş her karede tekrarlanır ve her tekrar bir komut ekler. Aşağıdaki yardımcı, art arda gelen kareler için karşılaştırma yapar ve her geçişin komut sayısını döndürür.

// frames.mjs — art arda gelen karelerin her gecisindeki komut sayisini verir
import { reconcile } from "./fate.mjs";

export function stepCosts(frames, render) {
  const costs = [];
  for (let i = 1; i < frames.length; i++) {
    const ops = reconcile(render(frames[i - 1]), render(frames[i]));
    costs.push(ops.filter((op) => op.fate !== "korunan").length);
  }
  return costs;
}

İki konum dizisi, biri üç kareli biri dört kareli, konumu ağaca yazan yoldan geçirilir. Karşılaştırma için aynı diziler yerel sürülen yoldan da geçirilir.

// main.mjs
import { stepCosts } from "./frames.mjs";
import { renderDeck, renderDriven } from "./deck.mjs";

console.log("konum agacta, 3 kare: " + stepCosts([0, 40, 80], renderDeck).join(" "));
console.log("konum agacta, 4 kare: " + stepCosts([0, 40, 80, 120], renderDeck).join(" "));
console.log("yerel surulen, 3 kare: " + stepCosts([0, 40, 80], renderDriven).join(" "));
console.log("yerel surulen, 4 kare: " + stepCosts([0, 40, 80, 120], renderDriven).join(" "));
konum agacta, 3 kare: 1 1
konum agacta, 4 kare: 1 1 1
yerel surulen, 3 kare: 0 0
yerel surulen, 4 kare: 0 0 0

Konum ağaçta olduğu sürece her geçiş bir komuttur ve hareket uzadıkça komut sayısı kare sayısıyla birlikte artar. Yerel sürülen yolda her geçişin maliyeti 0’dır; hareketin uzunluğu ağaçta görünmez.

Yerel Sürülen Hareket

Yerel sürücü hareketi baştan sona kendisi ilerletir. Ağaç yalnızca kartın var olduğunu ve hareketin yerel tarafta sürüldüğünü bilir. Aradaki karelerde ağaç aynı kalır.

// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderDriven } from "./deck.mjs";

const ops = reconcile(renderDriven(), renderDriven());
printOps(ops);
countFates(ops);
korunan: /Deck
korunan: /Deck/Card
korunan: /Deck/Card/Text
korunan: 3
guncellenen: 0
kurulan: 0
sokulen: 0
komut: 0

/Deck/Card korunandır. Kart ekranda kayıyor olsa da ağaç için hiçbir şey olmuyor: üç düğümün üçü de korunan, komut 0. Hareketin akıcılığı yerel tarafın işidir; konum özelliği ağaca hiç girmez, bu yüzden her kare için yeniden karşılaştırma gerekmez.

Kaynak Parçayı Satır Satır Okumak

SlideCard parçasında hareketin tamamı bir çağrıya sığar. Bileşen bir animasyon değeri x alır: başlangıç konumu bu değerin içindedir ve durumda tutulmaz. useEffect bloğu açılışta bir kez çalışan yan etkidir; hareketi o başlatır. Çağrıdaki toValue bitiş konumunu verir, useNativeDriver: true ise ara karelerin yerel tarafta üretileceğini söyler. return satırı çizimdir ve konumu bir dönüşüm olarak karta bağlar.

Parçada eksik olan şey de öğreticidir: konumu güncelleyen bir durum çağrısı yoktur. Hareket sürerken uygulamanın durumu değişmez, dolayısıyla ağaç yeniden kurulmaz. Modeldeki renderDriven, bu eksikliğin ağaçtaki karşılığıdır: kart konum taşımaz, yalnız hareketin yerel tarafta sürüldüğünü bildirir. Konumu durumda tutan başka bir yazım ise renderDeck karşılığında olurdu ve her kare için durumu değiştiren bir çağrı gerektirirdi.

Bu iki yazım arasındaki seçimin tek koşulu vardır: hareketin ara değerlerine uygulama mantığının ihtiyacı olup olmadığı. Mantık ara konumu okuyacaksa, örneğin kart belli bir noktayı geçince başka bir şey olacaksa, değer durumda tutulur. Yalnız görsel bir kayma gerekiyorsa yerel sürücü yeterlidir.

Birden Fazla Kart

Gerçek bir ekranda aynı anda tek kart kaymaz. Bir deste açıldığında birkaç kart birlikte hareket eder. Konum ağaca yazılıyorsa her kartın konumu ayrı bir özelliktir ve her geçişte hepsi güncellenir. Bunu kart sayısını değiştirerek ölçelim.

// main.mjs
import { el } from "./fate.mjs";
import { stepCosts } from "./frames.mjs";

function renderMany(count, driven) {
  return (position) => el("Deck", {}, Array.from({ length: count }, (_, i) =>
    el("Card", driven ? { surucu: "yerel" } : { konum: position },
      [el("Text", { metin: "Siparis" })], "k" + i)));
}

const frames = [0, 40, 80];
console.log("1 kart, konum agacta: " + stepCosts(frames, renderMany(1, false)).join(" "));
console.log("2 kart, konum agacta: " + stepCosts(frames, renderMany(2, false)).join(" "));
console.log("3 kart, konum agacta: " + stepCosts(frames, renderMany(3, false)).join(" "));
console.log("3 kart, yerel surulen: " + stepCosts(frames, renderMany(3, true)).join(" "));
1 kart, konum agacta: 1 1
2 kart, konum agacta: 2 2
3 kart, konum agacta: 3 3
3 kart, yerel surulen: 0 0

Her geçişin maliyeti kart sayısına eşittir: bir kartta 1, iki kartta 2, üç kartta 3. Dolayısıyla toplam maliyet iki çarpanın ürünüdür: kare sayısı ve kart sayısı. Yerel sürülen yolda üç kart da kaysa maliyet sıfırdır. Hareket ne kadar çok düğümü ilgilendirirse, konumu ağaca yazmanın bedeli o kadar artar.

Bu çarpım hareketin neden pahalı olabileceğini açıklar, ama bir eşik vermez. Kaç kartın “çok” olduğu uygulamaya ve platforma bağlıdır; bu ders yalnız maliyetin hangi çarpanlara bağlı olduğunu ölçer.

Karşı Örnek: Konumun Ağaçta Gerektiği Yer

Yerel sürücü her zaman doğru çözüm değildir. Kartın konumu başka bir düğümün durumunu belirliyorsa, örneğin sürüklenen kart bir bırakma alanının üstüne gelince alan vurgulanacaksa, uygulamanın ara konumu bilmesi gerekir. Yerel sürücü bu bilgiyi uygulamaya her kare iletmez. Bu durumda ya konum durumda tutulur ve bedeli ödenir ya da yalnız eşik anında uygulamaya bir olay gönderilir. İkinci seçenek ağaca yalnız o anı yazar: hareket boyunca komut 0, eşik anında bir güncelleme.

Bu seçimde ağacın akıbeti kararı vermez, kararı hareketin işlevi verir. Akıbet yalnız iki yolun bedelini yan yana koyar.

Sınırlayıcı Ölçüm

Yerel sürülen hareketin ağaca hiç uğramadığını söylemek yanlış olur. Hareket bitince uygulamanın kartın son konumunu bilmesi gerekir: kart yerine yerleşmiş mi, silinmiş mi, kapanmış mı. Bu bilgi bir kez durumdan ağaca yazılır.

// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderDeck, renderDriven } from "./deck.mjs";

const ops = reconcile(renderDriven(), renderDeck(160));
printOps(ops);
countFates(ops);
korunan: /Deck
guncellenen: /Deck/Card
korunan: /Deck/Card/Text
korunan: 2
guncellenen: 1
kurulan: 0
sokulen: 0
komut: 1

Kart bu kez güncellenendir: sürücü özelliği gitti, konum özelliği geldi. Komut sayısı 1’dir. Hareket boyunca 0 komut, bitişte 1 komut. Akıbet hareketin akıcı olup olmadığını ölçmez; ağaca ne yazıldığını ölçer. Yerel sürülen hareketin ağaçta komutu yoktur, ama bitişte yazılan konum yine bir güncellemedir.

Yaygın Yanılgılar

  • “Her animasyon yerel sürülür, dolayısıyla ağaç hiç değişmez.” Sürücü bir seçimdir. Konum durumda tutuluyorsa her kare ağaca girer.
  • “Komut 0 ise hareket akıcıdır.” Komut sayısı ağaçla yerel taraf arasındaki yazımı ölçer; akıcılığı ölçmez.
  • “Hareket bitince ağaç aynı kalır.” Son konum uygulama için gerekliyse bir kez yazılır ve bu tek bir güncellemedir.
  • “Kart sayısı ağaç maliyetini değiştirmez.” Konum ağaca yazılıyorsa her kart ayrı bir güncellemedir. Birden fazla kartlı ölçümde geçiş başına komut, kart sayısına eşit çıktı.
  • “Yerel sürücü kullanmak kodu karmaşıklaştırır, ağaç zaten yeterince hızlıdır.” Bu bir başarım iddiasıdır ve ders bunu ölçmez. Ölçülen şey yalnızca ağaca yazılan komut sayısıdır; ağaç hızı ya da kare süresi ölçülmedi.
  • “Hareketi durumdan sürmek yanlıştır.” Ara konuma mantık bağlıysa durumdan sürmek doğru seçimdir. Yanlış olan, ihtiyaç yokken her kareyi ağaca yazmaktır.
  • “Hareketin durumu ile ağaç aynı şeydir.” Animasyon değeri hareketin içindeki sayıdır; ağaç ise ekranın o anki hâlidir. Yerel sürücü sayıyı ilerletir ama ağaca yazmaz. İki ayrı şeyin bu ayrımı, ölçümlerdeki sıfır komutu açıklar.
  • “Birden çok hareket aynı komutu paylaşır.” Ağaca yazılan her kartın konumu ayrı bir özelliktir; komutlar kart sayısı kadar çoğalır. Çoklu kart ölçümünde paylaşım görülmedi.
  • “Animasyonun süresi komut sayısını belirler.” Bu derste süre ölçülmedi. Ölçülen, geçiş sayısıdır: konum ağaçta olduğunda her kare bir geçiş ve bir komut getirir; sürenin kare sayısına nasıl dönüştüğü platforma bağlıdır ve bu derste yer almaz.
  • “Yerel sürücü yalnız kaydırma içindir.” Sürücü, konum dışında dönüşüm gibi başka görsel özellikler için de kullanılabilir. Ortak koşul, ara değerlerin uygulama mantığına gerekmemesidir.
  • “Hareket ile geçiş aynı şeydir.” Hareket, birçok karenin sürekli akışıdır; geçiş ise iki ağaç arasındaki tek bir karşılaştırmadır. Ders hareketi geçişlere bölerek ölçer ve bu bölme, ağaç kuralının hareketi nasıl gördüğünü açıklar.
  • “Kartı ağaçtan kaldırmak hareketi durdurur.” Kart sökülürse hareket de yok olur; ama bu başka bir akıbettir. Hareketin ortasında kartı kaldırmak ağaçta sökülen bir düğüm üretir ve bu ölçümün konusu olmadı.

Özet

  • Bu dersin akıbeti: korunan. Yerel sürülen harekette aradaki karelerde /Deck/Card korunandır ve komut 0’dır.
  • Konum ağaca özellik olarak yazılırsa her kare /Deck/Card düğümünü günceller ve komut sayısı kare sayısıyla artar.
  • Yerel sürülen harekette kare sayısı arttıkça komut sayısı değişmez.
  • Hareket bitince konumu ağaca bir kez yazmak tek bir güncelleme komutudur.
  • Akıbet hareketin akıcılığını değil, ağaca yazılanı sınıflar.

Sonraki Adım

Bu dersle uygulamanın katmanları tamamlandı: gezinme, durum, ağ, depolama, bildirim ve hareket. Her ders ağaçtaki bir değişikliğin akıbetini ölçtü. Sıradaki derslerde aynı ölçü başarım sorularına döner. Bir sonraki bölümün ilk dersi, Liste Başarımı, bir listenin başına öge eklenince satır düğümlerine ne olduğunu ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat