Flutter Liquid Glass Design
Merhaba değerli okurlar, hem iş yoğunluğu hemde yaşadığım sağlık sorunları nedeniyle makale yazma işine ara vermek durumunda kalmıştım ancak tekrar sizlerle birlikte olduğum için mutluyum.
Bu yazımızda sizlerle Apple tarafının iOS 26 adıyla WWDC 2025'te duyurduğu yeni tasarım dili olan Liquid Glass konseptini konuşuyor olacağız.
Öncelikle gelin Liquid Glass ne imiş buna göz atalım. Bu konuda Apple tarafı;
Liquid Glass, optik cama benzer niteliklere sahip, tamamen yeni, etkileyici bir materyaldir. Oldukça modern bir görünüme sahiptir ve VisionOS’un fizikselliği ile zenginliğinden ilham alınarak oluşturulmuştur. Saf dijital olmasına rağmen doğal ve canlı hissettirmek üzere tasarlanmıştır
şeklinde açıklama yapmış. Bu yeni tasarım dilinin Apple tarafına ve geliştirici camiasına hayırlı olmasını umuyorum =)
Uygulama ve telefon içindeki ögelerde bu şekilde bir tasarım kullanılması kişiden kişiye göre değişsede bir yenilik olarak karşımıza çıkıyor. Benim için Glass Morphism tasarım dilini anımsatan bu yeni yapıyla ilgili kullanıcılardan gelen onlarca eleştiri olduğunu gördüm. Okunabilirlik, erişebilirlik ve alışılabilirlik noktasında faciaya yol açtığına dair yorumlar okudum. Özellikle arayüzün bir kısımda değil tüm arayüzde bu tasarım dilinin uygulanması şimdilik pek hoş karşılanmıyor gibi bir durum var. Ancak bu yeni ara yüz beta olduğu için henüz herkesin erişimine açıldığında şu anki haliyle devam edilip edilmeyeceği bilinmiyor.
Şimdi tüm bunları bir kenara bırakıp Flutter tarafının bu tasarım diline olan hazırlığına bakalım.
Gördüğüm kadarıyla Flutter resmi ekibinin henüz bu konuda bir çalışma açıklaması yok. Bence açıklama konusu içinde çok erken. Çünkü Apple tarafının bu tasarım diliyle yoluna devam edip etmeyeceği henüz kesinlenmiş değil.
Flutter resmi ekibi bu gelişmeleri takip ede dursun Flutter topluluğu her zamanki gibi çok seri davranarak bu anlamda ilgili çalışmalara başlamış görünüyor. Pub dev üzerinde liquid glass adında arama yaptığınızda karşınıza bu sonuçlar çıkacaktır; Görmek İçin Tıklayın
Paketler henüz geliştirme aşamasında olsa bile ilgili yapıyı entegre etme ve deneyimleme noktasında çok aydınlatıcı diyebilirim. Ayrıca konuyla ilgili paketlerin hemen çıkması topluluk gücünü ve önemini tekrar hatırlatıyor.
Bu yazıyı yazmanın asıl nedenine gelelim. Peki biz bu tasarım dilini Flutter uygulamamıza uygulamak istersek nasıl bir yol izleyebiliriz? Bu soruyu kendimede sordum ve örnek bir uygulama yapmaya çalıştım. Yüzde yüz bir bir benzerlik noktasında olumlu değilim ancak gidiş yolumdan kendime puan verdim diyebilirim =)
Bu tasarım dilini herhangi bir liquid glass paketinden yardım almadan nasıl yapabilirim diye araştırırken karşıma Shader(GLSL) kavramı çıktı.
Nedir bu Shader diye araştırdığımda bunları öğrendim;
Uygulamalarımızda gördüğümüz her bir piksel, telefonun Grafik İşlem Birimi (GPU) tarafından hesaplanır. Shader’lar, bu hesaplamanın nasıl yapılacağını GPU’ya anlatan küçük programlardır. Tıpkı bir yemeğin tarifini şefe vermek gibi, biz de shader’larla GPU’ya “bu pikselleri şu renge boya”, “bu pikselleri biraz bük”, “ışık vuruyormuş gibi parlat” gibi komutlar veririz.
Shader’lar doğrudan GPU’da çalışır. Binlerce pikseli aynı anda işleyebilirler. Bu, CPU’da yapılması çok zor olan animasyonları (dalgalanma, erime, bükülme) saniyede 60 kare (60 FPS) hızında akıcı bir şekilde yapmamızı sağlar. Bu sayede Liquid Glass gibi bir tasarım desenini uygulamamıza ekleyebilmemize olanak sağlıyor. İnteraktif bir yapısı yani dışarıdan parametre aldığı için efekt konusunda güzel işler çıkarmamızı sağlıyor.
Flutter uygulamamızda Shader Kullanımı ise şu şekilde;
.frag uzantılı bir dosya oluşturmanız gerekiyor bu dosyaya shader kodlarınızı yazmanız gerekiyor. Konusu açılmışken hemen onada bakalım
Shader kodları şu ana parçalardan oluşur;
a. uniform Değişkenler: Dart’tan Gelen Veri Kapıları
uniform anahtar kelimesi, bu değişkenin değerinin dışarıdan, yani Dart kodumuzdan geleceğini söyler. u ön eki (uniform’un ‘u’su) bir gelenektir. layout(location = X) ise bu değişkenin “adresi” veya “konum numarası”dır. Dart’tan veri gönderirken bu numaraları kullanacağız.
- uWidth, uHeight: Efektin uygulanacağı alanın boyutları.
- uActiveTab, uAnimationProgress: Animasyonu ve aktif sekmeyi kontrol etmek için Dart’tan gelen dinamik değerler.
- uGlassColorR, G, B, A: Camın rengini belirlemek için 4 ayrı float.
// layout(location = X) uniform float uDegiskenAdi;
layout(location = 0) uniform float uWidth;
layout(location = 1) uniform float uHeight;
layout(location = 4) uniform float uTabCount;
layout(location = 11) uniform float uLightIntensity;
b. sampler2D out vec4 fragColor: Özel Değişkenler
- Girdi (Input): Rengi hesaplamak için ihtiyaç duyduğu bilgiler. Bu, bir resim, bir doku veya arka plandaki görüntü olabilir.
- Çıktı (Output): Hesaplamaları bittikten sonra GPU’ya teslim ettiği nihai renk değeri.
c. SDF (Signed Distance Function): Şekilleri Matematikle Çizmek
Shader’da “if-else” ile şekil çizmek verimsizdir. Bunun yerine, bir noktanın (p) bir şekle olan en kısa mesafesini hesaplayan matematiksel fonksiyonlar kullanılır. Bu fonksiyonlara SDF denir.
- Mesafe < 0 ise, nokta şeklin içindedir.
- Mesafe = 0 ise, nokta şeklin kenarındadır.
- Mesafe > 0 ise, nokta şeklin dışındadır.
navbarSDF fonksiyonu, temel bir yuvarlak dörtgen ile aktif sekmenin “baloncuk” şeklini smoothUnion ile pürüzsüzce birleştirerek ana şeklimizi oluşturur.
float sdfRoundedRect(vec2 p, vec2 b, float r) { … }
float navbarSDF(vec2 p) { … }
d. main() Fonksiyonu: Her Piksel İçin Çalışan Kalp
Bu fonksiyon, GPU tarafından ekrandaki her bir piksel için ayrı ayrı çalıştırılır. Amacı, o pikselin rengini (fragColor) belirlemektir.
void main() {
// …
float sd = navbarSDF(p); // 1. Bu pikselin şekle mesafesini hesapla
float alpha = 1.0 - smoothstep(-2.0, 2.0, sd); // 2. Mesafeye göre yumuşak bir alpha (görünürlük) değeri oluştur
// … kodun geri kalanı …
fragColor = mix(backgroundColor, glassEffect, alpha); // 3. Son rengi hesapla ve çıkışa ata
}
Tüm bunları öğrendik ve yaptık diyelim Flutter tarafında görünmesi için yeterli değil bu işlemler. Yol yakınken paket kullanmayı düşünebilirsiniz =)
Shader’i Flutter Projemize Entegre Etmek
Teoriyi öğrendik, şimdi pratiğe geçelim. Shader’ı Flutter’da kullanmak için üç temel araca ihtiyacımız var:
- FragmentProgram: Derlenmiş shader’ı tutan nesne.
- CustomPainter: Shader’ı tuvale nasıl çizeceğimizi ve uniform değerlerini nasıl göndereceğimizi belirten sınıf.
- CustomPaint Widget’ı: CustomPainter’ımızı ekranda çalıştıran widget.
Yol yakınken paket kullanmayı düşünebilirsiniz, ancak biz maceramıza devam edelim! =)
frag dosyası için yazımın sonunda eklediğim github repomdan faydalanabilirsiniz!
Adım Adım Flutter Tarafında Kullanımı
Frag dosyamız olduğunu varsayarak devam ediyorum değerli okurlar.
- Klasör Oluşturma: Projenizin ana dizininde shaders adında bir klasör oluşturun.
- Shader Dosyasını Ekleme: Size verdiğim .frag dosyasını shaders/liquid_navbar.frag olarak bu klasörün içine kopyalayın.
- pubspec.yaml’ı Düzenleme: Projemize shader dosyamızı tanıtalım.
flutter:
uses-material-design: true
shaders:
- shaders/liquid_navbar.frag
Ana Ekran (HomePage) ve Temel Yapıyı Oluşturma
Navbar’ımızın “cam” gibi davranabilmesi için, altındaki içeriği görmesi gerekir. Bu yüzden Scaffold’u extendBody: true ile kullanacağız ve ana içeriğimizi bir Stack içine yerleştireceğiz.
main.dart dosyanızın içeriği, verdiğim örnek koda çok benzer şekilde olabilir:
// main.dart
import 'package:flutter/material.dart';
// ... diğer import'lar ve widget'larınız
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget { /* ... */ }
class HomePage extends StatefulWidget { /* ... */ }
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
// Örnek sayfalar
final List<Widget> _pages = [
const HomeTab(),
const SearchTab(),
// ... diğer sekmeler
];
@override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true, // Body'nin navbar'ın arkasına uzanmasını sağlar
backgroundColor: Colors.transparent, // Arka plan efektleri için
body: Stack(
children: [
// 1. Arka Plan (Dinamik bir gradient olabilir)
AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: _pageGradients[_currentIndex], // Aktif sekmeye göre renk
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
// 2. Sayfa İçeriği
IndexedStack(
index: _currentIndex,
children: _pages,
),
// 3. Liquid Glass Navbar'ımız
Positioned(
bottom: 0,
left: 0,
right: 0,
child: LiquidBottomNavbar(
items: _navbarItems, // Kendi item listeniz
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
// ... diğer ayarlar
),
),
],
),
);
}
RenderObject ve Widget Katmanlarını Oluşturma
Bu mimaride, görevleri ayıran üç ana sınıfımız olacak:
- LiquidBottomNavbar (StatelessWidget): Kullanıcının göreceği ve kullanacağı ana widget. Görevi, render katmanına gerekli parametreleri (items, currentIndex vb.) iletmektir.
- _RenderObjectCreator (LeafRenderObjectWidget): RenderObject’imizi oluşturan ve güncelleyen özel bir widget. Bu, Flutter’a “Hey, standart bir kutu yerine benim özel çizim nesnemi kullan!” demenin yoludur.
- RenderLiquidNavbar (RenderProxyBox): Asıl sihrin gerçekleştiği yer. Bu sınıf, shader’ı alan, animasyonları yöneten ve tuvale çizen render nesnesidir.
// liquid_bottom_navbar.dart
class LiquidBottomNavbar extends StatelessWidget {
final int currentIndex;
final int tabCount;
final LiquidGlassSettings settings;
final Widget child; // İkonlar ve metinler bu child olacak
const LiquidBottomNavbar({
super.key,
required this.currentIndex,
required this.tabCount,
required this.settings,
required this.child,
});
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
// 1. Arka Plan: Shader efektini çizen katman
_RenderObjectCreator(
currentIndex: currentIndex,
tabCount: tabCount,
settings: settings,
),
// 2. Ön Plan: Tıklanabilir ikonlar ve metinler
child,
],
);
}
}
// liquid_bottom_navbar.dart içinde devam
class _RenderObjectCreator extends LeafRenderObjectWidget {
final int currentIndex;
final int tabCount;
final LiquidGlassSettings settings;
const _RenderObjectCreator({
required this.currentIndex,
required this.tabCount,
required this.settings,
});
// Bu, RenderObject'in ilk oluşturulduğu yerdir.
// Shader'ı burada yükleyip Render nesnesine veririz.
@override
RenderObject createRenderObject(BuildContext context) {
// Shader yükleme mantığını basitleştirmek için,
// ana widget'ın state'inde yüklendiğini varsayıyoruz.
final shader = LiquidShaderManager.instance.shader; // Örnek bir singleton yönetici
return RenderLiquidNavbar(
shader: shader,
settings: settings,
ticker: (context as TickerProvider), // TickerProvider'ı context'ten al
currentIndex: currentIndex,
tabCount: tabCount,
);
}
// Widget ağacı güncellendiğinde bu metod çağrılır.
// Render nesnesinin özelliklerini güncelleriz.
@override
void updateRenderObject(BuildContext context, RenderLiquidNavbar renderObject) {
renderObject
..settings = settings
..currentIndex = currentIndex
..tabCount = tabCount;
}
}
// Shader'ı yönetmek için basit bir singleton
class LiquidShaderManager {
LiquidShaderManager._();
static final instance = LiquidShaderManager._();
late ui.FragmentShader shader;
Future<void> loadShader() async {
final program = await ui.FragmentProgram.fromAsset('shaders/liquid_navbar.frag');
shader = program.fragmentShader();
}
}
RenderLiquidNavbar’ın Detayları
// render_liquid_navbar.dart
class RenderLiquidNavbar extends RenderProxyBox {
// ... constructor ve değişkenler ...
// ÖZELLİKLER
final FragmentShader _shader; // Çizim için kullanılacak derlenmiş shader.
AnimationController _animationController; // Animasyonu yönetir.
Ticker _ticker; // Her karede çizimi tetikleyen kalp atışı.
// SETTER'LAR
// Dışarıdan bir parametre (örneğin currentIndex) değiştiğinde,
// updateRenderObject bu setter'ları çağırır.
set currentIndex(int value) {
if (_currentIndex == value) return; // Değişiklik yoksa bir şey yapma.
_currentIndex = value;
_animationController.forward(from: 0.0); // Animasyonu başlat!
markNeedsPaint(); // Flutter'a "Ben değiştim, beni yeniden çiz!" de.
}
// ANİMASYON YÖNETİMİ
void _initializeAnimation() { /* ... */ }
// ATTACH / DETACH
// Bu render nesnesi ekrana eklendiğinde (`attach`) ticker'ı başlatırız.
// Ekrandan kaldırıldığında (`detach`) ise kaynak sızıntısını önlemek için durdururuz.
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_ticker.start();
}
@override
void detach() {
_ticker.stop();
super.detach();
}
// ÇİZİM METODU: PAINT
// Flutter, "Sıra sende, çizimini yap!" dediğinde bu metodu çağırır.
@override
void paint(PaintingContext context, Offset offset) {
if (size.isEmpty) return;
// 1. Güncel Değerleri Al: Zaman ve animasyon ilerlemesi gibi
final now = DateTime.now().millisecondsSinceEpoch / 1000.0;
final animationProgress = _animation.value;
// 2. Shader'a Verileri Gönder: Uniform'ları ayarla
_shader
..setFloat(0, size.width)
..setFloat(1, size.height)
..setFloat(2, _currentIndex.toDouble())
..setFloat(3, animationProgress)
// ... diğer tüm uniform'lar ...
// 3. Çizimi Yap:
// Bu kısım çok önemli. Doğrudan çizim yapmak yerine, bir 'Layer' oluşturuyoruz.
// BackdropFilterLayer, altındaki her şeye verdiğimiz shader'ı uygular.
final rect = offset & size;
final rrect = RRect.fromRectAndRadius(rect, Radius.circular(25));
// pushClipRRect: Efektin sadece yuvarlak köşeli alan içinde kalmasını sağlar.
context.pushClipRRect(needsCompositing, offset, rect, rrect,
(context, offset) {
// pushLayer: Yeni bir çizim katmanı oluşturur.
context.pushLayer(
BackdropFilterLayer(
filter: ImageFilter.shader(source: _shader),
),
(context, offset) {
// super.paint: Eğer bu render nesnesinin bir 'child'ı olsaydı,
// onu çizerdi. Bizim durumumuzda bu gereksiz çünkü child'ı
// Stack ile dışarıdan veriyoruz.
},
offset,
);
},
);
}
}
Ve Son Hepsini Bir Araya Getirmek
main.dart’ta Shader’ı Yükle:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await LiquidShaderManager.instance.loadShader(); // Uygulama başlamadan shader'ı yükle
runApp(const MyApp());
}
HomePage’de Yapıyı Kur:
HomePage’in build metodunda, Stack kullanarak LiquidBottomNavbar’ı ve onun child’ı olarak da _NavbarContent widget’ını (ikonları içeren) yerleştirin.
// HomePage > build
Positioned(
bottom: 0,
left: 0,
right: 0,
child: LiquidBottomNavbar(
currentIndex: _currentIndex,
tabCount: _navbarItems.length,
settings: const LiquidGlassSettings(),
child: _NavbarContent( // İkonları ve metinleri içeren widget
items: _navbarItems,
currentIndex: _currentIndex,
onTap: (index) {
setState(() { _currentIndex = index; });
},
),
),
),
Elimden geldiğince konuyu örneklerle açıklamaya çalıştım. Biraz acemilik atma makalesi oldu ancak faydalı olacağını düşünüyorum.
İlgili github repo linkim aşağıda. Repomu inceleyerek ben ne yapmışım nasıl ilerlemişim inceleyebilirsiniz. Repo hoşunuza giderse yıldız vermeyide değerlendirebilirsiniz =))
Sağlıcakla kalın ve kodlamayla kalın Flutter ile kalın =)