Swift UI – 将长文本行动画滚动左右

huangapple go评论82阅读模式
英文:

Swift UI - Animating long line of text to scroll left and right

问题

I would like to animate long single line of text to display the entire text by scrolling left and right on repeat.

我想要通过左右滚动来动画显示长单行文本的整个文本。

I tried to do this myself using the following code, but it just scrolls to the end of the text when the view is loaded. It doesn't scroll back.

我尝试使用以下代码自己实现这个效果,但它在视图加载时只会滚动到文本的末尾,而不会滚动回来。

英文:

I would like to animate long single line of text to display the entire text by scrolling left and right on repeat.

I tried to do this myself using the following code, but it just scrolls to the end of the text when the view is loaded. It doesn't scroll back.

private func scrollViewForLongName(
    _ name: String) -> some View {
        let topID = 1
        let bottomID = 29
        
        return ScrollViewReader { proxy in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack {
                    Text(name)
                        .id(topID)
                        .onAppear {
                            let baseAnimation = Animation.easeInOut(duration: 1)
                            let repeated = baseAnimation.repeatForever(autoreverses: true)
                            
                            DispatchQueue.main.async {
                                withAnimation(repeated) {
                                    proxy.scrollTo(bottomID)
                                }
                            }
                        }
                    
                    Text(" ")
                        .id(bottomID)
                }
            }
        }
    }

答案1

得分: 0

以下是代码的翻译部分:

onAppear 视图修饰符仅运行一次,但您需要在1分钟后重复执行该操作。看一下这段代码可能会有所帮助。

    struct ContentView: View {
        @Namespace var topID
        @Namespace var bottomID
        @State var atTop: Bool = false
    
        var body: some View {
            let baseAnimation = Animation.easeInOut(duration: 5)
    
            ScrollViewReader { proxy in
                ScrollView {
                    Text("")
                    .id(topID)
    
                    VStack(spacing: 0) {
                        ForEach(0..<100) { i in
                            color(fraction: Double(i) / 100)
                                .frame(height: 32)
                        }
                    }
                    
                    Text("")
                    .id(bottomID)
                }
                .onAppear{
                    atTop = true
                }
                .onChange(of: atTop) { newValue in
                    if newValue{
                        withAnimation(baseAnimation) {
                            proxy.scrollTo(bottomID)
                            DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
                                atTop = false
                            }
                        }
                    }else{
                        withAnimation(baseAnimation) {
                            proxy.scrollTo(topID)
                            DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
                                atTop = true
                            }
                        }
                    }
                }
            }
        }
        
        func color(fraction: Double) -> Color {
            Color(red: fraction, green: 1 - fraction, blue: 0.5)
        }
    }

请注意,我已将HTML实体编码(如&quot;&lt;)翻译为正常的Swift代码。

英文:

onAppear view modifier only runs once, but you need to repeat the action after 1 min. take a look at this code may be helpful.

struct ContentView: View {
    @Namespace var topID
    @Namespace var bottomID
    @State var atTop: Bool = false

    var body: some View {
        let baseAnimation = Animation.easeInOut(duration: 5)

        ScrollViewReader { proxy in
            ScrollView {
                Text(&quot;&quot;)
                .id(topID)

                VStack(spacing: 0) {
                    ForEach(0..&lt;100) { i in
                        color(fraction: Double(i) / 100)
                            .frame(height: 32)
                    }
                }
                
                Text(&quot;&quot;)
                .id(bottomID)
            }
            .onAppear{
                atTop = true
            }
            .onChange(of: atTop) { newValue in
                if newValue{
                    withAnimation(baseAnimation) {
                        proxy.scrollTo(bottomID)
                        DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
                            atTop = false
                        }
                    }
                }else{
                    withAnimation(baseAnimation) {
                        proxy.scrollTo(topID)
                        DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
                            atTop = true
                        }
                    }
                }
            }
        }
    }
    

    func color(fraction: Double) -&gt; Color {
        Color(red: fraction, green: 1 - fraction, blue: 0.5)
    }
}

答案2

得分: 0

这是为您提供的解决方案。在您的标题和问题中,您说您正在寻找一个能左右滚动并重复播放的动画,所以我假设您希望看到它横向滚动,然后倒带并再次播放。这是一种不同于常规滚动字幕的动画,通常只是以相同方向不断循环。但是,如果您需要一个字幕,您可以通过搜索 Stack Overflow 找到解决方案,或者您可以调整此解决方案(这将更简单)。

此解决方案先横向滚动,然后以更快的速度倒带并重复播放动画。为此,它使用我在 Change Reverse Animation Speed SwiftUI 中发布的技术。文本的宽度是通过将其显示为基本视图来确定的,然后将动画应用为其顶部的叠加层。通过在叠加层上应用背景,将基本视图屏蔽掉。

编辑:代码已更新以在开头和结尾应用相同的延迟。
编辑(2):添加了在字符串较短时防止除以零的检查。

struct OffsetModifier: ViewModifier, Animatable {

    private let maxOffset: CGFloat
    private let rewindSpeedFactor: Int
    private let endWaitFraction: CGFloat
    private var progress: CGFloat

    // The progress value at which the end wait begins
    private let endWaitThreshold: CGFloat

    // The progress value at which rewind begins
    private let rewindThreshold: CGFloat

    init(
        maxOffset: CGFloat,
        rewindSpeedFactor: Int = 4,
        endWaitFraction: CGFloat = 0,
        progress: CGFloat
    ) {
        self.maxOffset = maxOffset
        self.rewindSpeedFactor = rewindSpeedFactor
        self.endWaitFraction = endWaitFraction
        self.progress = progress

        // Compute the thresholds for waiting and for rewinding
        let rewindFraction = (CGFloat(1) - endWaitFraction) / CGFloat(rewindSpeedFactor + 1)
        self.rewindThreshold = CGFloat(1) - rewindFraction
        self.endWaitThreshold = CGFloat(1) - rewindFraction - endWaitFraction
    }

    /// Implementation of protocol property
    var animatableData: CGFloat {
        get { progress }
        set { progress = newValue }
    }

    var xOffset: CGFloat {
        let fraction: CGFloat
        if progress > rewindThreshold {
            fraction = endWaitThreshold - ((progress - rewindThreshold) * CGFloat(rewindSpeedFactor))
        } else {
            fraction = min(progress, endWaitThreshold)
        }
        return endWaitThreshold > 0 ? (fraction / endWaitThreshold) * maxOffset : 0
    }

    func body(content: Content) -> some View {
        content.offset(x: xOffset)
    }
}

struct RewindingTextTicker: View {

    let textKey: String
    let viewWidth: CGFloat
    let beginEndDelaySecs: TimeInterval

    init(
        textKey: String,
        viewWidth: CGFloat,
        beginEndDelaySecs: TimeInterval = 1.0
    ) {
        self.textKey = textKey
        self.viewWidth = viewWidth
        self.beginEndDelaySecs = beginEndDelaySecs
    }

    let pixelsPerSec = 100
    @State private var progress = CGFloat.zero

    private func duration(width: CGFloat) -> TimeInterval {
        (TimeInterval(max(width, 0)) / TimeInterval(pixelsPerSec)) + beginEndDelaySecs
    }

    private func endWaitFraction(textWidth: CGFloat) -> CGFloat {
        let totalDuration = duration(width: textWidth - viewWidth)
        return totalDuration > 0 ? beginEndDelaySecs / totalDuration : 0
    }

    var body: some View {

        // Display the full text on one line.
        // This establishes the width that is needed
        Text(LocalizedStringKey(textKey))
            .lineLimit(1)
            .fixedSize(horizontal: true, vertical: true)

            // Perform the animation in an overlay
            .overlay(
                GeometryReader { proxy in
                    Text(LocalizedStringKey(textKey))
                        .modifier(
                            OffsetModifier(
                                maxOffset: viewWidth - proxy.size.width,
                                endWaitFraction: endWaitFraction(textWidth: proxy.size.width),
                                progress: progress
                            )
                        )
                        .animation(
                            .linear(duration: duration(width: proxy.size.width - viewWidth))
                            .delay(beginEndDelaySecs)
                            .repeatForever(autoreverses: false),
                            value: progress
                        )
                        // Mask out the base view
                        .background(Color(UIColor.systemBackground))
                }
            )
            .onAppear { progress = 1.0 }
    }
}

struct ContentView: View {

    private let fullText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."

    var body: some View {
        VStack {
            GeometryReader { proxy in
                RewindingTextTicker(
                    textKey: fullText,
                    viewWidth: proxy.size.width
                )
            }
            .frame(height: 50)
        }
        .padding(.horizontal, 50)
    }
}

这是它的运行示例。实际上,在滚动的开头和结尾都有延迟,但是这个延迟在动画的 GIF 中不可见。
Swift UI – 将长文本行动画滚动左右

英文:

Here is a solution for you. In your title and question you said you were looking for an animation that scrolls left and right on repeat, so I assume you want to see it scroll across, then rewind and play again. This is a different kind of animation to a normal ticker, which usually just keeps looping in the same direction. However, if it is a ticker that you want then you can find solutions by searching SO, or you can adapt this one (it would be simpler).

This solution scrolls across, then rewinds at a faster speed and repeats the animation again. To do this it uses the technique I posted as an answer to Change Reverse Animation Speed SwiftUI. The width of the text is established by displaying it as the base view, then the animation is applied as an overlay over the top of it. The base view is masked out by applying a background to the overlay.

EDIT: Code updated to apply the same delay at begin and end.
EDIT(2): Added checks to prevent division by zero when the string is short.

struct OffsetModifier: ViewModifier, Animatable {

    private let maxOffset: CGFloat
    private let rewindSpeedFactor: Int
    private let endWaitFraction: CGFloat
    private var progress: CGFloat

    // The progress value at which the end wait begins
    private let endWaitThreshold: CGFloat

    // The progress value at which rewind begins
    private let rewindThreshold: CGFloat

    init(
        maxOffset: CGFloat,
        rewindSpeedFactor: Int = 4,
        endWaitFraction: CGFloat = 0,
        progress: CGFloat
    ) {
        self.maxOffset = maxOffset
        self.rewindSpeedFactor = rewindSpeedFactor
        self.endWaitFraction = endWaitFraction
        self.progress = progress

        // Compute the thresholds for waiting and for rewinding
        let rewindFraction = (CGFloat(1) - endWaitFraction) / CGFloat(rewindSpeedFactor + 1)
        self.rewindThreshold = CGFloat(1) - rewindFraction
        self.endWaitThreshold = CGFloat(1) - rewindFraction - endWaitFraction
    }

    /// Implementation of protocol property
    var animatableData: CGFloat {
        get { progress }
        set { progress = newValue }
    }

    var xOffset: CGFloat {
        let fraction: CGFloat
        if progress &gt; rewindThreshold {
            fraction = endWaitThreshold - ((progress - rewindThreshold) * CGFloat(rewindSpeedFactor))
        } else {
            fraction = min(progress, endWaitThreshold)
        }
        return endWaitThreshold &gt; 0 ? (fraction / endWaitThreshold) * maxOffset : 0
    }

    func body(content: Content) -&gt; some View {
        content.offset(x: xOffset)
    }
}

struct RewindingTextTicker: View {

    let textKey: String
    let viewWidth: CGFloat
    let beginEndDelaySecs: TimeInterval

    init(
        textKey: String,
        viewWidth: CGFloat,
        beginEndDelaySecs: TimeInterval = 1.0
    ) {
        self.textKey = textKey
        self.viewWidth = viewWidth
        self.beginEndDelaySecs = beginEndDelaySecs
    }

    let pixelsPerSec = 100
    @State private var progress = CGFloat.zero

    private func duration(width: CGFloat) -&gt; TimeInterval {
        (TimeInterval(max(width, 0)) / TimeInterval(pixelsPerSec)) + beginEndDelaySecs
    }

    private func endWaitFraction(textWidth: CGFloat) -&gt; CGFloat {
        let totalDuration = duration(width: textWidth - viewWidth)
        return totalDuration &gt; 0 ? beginEndDelaySecs / totalDuration : 0
    }

    var body: some View {

        // Display the full text on one line.
        // This establishes the width that is needed
        Text(LocalizedStringKey(textKey))
            .lineLimit(1)
            .fixedSize(horizontal: true, vertical: true)

            // Perform the animation in an overlay
            .overlay(
                GeometryReader { proxy in
                    Text(LocalizedStringKey(textKey))
                        .modifier(
                            OffsetModifier(
                                maxOffset: viewWidth - proxy.size.width,
                                endWaitFraction: endWaitFraction(textWidth: proxy.size.width),
                                progress: progress
                            )
                        )
                        .animation(
                            .linear(duration: duration(width: proxy.size.width - viewWidth))
                            .delay(beginEndDelaySecs)
                            .repeatForever(autoreverses: false),
                            value: progress
                        )
                        // Mask out the base view
                        .background(Color(UIColor.systemBackground))
                }
            )
            .onAppear { progress = 1.0 }
    }
}

struct ContentView: View {

    private let fullText = &quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.&quot;

    var body: some View {
        VStack {
            GeometryReader { proxy in
                RewindingTextTicker(
                    textKey: fullText,
                    viewWidth: proxy.size.width
                )
            }
            .frame(height: 50)
        }
        .padding(.horizontal, 50)
    }
}

Here it is running. There is actually a delay at begin and end of scroll, but the delay does not show in the animated gif.
Swift UI – 将长文本行动画滚动左右

huangapple
  • 本文由 发表于 2023年6月13日 17:45:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76463622.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定