İçeriğe geç
academia.sh

Ders 05 / 19

Üç Ağaç Modeli

Çerçeve aynı arayüzü üç ayrı ağaçta tutar: tarif ağacı, yaşayan öge ağacı ve düzeni yapan çizim nesnesi ağacı. Ders hangi ağacın ne yaptığını anlatır ve düzen sonucunun neden yalnız son ağaca ait olduğunu ölçer.

Yayın Güncelleme

Bu derste

  • Bileşen, öge ve çizim nesnesi ağaçlarının her birinin görevini ayırt eder
  • Durum değişince hangi ağacın yeniden kurulduğunu, hangisinin korunduğunu açıklar
  • Korunan bir düğümün kısıtı değişince başka bir genişlik alabildiğini ölçer
İçindekiler

Bir önceki derste durum değişince tarifin yeniden kurulduğunu gördük. Ama ekrandaki nesneler her seferinde baştan yaratılsaydı, bir kaydırma konumu ya da bir yazı alanındaki imleç her yeniden kurulumda kaybolurdu. Çerçeve bu yüzden aynı arayüzü üç ağaçta tutar ve her ağacın ömrü başka bir hızda akar.

Tarif Ağacı

Birinci ağaç, yazdığınız bileşenlerin ağacıdır. Bu ağaç değişmez tariflerden oluşur ve her yeniden kurulumda baştan üretilir. Ucuzdur, çünkü yalnız “ne istiyorum” sorusunu cevaplar: ekranda bir iç boşluk olsun, içinde ortalanmış bir logo olsun.

Dersin çekirdeği yine ilk dersin sizing.dart dosyasıdır; model bu ağacın karşılığıdır.

// 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}');
}

Modeldeki pad('Screen', 16, center('Center', fixed('Logo', 96, leaf('Image', 200)))) çağrısı bir tarif ağacıdır. Düğümler yalnız ad, tür ve istek taşır; konum ya da kalıcı bir durum taşımaz.

DB1. Modelin ağacı tek bir ağaçtır. Çerçevedeki üç ağaç modelde ayrı ayrı kurulmaz; üçüncü ağacın yaptığı iş, yani kısıtı aşağı indirip boyutu yukarı çıkarmak measure çağrısıdır.

Öge Ağacı

İkinci ağaç, öge ağacıdır. Çerçeve bir tarifi ilk kez gördüğünde ona karşılık gelen bir öge yaratır. Öge, ağaçta o tarifin yerini tutar ve yaşar: durum nesneleri ögelere bağlıdır. Yeniden kurulumda çerçeve yeni tarifi eski ögeyle karşılaştırır. Tür ve anahtar aynıysa öge korunur ve yeni tarifi alır; farklıysa öge atılır ve yenisi yaratılır.

Bu karşılaştırma durumun neden yaşadığını açıklar. Durumlu bir bileşenin durum nesnesi ögeye bağlıdır; öge korunduğu sürece durum da korunur. Bir önceki dersteki arama kutusunun expanded değeri, kutunun ögesi yaşadığı için yaşamaya devam eder.

Çizim Nesnesi Ağacı

Üçüncü ağaç çizim nesnesi ağacıdır. Boyutu hesaplayan, düğümü yerleştiren ve ekrana çizen nesneler buradadır. Kısıtın aşağı indiği ve boyutun yukarı çıktığı akış bu ağaçta işler. Aşağıdaki parça bir çizim nesnesinin düzeni nasıl yaptığını gösterir; model dışıdır ve ölçülmez.

// Platform kodu: cizim nesnesi
class RenderLogo extends RenderBox {
  @override
  void performLayout() {
    size = constraints.constrain(const Size(96, 96));
  }
}

performLayout, nesnenin kısıtı alıp boyutunu seçtiği yerdir. constraints.constrain isteği kısıta sıkıştırır; istek aralığın içindeyse olduğu gibi kalır. Modelin classify kuralı bu davranışı sadeleştirir.

Üç ağaç şöyle bölüşür: tarif “ne istediğini” söyler, öge “kim kimdir, kimin durumu var” bilgisini taşır, çizim nesnesi “ne kadar yer kaplıyor” sorusunu cevaplar. Genişliği belirleyen ağaç her zaman sonuncusudur.

Aynı Düğüm, Başka Kısıt

Şimdi aynı ağacı ölçelim. center kısıtı gevşetir, fixed ise çocuğuna sıkı bir genişlik verir.

// main.dart
import 'sizing.dart';

Part logoTree(int imageWidth) {
  return pad('Screen', 16, center('Center', fixed('Logo', 96, leaf('Image', imageWidth))));
}

void main() {
  printSizes(measure(logoTree(200), 360));
}
dayatilan: /Screen genislik=360 kisit=360..360
dayatilan: /Screen/Center genislik=328 kisit=328..328
secilen: /Screen/Center/Logo genislik=96 kisit=0..328
dayatilan: /Screen/Center/Logo/Image genislik=96 kisit=96..96

Center, ebeveyninden 328 sıkı kısıt alır ve onu Logo‘ya 0..328 olarak gevşetir. Logo 96 ister ve aralıkta olduğu için 96’yı seçer. Logo’nun içindeki Image ise 200 ister ama fixed ona 96..96 verir ve genişliği dayatılır.

Şimdi iki şeyi karşılaştıralım. Önce tarif değişir: Image isteği 200’den 120’ye iner ve yollar aynı kalır, yani öge ağacı korunur. Sonra tarif aynı kalır, ekran 360’tan 320’ye iner.

// main.dart
import 'sizing.dart';

Part logoTree(int imageWidth) {
  return pad('Screen', 16, center('Center', fixed('Logo', 96, leaf('Image', imageWidth))));
}

void main() {
  final first = measure(logoTree(200), 360);
  final newRecipe = measure(logoTree(120), 360);
  final newScreen = measure(logoTree(200), 320);
  final samePaths = [for (final e in first) e.path].join(',') ==
      [for (final e in newRecipe) e.path].join(',');
  print('korunan yollar: $samePaths');
  print('Image, yeni tarif: ${first[3].width} -> ${newRecipe[3].width}');
  print('Center, yeni ekran: ${first[1].width} -> ${newScreen[1].width}');
  print('Logo, yeni ekran: ${first[2].width} -> ${newScreen[2].width}');
}
korunan yollar: true
Image, yeni tarif: 96 -> 96
Center, yeni ekran: 328 -> 288
Logo, yeni ekran: 96 -> 96

Tarif değişince düğümlerin yolları aynı kalır, yani çerçeve ögeleri korur. Image isteğini 120’ye indirmiştir ama fixed yine 96 dayattığından genişliği değişmez. Ekran değişince ise ağaç aynıdır, yine de Center 328’den 288’e iner: korunan düğüm kısıtı değişince başka bir genişlik alır. Logo ise ikisinde de 96’dır.

React Native kursunda güncellemenin görünüm ağacındaki bir düğüme ne yaptığı sorulur: korundu mu, güncellendi mi, kuruldu mu, söküldü mü. Bu akıbet iki ağacın karşılaştırmasıdır. Boyutlanma ise tek ağacın düzenidir ve o karşılaştırmanın sonucunu bilmeden de sorulur. Yukarıdaki çıktıda Center’ın korunduğunu biliyoruz; yine de genişliği ekrana göre değişiyor. Korunma genişliği garanti etmez.

DB2. Ögenin korunması yalnız durumun ve kimliğin korunması demektir. Bir düğümün bir önceki ölçümdeki genişliğini koruyacağı anlamına gelmez; genişliği her ölçümde o anki kısıt belirler.

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

Odak düğüm /Screen/Center/Logo’dur. Aynı ağacı iki ekran genişliğinde ölçelim.

// main.dart
import 'sizing.dart';

Part logoTree() {
  return pad('Screen', 16, center('Center', fixed('Logo', 96, leaf('Image', 200))));
}

void main() {
  for (final width in [480, 320]) {
    print('ekran: $width');
    printSizes(measure(logoTree(), width));
  }
}
ekran: 480
dayatilan: /Screen genislik=480 kisit=480..480
dayatilan: /Screen/Center genislik=448 kisit=448..448
secilen: /Screen/Center/Logo genislik=96 kisit=0..448
dayatilan: /Screen/Center/Logo/Image genislik=96 kisit=96..96
ekran: 320
dayatilan: /Screen genislik=320 kisit=320..320
dayatilan: /Screen/Center genislik=288 kisit=288..288
secilen: /Screen/Center/Logo genislik=96 kisit=0..288
dayatilan: /Screen/Center/Logo/Image genislik=96 kisit=96..96

Bu dersin boyutlanması: seçilen. /Screen/Center/Logo 96 ister; Center kısıtı gevşettiği için aralık 0..448 ya da 0..288 olur ve 96 bunun içindedir. Genişliği düğümün kendisi seçer. Ekran değişse de Logo 96’da kalır, Center ise 448 ve 288 alır. İçindeki Image bu durumda dayatılandır: genişliğini fixed verir.

Özet

  • Bu dersin boyutlanması: seçilen. /Screen/Center/Logo gevşetilmiş bir aralıkta 96 ister ve iki ekran genişliğinde de 96’yı seçer.
  • Çerçeve aynı arayüzü üç ağaçta tutar: değişmez tariflerden oluşan bileşen ağacı, tarifin yerini tutan öge ağacı ve düzeni yapan çizim nesnesi ağacı.
  • Yeniden kurulumda tarif yeni gelir; tür ve anahtar aynıysa öge korunur, durum da onunla birlikte korunur.
  • Genişliği her zaman çizim nesnesi ağacı belirler; korunma genişliği garanti etmez.
  • Aynı ağaçta Center 328, 448 ve 288 alır, Logo 96’da kalır: korunan düğüm kısıtı değişince başka genişlik alabilir.

Sonraki Adım

Çizim nesnesi ağacında kısıtı geçiren bileşenlerin çoğu düzen bileşenidir. Düzen Bileşenleri dersi sütun, satır ve esnek çocukları ele alır; esnek bir çocuğun içindeki bileşenin genişliğini kimin verdiğini ö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