接入友盟统计与 U-APM,并优化宣传页打卡点标签交互。
友盟 SDK 改为隐私同意后再初始化;宣传页标签新增滑块位移动画,超过 5 个时选中项自动滚动居中。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -7,12 +7,28 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// 宣传页打卡点标签区,5 个以内等分,超过 5 个时横向滚动。
|
||||
/// 宣传页打卡点标签区,5 个以内等分,超过 5 个时横向滚动并将选中项居中。
|
||||
struct ScenicPromotionSpotTabsView: View {
|
||||
let spots: [ScenicPromotionSpot]
|
||||
let selectedSpotID: String?
|
||||
let onSelectSpot: (ScenicPromotionSpot) -> Void
|
||||
|
||||
/// 选中背景滑块动画曲线。
|
||||
private static let selectionAnimation = Animation.spring(response: 0.35, dampingFraction: 0.82)
|
||||
/// 选中标签滚动居中动画曲线。
|
||||
private static let scrollAnimation = Animation.easeInOut(duration: 0.25)
|
||||
|
||||
/// 当前选中标签在列表中的索引,未选中时回退到 0。
|
||||
private var selectedIndex: Int {
|
||||
guard let selectedSpotID else { return 0 }
|
||||
return spots.firstIndex(where: { $0.id == selectedSpotID }) ?? 0
|
||||
}
|
||||
|
||||
/// 是否需要启用横向滚动(超过 5 个标签)。
|
||||
private var shouldEnableScroll: Bool {
|
||||
spots.count > Int(ScenicPromotionLayout.visibleSpotTabCount)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if spots.isEmpty {
|
||||
EmptyView()
|
||||
@ -22,22 +38,38 @@ struct ScenicPromotionSpotTabsView: View {
|
||||
.frame(height: 42)
|
||||
.overlay {
|
||||
GeometryReader { proxy in
|
||||
let visibleCount = min(CGFloat(max(spots.count, 1)), ScenicPromotionLayout.visibleSpotTabCount)
|
||||
let visibleCount = min(
|
||||
CGFloat(max(spots.count, 1)),
|
||||
ScenicPromotionLayout.visibleSpotTabCount
|
||||
)
|
||||
let tabWidth = proxy.size.width / visibleCount
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(spots) { spot in
|
||||
ScenicPromotionSpotTabButton(
|
||||
spot: spot,
|
||||
isSelected: selectedSpotID == spot.id,
|
||||
onTap: { onSelectSpot(spot) }
|
||||
)
|
||||
.frame(width: tabWidth)
|
||||
ScrollViewReader { scrollProxy in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
ZStack(alignment: .leading) {
|
||||
selectionBackground(width: tabWidth)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
ForEach(spots) { spot in
|
||||
ScenicPromotionSpotTabButton(
|
||||
spot: spot,
|
||||
isSelected: selectedSpotID == spot.id,
|
||||
onTap: { onSelectSpot(spot) }
|
||||
)
|
||||
.frame(width: tabWidth)
|
||||
.id(spot.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollDisabled(!shouldEnableScroll)
|
||||
.onAppear {
|
||||
scrollSelectedTabToCenter(using: scrollProxy, animated: false)
|
||||
}
|
||||
.onChange(of: selectedSpotID) { _ in
|
||||
scrollSelectedTabToCenter(using: scrollProxy, animated: true)
|
||||
}
|
||||
}
|
||||
.scrollDisabled(spots.count <= Int(ScenicPromotionLayout.visibleSpotTabCount))
|
||||
}
|
||||
}
|
||||
.background(.white.opacity(0.84), in: RoundedRectangle(cornerRadius: 14))
|
||||
@ -49,6 +81,35 @@ struct ScenicPromotionSpotTabsView: View {
|
||||
.shadow(color: Color(hex: 0x0D47A1, alpha: 0.12), radius: 12, y: 5)
|
||||
}
|
||||
}
|
||||
|
||||
/// 选中标签的渐变背景滑块,随选中项横向位移。
|
||||
@ViewBuilder
|
||||
private func selectionBackground(width: CGFloat) -> some View {
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [Color(hex: 0x1286FF), Color(hex: 0x006BFF)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
.frame(width: width, height: 42)
|
||||
.offset(x: CGFloat(selectedIndex) * width)
|
||||
.animation(Self.selectionAnimation, value: selectedIndex)
|
||||
}
|
||||
|
||||
/// 超过可见数量时,将选中标签滚动到可视区域中间。
|
||||
private func scrollSelectedTabToCenter(using scrollProxy: ScrollViewProxy, animated: Bool) {
|
||||
guard shouldEnableScroll, let selectedSpotID else { return }
|
||||
|
||||
if animated {
|
||||
withAnimation(Self.scrollAnimation) {
|
||||
scrollProxy.scrollTo(selectedSpotID, anchor: .center)
|
||||
}
|
||||
} else {
|
||||
scrollProxy.scrollTo(selectedSpotID, anchor: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个打卡点标签按钮。
|
||||
@ -68,22 +129,9 @@ struct ScenicPromotionSpotTabButton: View {
|
||||
.minimumScaleFactor(0.66)
|
||||
}
|
||||
.foregroundStyle(isSelected ? .white : Color(hex: 0x14213D))
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 42)
|
||||
.padding(.horizontal, 4)
|
||||
.background(
|
||||
Group {
|
||||
if isSelected {
|
||||
LinearGradient(
|
||||
colors: [Color(hex: 0x1286FF), Color(hex: 0x006BFF)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
} else {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user