技术:SwiftUI、SwiftUI4.0、双击动画、心形动画、动画
运行环境:
SwiftUI4.0 + Xcode14 + MacOS12.6 + iPhone Simulator iPhone 14 Pro Max
概述
使用SwiftUI创建用户双击帖子时的心形动画
详细
一、运行效果
二、项目结构图
三、程序实现 - 过程
思路:
- 构建列表滚动模块 - 使用ScrollView搭建
- 构建双击心形动画 HeartLike -
封装起来 包含点击的次数、是否点击、背景动画、是否重置动画、是否烟花动画、动画是否结束、动画是否完成等参数
1.创建一个项目命名为 SpotifyResponvieUI
1.1.引入资源文件和颜色
无
2. 创建一个虚拟文件New Group
命名为 View
3. 创建一个虚拟文件New Group
命名为 Model
4. 创建一个文件New File
选择SwiftUI View
类型 命名为Post
删除预览图 并且继承Identifiable
作为模型
5. 创建一个文件New File
选择SwiftUI View
类型 命名为Home
主要是:
展示
列表视图 和 双击时的心形烟花释放动画
Code
ContentView - 主窗口
主要是展示主窗口
Home
//
// ContentView.swift
// LikedAnimation
//
// Created by 李宇鸿 on 2022/9/27.
//
import SwiftUI
struct ContentView: View {
var body: some View {
Home()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Home - 主页
思路
- 构建列表滚动模块 - 使用ScrollView搭建
- 构建双击心形动画 HeartLike -
封装起来 包含点击的次数、是否点击、背景动画、是否重置动画、是否烟花动画、动画是否结束、动画是否完成等参数
//
// Home.swift
// LikedAnimation
//
// Created by 李宇鸿 on 2022/9/27.
//
import SwiftUI
struct Home: View {
//
@State var posts : [Post] = [
Post(imageName: "Pic1"),
Post(imageName: "Pic2"),
Post(imageName: "Pic3"),
Post(imageName: "Pic4"),
Post(imageName: "Pic5"),
]
var body: some View {
NavigationView{
ScrollView(.vertical,showsIndicators: false) {
VStack(alignment: .leading,spacing:16) {
ForEach(posts){
post in
VStack(alignment: .leading,spacing: 12) {
GeometryReader{
proxy in
Image(post.imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.cornerRadius(15)
}
.frame(height:280)
.overlay(
HeartLike(isTapped:$posts[getIndex(post: post)].isLiked,taps: 2)
)
.cornerRadius(15)
Button {
posts[getIndex(post: post)].isLiked.toggle()
} label: {
Image(systemName: post.isLiked ? "suit.heart.fill" : "suit.heart")
.font(.title2)
.foregroundColor(post.isLiked ? .red : .gray)
}
}
}
}
.padding()
}
.navigationTitle("Heart Animation")
}
}
// 获取索引
func getIndex(post : Post)-> Int {
let index = posts.firstIndex{
currentPost in
return currentPost.id == post.id
} ?? 0
return index
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct HeartLike:View {
// 在弹出的菜单动画……
@Binding var isTapped : Bool
@State var startAnimation = false
@State var bgAnimation = false
// 重置Bg……
@State var resetBG = false
// 烟花动画
@State var fireworkAnimation = false
// 动画是否结束
@State var animationEnded = false
// 避免在动画中轻拍…
@State var tapComplete = false
// 设置多少次点击…
var taps : Int = 1
var body: some View {
// 心如动画……
Image(systemName: resetBG ? "suit.heart.fill" : "suit.heart")
.font(.system(size: 45))
.foregroundColor(resetBG ? .red : .gray)
// 扩展……
.scaleEffect(startAnimation && !resetBG ? 0 : 1)
.opacity(startAnimation && !animationEnded ? 1 : 0)
.background(
ZStack{
CustomShape(radius: resetBG ? 29 : 0)
.fill(Color.purple)
.clipShape(Circle())
// Fixed Size
.frame(width:50,height:50)
.scaleEffect(bgAnimation ? 2.2 : 0)
ZStack{
// 烟花的随机颜色
let colors : [Color] = [.red,.purple,.green,.yellow,.pink]
ForEach(1...6,id:\.self){
index in
Circle()
.fill(colors.randomElement()!)
.frame(width: 8,height: 8)
.offset(x: fireworkAnimation ? 64 : 24)
.rotationEffect(.init(degrees: Double(index) * 60))
}
ForEach(1...6,id:\.self){
index in
Circle()
.fill(colors.randomElement()!)
.frame(width: 12,height: 12)
.offset(x: fireworkAnimation ? 80 : 40)
.rotationEffect(.init(degrees: Double(index) * 60))
.rotationEffect(.init(degrees: -45))
}
}
.opacity(resetBG ? 1 : 0)
.opacity(animationEnded ? 0 : 1)
}
)
.frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .center)
.contentShape(Rectangle())
.onTapGesture(count:taps) {
if tapComplete{
updateFields(value: false)
return
}
// 动画正在执行 就不执行
if startAnimation {
return
}
isTapped = true
withAnimation(.interactiveSpring(response: 0.5,dampingFraction: 0.6,blendDuration: 0.6)){
startAnimation = true
}
// 顺列动画……
// 链动画……
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25){
withAnimation(.interactiveSpring(response: 0.4,dampingFraction: 0.5,blendDuration: 0.5)){
bgAnimation = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3){
withAnimation(.interactiveSpring(response: 0.5,dampingFraction: 0.6,blendDuration: 0.6)){
resetBG = true
}
// 烟花……
withAnimation(.spring()) {
fireworkAnimation = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4){
withAnimation(.easeOut(duration: 0.4)) {
animationEnded = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3){
tapComplete = true
}
}
}
}
}
.onChange(of: isTapped) {
newValue in
if isTapped && !startAnimation {
// 设置一切为真…
updateFields(value: true)
}
}
}
// 更新动画字段
func updateFields(value: Bool){
startAnimation = value
bgAnimation = value
resetBG = value
fireworkAnimation = value
animationEnded = value
tapComplete = value
isTapped = value
}
}
//自定义形状
//从中心重置…
struct CustomShape : Shape {
// value...
var radius : CGFloat
// 动画路径…
var animatableData: CGFloat{
get {
return radius}
set {
radius = newValue}
}
// 可动画路径在预览时无效
func path(in rect: CGRect) -> Path {
return Path {
path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: rect.width, y: 0))
// 增加中心圆……
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
path.move(to: center)
path.addArc(center: center, radius: radius, startAngle: .zero, endAngle: .init(degrees: 360), clockwise:false)
}
}
}
Post - 模型
//
// Post.swift
// LikedAnimation
//
// Created by 李宇鸿 on 2022/9/27.
//
import SwiftUI
// Post Model...
struct Post : Identifiable {
var id = UUID().uuidString
var imageName : String
var isLiked : Bool = false
}