SwiftUI - 如何给ScrollView添加淡出效果

8
我有一个生成的超大图表,我将它放在ScrollView中,以便用户可以向右滚动并查看所有值。我想通过将ScrollView淡出来向用户指示右侧还有“更多内容”。在Swift中,通过应用CAGradientLayer很容易实现这一点。
我的方法是应用一个覆盖层,从透明度为80%开始,渐变到系统背景颜色(透明度为100%)。结果可以看到附加的截图。
问题1:看起来不像应该看起来的样子。
问题2:尽管将叠加层的zIndex设置为-1,但一旦应用了叠加层,ScrollView就无法再滚动。
有什么想法可以实现这一点吗?谢谢!

The ScrollView with an Rectangle overlay

这是我的代码:

struct HealthExportPreview: View {
    @ObservedObject var carbsEntries: CarbsEntries
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack {
                ForEach(0..<self.carbsEntries.carbsRegime.count, id: \.self) { index in
                    ChartBar(carbsEntries: self.carbsEntries, entry: self.carbsEntries.carbsRegime[index], requiresTimeSplitting: index == self.carbsEntries.timeSplittingAfterIndex)
                }
            }
            .padding()
            .animation(.interactiveSpring())
        }
        .overlay(Rectangle()
            .fill(
                LinearGradient(gradient: Gradient(stops: [
                    .init(color: .clear, location: 0.8),
                    .init(color: Color(UIColor.systemBackground), location: 1.0)
                ]), startPoint: .leading, endPoint: .trailing)
            )
            .zIndex(-1)
        )
        .frame(height: CGFloat(carbsEntries.previewHeight + 80))
        .onAppear() {
            self.carbsEntries.fitCarbChartBars()
        }
    }
}

struct ChartBar: View {
    var carbsEntries: CarbsEntries
    var entry: (date: Date, carbs: Double)
    var requiresTimeSplitting: Bool
    
    static var timeStyle: DateFormatter {
        let formatter = DateFormatter()
        formatter.timeStyle = .short
        return formatter
    }
    
    var body: some View {
        VStack {
            Spacer()
            Text(FoodItemViewModel.doubleFormatter(numberOfDigits: entry.carbs >= 100 ? 0 : (entry.carbs >= 10 ? 1 : 2)).string(from: NSNumber(value: entry.carbs))!)
                .font(.footnote)
                .rotationEffect(.degrees(-90))
                .offset(y: self.carbsEntries.appliedMultiplier * entry.carbs <= 40 ? 0 : 40)
                .zIndex(1)
            
            if entry.carbs <= self.carbsEntries.maxCarbsWithoutSplitting {
                Rectangle()
                    .fill(Color.green)
                    .frame(width: 15, height: CGFloat(self.carbsEntries.appliedMultiplier * entry.carbs))
            } else {
                Rectangle()
                    .fill(Color.green)
                    .frame(width: 15, height: CGFloat(self.carbsEntries.getSplitBarHeight(carbs: entry.carbs)))
                    .overlay(Rectangle()
                        .fill(Color(UIColor.systemBackground))
                        .frame(width: 20, height: 5)
                        .padding([.bottom, .top], 1.0)
                        .background(Color.primary)
                        .rotationEffect(.degrees(-10))
                        .offset(y: CGFloat(self.carbsEntries.getSplitBarHeight(carbs: entry.carbs) / 2 - 10))
                )
            }
            
            if self.requiresTimeSplitting {
                Rectangle()
                    .fill(Color(UIColor.systemBackground))
                    .frame(width: 40, height: 0)
                    .padding([.top], 2.0)
                    .background(Color.primary)
                    .overlay(Rectangle()
                        .fill(Color(UIColor.systemBackground))
                        .frame(width: 20, height: 5)
                        .padding([.bottom, .top], 1.0)
                        .background(Color.black)
                        .rotationEffect(.degrees(80))
                        .offset(x: 20)
                        .zIndex(1)
                    )
            } else {
                Rectangle()
                    .fill(Color(UIColor.systemBackground))
                    .frame(width: 40, height: 0)
                    .padding([.top], 2.0)
                    .background(Color.primary)
            }
            
            Text(ChartBar.timeStyle.string(from: entry.date))
                .fixedSize()
                .layoutPriority(1)
                .font(.footnote)
                .rotationEffect(.degrees(-90))
                .offset(y: 10)
                .frame(height: 50)
                .lineLimit(1)
        }.frame(width: 30)
    }
}
3个回答

18

使用 alpha .mask 怎么样?

可以像这样使用 alpha mask 代替 .overlay:

.mask(
    HStack(spacing: 0) {

        // Left gradient
        LinearGradient(gradient: 
           Gradient(
               colors: [Color.black.opacity(0), Color.black]), 
               startPoint: .leading, endPoint: .trailing
           )
           .frame(width: 50)

        // Middle
        Rectangle().fill(Color.black)

        // Right gradient
        LinearGradient(gradient: 
           Gradient(
               colors: [Color.black, Color.black.opacity(0)]), 
               startPoint: .leading, endPoint: .trailing
           )
           .frame(width: 50)
    }
 )

8

Xcode 13.4 / iOS 15.5更新

现在通过使用.allowsHitTesting(false),渐变色tap through功能可用。

demo2

原始内容

好的,已知SwiftUI存在一个问题,即即使是透明的叠加层也无法传递一些手势。

这里有可能用来解决这个问题的方法-思路是让渐变仅覆盖边缘位置的一个小区域,以便可以直接访问滚动视图的其他部分(是的,在渐变下面它仍然不可拖动,但它只是一个小部分)

使用 Xcode 11.7 / iOS 13.7 准备并测试了演示效果

demo

(原始视图的简化版本)

struct HealthExportPreview: View {
    var body: some View {
        GeometryReader { gp in
            ZStack {
                ScrollView(.horizontal) {
                    HStack {
                       // simplified content
                        ForEach(0..<20, id: \.self) { index in
                            Rectangle().fill(Color.red)
                                .frame(width: 40, height: 80)
                        }
                    }
                    .padding()
                }
                .clipped()

                // inject gradient at right side only
                Rectangle()
                    .fill(
                        LinearGradient(gradient: Gradient(stops: [
                            .init(color: Color(UIColor.systemBackground).opacity(0.01), location: 0),
                            .init(color: Color(UIColor.systemBackground), location: 1)
                        ]), startPoint: .leading, endPoint: .trailing)
                    ).frame(width: 0.2 * gp.size.width)
                    .frame(maxWidth: .infinity, alignment: .trailing)

                    .allowsHitTesting(false)  // << now works !!

            }.fixedSize(horizontal: false, vertical: true)
        }
    }
}

使用 .allowsHitTesting(false) 可以使其可拖动。 - Ray Xu
很遗憾,@RayXu,在iOS < 15上无法工作。 - saltwat5r

2
刚刚将lmunck的优秀答案转化为视图扩展,以提高可读性,并添加了一个变量来控制淡出效果的大小(宽度)。
使用方法:
ScrollView {
    // Content
}
    .fadeOutSides() // fade out with default mask
    //.fadeOutSides(fadeLength:200) // specified fade gradient size

这是扩展的部分

extension View {
    func fadeOutSides(fadeLength:CGFloat=50) -> some View {
        return mask(
            HStack(spacing: 0) {
                
                // Left gradient
                LinearGradient(gradient: Gradient(
                    colors: [Color.black.opacity(0), Color.black]),
                    startPoint: .leading, endPoint: .trailing
                )
                .frame(height: fadeLength)
                
                // Middle
                Rectangle().fill(Color.black)
                
                // Right gradient
                LinearGradient(gradient: Gradient(
                    colors: [Color.black, Color.black.opacity(0)]),
                    startPoint: .leading, endPoint: .trailing
                )
                .frame(width: fadeLength)
            }
        )
    }
}

我在做的时候,我还制作了一个用于淡出ScrollView顶部的效果

使用方法:

ScrollView {
    // Content
}
    .fadeOutTop() // fade out with default mask
    //.fadeOutTop(fadeLength:200) // specified fade gradient size

还有这个扩展

extension View {
    func fadeOutTop(fadeLength:CGFloat=50) -> some View {
        return mask(
            VStack(spacing: 0) {

                // Top gradient
                LinearGradient(gradient:
                   Gradient(
                       colors: [Color.black.opacity(0), Color.black]),
                       startPoint: .top, endPoint: .bottom
                   )
                   .frame(height: fadeLength)
                
                Rectangle().fill(Color.black)
            }
        )
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接