从顶部而不是内在大小的中心进行动画处理

Posted

技术标签:

【中文标题】从顶部而不是内在大小的中心进行动画处理【英文标题】:Animating from top and not center of intrinsic size 【发布时间】:2018-02-12 14:11:10 【问题描述】:

我正在尝试让我的视图从上到下进行动画处理。目前,在更改我的标签文本时,在 nil 和一些“错误消息”之间,标签从其固有大小的中心动画,但我希望常规“标签”是“静态的”并且只为错误标签设置动画。基本上错误标签应该位于常规标签的正下方,并且错误标签应该根据其(固有)高度展开。这本质上是一个复选框。我想在用户尚未选中复选框但正在尝试进一步处理时显示错误消息。该代码只是解释问题的基本实现。我已经尝试为 containerview 调整 anchorPoint 和 contentMode 但这些似乎不像我想的那样工作。很抱歉,如果缩进很奇怪

import UIKit
class ViewController: UIViewController 

    let container = UIView()
    let errorLabel = UILabel()


    var bottomLabel: NSLayoutConstraint!
    override func viewDidLoad() 
        super.viewDidLoad()

        view.addSubview(container)
        container.contentMode = .top
        container.translatesAutoresizingMaskIntoConstraints = false
        container.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        container.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor).isActive = true
        container.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        container.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true

        let label = UILabel()
        label.text = "Very long text that i would like to show to full extent and eventually add an error message to. It'll work on multiple rows obviously"
        label.numberOfLines = 0
        container.contentMode = .top
        container.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
        label.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true

        container.addSubview(errorLabel)
        errorLabel.setContentHuggingPriority(UILayoutPriority(300), for: .vertical)
        errorLabel.translatesAutoresizingMaskIntoConstraints = false
        errorLabel.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
        errorLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
        errorLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true

        bottomLabel = errorLabel.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor)
        bottomLabel.isActive = false

        errorLabel.numberOfLines = 0

        container.backgroundColor = .green
        let tapRecognizer = UITapGestureRecognizer()
        tapRecognizer.addTarget(self, action: #selector(onTap))
        container.addGestureRecognizer(tapRecognizer)
    

    @objc func onTap() 

        self.container.layoutIfNeeded()
        UIView.animate(withDuration: 0.3, animations: 
            let active = !self.bottomLabel.isActive
            self.bottomLabel.isActive = active
            self.errorLabel.text = active ? "A veru very veru very veru very veru very veru very veru very veru very veru very long Error message" : nil


            self.container.layoutIfNeeded()
        )
    

    override func didReceiveMemoryWarning() 
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    

【问题讨论】:

您希望您的错误消息标签“滑下”到视图中吗?或者您是否希望动画看起来好像错误标签已经存在,但正在被向下滑动的“封面”“揭示”? 我认为这是你所说的我所追求的第二个选项。我希望将错误标签顶部固定到标签的底部(就像现在一样),并扩展其固有高度并如您所说的那样“显示”。把它想象成一个“表单”,如果输入格式不正确,我想显示一条错误消息。也可以说,一个输入视图,如果输入不正确,我想在下面显示一个错误。 【参考方案1】:

我发现让动态多行标签以我想要的方式“动画化”有点困难 - 特别是当我想“隐藏”标签时。

一种方法:创建 2 个“错误”标签,一个叠加在另一个之上。使用“隐藏”标签来控制容器视图上的约束。为更改设置动画时,容器视图的边界将有效地“显示”和“隐藏”(显示/隐藏)“可见”标签。

这是一个示例,您可以直接在 Playground 页面中运行:

import UIKit
import PlaygroundSupport

class RevealViewController: UIViewController 

    let container = UIView()
    let staticLabel = UILabel()
    let hiddenErrorLabel = UILabel()
    let visibleErrorLabel = UILabel()

    override func viewDidLoad() 
        super.viewDidLoad()

        // colors, just so we can see the bounds of the labels
        view.backgroundColor = .lightGray
        container.backgroundColor = .green
        staticLabel.backgroundColor = .yellow
        visibleErrorLabel.backgroundColor = .cyan

        // we don't want to see this label, so set its alpha to zero
        hiddenErrorLabel.alpha = 0.0

        // we want the Error Label to be "revealed" - so when it is has text it is initially "covered"
        container.clipsToBounds = true

        // all labels may be multiple lines
        staticLabel.numberOfLines = 0
        hiddenErrorLabel.numberOfLines = 0
        visibleErrorLabel.numberOfLines = 0

        // initial text in the "static" label
        staticLabel.text = "Very long text that i would like to show to full extent and eventually add an error message to. It'll work on multiple rows obviously"

        // add the container view to the VC's view
        // pin it to the sides, and 100-pts from the top
        // NO bottom constraint
        view.addSubview(container)
        container.translatesAutoresizingMaskIntoConstraints = false
        container.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0).isActive = true
        container.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        container.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true

        // add the static label to the container
        // pin it to the top and sides
        // NO bottom constraint
        container.addSubview(staticLabel)
        staticLabel.translatesAutoresizingMaskIntoConstraints = false
        staticLabel.topAnchor.constraint(equalTo: container.topAnchor).isActive = true
        staticLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
        staticLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true

        // add the "hidden" error label to the container
        // pin it to the sides, and  pin its top to the bottom of the static label
        // NO bottom constraint
        container.addSubview(hiddenErrorLabel)
        hiddenErrorLabel.translatesAutoresizingMaskIntoConstraints = false
        hiddenErrorLabel.topAnchor.constraint(equalTo: staticLabel.bottomAnchor).isActive = true
        hiddenErrorLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
        hiddenErrorLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true

        // add the "visible" error label to the container
        // pin its top, leading and trailing constraints to the hidden label
        container.addSubview(visibleErrorLabel)
        visibleErrorLabel.translatesAutoresizingMaskIntoConstraints = false
        visibleErrorLabel.topAnchor.constraint(equalTo: hiddenErrorLabel.topAnchor).isActive = true
        visibleErrorLabel.leadingAnchor.constraint(equalTo: hiddenErrorLabel.leadingAnchor).isActive = true
        visibleErrorLabel.trailingAnchor.constraint(equalTo: hiddenErrorLabel.trailingAnchor).isActive = true

        // pin the bottom of the hidden label ot the bottom of the container
        // now, when we change the text of the hidden label, it will
        // "push down / pull up" the bottom of the container view
        hiddenErrorLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor).isActive = true

        // add a tap gesture
        let tapRecognizer = UITapGestureRecognizer()
        tapRecognizer.addTarget(self, action: #selector(onTap))
        container.addGestureRecognizer(tapRecognizer)

    

    var myActive = false

    @objc func onTap() 

        let errorText = "A veru very veru very veru very veru very veru very veru very veru very veru very long Error message"

        self.myActive = !self.myActive

        if self.myActive 

            // we want to SHOW the error message

            // set the error message in the VISIBLE error label
            self.visibleErrorLabel.text = errorText

            // "animate" it, with duration of 0.0 - so it is filled instantly
            // it will extend below the bottom of the container view, but won't be
            // visible yet because we set .clipsToBounds = true on the container
            UIView.animate(withDuration: 0.0, animations: 

            , completion: 
                _ in

                // now, set the error message in the HIDDEN error label
                self.hiddenErrorLabel.text = errorText

                // the hidden label will now "push down" the bottom of the container view
                // so we can animate the "reveal"
                UIView.animate(withDuration: 0.3, animations: 
                    self.view.layoutIfNeeded()
                )

            )

         else 

            // we want to HIDE the error message

            // clear the text from the HIDDEN error label
            self.hiddenErrorLabel.text = ""

            // the hidden label will now "pull up" the bottom of the container view
            // so we can animate the "conceal"
            UIView.animate(withDuration: 0.3, animations: 
                self.view.layoutIfNeeded()
            , completion: 
                _ in

                // after its hidden, clear the text of the VISIBLE error label
                self.visibleErrorLabel.text = ""

            )

        

    



let vc = RevealViewController()
PlaygroundPage.current.liveView = vc

【讨论】:

谢谢,我之前没有测试过游乐场,也不知道它是如何工作的。但是,我确实测试了您的方法,但没有使其正常工作。它确实像我希望的那样做了一个“折叠”动画,但是(容器)框架在制作动画时仍然发生了跳跃,这使它在我的容器视图上方的内容上布局了我的视图。当只需要一种观点时,有两种观点也是错误的。不过我想出了一个不同的解决方案【参考方案2】:

因此,由于在这种情况下我想创建一个带有错误消息的控件(复选框),因此我根据边界直接操作了框架。所以为了让它正常工作,我结合使用了覆盖 intrinsicContentSize 和 layoutSubviews 以及一些额外的小东西。该类包含的内容比提供的要多,但提供的代码应该有望解释我采用的方法。

open class Checkbox: UIView 



let imageView = UIImageView()
let textView = ThemeableTapLabel()
private let errorLabel = UILabel()
var errorVisible: Bool = false
let checkboxPad: CGFloat = 8

override open var bounds: CGRect 
    didSet 
        // fixes layout when bounds change
        invalidateIntrinsicContentSize()
    



open var errorMessage: String? 
    didSet 
        self.errorVisible = self.errorMessage != nil
        UIView.animate(withDuration: 0.3, animations: 
            if self.errorMessage != nil 
                self.errorLabel.text = self.errorMessage
            
            self.setNeedsLayout()
            self.invalidateIntrinsicContentSize()
            self.layoutIfNeeded()
        , completion:  success in

            if self.errorMessage == nil 
                self.errorLabel.text = nil
            
        )
    



func checkboxSize() -> CGSize 
    return CGSize(width: imageView.image?.size.width ?? 0, height: imageView.image?.size.height ?? 0)



override open func layoutSubviews() 
    super.layoutSubviews()

    frame = bounds
    let imageFrame = CGRect(x: 0, y: 0, width: checkboxSize().width, height: checkboxSize().height)
    imageView.frame = imageFrame

    let textRect = textView.textRect(forBounds: CGRect(x: (imageFrame.width + checkboxPad), y: 0, width: bounds.width - (imageFrame.width + checkboxPad), height: 10000), limitedToNumberOfLines: textView.numberOfLines)
    textView.frame = textRect


    let largestHeight = max(checkboxSize().height, textRect.height)
    let rect = errorLabel.textRect(forBounds: CGRect(x: 0, y: 0, width: bounds.width, height: 10000), limitedToNumberOfLines: errorLabel.numberOfLines)
    //po bourect = rect.offsetBy(dx: 0, dy: imageFrame.maxY)
    let errorHeight = errorVisible ? rect.height : 0
    errorLabel.frame = CGRect(x: 0, y: largestHeight, width: bounds.width, height: errorHeight)



override open var intrinsicContentSize: CGSize 
    get 

        let textRect = textView.textRect(forBounds: CGRect(x: (checkboxSize().width + checkboxPad), y: 0, width: bounds.width - (checkboxSize().width + checkboxPad), height: 10000), limitedToNumberOfLines: textView.numberOfLines)

        let rect = errorLabel.textRect(forBounds: CGRect(x: 0, y: 0, width: bounds.width, height: 10000), limitedToNumberOfLines: errorLabel.numberOfLines)
        let errorHeight = errorVisible ? rect.height : 0
        let largestHeight = max(checkboxSize().height, textRect.height)
        return CGSize(width: checkboxSize().width + 200, height: largestHeight + errorHeight)
    



public required init?(coder aDecoder: NSCoder) 
    super.init(coder: aDecoder)
    setup()


func setup() 

//...
    addSubview(imageView)
    imageView.translatesAutoresizingMaskIntoConstraints = false

    addSubview(textView)
    textView.translatesAutoresizingMaskIntoConstraints = false
    textView.numberOfLines = 0
    contentMode = .top

    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(checkboxTap(sender:)))
    self.isUserInteractionEnabled = true
    self.addGestureRecognizer(tapGesture)

    addSubview(errorLabel)
    errorLabel.contentMode = .top
    errorLabel.textColor = .red
    errorLabel.numberOfLines = 0



【讨论】:

以上是关于从顶部而不是内在大小的中心进行动画处理的主要内容,如果未能解决你的问题,请参考以下文章

js动画函数滚动到div的底部而不是顶部

iOS - TableView willDisplayCell 动画仅在用户向下滚动而不是顶部时发生

故事板iOS - 如何以屏幕尺寸的百分比而不是静态值顶部引脚边距给出上边距?

顶部安全区域约束动画

图像顶部的波纹效果 - Android

使用 Jquery Animate 调整图像大小