当前位置: 首页 > 知识库问答 >
问题:

macos - QQ 音乐 macOS 客户端每日推荐的轮播图组件是怎么实现的?

小牛24506
2025-11-27

如题,QQ 音乐 macOS 客户端每日推荐的轮播图组件是怎么实现的?使用 SwiftUI 能编写类似的效果吗?关键的需求如下:

  • 每一屏展示多张图片,可以左右滑动
  • 根据窗口宽度的变化,轮播图里的图片会自动等比缩放
  • 窗口宽度变化超过某个阈值,每一屏展示的图片数量也会相应增减

我是个 Swift 新手,但我询问了 ChatGPT、检索了 OverflowStack 都没找到比较合理的答案,自己实现了一个接近的版本,但很容易出问题(https://stackoverflow.com/a/79830687/13050251)。(答案已更新,现在版本已经比较鲁棒了)

我实现过程中遇到的问题主要在于,为了让一屏显示多个图片,就得获取父容器的宽度来计算每个图片的宽度,并根据给定的比例来计算图片高度。此时就不可避免的使用 GeometryReader 容器,但是 GeometryReader 的高度完全不受内部图片高度的影响,空间充足时它会撑开所有空间导致轮播图下面出现留白,空间不足时它会挤压轮播图的空间。

我在上面给出的例子( https://stackoverflow.com/a/79830687/13050251)中使用了多个 PreferenceKey来报道子元素的高度然后设置父元素的高度,但这里会出现无限循环的问题,在某些场景下还用不了。

请问有没有做过类似需求的同学来指点一下,这种需求应该怎么实现?

共有1个答案

祁飞扬
2025-11-27

终于实现了一个版本,从表现上看几乎完美符合需求。但感觉在 PreferenceKey 中设置 value 时进行非 0 判断的操作有些“邪修”,可是如果不这么做获取到的 width 会一直为 0。在运行过程中,Xcode 会给出警告,应该也和这里有关系,不知道影响有多大。

Screenshot 2025-11-27 at 1.28.52 AM.png

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实现简单音乐播放控件的使用技巧和注意事项,需要的朋友参考一下 之前看到网页版的网易音乐播放控件, 正好在一个开源学习项目中需要简单的音乐播放功能。所以想是不是可以封装一个音乐播放控件,提供一个类似网易播放控件的默认界面,而且提供更换界面的功能。使用时,只需要去设计界面, 而不用再去管音乐播放的逻辑,所以就实现了一个简单的音乐播放