8b4498f4af10d6b33b9ce090131082d6
SwiftUI 特色组件之创建强大的Marquee 滚动条组件(教程含源码)

实战需求

创建强大的Marquee 滚动

本文价值与收获

看完本文后,您将能够作出下面的界面

创建强大的Marquee 滚动

创建强大的Marquee 滚动

解决方案

什么样的跑马灯才厉害?

  • 它必须支持任何内容视图(MarqueeLabel仅支持文本)。
  • 它可以自定义动画持续时间、自动反转、方向等。
  • 它可以任意组合使用。

跑马灯动画原理

image.png

image.png

image.png

image.png

其原理是,内容视图从移动一端选取框到另一个,并且循环重复完成之后。

实施步骤

所述第一步骤是获得选取框和内容视图的宽度。关于这一点,可以使用GeometryReader和PreferenceKey来实现。
GeometryReader 为 我们提供了一个输入值,告诉我们可用的宽度和高度,然后我们可以将其用于我们需要的任何计算。

image.png

image.png

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            Text("width: \(geometry.size.width)")
                .frame(width: geometry.size.width, height: 50)
                .background(Color.yellow)
        }
    }
}

众所周知,SwiftUI 具有环境概念,我们可以使用它来将数据向下传递到视图层次结构中。父视图与子视图共享它们的环境并订阅更改。但有时我们需要将数据从孩子的视图传递到父母的视图,这就是偏好的亮点。

因为跑马灯需要知道content view的宽度才能制作动画,这里需要使用PreferenceKey来实现。

image.png

image.png

struct ContentView: View {
    @State var text: String = "\(Date())"
    @State var textWidth: CGFloat = 0

    var body: some View {
        GeometryReader { geometry in
            VStack {
                Text(text)
                    .background(GeometryBackground())
                    .background(Color.yellow)

                Text("text width: \(textWidth)")

                Button(action: {
                    self.text = "\(Date())"
                }, label: {
                    Text("change text")
                })
            }
        }
        // Listen content width changes
        .onPreferenceChange(WidthKey.self, perform: { value in
            self.textWidth = value
        })
    }
}
struct GeometryBackground: View {
    var body: some View {
        GeometryReader { geometry in
            return Color.clear.preference(key: WidthKey.self, value: geometry.size.width)
        }
    }
}
struct WidthKey: PreferenceKey {
    static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
    typealias Value = CGFloat
}

所述第二步骤是实现偏移动画。

image.png

image.png

struct ContentView : View {
    @State private var offset: CGFloat = 0
    var body: some View {
        Text("offset animation")
            .offset(x: offset, y: 0)
            .onAppear {
                withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: true)) {
                    self.offset = 100
                }
            }.background(Color.yellow)
    }
}

所述第三步骤是使用ViewBuilder支持任何内容视图。

image.png

image.png

struct ContentView : View {
    @State private var offset: CGFloat = 0
var body: some View {
        ViewBuilderView {
            Text("content view")
        }
    }
}
struct ViewBuilderView<Content> : View where Content : View {
    private var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    public var body: some View {
        VStack {
            Text("---")
            content()
                .background(Color.yellow)
            Text("---")
        }.background(Color.blue)
    }
}
top Created with Sketch.