SwiftUI - 淡出滚动视图
SwiftUI - fade out a ScrollView
我有一个生成的超大图表,我将其放入 ScrollView 中,以便用户可以向右滚动并查看所有值。我想通过淡出 ScrollView 向用户表明右侧有“更多内容”。 Swift 中的某些内容通过应用 CAGradientLayer 很容易。
我的方法是使用从透明(从 80% 开始)到系统背景色(以 100% 结束)的渐变应用叠加层。结果可见附件截图。
问题没有。 1:看起来不像应该的样子。
问题没有。 2: 尽管将 zIndex 应用到覆盖层,但一旦应用覆盖层,ScrollView 将不再滚动。
知道如何实现吗?谢谢!
这是我的代码:
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)
}
}
好的,已知 SwiftUI 问题是它不会通过叠加层传递一些手势,即使是透明的。
这里是解决这个问题的可能方法 - 这个想法是让渐变只覆盖小的边缘位置,所以滚动视图的其他部分可以直接访问(是的,在渐变下它仍然不可拖动,但它是小部分)。
使用 Xcode 11.7 / iOS 13.7
准备和测试的演示
(您的观点的简化变体)
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()
.animation(.interactiveSpring())
}
// 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)
}.fixedSize(horizontal: false, vertical: true)
}
}
}
使用 alpha .mask 怎么样?
您可以像这样使用 alpha 蒙版来代替您的 .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)
}
)
我有一个生成的超大图表,我将其放入 ScrollView 中,以便用户可以向右滚动并查看所有值。我想通过淡出 ScrollView 向用户表明右侧有“更多内容”。 Swift 中的某些内容通过应用 CAGradientLayer 很容易。
我的方法是使用从透明(从 80% 开始)到系统背景色(以 100% 结束)的渐变应用叠加层。结果可见附件截图。
问题没有。 1:看起来不像应该的样子。
问题没有。 2: 尽管将 zIndex 应用到覆盖层,但一旦应用覆盖层,ScrollView 将不再滚动。
知道如何实现吗?谢谢!
这是我的代码:
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)
}
}
好的,已知 SwiftUI 问题是它不会通过叠加层传递一些手势,即使是透明的。
这里是解决这个问题的可能方法 - 这个想法是让渐变只覆盖小的边缘位置,所以滚动视图的其他部分可以直接访问(是的,在渐变下它仍然不可拖动,但它是小部分)。
使用 Xcode 11.7 / iOS 13.7
准备和测试的演示(您的观点的简化变体)
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()
.animation(.interactiveSpring())
}
// 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)
}.fixedSize(horizontal: false, vertical: true)
}
}
}
使用 alpha .mask 怎么样?
您可以像这样使用 alpha 蒙版来代替您的 .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)
}
)