使用SwiftUI将ScrollView中与矩形重叠的文本变成白色

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

我创建了一个类似于滚轮选择器的视图,当我滑动scrollView时,我希望文本的颜色与矩形重合时变为白色。我可以使用

.blendMode
来实现图像中的效果,但当背景改变时,效果也会改变。我想知道有没有一种方法可以不受背景颜色的影响。我找了很久没有找到合适的解决方案。如果您能提供任何帮助,我将不胜感激。

import SwiftUI
import SwiftUIIntrospect
import Combine
@_spi(Advanced) import SwiftUIIntrospect

struct TimePickerView: View {
    @StateObject var viewModel = TimerPickerViewModel()
    private let date = ["", "1", "5", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55", "60", "70", "80", "90", "100", "110", "120", ""]
    var body: some View {
        ZStack {
            Image("bg")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .ignoresSafeArea(.all)
            
            ScrollView(showsIndicators: false) {
                LazyVStack(spacing: 0) {
                    ForEach(0..<date.count, id: \.self) { index in
                        let time = date[index]
                        Text(time + (time.isEmpty ? "":" MIN"))
                            .frame(width: 200, height: 50)
                    }
                }
                .font(.system(size: 25))
                .foregroundStyle(Color(hex: 0x638FFF))
                .background(GeometryReader { geometry in
                    Color.clear
                        .preference(key: TimerOffsetPreferenceKey.self,
                                    value: geometry.frame(in: .named("TimePickeView")).origin.y)
                })
                .onPreferenceChange(TimerOffsetPreferenceKey.self) { value in
                    viewModel.contentOffset = -value
                }
            }
            .introspect(.scrollView, on: .iOS(.v15, .v16, .v17), customize: { scrollView in
                viewModel.scrollView = scrollView
            })
            .frame(height: 150)
            .coordinateSpace(name: "TimePickeView")
            
            RoundedRectangle(cornerRadius: 25)
                .fill(Color(hex: 0x638FFF))
                .frame(width: 180, height: 50)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

class TimerPickerViewModel: ObservableObject {
    @Published var contentOffset: CGFloat = 0
    private var cancellable = Set<AnyCancellable>()
    @Weak var scrollView: UIScrollView?
    init() {
        $contentOffset
            .dropFirst(2)
            .debounce(for: .seconds(0.3), scheduler: RunLoop.main)
            .sink { [weak self] output in
                let remainder = Int(output) % 50
                var offset = Int(output) - remainder + (remainder > 25 ? 50:0)
                offset = max(0, min(offset, 1000))
                self?.scrollView?.setContentOffset(CGPoint(x: 0, y: CGFloat(offset)), animated: true)
            }
            .store(in: &cancellable)
    }
}

struct TimerOffsetPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value += nextValue()
    }
}

#Preview {
    TimePickerView()
}

以下是我想要的类似效果:

swiftui scrollview blend-mode
1个回答
0
投票

我建议显示一个

Capsule
形状,其中日期列表显示为叠加层,并剪裁到形状上。这样,您就可以使用任何您喜欢的样式。

  • 为了保持覆盖层的位置与其后面的滚动列表同步,可以将滚动偏移应用为 y 偏移。
  • 可以使用后台的
    GeometryReader
    检测滚动偏移。您已经这样做了,但是您随后使用
    PreferenceKey
    来传递值。我建议,使用
    .onChange
    处理程序来更新状态变量更简单。
  • 可以使用
    .scrollTargetLayout
    将滚动位置设置为粘性。
  • 可以使用
    .scrollPosition
    检测滚动项的索引。然后可以从滚动索引中导出所选索引。

这是一个如何以这种方式实现的示例。它不需要

PreferenceKey
,也不需要内省。

struct TimePicker: View {
    private let date = ["", "1", "5", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55", "60", "70", "80", "90", "100", "110", "120", ""]
    @State private var baseOffset = CGFloat.zero
    @State private var scrollOffset = CGFloat.zero
    @State var scrolledIndex: Int?

    var selectedIndex: Int {
        min((scrolledIndex ?? 0) + 1, date.count - 2)
    }

    private var dateList: some View {
        VStack(spacing: 0) {
            ForEach(Array(date.enumerated()), id: \.offset) { index, time in
                Text(time + (time.isEmpty ? "":" MIN"))
                    .frame(width: 200, height: 50)
            }
        }
        .font(.system(size: 25))
    }

    private var scrollDetector: some View {
        GeometryReader { proxy in
            let minY = proxy.frame(in: .scrollView).minY
            Color.clear
                .onChange(of: minY, initial: true) { oldVal, newVal in
                    scrollOffset = minY
                }
        }
    }

    var body: some View {
        ZStack {
            ScrollView(showsIndicators: false) {
                dateList
                    .foregroundStyle(.blue) // 0x638FFF
                    .background(scrollDetector)
            }
            .scrollPosition(id: $scrolledIndex)
            .scrollTargetLayout()
            .scrollTargetBehavior(.viewAligned)

            Capsule()
                .fill(.blue) // 0x638FFF
                .frame(width: 180, height: 50)
                .overlay(alignment: .top) {
                    dateList
                        .offset(y: -baseOffset + scrollOffset)
                        .foregroundStyle(.white)
                }
                .clipped()
                .allowsHitTesting(false)
                .background {
                    GeometryReader { proxy in
                        let minY = proxy.frame(in: .named("ZStack")).minY
                        Color.clear
                            .onAppear { baseOffset = minY }
                    }
                }

        }
        .coordinateSpace(.named("ZStack"))
        .frame(height: 150)
        .padding(.bottom, 50)
        .overlay(alignment: .bottom) {
            Text("Selected index = \(selectedIndex)")
        }
    }
}

Animation

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