从顶部开始制作动画而不是固有大小的中心

时间:2018-02-12 14:11:10

标签: ios swift animation uiview intrinsic-content-size

我正试图让我的观点从上到下进行动画制作。目前,当更改我的标签文本时,在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.
    }
}

2 个答案:

答案 0 :(得分:0)

我发现让动态多线标签变得有点困难"动画"以我想要的方式 - 特别是当我想要"隐藏"标签。

一种方法:创建2"错误"标签,一个覆盖在另一个之上。使用"隐藏" label来控制容器视图的约束。在动画更改时,容器视图的界限将有效地显示"并且"隐藏" (显示/隐藏)"可见"标签

以下是一个示例,您可以直接在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

答案 1 :(得分:0)

因此,由于它是我想要创建的控件(复选框),在这种情况下带有错误消息,我根据边界直接操作帧。因此,为了使其正常工作,我使用了覆盖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

}
}