如何使这个打字机动画更流畅,减少模糊?

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

How to make this typewriter animation smoother and less blurry?

问题

我正在创建这个动画,想知道是否有办法让它更有趣。

这里是代码:

import SwiftUI

struct TypewriterView: View {
    let text: String
    @State private var animatedText = ""

    var body: some View {
        Text(animatedText)
            .font(.title)
            .padding()
            .onAppear {
                animateText()
            }
    }

    private func animateText() {
        for (index, character) in text.enumerated() {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.05) {
                withAnimation {
                    animatedText += String(character)
                }
            }
        }
    }
}

// Usage example
struct ContentView: View {
    var body: some View {
        TypewriterView(text: "Hello, Twitter! This is a typewriter animation.")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

也许我应该在 withAnimation 中设置一个动画...

有什么想法?

英文:

I'm creating this animation and would like to know if there is a way to make it more enjoyable.

如何使这个打字机动画更流畅,减少模糊?

here is the code:

import SwiftUI

struct TypewriterView: View {
    let text: String
    @State private var animatedText = ""

    var body: some View {
        Text(animatedText)
            .font(.title)
            .padding()
            .onAppear {
                animateText()
            }
    }

    private func animateText() {
        for (index, character) in text.enumerated() {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.05) {
                withAnimation {
                    animatedText += String(character)
                }
                
            }
        }
    }
}

// Usage example
struct ContentView: View {
    var body: some View {
        TypewriterView(text: "Hello, Twitter! This is a typewriter animation.")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Maybe I should set an Animation in withAnimation...

Any thoughts?

答案1

得分: 1

这是您要翻译的内容:

You want just the opposite here. You don't want an animation. That's leading to it blending between the two states. You just want to add characters. I recommend this approach, replacing GCD with async/await:

这里您希望完全相反。您想要动画。这会导致它在两种状态之间混合。您只想要添加字符。我建议使用async/await来实现这一点:

var body: some View {
    Text(animatedText)
        .font(.title)
        .padding()
        .task { await animateText() } // 使用.task来创建异步上下文
}

private func animateText() async {
    for character in text {
        animatedText.append(character)
        // 使用`.sleep`来分隔更新,而不是`asyncAfter`
        try! await Task.sleep(for: .milliseconds(75))
    }
}

这里还有一个不相关的问题,我发现当文字自动换行时,Text组件会改变其布局方式。当文本删除最后一个单词时,“a”会与“typewriter”放在第二行。当第三个单词添加到该行时,视图会重新调整大小,“a”会上移一行,这会破坏“打字机”效果。我还没有找到一个好的解决方案(但这与动画问题无关)。


To fix the problem with the text reformatting, it does seem to work to create a string of spaces of the correct length, and then swap in letters. It works even better if you add .monospaced() to the Text, which better matches the look of a typewriter, but this isn't required.

要解决文本重新格式化的问题,似乎可以创建一个正确长度的空格字符串,然后逐渐替换为字母。如果在Text上添加.monospaced(),效果会更好,因为它更符合打字机的外观,但这不是必需的。

var body: some View {
    Text(animatedText)
        .font(.title)
        .monospaced() // 如果需要的话
        .padding()
        .task { await animateText() }
        .onTapGesture {
            // 这样更容易测试
            animatedText = ""
            Task { await animateText() }
        }
}

private func animateText() async {
    var characters = Array(repeating: Character(" "),
                           count: text.count)
    for (index, character) in zip(characters.indices, text) {
        characters[index] = character
        animatedText = String(characters)
        try! await Task.sleep(for: .milliseconds(50))
    }
}

文本包装仍然有点尴尬,因为它不会在超过行宽度之前自动换行,所以更像是一个文字处理器而不是打字机,但这是一小段代码的一个相当好的效果。


I did a little more thinking on this, and a possibly better approach is to make all the text clear, and then progressively remove that attribute. This way there's only one layout step. Here's an updated version that does that and also restarts the animation any time the text is changed.

我对此进行了更多思考,可能更好的方法是将所有文本设置为透明,然后逐渐删除该属性。这样只有一个布局步骤。这是一个更新后的版本,它这样做,并且每当文本更改时都会重新启动动画。

struct TypewriterView: View {
    var text: String
    var typingDelay: Duration = .milliseconds(50)

    @State private var animatedText: AttributedString = ""
    @State private var typingTask: Task<Void, Error>?

    var body: some View {
        Text(animatedText)
            .onChange(of: text) { _ in animateText() }
            .onAppear() { animateText() }
    }

    private func animateText() {
        typingTask?.cancel()

        typingTask = Task {
            let defaultAttributes = AttributeContainer()
            animatedText = AttributedString(text,
                                            attributes: defaultAttributes.foregroundColor(.clear)
            )

            var index = animatedText.startIndex
            while index < animatedText.endIndex {
                try Task.checkCancellation()

                // 更新样式
                animatedText[animatedText.startIndex...index]
                    .setAttributes(defaultAttributes)

                // 等待
                try await Task.sleep(for: typingDelay)

                // 逐字符前进
                index = animatedText.index(afterCharacter: index)
            }
        }
    }
}

// 使用示例
struct ContentView: View {
    @State var text = "Hello, Twitter! This is a typewriter animation."

    var body: some View {
        TypewriterView(text: text)
            .font(.title)
            .padding()
    }
}

以上是您要求的翻译部分。

英文:

You want just the opposite here. You don't want an animation. That's leading to it blending between the two states. You just want to add characters. I recommend this approach, replacing GCD with async/await:

var body: some View {
    Text(animatedText)
        .font(.title)
        .padding()
        .task { await animateText() } // Use .task to create async context
}

private func animateText() async {
    for character in text {
        animatedText.append(character)
        // Use `.sleep` to separate the updates rather than `asyncAfter`
        try! await Task.sleep(for: .milliseconds(75))
    }
}

There is a unrelated problem I see with this where Text changes its mind about its layout when word-wrapping. When the text drops the last word, the "a" is put on the second line with "typewriter." When a third word is added to that line, then the view resizes and the "a" moves up a line, which breaks the "typewriter" illusion. I haven't figure out a good solution to that (but it's separate from the animation question).


To fix the problem with the text reformatting, it does seem to work to create a string of spaces of the correct length, and then swap in letters. It works even better if you add .monospaced() to the Text, which better matches the look of a typewriter, but this isn't required.

var body: some View {
    Text(animatedText)
        .font(.title)
        .monospaced() // If desired
        .padding()
        .task { await animateText() }
        .onTapGesture {
            // makes testing it easier
            animatedText = &quot;&quot;
            Task { await animateText() }
        }
}

private func animateText() async {
    var characters = Array(repeating: Character(&quot; &quot;),
                           count: text.count)
    for (index, character) in zip(characters.indices, text) {
        characters[index] = character
        animatedText = String(characters)
        try! await Task.sleep(for: .milliseconds(50))
    }
}

Wrapping is still a bit awkward, since it won't wrap until it exceeds the width of the line, so it's more like a word processor than a typewriter, but it's a fairly good effect for a small amount of code.


I did a little more thinking on this, and a possibly better approach is to make all the text clear, and then progressively remove that attribute. This way there's only one layout step. Here's an updated version that does that and also restarts the animation any time the text is changed.

struct TypewriterView: View {
    var text: String
    var typingDelay: Duration = .milliseconds(50)

    @State private var animatedText: AttributedString = &quot;&quot;
    @State private var typingTask: Task&lt;Void, Error&gt;?

    var body: some View {
        Text(animatedText)
            .onChange(of: text) { _ in animateText() }
            .onAppear() { animateText() }
    }

    private func animateText() {
        typingTask?.cancel()

        typingTask = Task {
            let defaultAttributes = AttributeContainer()
            animatedText = AttributedString(text,
                                            attributes: defaultAttributes.foregroundColor(.clear)
            )

            var index = animatedText.startIndex
            while index &lt; animatedText.endIndex {
                try Task.checkCancellation()

                // Update the style
                animatedText[animatedText.startIndex...index]
                    .setAttributes(defaultAttributes)

                // Wait
                try await Task.sleep(for: typingDelay)

                // Advance the index, character by character
                index = animatedText.index(afterCharacter: index)
            }
        }
    }
}

// Usage example
struct ContentView: View {
    @State var text = &quot;Hello, Twitter! This is a typewriter animation.&quot;

    var body: some View {
        TypewriterView(text: text)
            .font(.title)
            .padding()
    }
}

huangapple
  • 本文由 发表于 2023年5月28日 05:11:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/76349065.html
匿名

发表评论

匿名网友

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

确定