UIKit中如何用占位符文本实现这种动画?

问题描述 投票:0回答:1

In this gif, the placeholder text keeps changing dynamically, one after another

这是我用来实现此目的的代码:

import UIKit

class ViewController: UIViewController, UISearchBarDelegate {
    
    var placeholderIndex = 0
    var placeholderTexts = ["Text 1", "Text 2", "Text 3"]
    var searchBar: UISearchBar!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        searchBar = UISearchBar()
        searchBar.placeholder = "Search...." // Set initial placeholder
        searchBar.searchBarStyle = .prominent
        searchBar.sizeToFit()
        searchBar.isTranslucent = true
        searchBar.delegate = self
        searchBar.backgroundImage = UIImage()
        searchBar.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(searchBar)
        
        Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(updatePlaceholder), userInfo: nil, repeats: true)
        
        NSLayoutConstraint.activate([
            searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            searchBar.heightAnchor.constraint(equalToConstant: 70)
        ])
    }
    
    
    @objc func updatePlaceholder() {
        let nextPlaceholderLabel = UILabel(frame: CGRect(x: 0, y: -searchBar.frame.height, width: searchBar.frame.width, height: searchBar.frame.height))
        nextPlaceholderLabel.text = placeholderTexts[placeholderIndex]
        nextPlaceholderLabel.textAlignment = .center
        nextPlaceholderLabel.font = searchBar.searchTextField.font
        nextPlaceholderLabel.textColor = searchBar.searchTextField.textColor
        searchBar.addSubview(nextPlaceholderLabel)

        UIView.animate(withDuration: 0.5, animations: {
            nextPlaceholderLabel.frame.origin.y = 0
            nextPlaceholderLabel.alpha = 1.0
            self.searchBar.layoutIfNeeded()
        }) { _ in
            self.searchBar.placeholder = self.placeholderTexts[self.placeholderIndex]
            nextPlaceholderLabel.removeFromSuperview()
        }

        placeholderIndex = (placeholderIndex + 1) % placeholderTexts.count
    }
}

函数 updatePlaceholder() 应该实现这个动画,但是我的结果是这样的:

It's nowhere close to what I want

我需要在这里做哪些改变? 预先感谢

ios swift uikit
1个回答
0
投票

有多种方法可以解决这个问题...这是一种...

您想要复制的效果是为各个单词设置动画。更改占位符文本时,当前文本会逐字向下动画并淡出,然后新文本会逐字向下动画(从上方)并淡入。

因此,当我们更新占位符文本时,我们可以:

  • 获取当前文本
    • 将其拆分为单词(按空格分隔)
    • 为每个单词创建并放置标签
    • 将其动画化并淡出

所以我们可以看到发生了什么,我用红色勾勒出标签,它们还没有“淡出”。

  • 获取新文本
    • 将其拆分为单词(按空格分隔)
    • 为每个单词创建并放置标签
    • 将其动画化并淡入

再次,为了让我们可以看到发生了什么,我用蓝色勾勒出这些标签,并在开始时可见。 (它们最终将从 alpha-0 开始,然后“淡入”。)

最后一步是更新

.placeholder
文本属性,并删除所有动画标签。

这是一个简单的例子:

class ViewController: UIViewController, UISearchBarDelegate {
    
    // start index at -1 so we get the first item -- [0] -- with the first increment
    var placeholderIndex = -1
    
    let placeholderTexts = [
        "What would you like to do?",
        "Check your Balance",
        "Send money to Mom",
        "Text 1",
        "Text 2",
        "Text 3",
    ]
    
    var searchBar: UISearchBar!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        searchBar = UISearchBar()
        searchBar.placeholder = "Search...." // Set initial placeholder
        searchBar.searchBarStyle = .prominent
        searchBar.isTranslucent = true
        searchBar.delegate = self
        searchBar.backgroundImage = UIImage()
        searchBar.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(searchBar)
        
        NSLayoutConstraint.activate([
            searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
            searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            searchBar.heightAnchor.constraint(equalToConstant: 70)
        ])
        
        Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(updatePlaceholder), userInfo: nil, repeats: true)
        
    }
    
    @objc func updatePlaceholder() {
        
        // use these values for the placeholder label frame origin
        //  if we can't get find it via code
        var baseX: CGFloat = 38.0
        var baseY: CGFloat = 25.0
        
        // try to find the placeholder label
        //  Apple did not make it public, so this is one approach
        //  note that if Apple changes the structure in the future, this may fail
        var placeholderLabelFrame: CGRect = .zero
        let tf: UITextField = searchBar.searchTextField
        for subview in tf.subviews {
            // Check if the subview is a UILabel and has the same text as the placeholder
            if let label = subview as? UILabel, label.text == tf.placeholder {
                placeholderLabelFrame = label.frame
            }
        }

        if placeholderLabelFrame != .zero {
            let cvtFrame = searchBar.searchTextField.convert(placeholderLabelFrame, to: searchBar)
            baseX = cvtFrame.origin.x
            baseY = cvtFrame.origin.y
        }

        // used to offset the labels vertically
        var yOffset: CGFloat = 0.0
        let hideIncrement: CGFloat = 2.0
        let showIncrement: CGFloat = 8.0

        // for positioning the labels
        var currentX: CGFloat = 0

        var toHideWords: [String] = []
        var toShowWords: [String] = []
        
        var toHideLabels: [UILabel] = []
        var toShowLabels: [UILabel] = []
        
        // array of Y-positions for the toHide labels to animate to
        var targetY: [CGFloat] = []

        let sbFont = searchBar.searchTextField.font
        
        // split the current placeholder text into words
        let hidePlaceholder: String = searchBar.placeholder ?? ""
        toHideWords = hidePlaceholder.components(separatedBy: " ")
            
        currentX = baseX
        
        // create and position labels for each word
        for word in toHideWords {
            let wordLabel = UILabel()
            wordLabel.text = word + " "
            wordLabel.font = sbFont
            wordLabel.textColor = .gray
            wordLabel.sizeToFit()
            wordLabel.frame.origin = .init(x: currentX, y: baseY)
            currentX += wordLabel.frame.width
            toHideLabels.append(wordLabel)
            searchBar.addSubview(wordLabel)
        }
        
        // increment index into placeholderTexts
        placeholderIndex = (placeholderIndex + 1) % placeholderTexts.count

        // split the new placeholder text into words
        let showPlaceholder: String = placeholderTexts[placeholderIndex]
        toShowWords = showPlaceholder.components(separatedBy: " ")
        
        currentX = baseX
        
        // create and position labels for each word
        for word in toShowWords {
            let wordLabel = UILabel()
            wordLabel.text = word + " "
            wordLabel.font = sbFont
            wordLabel.textColor = .gray
            wordLabel.sizeToFit()
            wordLabel.frame.origin = .init(x: currentX, y: baseY)
            currentX += wordLabel.frame.width
            toShowLabels.append(wordLabel)
            searchBar.addSubview(wordLabel)
        }

        // we've added both sets of labels...
        //  so now we need to set the
        //      starting y-positions of the toShow labels
        //  and the
        //      ending y-positions of the toHide labels
        
        //  the toShow labels will be animated down *to* the baseY
        //  and faded in
        yOffset = showIncrement
        for wordLabel in toShowLabels {
            wordLabel.alpha = 0.0
            wordLabel.frame.origin.y = baseY - yOffset
            yOffset += showIncrement
        }
        
        // the toHide labels will be animated down *from* the baseY
        //  and faded out
        yOffset = baseY + hideIncrement
        for wordLabel in toHideLabels {
            wordLabel.alpha = 1.0
            wordLabel.frame.origin.y = baseY
            targetY.append(yOffset)
            yOffset += hideIncrement
        }
        
        // clear the current placeholder text
        self.searchBar.placeholder = ""
        
        // we need to reverse the order of the toHide labels
        //  because we want the animation to start at the right-end
        toHideLabels.reverse()
        targetY.reverse()
        
        var totalDur: Double = 0.0
        var relStart: Double = 0.0
        
        // all times in seconds (0.1 == 1/10th of a second, 0.05 = 1/20th, etc)
        var hideSpeed: Double = 0.1
        var hideInterval: Double = 0.05
        
        // pause between hiding the "current" placeholder and showing the "new" placeholder
        var pause: Double = 0.5
        
        var showSpeed: Double = 0.2
        var showInterval: Double = 0.1
        
        let numToHide: Double = Double(toHideLabels.count)
        let numToShow: Double = Double(toShowLabels.count)
        
        // total duration in seconds
        totalDur = (numToHide * hideSpeed) + pause + (numToShow * showSpeed)
        
        // keyFrame start times and durations are relative to the total duration
        //  so convert them to percentages
        hideSpeed /= totalDur
        hideInterval /= totalDur
        pause /= totalDur
        showSpeed /= totalDur
        showInterval /= totalDur
        
        UIView.animateKeyframes(withDuration: totalDur, delay: 0, options: [.calculationModeLinear], animations: {
            
            for (index, wordLabel) in toHideLabels.enumerated() {
                UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: hideSpeed, animations: {
                    wordLabel.alpha = 0.0
                    wordLabel.frame.origin.y = targetY[index]
                })
                relStart += hideInterval
            }
            
            relStart += pause
            
            for wordLabel in toShowLabels {
                UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: showSpeed, animations: {
                    wordLabel.alpha = 1.0
                    wordLabel.frame.origin.y = baseY
                })
                relStart += showInterval
            }
            
        }, completion: { _ in
            
            // set the new placeholder text
            self.searchBar.placeholder = showPlaceholder
            
            // remove all of the word labels
            for wordLabel in toShowLabels {
                wordLabel.removeFromSuperview()
            }
            for wordLabel in toHideLabels {
                wordLabel.removeFromSuperview()
            }
            
        })
        
    }
}

运行时看起来像这样:

© www.soinside.com 2019 - 2024. All rights reserved.