List Detail Scaffold
Bir liste ve içinde açılan öğenin ayrıntısı: yerde ikisine yer varsa yan yana, yoksa ayrıntı listenin yerinde; ayrıca ayrıntının yanında ya da üstünde, scaffold'un kendi yan sheet'inde ekstra bir panel.
import com.pantrixui.components.container.listdetail.PantrixListDetailScaffold
- Kaç panel.
CompactveMediumbir yerde bir panel,Expanded'dan itibaren iki,Large'dan itibaren ekstra panel verilmişse üç. Kitap gibi tutulan bir katlanabilirde her genişlikte iki: liste ve ayrıntı kat yerinde buluşur. - Tek panelde geri. Ayrıntı açıkken geri tuşu, Escape ve iOS'un kenardan kaydırması
back()'i çağırır. Kenardan kaydırırken ayrıntı parmakla birlikte kayar; yarıda bırakılırsa yerine döner. Klavyenin odağı gösterilen panelle gider: listeden açılan öğenin ayrıntısına, geri dönünce de o öğeye. - İki panelde geri uygulamanındır. Liste yanda dururken scaffold geriye dokunmaz; ekranın kendi geri davranışı çalışır.
- Boş ayrıntı. İki panelde açık bir öğe yokken ayrıntının yerinde
emptyDetaildurur. - Panellere söylenen. Her panel
ListDetailScopeiçinde kurulur:isSinglePanetek panelde geri düğmesini,isExtraInSheetekstra panelin başlığını göstermeyi seçtirir;paneCountkaç panele yer olduğunu söyler. - Ekstra panel.
extraverilmişseshowExtra()onu açar: üç panele yer varsa ayrıntının yanında, yoksa iskelenin kendi yerinde, ayrıntının üstüne sondan açılan bir sheet'te, başlığıextraTitle. Klavyenin odağı sheet'e girer ve o kapanana kadar içinde kalır. İki yerde de aynı kompozisyondur: yer değişince hatırladığı kalır. - Her panel bir pencere. Panelin içindeki her şey panelin genişliğini pencere olarak okur:
PantrixTheme.window, sınıfları vePantrixTheme.layoutpanele göre cevap verir. Duruş pencerenin kalır. - Panellerin adı.
listTitle,detailTitleveextraTitlepanelleri ekran okuyucuya adlarıyla tanıtır; Android'de TalkBack gelen panelin adını söyler. - Listenin genişliği.
resizable = truepanellerin arasına bir tutamaç koyar: sürüklenir, ok tuşlarıyla ayarlanır, ekran okuyucu değerini değiştirebilir. GenişlikListDetailState.listWidth'te kalır. - Durum korunur. Yer değişip bir panelden ikiye geçilince liste ve ayrıntı kompozisyonda yerinde kalır: kaydırma yeri, yazılan metin kaybolmaz.
- Hareketi azalt. Sistem daha az hareket istiyorsa ayrıntı kaymadan açılır ve kapanır.
- Okuma yönü. Sağdan sola bir dilde liste sağdadır; ayrıntı soldan kayarak gelir.
Örnekler
Gelen kutusu
En sık kullanılan biçim: bir ileti listesi ve açılan ileti. Liste bir iletiyi inbox.select(id) ile açar; ayrıntıya açılan iletinin anahtarı verilir.
Karttaki pencere seçicisinden "Dar"ı seç ve bir iletiye bas: ileti listenin yerini alır. Başındaki geri düğmesi, Android'in geri tuşu, masaüstünde Escape ve iOS'ta ekranın kenarından kaydırma listeye döndürür. Geri düğmesi yalnızca tek panelde durur: ayrıntı onu isSinglePane'e bakarak gösterir. listTitle ile detailTitle panelleri ekran okuyucuya tanıtır: TalkBack ileti açılınca konusunu, listeye dönülünce "Gelen kutusu"nu söyler.
"Kısa", "Geniş" ya da "Kitap"ı seç: liste ve ileti yan yana durur, açık bir ileti yokken sağda emptyDetail görünür. Bir iletiyi açıp "Dar" ile "Geniş" arasında geç: açılan ileti de listenin kaydırma yeri de kalır.
val inbox = rememberListDetailState()
PantrixListDetailScaffold(
state = inbox,
list = {
MailList(opened = inbox.selectedKey, onOpen = { id -> inbox.select(id) })
},
detail = { id ->
MailDetail(
mail = MAIL.first { it.id == id },
// Tek panelde ileti listenin yerindedir: geri düğmesi yalnızca o zaman durur.
onBack = if (isSinglePane) ({ inbox.back() }) else null
)
},
modifier = Modifier.fillMaxWidth().height(EXAMPLE_HEIGHT),
emptyDetail = { Placeholder(text = "Okumak için bir ileti seç", icon = PantrixIcons.Inbox, tagKey = "inbox") },
listTitle = "Gelen kutusu",
detailTitle = { id -> MAIL.first { it.id == id }.subject },
tagKey = "inbox"
)
@Composable
private fun MailList(
opened: String?,
onOpen: (String) -> Unit
) {
PaneSurface {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
MAIL.forEachIndexed { index, mail ->
if (index > 0) PantrixHorizontalDivider()
ListRow(
title = mail.from,
subtitle = mail.subject,
selected = mail.id == opened,
onClick = { onOpen(mail.id) },
tagKey = "mail_${mail.id}"
)
}
}
}
}
@Composable
private fun MailDetail(
mail: DemoMail,
onBack: (() -> Unit)?
) {
PaneSurface {
Column(modifier = Modifier.fillMaxSize()) {
PantrixTopBar(
title = mail.subject,
navigationIcon = onBack?.let { back -> { PantrixBackButton(onClick = back, tagKey = "mail_back") } },
tagKey = "mail"
)
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(PantrixTheme.spacing.lg),
verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
PantrixText(
text = mail.from,
style = PantrixTheme.typography.footnote,
color = PantrixTheme.colors.textSecondary
)
PantrixText(text = mail.body, tagKey = "mail_body")
}
}
}
}
@Composable
private fun Placeholder(
text: String,
icon: ImageVector,
tagKey: String
) {
PaneSurface {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
PantrixIcon(icon, contentDescription = null, size = 40.dp, tint = PantrixTheme.colors.textSecondary)
PantrixText(text = text, color = PantrixTheme.colors.textSecondary, tagKey = "${tagKey}_placeholder")
}
}
}
private val MAIL = listOf(
DemoMail("1", "Elif Kaya", "Yarınki toplantı", "Toplantıyı saat 10'a aldım. Sunumu bir gün önce paylaşır mısın?"),
DemoMail("2", "Can Demir", "Sürüm notları", "1.4 sürümünün notlarını ekledim. Eksik gördüğün bir şey olursa yaz."),
DemoMail("3", "Ayşe Yılmaz", "Tasarım incelemesi", "Yeni ayarlar ekranının taslakları hazır; perşembe bakalım mı?"),
DemoMail("4", "Mert Şahin", "Fatura", "Ekim faturası ektedir. Ödeme ayın 30'una kadar yapılabilir."),
DemoMail("5", "Zeynep Arslan", "Test sonuçları", "Gece çalışan testlerin hepsi geçti; tablette bir uyarı var."),
DemoMail("6", "Deniz Aydın", "Öğle yemeği", "Cuma günü ekipçe dışarıda yiyelim mi? Yer önerisi olan yazsın."),
DemoMail("7", "Burak Çelik", "Erişilebilirlik", "Ekran okuyucuyla bütün akışı denedim; iki düğmenin adı eksik."),
DemoMail("8", "Selin Koç", "Yeni katılan", "Pazartesi ekibe yeni biri katılıyor; ilk haftayı planlayalım.")
)
private val EXAMPLE_HEIGHT = 460.dp
@Composable
private fun ListRow(
title: String,
subtitle: String?,
selected: Boolean,
onClick: () -> Unit,
tagKey: String,
initials: String? = null,
icon: ImageVector? = null
) {
val colors = PantrixTheme.colors
val spacing = PantrixTheme.spacing
val ink = if (selected) colors.primary.onContainer else Color.Unspecified
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(selected = selected, onClick = onClick)
.background(if (selected) colors.primary.container else Color.Transparent)
.padding(spacing.md),
horizontalArrangement = Arrangement.spacedBy(spacing.md),
verticalAlignment = Alignment.CenterVertically
) {
if (initials != null) PantrixAvatar(contentDescription = null, initials = initials)
if (icon != null) PantrixIcon(icon, contentDescription = null, tint = ink)
Column(verticalArrangement = Arrangement.spacedBy(spacing.xs)) {
PantrixText(
text = title,
style = PantrixTheme.typography.callout,
color = ink,
maxLines = 1,
tagKey = tagKey
)
if (subtitle != null) {
PantrixText(
text = subtitle,
style = PantrixTheme.typography.footnote,
color = if (selected) ink else colors.textSecondary,
maxLines = 1
)
}
}
}
}
@Composable
private fun PaneSurface(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit
) {
PantrixSurface(modifier = modifier.fillMaxSize(), border = PantrixTheme.colors.border) { content() }
}
private class DemoMail(
val id: String,
val from: String,
val subject: String,
val body: String
)
Ayarlar: iki panelde ilk bölüm açık
Ayarlar ekranı: solda bölümler, sağda seçilen bölümün ayarları. İki panelde boş bir ayrıntı yerine ilk bölüm açık durur; tek panelde ise önce bölümlerin listesi görünür.
Bunun için emptyDetail bir şey çizmez, ilk bölümü açar: LaunchedEffect(Unit) { settings.select(...) }. Boş ayrıntı yalnızca iki panelde gösterildiği için bu tek panelde hiç çalışmaz.
Ayarların değerleri ayrıntıda değil, örnekte tutulur: values. Her bölüm açıldığında yeniden kurulur; değerleri ayrıntının içinde hatırlansaydı başka bir bölüme geçip dönünce kaybolurdu. "Dar"da bir anahtarı değiştir, geri dön, bölümü yeniden aç: değer yerindedir.
val settings = rememberListDetailState()
val values = remember { mutableStateMapOf<String, Boolean>() }
PantrixListDetailScaffold(
state = settings,
list = {
SectionList(chosen = settings.selectedKey, onChoose = { key -> settings.select(key) })
},
detail = { key ->
val section = SECTIONS.first { it.key == key }
SettingsPage(
section = section,
isOn = { option -> values[option] ?: false },
onChange = { option, on -> values[option] = on },
onBack = if (isSinglePane) ({ settings.back() }) else null
)
},
modifier = Modifier.fillMaxWidth().height(EXAMPLE_HEIGHT),
// İki panelde boş ayrıntı yerine ilk bölüm açılır. Tek panelde boş ayrıntı gösterilmez: liste kalır.
emptyDetail = { LaunchedEffect(Unit) { settings.select(SECTIONS.first().key) } },
tagKey = "settings"
)
@Composable
private fun SectionList(
chosen: String?,
onChoose: (String) -> Unit
) {
PaneSurface {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
SECTIONS.forEach { section ->
ListRow(
title = section.title,
subtitle = "${section.options.size} ayar",
selected = section.key == chosen,
onClick = { onChoose(section.key) },
tagKey = "section_${section.key}"
)
}
}
}
}
@Composable
private fun SettingsPage(
section: SettingsSection,
isOn: (String) -> Boolean,
onChange: (String, Boolean) -> Unit,
onBack: (() -> Unit)?
) {
PaneSurface {
Column(modifier = Modifier.fillMaxSize()) {
PantrixTopBar(
title = section.title,
navigationIcon = onBack?.let { back ->
{ PantrixBackButton(onClick = back, tagKey = "settings_back") }
},
tagKey = "settings"
)
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(PantrixTheme.spacing.lg),
verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)
) {
section.options.forEach { option ->
PantrixSwitchButton(
checked = isOn(option),
onCheckedChange = { onChange(option, it) },
modifier = Modifier.fillMaxWidth(),
label = option,
tagKey = "option_${section.key}_${section.options.indexOf(option)}"
)
}
}
}
}
}
private val SECTIONS = listOf(
SettingsSection("general", "Genel", listOf("Açılışta son sayfayı göster", "Güncellemeleri kendisi kursun")),
SettingsSection("notifications", "Bildirimler", listOf("Yeni iletiler", "Takvim hatırlatmaları", "Sesli uyarı")),
SettingsSection("privacy", "Gizlilik", listOf("Kullanım verisi gönder", "Konumu paylaş")),
SettingsSection("display", "Görünüm", listOf("Koyu tema", "Daha az hareket", "Büyük yazı"))
)
private val EXAMPLE_HEIGHT = 460.dp
@Composable
private fun ListRow(
title: String,
subtitle: String?,
selected: Boolean,
onClick: () -> Unit,
tagKey: String,
initials: String? = null,
icon: ImageVector? = null
) {
val colors = PantrixTheme.colors
val spacing = PantrixTheme.spacing
val ink = if (selected) colors.primary.onContainer else Color.Unspecified
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(selected = selected, onClick = onClick)
.background(if (selected) colors.primary.container else Color.Transparent)
.padding(spacing.md),
horizontalArrangement = Arrangement.spacedBy(spacing.md),
verticalAlignment = Alignment.CenterVertically
) {
if (initials != null) PantrixAvatar(contentDescription = null, initials = initials)
if (icon != null) PantrixIcon(icon, contentDescription = null, tint = ink)
Column(verticalArrangement = Arrangement.spacedBy(spacing.xs)) {
PantrixText(
text = title,
style = PantrixTheme.typography.callout,
color = ink,
maxLines = 1,
tagKey = tagKey
)
if (subtitle != null) {
PantrixText(
text = subtitle,
style = PantrixTheme.typography.footnote,
color = if (selected) ink else colors.textSecondary,
maxLines = 1
)
}
}
}
}
@Composable
private fun PaneSurface(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit
) {
PantrixSurface(modifier = modifier.fillMaxSize(), border = PantrixTheme.colors.border) { content() }
}
private class SettingsSection(
val key: String,
val title: String,
val options: List<String>
)
Kişiler ve yanlarında geçmişleri
Bir kişinin yanında üçüncü bir panel: geçmişi. Ayrıntıdaki "Geçmiş" düğmesi contacts.showExtra() ile onu açar.
"Geniş"i seç: üç panele yer vardır ve geçmiş ayrıntının yanında, sonda durur; başlığı ve kapatma düğmesi kendi içindedir. "Kısa"yı seç: iki panele yer vardır, geçmiş iskelenin içinde, ayrıntının üstüne sondan açılan bir sheet'te görünür. Sheet'in başlığı extraTitle'dır ve kapatma düğmesi sheet'indir; panel kendi başlığını bu yüzden isExtraInSheet'e bakarak göstermez. Scrim'e basmak, geri tuşu ve Escape sheet'i kapatır. "Dar"da da sheet'te açılır.
Sheet açıkken geçmişi kaydır, sonra "Geniş"e geç: geçmiş ayrıntının yanına geçer ve kaydırma yeri kalır. Panel iki yerde de aynı kompozisyondur.
Bir kişiden ötekine geçince geçmiş o kişinin olur; listeye dönülünce, back(), geçmiş de kapanır.
val contacts = rememberListDetailState()
PantrixListDetailScaffold(
state = contacts,
list = {
ContactList(opened = contacts.selectedKey, onOpen = { id -> contacts.select(id) })
},
detail = { id ->
ContactCard(
contact = CONTACTS.first { it.id == id },
onHistory = { contacts.showExtra() },
onBack = if (isSinglePane) ({ contacts.back() }) else null
)
},
modifier = Modifier.fillMaxWidth().height(EXAMPLE_HEIGHT),
emptyDetail = { Placeholder(text = "Bir kişi seç", icon = PantrixIcons.Person, tagKey = "contacts") },
extra = { id ->
History(
contact = CONTACTS.first { it.id == id },
// Sheet'te başlık ve kapatma düğmesi sheet'indir.
onClose = if (isExtraInSheet) null else ({ contacts.hideExtra() })
)
},
listTitle = "Kişiler",
detailTitle = { id -> CONTACTS.first { it.id == id }.name },
extraTitle = "Geçmiş",
tagKey = "contacts"
)
@Composable
private fun ContactList(
opened: String?,
onOpen: (String) -> Unit
) {
PaneSurface {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
CONTACTS.forEach { contact ->
ListRow(
title = contact.name,
subtitle = contact.role,
selected = contact.id == opened,
onClick = { onOpen(contact.id) },
initials = contact.initials,
tagKey = "contact_${contact.id}"
)
}
}
}
}
@Composable
private fun ContactCard(
contact: DemoContact,
onHistory: () -> Unit,
onBack: (() -> Unit)?
) {
val spacing = PantrixTheme.spacing
PaneSurface {
Column(modifier = Modifier.fillMaxSize()) {
PantrixTopBar(
title = contact.name,
navigationIcon = onBack?.let { back -> { PantrixBackButton(onClick = back, tagKey = "contact_back") } },
tagKey = "contact"
)
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(spacing.lg),
verticalArrangement = Arrangement.spacedBy(spacing.md)
) {
PantrixAvatar(contentDescription = null, initials = contact.initials, size = 64.dp)
PantrixText(text = contact.role, color = PantrixTheme.colors.textSecondary)
PantrixText(text = contact.email)
PantrixButton(
text = "Geçmiş",
onClick = onHistory,
variant = ButtonVariant.Outline,
leadingIcon = { PantrixIcon(PantrixIcons.Clock, contentDescription = null) },
tagKey = "history"
)
}
}
}
}
@Composable
private fun History(
contact: DemoContact,
onClose: (() -> Unit)?
) {
val lines = @Composable {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(PantrixTheme.spacing.lg),
verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)
) {
contact.history.forEach { line -> PantrixText(text = line, tagKey = "history_line") }
}
}
if (onClose == null) {
lines()
} else {
PaneSurface {
Column(modifier = Modifier.fillMaxSize()) {
PantrixTopBar(
title = "Geçmiş",
actions = {
PantrixIconButton(
imageVector = PantrixIcons.Close,
contentDescription = "Geçmişi kapat",
onClick = onClose,
tagKey = "history_close"
)
},
tagKey = "history"
)
lines()
}
}
}
}
@Composable
private fun Placeholder(
text: String,
icon: ImageVector,
tagKey: String
) {
PaneSurface {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
PantrixIcon(icon, contentDescription = null, size = 40.dp, tint = PantrixTheme.colors.textSecondary)
PantrixText(text = text, color = PantrixTheme.colors.textSecondary, tagKey = "${tagKey}_placeholder")
}
}
}
private val CONTACTS = listOf(
DemoContact(
"elif",
"Elif Kaya",
"EK",
"Ürün yöneticisi",
"elif@example.com",
listOf("Dün: yol haritası toplantısı", "Pazartesi: sürüm planı gönderildi", "Geçen hafta: kullanıcı görüşmesi")
),
DemoContact(
"can",
"Can Demir",
"CD",
"Geliştirici",
"can@example.com",
listOf("Bugün: kod incelemesi", "Dün: hata kaydı kapatıldı")
),
DemoContact(
"ayse",
"Ayşe Yılmaz",
"AY",
"Tasarımcı",
"ayse@example.com",
listOf("Bugün: ayarlar ekranının taslağı", "Salı: renk paleti onaylandı", "Geçen ay: tasarım sistemi")
),
DemoContact(
"mert",
"Mert Şahin",
"MŞ",
"Muhasebe",
"mert@example.com",
listOf("Bu ay: ekim faturası", "Geçen ay: bütçe raporu")
)
)
private val EXAMPLE_HEIGHT = 460.dp
@Composable
private fun ListRow(
title: String,
subtitle: String?,
selected: Boolean,
onClick: () -> Unit,
tagKey: String,
initials: String? = null,
icon: ImageVector? = null
) {
val colors = PantrixTheme.colors
val spacing = PantrixTheme.spacing
val ink = if (selected) colors.primary.onContainer else Color.Unspecified
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(selected = selected, onClick = onClick)
.background(if (selected) colors.primary.container else Color.Transparent)
.padding(spacing.md),
horizontalArrangement = Arrangement.spacedBy(spacing.md),
verticalAlignment = Alignment.CenterVertically
) {
if (initials != null) PantrixAvatar(contentDescription = null, initials = initials)
if (icon != null) PantrixIcon(icon, contentDescription = null, tint = ink)
Column(verticalArrangement = Arrangement.spacedBy(spacing.xs)) {
PantrixText(
text = title,
style = PantrixTheme.typography.callout,
color = ink,
maxLines = 1,
tagKey = tagKey
)
if (subtitle != null) {
PantrixText(
text = subtitle,
style = PantrixTheme.typography.footnote,
color = if (selected) ink else colors.textSecondary,
maxLines = 1
)
}
}
}
}
@Composable
private fun PaneSurface(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit
) {
PantrixSurface(modifier = modifier.fillMaxSize(), border = PantrixTheme.colors.border) { content() }
}
private class DemoContact(
val id: String,
val name: String,
val initials: String,
val role: String,
val email: String,
val history: List<String>
)
Dosyalar: listenin genişliği sürüklenir
Listenin genişliği kullanıcının elinde: resizable = true panellerin arasına bir tutamaç koyar. Fareyle ya da parmakla sürükle; klavyede Tab ile tutamaca gel, ok tuşları 16 dp'lik adımlarla, Home ve End en dar ve en geniş hâline götürür. Ekran okuyucu tutamacı "Liste genişliği" diye okur ve değerini değiştirebilir.
Genişlik files.listWidth'te durur; ayrıntıdaki satır onu yazar, "Varsayılana dön" Dp.Unspecified ile stilin genişliğine döndürür. Stil burada liste için 280 dp ve en az 200 dp ister: ListDetailScaffoldStyle(listWidth = 280.dp, minListWidth = 200.dp). Liste, ayrıntıya en az 320 dp bırakacak kadar genişleyebilir.
Tutamaç yalnızca iki panelde durur: "Kısa" ya da "Geniş"i seç.
val files = rememberListDetailState(initialSelectedKey = FOLDERS.first().name)
PantrixListDetailScaffold(
state = files,
list = {
FolderList(opened = files.selectedKey, onOpen = { name -> files.select(name) })
},
detail = { name ->
FolderContents(
folder = FOLDERS.first { it.name == name },
listWidth = files.listWidth,
onResetWidth = { files.listWidth = Dp.Unspecified },
onBack = if (isSinglePane) ({ files.back() }) else null
)
},
modifier = Modifier.fillMaxWidth().height(EXAMPLE_HEIGHT),
resizable = true,
style = ListDetailScaffoldStyle(listWidth = 280.dp, minListWidth = 200.dp),
tagKey = "files"
)
@Composable
private fun FolderList(
opened: String?,
onOpen: (String) -> Unit
) {
PaneSurface {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
FOLDERS.forEach { folder ->
ListRow(
title = folder.name,
subtitle = "${folder.files.size} dosya",
selected = folder.name == opened,
onClick = { onOpen(folder.name) },
icon = PantrixIcons.Folder,
tagKey = "folder_${FOLDERS.indexOf(folder)}"
)
}
}
}
}
@Composable
private fun FolderContents(
folder: DemoFolder,
listWidth: Dp,
onResetWidth: () -> Unit,
onBack: (() -> Unit)?
) {
val spacing = PantrixTheme.spacing
PaneSurface {
Column(modifier = Modifier.fillMaxSize()) {
PantrixTopBar(
title = folder.name,
navigationIcon = onBack?.let { back -> { PantrixBackButton(onClick = back, tagKey = "folder_back") } },
tagKey = "folder"
)
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(spacing.lg),
verticalArrangement = Arrangement.spacedBy(spacing.sm)
) {
PantrixText(
text = if (listWidth.isSpecified) {
"Listenin genişliği: ${listWidth.value.roundToInt()} dp"
} else {
"Listenin genişliği: stilinki"
},
style = PantrixTheme.typography.footnote,
color = PantrixTheme.colors.textSecondary,
tagKey = "list_width"
)
PantrixButton(
text = "Varsayılana dön",
onClick = onResetWidth,
variant = ButtonVariant.Text,
enabled = listWidth.isSpecified,
tagKey = "reset_width"
)
folder.files.forEach { file ->
Row(
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
PantrixIcon(PantrixIcons.File, contentDescription = null)
PantrixText(text = file)
}
}
}
}
}
}
private val FOLDERS = listOf(
DemoFolder("Belgeler", listOf("Sözleşme.pdf", "Toplantı notları.txt", "Bütçe 2026.xlsx")),
DemoFolder("Fotoğraflar", listOf("Tatil 01.jpg", "Tatil 02.jpg", "Ekip fotoğrafı.png", "Logo.svg")),
DemoFolder("Müzik", listOf("Çalma listesi.m3u", "Podcast bölümü.mp3")),
DemoFolder("İndirilenler", listOf("Kurulum.dmg", "Rapor taslağı.docx"))
)
private val EXAMPLE_HEIGHT = 460.dp
@Composable
private fun ListRow(
title: String,
subtitle: String?,
selected: Boolean,
onClick: () -> Unit,
tagKey: String,
initials: String? = null,
icon: ImageVector? = null
) {
val colors = PantrixTheme.colors
val spacing = PantrixTheme.spacing
val ink = if (selected) colors.primary.onContainer else Color.Unspecified
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(selected = selected, onClick = onClick)
.background(if (selected) colors.primary.container else Color.Transparent)
.padding(spacing.md),
horizontalArrangement = Arrangement.spacedBy(spacing.md),
verticalAlignment = Alignment.CenterVertically
) {
if (initials != null) PantrixAvatar(contentDescription = null, initials = initials)
if (icon != null) PantrixIcon(icon, contentDescription = null, tint = ink)
Column(verticalArrangement = Arrangement.spacedBy(spacing.xs)) {
PantrixText(
text = title,
style = PantrixTheme.typography.callout,
color = ink,
maxLines = 1,
tagKey = tagKey
)
if (subtitle != null) {
PantrixText(
text = subtitle,
style = PantrixTheme.typography.footnote,
color = if (selected) ink else colors.textSecondary,
maxLines = 1
)
}
}
}
}
@Composable
private fun PaneSurface(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit
) {
PantrixSurface(modifier = modifier.fillMaxSize(), border = PantrixTheme.colors.border) { content() }
}
private class DemoFolder(
val name: String,
val files: List<String>
)
Katlanan cihazda: kat yerinde buluşur
Katlanan bir cihazda liste ve ayrıntı kat yerinde buluşur. Her panel kendi ölçüsünü ve kendi pencere sınıfını yazar; üstündeki satır ona söyleneni gösterir. Panel kendi penceresidir: "Kitap"ta pencere Expanded'dır ama 456 dp'lik iki panelin her biri Compact okur.
"Kitap"ı seç: 951 dp'lik pencere zaten iki panellik genişliktedir, ama liste 360 dp değil, kat yerine kadardır. Aradaki boşluk kat yerinin iki yanındaki 20'şer dp'lik şerittir; hiçbir şey kat yerinin üstüne düşmez. Daha dar bir katlanabilir de kitap gibi tutulunca iki panel alır. Tutamaç yoktur: panelleri kat yeri yerleştirir.
"Masa"yı seç: kat yeri yataydır ve yan yana iki paneli ayırmaz. 669 dp'lik pencere Medium'dur: tek panel.
val notes = rememberListDetailState(initialSelectedKey = NOTES.first())
PantrixListDetailScaffold(
state = notes,
list = {
Column(modifier = Modifier.fillMaxSize()) {
Told(scope = this@PantrixListDetailScaffold, tagKey = "notes_told_list")
MeasuredList(opened = notes.selectedKey, onOpen = { note -> notes.select(note) })
}
},
detail = { note ->
Column(modifier = Modifier.fillMaxSize()) {
Told(scope = this@PantrixListDetailScaffold, tagKey = "notes_told_detail")
MeasuredNote(note = note, onBack = if (isSinglePane) ({ notes.back() }) else null)
}
},
modifier = Modifier.fillMaxWidth().height(EXAMPLE_HEIGHT),
resizable = true,
tagKey = "notes"
)
@Composable
private fun Told(
scope: ListDetailScope,
tagKey: String
) {
PantrixText(
text = "paneCount: ${scope.paneCount} · isSinglePane: ${if (scope.isSinglePane) "evet" else "hayır"}",
modifier = Modifier.padding(PantrixTheme.spacing.md),
style = PantrixTheme.typography.footnote,
color = PantrixTheme.colors.textSecondary,
tagKey = tagKey
)
}
@Composable
private fun MeasuredList(
opened: String?,
onOpen: (String) -> Unit
) {
Measured(name = "Liste", tagKey = "notes_list_size") {
Column {
NOTES.forEach { note ->
ListRow(
title = note,
subtitle = null,
selected = note == opened,
onClick = { onOpen(note) },
tagKey = "note_${NOTES.indexOf(note)}"
)
}
}
}
}
@Composable
private fun MeasuredNote(
note: String,
onBack: (() -> Unit)?
) {
PaneSurface {
Column(modifier = Modifier.fillMaxSize()) {
PantrixTopBar(
title = note,
navigationIcon = onBack?.let { back -> { PantrixBackButton(onClick = back, tagKey = "note_back") } },
tagKey = "note"
)
Measured(name = "Ayrıntı", tagKey = "notes_detail_size") {}
}
}
}
private val NOTES = listOf("Alışveriş listesi", "Kitap önerileri", "Tatil planı", "Toplantı notları")
private val EXAMPLE_HEIGHT = 460.dp
@Composable
private fun Measured(
name: String,
tagKey: String,
content: @Composable () -> Unit
) {
var size by remember { mutableStateOf(IntSize.Zero) }
val density = LocalDensity.current
val words = with(density) { "${size.width.toDp().value.roundToInt()} × ${size.height.toDp().value.roundToInt()}" }
PaneSurface(modifier = Modifier.onSizeChanged { size = it }) {
Column(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.weight(1f)) { content() }
// Ölçüler soldan sağa yazılır: sağdan sola bir paragrafta genişlik ile yükseklik yer değiştirmiş okunurdu.
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
PantrixText(
// Panel kendi penceresidir: sınıfı panelin genişliğine göredir, pencereninkine değil.
text = "$name: $words dp · ${PantrixTheme.window.widthClass.name}",
modifier = Modifier.padding(PantrixTheme.spacing.md),
style = PantrixTheme.typography.footnote,
color = PantrixTheme.colors.textSecondary,
tagKey = tagKey
)
}
}
}
}
@Composable
private fun ListRow(
title: String,
subtitle: String?,
selected: Boolean,
onClick: () -> Unit,
tagKey: String,
initials: String? = null,
icon: ImageVector? = null
) {
val colors = PantrixTheme.colors
val spacing = PantrixTheme.spacing
val ink = if (selected) colors.primary.onContainer else Color.Unspecified
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(selected = selected, onClick = onClick)
.background(if (selected) colors.primary.container else Color.Transparent)
.padding(spacing.md),
horizontalArrangement = Arrangement.spacedBy(spacing.md),
verticalAlignment = Alignment.CenterVertically
) {
if (initials != null) PantrixAvatar(contentDescription = null, initials = initials)
if (icon != null) PantrixIcon(icon, contentDescription = null, tint = ink)
Column(verticalArrangement = Arrangement.spacedBy(spacing.xs)) {
PantrixText(
text = title,
style = PantrixTheme.typography.callout,
color = ink,
maxLines = 1,
tagKey = tagKey
)
if (subtitle != null) {
PantrixText(
text = subtitle,
style = PantrixTheme.typography.footnote,
color = if (selected) ink else colors.textSecondary,
maxLines = 1
)
}
}
}
}
@Composable
private fun PaneSurface(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit
) {
PantrixSurface(modifier = modifier.fillMaxSize(), border = PantrixTheme.colors.border) { content() }
}
API
state
Bir liste ve içinde açılan öğenin ayrıntısı: yerde ikisine yer varsa yan yana, yoksa ayrıntı listenin yerinde; ayrıca ayrıntının yanında ya da üstünde, scaffold'un kendi yan sheet'inde ekstra bir panel.
| Parametre | Tür | Varsayılan | Ne olduğu |
|---|---|---|---|
state | ListDetailState | açık olan: rememberListDetailState onu yapılandırma değişikliğinde korur. | |
list | @Composable ListDetailScope.() -> Unit | liste. Bir öğeyi ListDetailState.select ile açar. | |
detail | @Composable ListDetailScope.(key: String) -> Unit | anahtarı verilen öğenin ayrıntısı. Yeni bir anahtar onu yeniden kurar. | |
modifier | Modifier | Modifier | panellerin paylaştığı yer. |
emptyDetail | (@Composable ListDetailScope.() -> Unit)? | null | açık bir öğe yokken listenin yanındaki ayrıntı panelinde gösterilen: "Bir ileti seçin". Verilmezse orada bir şey gösterilmez. |
extra | (@Composable ListDetailScope.(key: String) -> Unit)? | null | açık öğe için ayrıntısının yanında üçüncü bir panel: kişinin geçmişi, bir dosyanın özellikleri. ListDetailState.showExtra çağrılınca gösterilir. |
listTitle | String? | null | listenin adı, görünür olurken ekran okuyucu için: "Gelen kutusu". |
detailTitle | ((key: String) -> String)? | null | anahtarı verilen öğenin ayrıntısının adı, görünür olurken ekran okuyucu için: iletinin konusu. |
extraTitle | String? | null | ekstra panelin adı: ayrıntının üstünde gösterildiği yan sheet'in başlığı, ve görünür olurken ekran okuyucu için. |
resizable | Boolean | false | liste ile ayrıntı arasında listeyi genişletip daraltan bir tutamaç olup olmadığı. |
style | ListDetailScaffoldStyle? | null | scaffold'un genişlikleri, temanınkilerin üstünde. |
tagKey | String? | null | scaffold'un otomasyon etiketlerinin anahtarı. |
ListDetailScaffoldStyle
PantrixListDetailScaffold'un ölçüleri: liste ve ekstra panel ayrıntının yanında ne kadar geniştir, tutamacı sürüklemek listeyi ve ayrıntıyı en çok ne kadar daraltabilir, ve tutamacın kendisi.
| Parametre | Tür | Varsayılan | Ne olduğu |
|---|---|---|---|
listWidth | Dp | Dp.Unspecified | Tutamacı sürüklenene kadar listenin ayrıntının yanındaki genişliği: bir telefonun genişliği, 360dp. |
minListWidth | Dp | Dp.Unspecified | Tutamacı sürüklemenin listeyi en çok daraltabildiği genişlik. |
minDetailWidth | Dp | Dp.Unspecified | Ayrıntının listenin yanında en çok daralabildiği genişlik: tutamacın sürüklenmesi ayrıntının daha dar kalacağı yerde durur, durumda ayarlanan bir liste genişliği de. |
extraWidth | Dp | Dp.Unspecified | Üç panele yer olduğunda ekstra panelin ayrıntının yanındaki genişliği. |
gap | Dp | Dp.Unspecified | Paneller arasında. Hazır ayarlarda belirtilmez: düzenin pencereyle büyüyen sütun arası, PantrixTheme.layout.gutter. Panelleri kenar kenara buluşan, aralarında kendi çizgisi olan bir marka 0dp verir. |
handleWidth | Dp | Dp.Unspecified | Genişliği değişebilen bir listenin tutamacının boşlukta çizdiği işaret: kısa, dik bir çubuk. |
handleHeight | Dp | Dp.Unspecified | Tutamacın dik çubuğunun ne kadar uzun olduğu: ne kadar kalın olduğu handleWidth'tir. |
handleTarget | Dp | Dp.Unspecified | Bir parmağın ya da işaretçinin tutamacı yakaladığı şeridin genişliği; boşluğun ortasında durur, boşluk daha darsa panellerin kenarlarının üstüne taşar: parmakla her platformun en küçük dokunma hedefi, işaretçiyle dar bir şerit. |