如何获得这样的动画
代码:使用此代码,我可以正常导航视图,但如果我在
AttendanceSwipeView(viewModel: viewModel)
,那么如果我点击仪表板,那么视图应该带有这种动画(就像从下到前的动画)...
我的意思是仪表板>出勤>摘要...导航可以正常但是
从 Feed 到出勤,再到出勤到仪表板...这里需要自下而上的动画。如果可能,请指导我如何实现这一点。
struct DashboardBottomView: View {
@StateObject var viewModel: AppDashboardViewModel
@State var selectedClassInd: Int = 0
@State private var stringArray = ["Dashboard", "Attendance", "Feed"]
@State private var selectedIndex: Int? = nil
@State private var selectedIndexStr: String? = "Dashboard"
private func viewForSelectedIndex() -> some View {
switch selectedIndexStr {
case "Dashboard":
return AnyView(DashboardView(viewModel: viewModel))
case "Attendance":
return AnyView(AttendanceSwipeView(viewModel: viewModel))
case "Feed":
return AnyView(DashboardFeedView())
default:
return AnyView(Text("Default View"))
}
}
var body: some View {
VStack(spacing: 0) {
Spacer()
ZStack {
viewForSelectedIndex()
.toolbar(.hidden, for: .navigationBar)
}
ZStack{
Color.appGreen2
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: [GridItem(.flexible(), spacing: 0)], spacing: 0) {
ForEach(stringArray, id: \.self) { data in
Button {
withAnimation {
selectedIndexStr = data
}
}
label: {
VStack {
Text(data)
.font(.calibriBold(with: 14))
.foregroundColor(Color.white)
.padding(.horizontal, 8)
if selectedIndexStr == data {
gradientDivider
.frame(height: 2)
}
}
.frame(height: 40)
.frame(minWidth: 108)
.animation(.default, value: selectedClassInd)
}
.buttonStyle(.plain)
}
}
.padding(.bottom, 0)
.background(Color.appGreen2)
}
.frame(height: 55)
}
}
.ignoresSafeArea()
.onAppear {
viewModel.fetchDashboardData { status in
if status {
stringArray = viewModel.dashboardButtonsArray
selectedIndexStr = stringArray.first
}
}
}
}
}
看起来我们在这里讨论的是自定义过渡。
自定义过渡可用于将
ViewModifier
应用于处于 active
(之前)和 identity
(之后)状态的视图。当转换为动画时,两个状态之间会自动插值。这对于简单的线性过渡来说很好,就像这里的情况一样。
以下
ViewModifier
将不透明度、旋转和偏移效果应用于视图:
struct SwipeMovement: ViewModifier {
let progress: Double
func body(content: Content) -> some View {
content
.opacity(progress)
.offset(y: -400)
.rotationEffect(.degrees((progress - 1) * 30))
.offset(y: 400 + ((1 - progress) * 100))
}
}
然后将自定义过渡作为
extension
添加到 AnyTransition
:
extension AnyTransition {
static var swipeMovement: AnyTransition {
.modifier(
active: SwipeMovement(progress: 0),
identity: SwipeMovement(progress: 1)
)
}
}
现在自定义过渡就可以使用了。下面的代码是您的示例的改编版本,具有以下更改:
添加了新的状态变量
previousSelectedIndexStr
,用于记录之前的导航目标。
函数
viewForSelectedIndex
已被标记为ViewBuilder
。这避免了必须将不同类型的结果包装为 AnyView
。
isSpecialTransitionNeeded
是一个计算属性,它根据 selectedIndexStr
的上一个和下一个值确定是否需要特殊转换。这是像您之前使用的那样进行字符串比较,但我建议在这里使用枚举可能会更好。
顶部
ZStack
包含一个 if
语句,用于确定是否应用不同的转换。这是一种相当原始但也是在不同类型的过渡之间切换的简单方法。
特殊过渡仅用于
insertion
过渡,removal
过渡始终为.opacity
。
.animation
修饰符已添加到ZStack
。这控制了转换的速度。
按钮回调将当前选择记录为
previousSelectedIndexStr
,然后更改 selectedIndexStr
。
原始示例中未编译的代码已被注释掉或替换为简单的替代方案。
struct DashboardBottomView: View {
// @StateObject var viewModel: AppDashboardViewModel
@State var selectedClassInd: Int = 0
@State private var stringArray = ["Dashboard", "Attendance", "Feed"]
@State private var selectedIndex: Int? = nil
@State private var previousSelectedIndexStr: String?
@State private var selectedIndexStr: String? = "Dashboard"
@ViewBuilder
private func viewForSelectedIndex() -> some View {
switch selectedIndexStr {
case "Dashboard":
Text("DashboardView")
.frame(maxWidth: .infinity, maxHeight: .infinity)
case "Attendance":
Text("AttendanceSwipeView")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.yellow)
case "Feed":
Text("DashboardFeedView")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.orange)
default:
Text("Default View")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
}
}
private var isSpecialTransitionNeeded: Bool {
if let previousSelectedIndexStr, let selectedIndexStr {
(previousSelectedIndexStr == "Feed" && selectedIndexStr == "Attendance") ||
(previousSelectedIndexStr == "Attendance" && selectedIndexStr == "Dashboard")
} else {
false
}
}
var body: some View {
VStack(spacing: 0) {
Spacer()
ZStack {
if isSpecialTransitionNeeded {
viewForSelectedIndex()
.transition(
.asymmetric(
insertion: .swipeMovement,
removal: .opacity
)
)
} else {
viewForSelectedIndex()
.transition(.opacity)
}
}
.animation(.easeInOut(duration: 1), value: selectedIndexStr)
.toolbar(.hidden, for: .navigationBar)
ZStack{
Color.green //appGreen2
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: [GridItem(.flexible(), spacing: 0)], spacing: 0) {
ForEach(stringArray, id: \.self) { data in
Button {
previousSelectedIndexStr = selectedIndexStr
withAnimation {
selectedIndexStr = data
}
}
label: {
VStack {
Text(data)
// .font(.calibriBold(with: 14))
.foregroundColor(Color.white)
.padding(.horizontal, 8)
// if selectedIndexStr == data {
// gradientDivider
// .frame(height: 2)
// }
}
.frame(height: 40)
.frame(minWidth: 108)
.animation(.default, value: selectedClassInd)
}
.buttonStyle(.plain)
}
}
.padding(.bottom, 0)
.background(Color.green) // appGreen2
}
.frame(height: 55)
}
}
.ignoresSafeArea()
// .onAppear {
// viewModel.fetchDashboardData { status in
// if status {
// stringArray = viewModel.dashboardButtonsArray
// selectedIndexStr = stringArray.first
// }
// }
// }
}
}