我有一个基本的水平滚动视图,其中每个图像具有相同的高度,但它们的宽度可以不同。目前,视图对齐修改器似乎会导致视图滚动时图像与前缘对齐。例如,如果我从屏幕边缘单击图像(不是当前对齐的视图),那么滚动视图将滚动到它并居中对齐它,可能是因为:
.scrollPosition(id: $detailScrollPosition, anchor: .center)
所以我可以通过单击图像来居中对齐图像,如何让它也居中对齐以滚动?
import SwiftUI
import Kingfisher
struct TopPostMediaView: View {
@State var detailScrollPosition: UUID? = nil
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
GeometryReader {
let size = $0.size
ScrollView(.horizontal) {
HStack(spacing: 10) {
ForEach(mynewtemp) { pic in
LazyHStack {
KFImage(URL(string: pic.photo))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: size.width)
.overlay(content: {
RoundedRectangle(cornerRadius: 10).stroke(Color.gray.opacity(0.4), lineWidth: 1.0)
})
.clipShape(RoundedRectangle(cornerRadius: 10))
.contentShape(RoundedRectangle(cornerRadius: 10))
.onTapGesture {
withAnimation(.easeInOut(duration: 0.15)){
detailScrollPosition = pic.id
}
}
}
.frame(maxWidth: size.width)
.frame(height: size.height)
.contentShape(.rect)
}
}
.scrollTargetLayout()
}
.scrollPosition(id: $detailScrollPosition, anchor: .center)
.scrollIndicators(.hidden)
.scrollTargetBehavior(.viewAligned)
.scrollClipDisabled()
}.frame(height: 250)
}
}
}
}
#Preview {
TopPostMediaView()
}
let mynewtemp: [tempStruct] = [tempStruct(id: UUID(), photo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmCy16nhIbV3pI1qLYHMJKwbH2458oiC9EmA&s"), tempStruct(id: UUID(), photo: "https://th.bing.com/th/id/OIG2.9O4YqGf98tiYzjKDvg7L"), tempStruct(id: UUID(), photo: "https://th.bing.com/th/id/OIG2.9O4YqGf98tiYzjKDvg7L")]
struct tempStruct: Identifiable, Hashable {
var id: UUID
var photo: String
}
看起来
ViewAlignedScrollTargetBehavior
总是与 ScrollView
的前缘对齐,并且无法指定不同的锚点。
当您使用
.scrollPostion
或 ScrollViewProxy.scrollTo
设置滚动位置时,也可以指定锚点。但这与滚动目标行为无关,对其没有任何影响。
要解决这个问题,您可能需要实现自己的
ScrollTargetBehavior
。对于这里的情况,这是特别困难的,因为容器是惰性的并且每个项目可以具有不同的大小。但如果您知道当前所选图像的 X 中位置,则可以将其设置为滚动到的目标:
struct ScrollTargetCentered: ScrollTargetBehavior {
let currentMidX: CGFloat
func updateTarget(_ target: inout ScrollTarget, context: TargetContext) {
// Only perform an adjustment if the target anchor is
// top-leading. This will be the case for a scroll action,
// but may not be the case on initial show.
if (target.anchor?.x ?? 0) == 0 {
target.rect.origin.x = currentMidX - (context.containerSize.width / 2)
}
}
}
最接近中心的图像已使用
.scrollPosition
进行跟踪,并且其 id 已保存到 detailScrollPosition
。因此,当视图滚动时,通过在每个图像后面使用 GeometryReader
,可以将具有匹配 id 的图像的 mid-X 位置记录到状态变量中。需要改变:
添加一个状态变量,用于保存当前图像的 mid-X 位置。
添加一个
Namespace
状态变量,用于命名 LazyVStack
的坐标空间。您可以只使用字符串,但命名空间可以避免拼写错误。
将
HStack
更改为 LazyHStack
并删除嵌套的 LazyHStack
(包括其修饰符)。
将
.scrollTargetLayout
应用于 LazyHStack
并使用 Namespace
状态变量命名其坐标空间。
在每个图像后面添加
GeometryReader
,以读取 LazyHStack
的坐标空间内的位置,并在图像为当前图像时记录该位置。
将
scrollTargetBehavior
更改为使用 ScrollTargetCentered
。
struct TopPostMediaView: View {
@State var detailScrollPosition: UUID? = nil
@State private var currentMidX = CGFloat.zero // 👈 added
@Namespace private var ns // 👈 added
private func positionReader(imageId: UUID) -> some View { // 👈 added
GeometryReader { proxy in
let midX: CGFloat? = imageId == detailScrollPosition
? proxy.frame(in: .named(ns)).midX
: nil
Color.clear
.onChange(of: midX) { oldVal, newVal in
if let newVal {
currentMidX = newVal
}
}
}
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
GeometryReader {
let size = $0.size
ScrollView(.horizontal) {
LazyHStack(spacing: 10) { // 👈 changed
ForEach(mynewtemp) { pic in
// LazyHStack { // 👈 removed (including modifiers)
// KFImage(URL(string: pic.photo))
Image(pic.photo) // 👈 changed for testing
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: size.width)
.overlay(content: {
RoundedRectangle(cornerRadius: 10).stroke(Color.gray.opacity(0.4), lineWidth: 1.0)
})
.clipShape(RoundedRectangle(cornerRadius: 10))
.contentShape(RoundedRectangle(cornerRadius: 10))
.onTapGesture {
withAnimation(.easeInOut(duration: 0.15)){
detailScrollPosition = pic.id
}
}
.background { positionReader(imageId: pic.id) } // 👈 added
// }
// .frame(maxWidth: size.width)
// .frame(height: size.height)
// .contentShape(.rect)
}
}
.scrollTargetLayout() // 👈 moved
.coordinateSpace(name: ns) // 👈 added
}
.scrollPosition(id: $detailScrollPosition, anchor: .center)
.scrollIndicators(.hidden)
// .scrollTargetBehavior(.viewAligned)
.scrollTargetBehavior( // 👈 changed
ScrollTargetCentered(currentMidX: currentMidX)
)
.scrollClipDisabled()
}.frame(height: 250)
}
}
}
}
我发现您在示例中提供的图像并不总是正确加载,可能是因为图像 2 + 3 具有相同的 URL。所以我用本地图像进行了测试:
let mynewtemp: [tempStruct] = [
// tempStruct(id: UUID(), photo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmCy16nhIbV3pI1qLYHMJKwbH2458oiC9EmA&s"),
// tempStruct(id: UUID(), photo: "https://th.bing.com/th/id/OIG2.9O4YqGf98tiYzjKDvg7L"),
// tempStruct(id: UUID(), photo: "https://th.bing.com/th/id/OIG2.9O4YqGf98tiYzjKDvg7L")
tempStruct(id: UUID(), photo: "image1"),
tempStruct(id: UUID(), photo: "image2"),
tempStruct(id: UUID(), photo: "image3"),
tempStruct(id: UUID(), photo: "image4")
]
自定义滚动行为对于慢速滚动表现得很好,但对于惯性滚动则表现不佳。但是,它不适用于滚动内容开头或结尾处的窄图像。这是因为末端较窄的图像不会到达屏幕中心,因此
detailScrollPosition
永远不会用其 id 进行更新。要解决此问题,您可以考虑向 LazyVStack
添加填充。有关使用此技术的示例,请参阅此答案。