具有两个视图的 SwiftUI Card Flip 动画,其中一个嵌入在 Stack 中
Posted
技术标签:
【中文标题】具有两个视图的 SwiftUI Card Flip 动画,其中一个嵌入在 Stack 中【英文标题】:SwiftUI Card Flip animation with two views, one of which is embedded within a Stack 【发布时间】:2020-07-06 11:23:20 【问题描述】:我正在尝试在两个视图之间创建“卡片翻转”动画:
视图“A”是CardView
内的LazyVGrid
视图“B”是自定义模式叠加视图
LazyVGrid
和视图“B”在一个 ZStack
中
具体来说,ContentView
的组织方式如下:
var body: some View
ZStack
NavigationView
ScrollView
LazyVGrid(columns: columns, spacing: 10)
ForEach(model.events, id: \.self) event in
SmallCardView(event: event)
.opacity(!showModal || event != modifiableEvent ? 1.0 : 0.0)
.brightness(self.showModal ? -0.1 : 0)
.blur(radius: self.showModal ? 16 : 0)
if self.showModal
AddEventView(
showModal: $showModal,
existingEvent: modifiableEvent,
)
.opacity(showModal ? 1.0 : 0.0)
.padding(.horizontal, 16)
我遇到了this SO 帖子,答案似乎很有希望,但是答案没有考虑到其中一个视图是否在堆栈/网格中,对我来说就是这种情况。所以,我的问题是,如果其中一个视图确实嵌入在堆栈或网格中,我该如何调整链接解决方案以使其按预期工作。
编辑:另外需要注意的是Views的大小和位置不同
我尝试将.modifier(FlipEffect(flipped: $showModal, angle: animate3d ? 180 : 0, axis: (x: 0, y: 1)))
添加到ZStack
和SmallCardView
,但是都没有产生预期的结果。
谢谢!
编辑:为了清楚起见,我想在这两个视图之间以卡片翻转样式制作动画:
【问题讨论】:
【参考方案1】:这个非常简单的结构应该可以帮助您理解所需的必要结构:
为此目的有一个特定的rotation3DEffect
修饰符。
struct ContentView: View
// What is the current status
@State var flipped: Bool = false
// Whats the initial "flip" degree
@State var degrees: Double = 180.0
@State var width: CGFloat = 200
@State var height: CGFloat = 300
var body: some View
ZStack
if flipped
//Cart Back
CardBack(width: self.$width, height: self.$height)
else
//Cart front
CardFront(width: self.$width, height: self.$height)
//Styling
.background(Color.gray)
.cornerRadius(20)
.rotation3DEffect(.degrees(degrees), axis: (x: 0, y: 1, z: 0))
// When tapped turn it around
.onTapGesture
if self.flipped
self.flipped = false
withAnimation
self.degrees += 180
self.width = 200 // add other animated stuff here
self.height = 300
else
self.flipped = true
withAnimation
self.degrees -= 180
self.width = 300 // add other animated stuff here
self.height = 500
struct CardBack: View
@Binding var width: CGFloat
@Binding var height: CGFloat
var body: some View
Rectangle().foregroundColor(Color.red).frame(width: self.width, height: self.height).overlay(Text("Back"))
struct CardFront: View
@Binding var width: CGFloat
@Binding var height: CGFloat
var body: some View
Rectangle().foregroundColor(Color.blue).frame(width: self.width, height: self.height).overlay(Text("Front"))
这会产生以下视图:
【讨论】:
当你两个视图的大小和位置不同时,这并不能很好地工作,在我的情况下它们是 我必须承认我并没有真正看到你的问题。这是一个模型,应该可以帮助您理解这个概念。不实施成品。只需添加其他属性并在 tapGesture 中更改它们。我添加了卡片宽度和高度的示例。 对我的不清楚的地方表示歉意。具体来说,我想要一个网格/堆栈内的视图,以及该堆栈的 顶部 的另一个视图(即,将网格内的视图移动到整个网格之上),而不仅仅是一个视图来回翻转。因此,动画必须从 Grid 内部移动到其上方作为示例,请参阅 ios 14 Widgets 的编辑动画【参考方案2】:当卡片在 LazyVGrid 中使用 .matchedGeometryEffect() 时,我从来没有设法让它正常工作。所以这是我在项目中使用的滥用偏移和缩放的相当混乱的解决方案:
struct TestView: View
@State var flippedCard: Int?
@State var frontCard: Int?
let cards = [1,2,3,4,5,6,7,8,9,10]
var body: some View
let columns = [
GridItem(.flexible(), spacing: 0),
GridItem(.flexible(), spacing: 0),
GridItem(.flexible(), spacing: 0)
]
GeometryReader screenGeometry in
ZStack
ScrollView
LazyVGrid(columns: columns, alignment: .center, spacing: 0)
ForEach(cards, id: \.self) card in
let isFaceUp = flippedCard == card
GeometryReader cardGeometry in
ZStack
CardBackView(card: card)
.modifier(FlipOpacity(pct: isFaceUp ? 0 : 1))
.rotation3DEffect(Angle.degrees(isFaceUp ? 180 : 360), axis: (0,1,0))
.frame(width: cardGeometry.size.width, height: cardGeometry.size.height)
.scaleEffect(isFaceUp ? screenGeometry.size.width / cardGeometry.size.width: 1)
CardFrontView(card: card)
.modifier(FlipOpacity(pct: isFaceUp ? 1 : 0))
.rotation3DEffect(Angle.degrees(isFaceUp ? 0 : 180), axis: (0,1,0))
.frame(width: screenGeometry.size.width, height: screenGeometry.size.height)
.scaleEffect(isFaceUp ? 1 : cardGeometry.size.width / screenGeometry.size.width)
.offset(x: isFaceUp ? -cardGeometry.frame(in: .named("mainFrame")).origin.x: -screenGeometry.size.width/2 + cardGeometry.size.width/2,
y: isFaceUp ? -cardGeometry.frame(in: .named("mainFrame")).origin.y: -screenGeometry.size.height/2 + cardGeometry.size.height/2)
.onTapGesture
withAnimation(.linear(duration: 1.0))
if flippedCard == nil
flippedCard = card
frontCard = card
else if flippedCard == card
flippedCard = nil
.aspectRatio(1, contentMode: .fit)
.zIndex(frontCard == card ? 1 : 0)
.background(Color.black)
.coordinateSpace(name: "mainFrame")
struct FlipOpacity: AnimatableModifier
var pct: CGFloat = 0
var animatableData: CGFloat
get pct
set pct = newValue
func body(content: Content) -> some View
return content.opacity(Double(pct.rounded()))
struct CardBackView: View
var card: Int
var body: some View
ZStack
RoundedRectangle(cornerRadius: 10)
.fill(Color.red)
.padding(5)
Text("Back \(card)")
struct CardFrontView: View
var card: Int
var body: some View
ZStack
RoundedRectangle(cornerRadius: 10)
.fill(Color.blue)
.padding(10)
.aspectRatio(1.0, contentMode: .fit)
Text("Front \(card)")
【讨论】:
非常感谢您分享这个示例和 gif 可视化。该代码允许基于这种工作方式理解和创建我自己的代码。【参考方案3】:所以为了解释答案,我想解释你需要实现什么。
您希望您的 view/editView 在它出现在前面时进行动画处理。这意味着我们需要使用transition
修饰符。
现在 Apple 的内置转场修饰符使用了许多转场,例如easeIn、out 等,而没有这个转场,所以我们需要创建自定义转场来实现它。让我们先这样做。
extension AnyTransition
static var rotate: AnyTransition get
AnyTransition.modifier(active: RotateTransition(percent: 0), identity: RotateTransition(percent: 1))
struct RotateTransition: GeometryEffect
var percent: Double
var animatableData: Double
get percent
set percent = newValue
func effectValue(size: CGSize) -> ProjectionTransform
let rotationPercent = percent
let a = CGFloat(Angle(degrees: 170 * (1-rotationPercent)).radians)
var transform3d = CATransform3DIdentity;
transform3d.m34 = -1/max(size.width, size.height)
transform3d = CATransform3DRotate(transform3d, a, 0, 1, 0)
transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
let affineTransform1 = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
let affineTransform2 = ProjectionTransform(CGAffineTransform(scaleX: CGFloat(percent * 2), y: CGFloat(percent * 2)))
if percent <= 0.5
return ProjectionTransform(transform3d).concatenating(affineTransform2).concatenating(affineTransform1)
else
return ProjectionTransform(transform3d).concatenating(affineTransform1)
现在我们有了自定义过渡,我们需要应用到那个视图。
所以这是考虑到你有一个 cardView 的代码。
cardView(card: cardName)
.transition(.rotate)
.matchedGeometryEffect(id: "popup", in: animation)
在您的情况下,父视图就像您单击编辑的视图
添加这个
ParentView() //your view
.matchedGeometryEffect(id: "popup", in: animation)
你可以在这里看到输出:
https://imgur.com/pIhBQ72
【讨论】:
以上是关于具有两个视图的 SwiftUI Card Flip 动画,其中一个嵌入在 Stack 中的主要内容,如果未能解决你的问题,请参考以下文章