Android XR ile F1 Pist Görüntüleyici: Uzamsal Bilişime Yolculuk
Mobil geliştirme dünyası hızla evrilirken, Android XR’ın tanıtımıyla birlikte uygulamaların geleneksel 2D arayüzlerin ötesine geçebildiği yeni bir çağa adım atıyoruz. Bu yazıda, Android XR ve Jetpack Compose kullanarak geliştirdiğim F1 Pist Görüntüleyici uygulamasının yapım sürecini paylaşacağım. Projenin tamamına buradan ulaşabilirsiniz.
Ne Geliştiriyoruz?
AndroidXRF1Tracks uygulamamız, Formula 1 pistlerini hem geleneksel 2D hem de uzamsal XR modlarında gösteriyor. Kullanıcılar bu modlar arasında sorunsuzca geçiş yapabilir, pist düzenlerini sürükleyici bir ortamda keşfederken, ihtiyaç duyduklarında geleneksel mobil arayüzün rahatlığına dönebilirler.
Temel Özellikler
- Çift mod görüntüleme (2D ve Uzamsal XR)
- Etkileşimli F1 pist keşfi
- Akıcı geçişler ve animasyonlar
- Hareket tabanlı kontroller
- Modern Material Design 3 arayüzü
Proje Detayları
Projenin temel XR ve Compose bağımlılıklarını içeren yapılandırma dosyası. Android XR’ın alpha sürümlerini ve gerekli kütüphaneleri tanımlıyor.
Öncelikle, projemizi gerekli bağımlılıklarla kuralım. libs.versions.toml dosyanızda şunlar olmalı:
[versions]
compose = "1.0.0-alpha01"
runtimeVersion = "1.0.0-alpha01"
scenecore = "1.0.0-alpha01"
[libraries]
androidx-compose = { group = "androidx.xr.compose", name = "compose", version.ref = "compose" }
runtime = { group = "androidx.xr.runtime", name = "runtime", version.ref = "runtimeVersion" }
androidx-scenecore = { group = "androidx.xr.scenecore", name = "scenecore", version.ref = "scenecore" }
XR Oturum Yönetimi ve Mod Kontrolü
Uygulamanın çalıştığı cihazın XR yeteneklerini kontrol ederek uygun görüntüleme modunu seçiyor. Spatial UI desteği varsa XR deneyimini, yoksa geleneksel 2D arayüzü kullanıcıya sunuyor. Oturum yönetimi sayesinde kullanıcılar Home Space ve Full Space modları arasında sorunsuz geçiş yapabiliyor.
class MainActivity : ComponentActivity() {
@SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AndroidXRBasicTheme {
val session = LocalSession.current
// Cihazın spatial UI özelliklerini kontrol et
if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
// Spatial UI destekleniyorsa, Subspace içinde MySpatialContent'i göster
Subspace {
MySpatialContent(onRequestHomeSpaceMode = { session?.requestHomeSpaceMode() })
}
} else {
// Spatial UI desteklenmiyorsa, normal 2D içeriği göster
My2DContent(onRequestFullSpaceMode = { session?.requestFullSpaceMode() })
}
}
}
}
}
Animasyon ve İçerik Yönetimi Bileşenleri
F1 pistleri arasındaki geçişleri yöneten animasyonları ve içerik gösterimini kontrol eden temel bileşenleri içeriyor. Slayt animasyonları, pist içeriği gösterimi ve indeks yönetimi gibi kritik fonksiyonları barındırıyor.
/**
* F1 pisti içeriğini gösteren composable fonksiyon
* @param track Gösterilecek F1 pisti
* @param imageHeight Görüntü yüksekliği
*/
@Composable
private fun TrackContent(
track: F1Track,
imageHeight: Dp
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Pist görselini göster
Image(
painter = painterResource(id = track.imageRes),
contentDescription = track.name,
modifier = Modifier
.fillMaxWidth()
.height(imageHeight)
)
// Pist adını göster
Text(
text = track.name,
modifier = Modifier.padding(16.dp),
fontSize = 24.sp
)
}
}
/**
* Pist indeksini güncelleyen yardımcı fonksiyon
* @param currentIndex Mevcut pist indeksi
* @param forward İleri yönde mi (true) yoksa geri yönde mi (false) hareket edileceği
* @param totalTracks Toplam pist sayısı
*/
private fun updateTrackIndex(currentIndex: Int, forward: Boolean, totalTracks: Int): Int =
when {
forward -> (currentIndex + 1) % totalTracks // İleri yönde döngüsel hareket
currentIndex == 0 -> totalTracks - 1 // Başlangıçtayken sona git
else -> currentIndex - 1 // Geri git
}
/**
* Slayt animasyonlarını yöneten yardımcı fonksiyon
* @param targetState Hedef durum indeksi
* @param initialState Başlangıç durum indeksi
* @param durationMillis Animasyon süresi (milisaniye)
*/
private fun getSlideAnimation(
targetState: Int,
initialState: Int,
durationMillis: Int = 500
) = if (targetState > initialState) {
// İleri yönde animasyon (sağdan sola)
(slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(durationMillis = durationMillis)
) + fadeIn(animationSpec = tween(durationMillis))).togetherWith(
slideOutHorizontally(
targetOffsetX = { fullWidth -> -fullWidth },
animationSpec = tween(durationMillis = durationMillis)
) + fadeOut(animationSpec = tween(durationMillis))
)
} else {
// Geri yönde animasyon (soldan sağa)
(slideInHorizontally(
initialOffsetX = { fullWidth -> -fullWidth },
animationSpec = tween(durationMillis = durationMillis)
) + fadeIn(animationSpec = tween(durationMillis))).togetherWith(
slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(durationMillis = durationMillis)
) + fadeOut(animationSpec = tween(durationMillis))
)
}
XR ve 2D Mod İçerik Yönetimi
Bu bölüm, uygulamanın iki farklı görüntüleme modundaki (Spatial UI ve geleneksel 2D) ana içerik bileşenlerini tanımlıyor. Her iki mod için özelleştirilmiş kullanıcı arayüzleri ve kontrol mekanizmaları içeriyor.
/**
* Spatial UI modunda içeriği gösteren ana composable
* @param onRequestHomeSpaceMode Home Space moduna geçiş isteği callback'i
*/
@Composable
fun MySpatialContent(onRequestHomeSpaceMode: () -> Unit) {
// F1 pistlerini ve mevcut pist indeksini al
val f1Tracks = F1TrackRepository.f1Tracks
var currentTrackIndex by rememberF1TrackIndex()
// Spatial panel oluştur
SpatialPanel(SubspaceModifier.width(1280.dp).height(800.dp).resizable().movable()) {
Surface {
// Animasyonlu içerik geçişi
AnimatedContent(
targetState = currentTrackIndex,
transitionSpec = { getSlideAnimation(targetState, initialState) }
) { targetIndex ->
TrackContent(
track = f1Tracks[targetIndex],
imageHeight = 600.dp
)
}
}
// Üst kısımda Home Space modu düğmesi
Orbiter(
position = OrbiterEdge.Top,
offset = EdgeOffset.inner(offset = 20.dp),
alignment = Alignment.End,
shape = SpatialRoundedCornerShape(CornerSize(28.dp))
) {
HomeSpaceModeIconButton(
onClick = onRequestHomeSpaceMode,
modifier = Modifier.size(56.dp)
)
}
// Alt sol kısımda geri düğmesi
Orbiter(
position = OrbiterEdge.Bottom,
offset = EdgeOffset.inner(offset = 20.dp),
alignment = Alignment.Start,
shape = SpatialRoundedCornerShape(CornerSize(28.dp))
) {
BackIconButton(
onClick = { currentTrackIndex = updateTrackIndex(currentTrackIndex, false, f1Tracks.size) },
modifier = Modifier.size(56.dp)
)
}
// Alt sağ kısımda ileri düğmesi
Orbiter(
position = OrbiterEdge.Bottom,
offset = EdgeOffset.inner(offset = 20.dp),
alignment = Alignment.End,
shape = SpatialRoundedCornerShape(CornerSize(28.dp))
) {
NextIconButton(
onClick = { currentTrackIndex = updateTrackIndex(currentTrackIndex, true, f1Tracks.size) },
modifier = Modifier.size(56.dp)
)
}
}
}
/**
* 2D modunda içeriği gösteren ana composable
* @param onRequestFullSpaceMode Full Space moduna geçiş isteği callback'i
*/
@Composable
fun My2DContent(onRequestFullSpaceMode: () -> Unit) {
// F1 pistlerini ve mevcut pist indeksini al
val f1Tracks = F1TrackRepository.f1Tracks
var currentTrackIndex by rememberF1TrackIndex()
// Ana container
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// Animasyonlu içerik geçişi
AnimatedContent(
targetState = currentTrackIndex,
transitionSpec = { getSlideAnimation(targetState, initialState) }
) { targetIndex ->
TrackContent(
track = f1Tracks[targetIndex],
imageHeight = 300.dp
)
}
// Üst sağ köşede Full Space modu düğmesi
IconButton(
onClick = onRequestFullSpaceMode,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(16.dp)
.size(56.dp)
) {
Icon(
painter = painterResource(id = R.drawable.ic_full_space_mode_switch),
contentDescription = stringResource(R.string.switch_to_full_space_mode)
)
}
// Alt sol köşede geri düğmesi
IconButton(
onClick = { currentTrackIndex = updateTrackIndex(currentTrackIndex, false, f1Tracks.size) },
modifier = Modifier
.align(Alignment.BottomStart)
.padding(16.dp)
.size(56.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
// Alt sağ köşede ileri düğmesi
IconButton(
onClick = { currentTrackIndex = updateTrackIndex(currentTrackIndex, true, f1Tracks.size) },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp)
.size(56.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "Next"
)
}
}
}
🎉🎉🎉🎉 Ve bu kadar. Artık projemiz hazır. Kullanmaya başlayabiliriz ❤
Android XR’ın Geleceği ve Projelerdeki Yeri
Android XR, mobil uygulamaların geleceğinde önemli bir rol oynayacak. İşte potansiyel uygulama alanları:
1. Eğitim ve Öğretim
- Sanal sınıflar
- Etkileşimli öğrenme ortamları
- Profesyonel eğitim simülasyonları
2. Eğlence ve Oyun
- Sürükleyici oyun deneyimleri
- Etkileşimli hikaye anlatımı
- Sanal konserler ve etkinlikler
3. Profesyonel Uygulamalar
- Mimari görselleştirme
- Tıbbi eğitim ve görselleştirme
- Mühendislik ve tasarım araçları
4. Perakende ve Ticaret
- Sanal showroomlar
- Ürün görselleştirme
- Etkileşimli alışveriş deneyimler
Proje Çıkarımları ve Öğrenilen Dersler
Bu F1 Pist Görüntüleyici’yi geliştirirken öğrendiğimiz önemli noktalar:
- 2D ve uzamsal deneyimler için tasarım önemi
- XR uygulamalarında performans değerlendirmeleri
- Uzamsal bilişimde kullanıcı deneyimi zorlukları
- Android XR’ın etkileyici uygulamalar yaratma potansiyeli
Sonuç
Android XR, geliştiricilere daha sürükleyici ve etkileyici uygulamalar yaratma fırsatı sunuyor. Teknoloji olgunlaştıkça ve yaygınlaştıkça, çeşitli sektörlerde giderek daha gelişmiş uzamsal bilişim uygulamaları göreceğiz.
F1 Pist Görüntüleyici projesi, geleneksel mobil uygulamaların XR yetenekleriyle nasıl geliştirilebileceğinin pratik bir örneği olarak hizmet ediyor ve kullanıcılara içerikle etkileşime girmenin yeni yollarını sunuyor.
Bir sonraki yazımda görüşmek üzere 👋
Kaynakça: