从顶部而不是固有大小的中心开始动画
Animating from top and not center of intrinsic size
我正在尝试让我的视图从上到下呈现动画效果。目前,当更改我的标签文本时,在 nil 和一些 "error message" 之间,标签从其固有大小的中心开始动画,但我希望常规 "label" 为 "static" 并且只为错误标签设置动画。基本上,错误标签应该位于常规标签的正下方,错误标签应该根据其(固有)高度展开。这本质上是一个复选框。我想在用户尚未选中该复选框但正在尝试进一步操作时显示错误消息。代码只是解释问题的基本实现。我已经尝试为 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.
}
}
我发现以我想要的方式将动态多行标签添加到 "animate" 有点困难 - 特别是当我想要 "hide" 标签时。
一种方法:创建 2 个 "error" 标签,一个叠加在另一个之上。使用 "hidden" 标签来控制容器视图上的约束。为更改设置动画时,容器视图的边界将有效地 "reveal" 和 "conceal" (show/hide) "visible" 标签。
这是一个示例,您可以直接在 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
因此,由于在这种情况下我想创建一个带有错误消息的控件(复选框),因此我根据边界直接操作了帧。因此,为了让它正常工作,我使用了覆盖 intrinsicContentSize 和 layoutSubviews 以及一些额外的小东西的组合。 class 包含的内容比提供的要多一些,但提供的代码应该可以解释我采用的方法。
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
}
}
我正在尝试让我的视图从上到下呈现动画效果。目前,当更改我的标签文本时,在 nil 和一些 "error message" 之间,标签从其固有大小的中心开始动画,但我希望常规 "label" 为 "static" 并且只为错误标签设置动画。基本上,错误标签应该位于常规标签的正下方,错误标签应该根据其(固有)高度展开。这本质上是一个复选框。我想在用户尚未选中该复选框但正在尝试进一步操作时显示错误消息。代码只是解释问题的基本实现。我已经尝试为 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.
}
}
我发现以我想要的方式将动态多行标签添加到 "animate" 有点困难 - 特别是当我想要 "hide" 标签时。
一种方法:创建 2 个 "error" 标签,一个叠加在另一个之上。使用 "hidden" 标签来控制容器视图上的约束。为更改设置动画时,容器视图的边界将有效地 "reveal" 和 "conceal" (show/hide) "visible" 标签。
这是一个示例,您可以直接在 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
因此,由于在这种情况下我想创建一个带有错误消息的控件(复选框),因此我根据边界直接操作了帧。因此,为了让它正常工作,我使用了覆盖 intrinsicContentSize 和 layoutSubviews 以及一些额外的小东西的组合。 class 包含的内容比提供的要多一些,但提供的代码应该可以解释我采用的方法。
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
}
}