中心视图对齐SwiftUI ScrollView

问题描述 投票:0回答:1

我有一个基本的水平滚动视图,其中每个图像具有相同的高度,但它们的宽度可以不同。目前,视图对齐修改器似乎会导致视图滚动时图像与前缘对齐。例如,如果我从屏幕边缘单击图像(不是当前对齐的视图),那么滚动视图将滚动到它并居中对齐它,可能是因为:

.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
}

swift swiftui
1个回答
0
投票

看起来

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")
]

Animation

自定义滚动行为对于慢速滚动表现得很好,但对于惯性滚动则表现不佳。但是,它不适用于滚动内容开头或结尾处的窄图像。这是因为末端较窄的图像不会到达屏幕中心,因此

detailScrollPosition
永远不会用其 id 进行更新。要解决此问题,您可以考虑向
LazyVStack
添加填充。有关使用此技术的示例,请参阅此答案

© www.soinside.com 2019 - 2024. All rights reserved.