diff --git a/Flixy/Views/Home/HeroCarousel.swift b/Flixy/Views/Home/HeroCarousel.swift index 4dc4629..053891c 100644 --- a/Flixy/Views/Home/HeroCarousel.swift +++ b/Flixy/Views/Home/HeroCarousel.swift @@ -17,8 +17,16 @@ import MediaKit /// gegeneinander, und die Naht dazwischen wird zum auffälligsten Ding auf dem /// Bildschirm. /// -/// Jetzt rutscht das neue Bild nur ein kleines Stück herein und blendet dabei -/// über das alte. Nichts wandert weit genug, als dass ein Versatz auffiele. +/// Jetzt rutscht das neue Bild nur ein kleines Stück herein und legt sich +/// dabei über das alte. Nichts wandert weit genug, als dass ein Versatz +/// auffiele. +/// +/// **Es wird nicht gekreuzt, sondern überdeckt.** Beim Kreuzblenden sind +/// beide Bilder gleichzeitig halbdurchsichtig — und dazwischen scheint der +/// Untergrund durch, was als helles Aufblitzen des vorigen Titels zu sehen +/// war. Hier bleibt das alte Bild voll deckend liegen, bis das neue darüber +/// undurchsichtig geworden ist; erst dann verschwindet es. Zu jedem Zeitpunkt +/// ist die Fläche gedeckt. struct HeroCarousel: View { let items: [MediaItem] @@ -35,6 +43,13 @@ struct HeroCarousel: View { private static let slide: CGFloat = 32 @State private var shownID: String? + /// Der Titel, der noch darunter liegt und gleich verschwindet. + /// + /// Er wird voll deckend gezeigt, nicht ausgeblendet — das Ausblenden + /// übernimmt das neue Bild, indem es ihn zudeckt. + @State private var fadingID: String? + /// Wie weit das neue Bild schon deckt: 0 beim Beginn, 1 am Ende. + @State private var cover: CGFloat = 1 /// Ob noch von selbst weitergewechselt wird. /// /// Hört auf, sobald jemand die Fläche berührt. Wer hier die Hand hat, @@ -45,8 +60,6 @@ struct HeroCarousel: View { /// das Bild auch von der anderen Seite hereinkommen sehen. @State private var isMovingForward = true - /// Bewegung, die von selbst losgeht, ist für manche Menschen unangenehm - /// bis unbrauchbar. Wer sie abgestellt hat, blättert von Hand. @Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.flixyAccent) private var accent @Environment(FlixySession.self) private var session @@ -58,24 +71,23 @@ struct HeroCarousel: View { var body: some View { VStack(spacing: FlixySpacing.regular) { ZStack { + // Unten: der vorige Titel, unverändert deckend. + if let fadingID, let previous = items.first(where: { $0.id == fadingID }) { + HeroSection(item: previous) + } + + // Oben: der neue, der ihn zudeckt und dabei hereinrutscht. if items.indices.contains(currentIndex) { HeroSection(item: items[currentIndex]) .id(items[currentIndex].id) - .transition(.asymmetric( - insertion: .opacity.combined( - with: .offset(x: isMovingForward ? Self.slide : -Self.slide) - ), - removal: .opacity.combined( - with: .offset(x: isMovingForward ? -Self.slide : Self.slide) - ) - )) + .opacity(cover) + .offset(x: (1 - cover) * (isMovingForward ? Self.slide : -Self.slide)) } } .aspectRatio(FlixyAspectRatio.hero, contentMode: .fit) // Das Bild wechselt, der Platz nicht: ohne das Beschneiden schöbe // das hereinrutschende Bild die Seite um seine Strecke breiter. .clipped() - .animation(.easeInOut(duration: 0.55), value: shownID) // Wischen blättert weiter, jede Berührung hält das Wechseln an — // auch die, die nur ein Antippen wird. Gleichzeitig, damit weder // dem Wischen noch den Knöpfen darunter etwas weggenommen wird. @@ -114,8 +126,20 @@ struct HeroCarousel: View { private func step(by direction: Int) { guard items.count > 1 else { return } let next = (currentIndex + direction + items.count) % items.count + isMovingForward = direction > 0 + fadingID = shownID shownID = items[next].id + cover = 0 + + withAnimation(.easeInOut(duration: 0.55)) { + cover = 1 + } completion: { + // Erst wenn oben nichts mehr durchscheint, darf unten etwas + // verschwinden. Andersherum gäbe es genau den Moment, in dem man + // durch beide hindurch auf den Untergrund sieht. + fadingID = nil + } } private func advance() async {