如题,QQ 音乐 macOS 客户端每日推荐的轮播图组件是怎么实现的?使用 SwiftUI 能编写类似的效果吗?关键的需求如下:
我是个 Swift 新手,但我询问了 ChatGPT、检索了 OverflowStack 都没找到比较合理的答案,自己实现了一个接近的版本,但很容易出问题(https://stackoverflow.com/a/79830687/13050251)。(答案已更新,现在版本已经比较鲁棒了)
我实现过程中遇到的问题主要在于,为了让一屏显示多个图片,就得获取父容器的宽度来计算每个图片的宽度,并根据给定的比例来计算图片高度。此时就不可避免的使用 GeometryReader 容器,但是 GeometryReader 的高度完全不受内部图片高度的影响,空间充足时它会撑开所有空间导致轮播图下面出现留白,空间不足时它会挤压轮播图的空间。
我在上面给出的例子(
https://stackoverflow.com/a/79830687/13050251)中使用了多个
PreferenceKey来报道子元素的高度然后设置父元素的高度,但这里会出现无限循环的问题,在某些场景下还用不了。
请问有没有做过类似需求的同学来指点一下,这种需求应该怎么实现?
终于实现了一个版本,从表现上看几乎完美符合需求。但感觉在 PreferenceKey 中设置 value 时进行非 0 判断的操作有些“邪修”,可是如果不这么做获取到的 width 会一直为 0。在运行过程中,Xcode 会给出警告,应该也和这里有关系,不知道影响有多大。

import SwiftUI
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
let width = nextValue()
if width > 0 {
value = width
}
}
}
let testColors: [Color] = [
.blue,
.orange,
.yellow,
.green,
.purple,
]
struct carousel2: View {
var data: [Color]
var itemRatio: CGFloat = 1.5
var spacing: CGFloat = 10
@State private var width: CGFloat = 0
@State private var touchOffsetX: CGFloat = 0
@State private var groupOffsetX: CGFloat = 0
@State private var hovering: Bool = false
@State var currentIndex: CGFloat = 0
var pageSize: Int {
width > 1000 ? 3 : 2
}
var itemWidth: CGFloat {
(width - spacing * CGFloat(pageSize - 1)) / CGFloat(pageSize)
}
var itemHeight: CGFloat {
itemWidth / itemRatio
}
func calcItemOffsetX(_ i: Int) -> CGFloat {
CGFloat(i) * itemWidth + spacing * CGFloat(i)
}
let groupOffsetXUpper: CGFloat = 0
var groupOffsetXLower: CGFloat {
-CGFloat(data.count - pageSize) * (itemWidth + spacing)
}
func calcGroupOffsetX() -> CGFloat {
let offsetX = -currentIndex * (itemWidth + spacing) + self.touchOffsetX
return min(groupOffsetXUpper, max(groupOffsetXLower, offsetX))
}
var body: some View {
ScrollView {
HStack {
Text("Header")
}.padding(.vertical, 20).frame(maxWidth: .infinity).background(Color.gray)
HStack(spacing: 0) {
Image(systemName: "chevron.left").font(.system(size: 30))
.opacity(!hovering ? 0 : currentIndex > 0 ? 1 : 0.2)
.padding(.horizontal, 10)
.contentShape(Rectangle())
.gesture(
TapGesture().onEnded {
currentIndex -= 1
currentIndex = max(0, min(CGFloat(self.data.count - self.pageSize), currentIndex))
withAnimation(.interactiveSpring(response: 0.25, dampingFraction: 0.85)) {
self.groupOffsetX = calcGroupOffsetX()
}
}
)
ZStack {
ForEach(data.indices, id: \.self) { i in
Rectangle()
.foregroundStyle(testColors[i])
.frame(maxWidth: itemWidth)
.frame(height: itemHeight)
.offset(x: calcItemOffsetX(i))
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.offset(x: groupOffsetX)
.contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
self.touchOffsetX = value.translation.width
self.groupOffsetX = calcGroupOffsetX()
}
.onEnded { value in
currentIndex -= (value.translation.width / self.itemWidth).rounded()
currentIndex = max(0, min(CGFloat(self.data.count - self.pageSize), currentIndex))
self.touchOffsetX = 0
withAnimation(.interactiveSpring(response: 0.25, dampingFraction: 0.85)) {
self.groupOffsetX = calcGroupOffsetX()
}
}
)
.background(
GeometryReader { geo in
Color.clear
.preference(key: WidthPreferenceKey.self, value: geo.size.width)
}
)
.onPreferenceChange(WidthPreferenceKey.self) { width in
self.width = width
}.clipped().padding(0)
Image(systemName: "chevron.right").font(.system(size: 30))
.opacity(
!hovering ? 0 : currentIndex < CGFloat(self.data.count - self.pageSize) ? 1 : 0.2
)
.padding(.horizontal, 10)
.contentShape(Rectangle())
.gesture(
TapGesture().onEnded {
currentIndex += 1
currentIndex = max(0, min(CGFloat(self.data.count - self.pageSize), currentIndex))
withAnimation(.interactiveSpring(response: 0.25, dampingFraction: 0.85)) {
self.groupOffsetX = calcGroupOffsetX()
}
}
)
}
.onHover { inside in
hovering = inside
}
VStack {
Text("Very long content").frame(maxWidth: .infinity).frame(height: 1000, alignment: .top)
.padding().background(Color.gray)
}
}.frame(maxWidth: .infinity)
}
}
struct DemoView: View {
var body: some View {
carousel2(data: testColors)
}
}
#Preview {
DemoView()
}
进程的内存有哪些分区?(栈区、堆区、全局区(静态区)、文字常量区和程序代码区) 堆和栈有什么区别? 一个程序的编译过程,从代码到可执行文件?(预处理、编译【词法分析、语法分析、语义分析、源代码优化、代码生成、目标代码优化】、汇编、链接) C++和C的区别 面向对象和面向过程的区别? Java语言的特点 Java的反射机制 平时在什么场景下会用到反射 反射存在什么问题 java的垃圾回收机制 不同的
本文向大家介绍JS原生轮播图的简单实现(推荐),包括了JS原生轮播图的简单实现(推荐)的使用技巧和注意事项,需要的朋友参考一下 哈喽!我的朋友们,最近有一个新项目。所以一直没更新!有没有想我啊!! 今天咱们来说一下JS原生轮播图! 话不多说: 直接来代码吧:下面是CSS部分: HTML部分! 接下来是JS部分: 就是这么简单!你学会了吗?? 以上这篇JS原生轮播图的简单实现(推荐)就是小编分享给大
本文向大家介绍纯javaScript、jQuery实现个性化图片轮播【推荐】,包括了纯javaScript、jQuery实现个性化图片轮播【推荐】的使用技巧和注意事项,需要的朋友参考一下 纯javaScript实现个性化图片轮播 轮播原理说明<如上图所示>: 1. 画布部分(可视区域)属性说明:overflow:hidden使得超出画布部分隐藏或说不可见。position:relative 会导致
如图,在uni中怎么实现这种轮播,不是纯图片,里面的数字需要动态获取 目前在寻找插件,
拷打项目 图片懒加载实现 ES6了解吗 promise(作用,API) ESm和cjs区别(ESM导出的变量是常量,cjs是变量) webpack和vite(只会用vite) https和http区别(TSL握手流程?) 浏览器弹出安全警告原因?(网站使用了HTTP而不是HTTPS,或者证书过期) 跨域是什么,怎么解决 XSS和CSRF攻击和防御 算法:快排#软件开发笔面经#
本文向大家介绍值得分享的JavaScript实现图片轮播组件,包括了值得分享的JavaScript实现图片轮播组件的使用技巧和注意事项,需要的朋友参考一下 本文实例为大家分享了JavaScript实现图片轮播组件的使用方法,供大家参考,具体内容如下 效果: 自动循环播放图片,下方有按钮可以切换到对应图片。 添加一个动画来实现图片切换。 鼠标停在图片上时,轮播停止,出现左右两个箭头,点击可以切换图片
简介 yii2 Carousel是一个基于JavaScript的图片轮播组件,使用Carousel可以快速的在网站任意位置放置一个图片轮播的效果 实例 <?php echo Carousel::widget([ 'items' => [ // 只有图片的格式 '<img src="http://www.yii-china.com/statics/images
本文向大家介绍Android实现简单音乐播放控件,包括了Android实现简单音乐播放控件的使用技巧和注意事项,需要的朋友参考一下 之前看到网页版的网易音乐播放控件, 正好在一个开源学习项目中需要简单的音乐播放功能。所以想是不是可以封装一个音乐播放控件,提供一个类似网易播放控件的默认界面,而且提供更换界面的功能。使用时,只需要去设计界面, 而不用再去管音乐播放的逻辑,所以就实现了一个简单的音乐播放