feat(chat-room) 타이핑 중인 것을 알려주는 애니메이션 아이템 추가
This commit is contained in:
@@ -6,13 +6,79 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
struct TypingIndicatorItemView: View {
|
struct TypingIndicatorItemView: View {
|
||||||
|
var dotCount: Int = 3
|
||||||
|
var size: CGFloat = 6
|
||||||
|
var spacing: CGFloat = 6
|
||||||
|
var color: Color = .secondary
|
||||||
|
var period: Double = 1.2 // 초
|
||||||
|
|
||||||
|
let characterName: String
|
||||||
|
let characterProfileUrl: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
HStack(alignment: .top, spacing: 9) {
|
||||||
|
KFImage(URL(string: characterProfileUrl))
|
||||||
|
.placeholder {
|
||||||
|
Image(systemName: "person.crop.circle")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
}
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 30, height: 30)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text(characterName)
|
||||||
|
.font(.custom(Font.preRegular.rawValue, size: 12))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
TimelineView(.animation) { context in
|
||||||
|
let t = context.date.timeIntervalSinceReferenceDate
|
||||||
|
HStack(spacing: spacing) {
|
||||||
|
ForEach(0..<dotCount, id: \.self) { i in
|
||||||
|
Circle()
|
||||||
|
.fill(color)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.opacity(opacity(for: i, time: t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 레이아웃이 미세하게 흔들리지 않도록 애니메이션은 투명도에만 적용
|
||||||
|
.animation(.easeInOut(duration: period / Double(dotCount)).repeatForever(autoreverses: true), value: context.date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(
|
||||||
|
Color.black.opacity(0.1)
|
||||||
|
)
|
||||||
|
.clipShape(AiMessageBubbleShape())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 각 점마다 위상(phase)을 어긋나게 해서 순차적으로 밝아지도록 함
|
||||||
|
private func opacity(for index: Int, time t: TimeInterval) -> Double {
|
||||||
|
// 0...1 구간에서 부드럽게 오르내리는 값(사인파 기반)
|
||||||
|
let base = (t.truncatingRemainder(dividingBy: period)) / period
|
||||||
|
let phase = base + Double(index) / Double(max(1, dotCount))
|
||||||
|
let wave = 0.5 + 0.5 * sin(2 * .pi * phase)
|
||||||
|
// 최소/최대 투명도 범위
|
||||||
|
return 0.25 + 0.75 * wave
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
TypingIndicatorItemView()
|
TypingIndicatorItemView(
|
||||||
|
characterName: "보라",
|
||||||
|
characterProfileUrl: "https://picsum.photos/1000"
|
||||||
|
)
|
||||||
|
.padding()
|
||||||
|
.background(Color.black)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user