从顶部而不是内在大小的中心进行动画处理
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
【讨论】:
以上是关于从顶部而不是内在大小的中心进行动画处理的主要内容,如果未能解决你的问题,请参考以下文章
iOS - TableView willDisplayCell 动画仅在用户向下滚动而不是顶部时发生