如何为输入附件视图的高度设置动画?

Posted

技术标签:

【中文标题】如何为输入附件视图的高度设置动画?【英文标题】:How do I animate the height of an input accessory view? 【发布时间】:2017-11-06 17:59:11 【问题描述】:

在为输入附件视图的高度设置动画时,我遇到了奇怪的行为。我做错了什么?

我创建了一个带有单个子视图的 UIInputView 子类 (InputView)。 InputView 及其intrinsicContentSize 的高度由子视图控制。当isVisibletrue 时,InputView 为 50 像素高,当isVisible 为 false 时为 0 像素。

import UIKit

class InputView: UIInputView 
    private let someHeight: CGFloat = 50.0, zeroHeight: CGFloat = 0.0
    private let subView = UIView()
    private var hide: NSLayoutConstraint?, show: NSLayoutConstraint?

    var isVisible: Bool 
        get 
            return show!.isActive
        
        set 
            // Always deactivate constraints before activating conflicting ones
            if newValue == true 
                hide?.isActive = false
                show?.isActive = true
             else 
                show?.isActive = false
                hide?.isActive = true
            
        
    

    // MARK: Sizing

    override func sizeThatFits(_ size: CGSize) -> CGSize 
        return CGSize(width: size.width, height: someHeight)
    

    override var intrinsicContentSize: CGSize 
        return CGSize.init(width: bounds.size.width, height: subView.bounds.size.height)
    

    // MARK: Initializers

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

    override init(frame: CGRect, inputViewStyle: UIInputViewStyle) 
        super.init(frame: frame, inputViewStyle: inputViewStyle)

        addSubview(subView)
        subView.backgroundColor = UIColor.purple

        translatesAutoresizingMaskIntoConstraints = false
        subView.translatesAutoresizingMaskIntoConstraints = false

        subView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true
        subView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        subView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        subView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true

        show = subView.heightAnchor.constraint(equalToConstant: someHeight)
        hide = subView.heightAnchor.constraint(equalToConstant: zeroHeight)
        hide?.isActive = true
    

当按下按钮时,主机视图控制器会在一秒钟的动画块中切换isVisible

import UIKit

class MainViewController: UIViewController 
    let testInputView = InputView.init(frame: .zero, inputViewStyle: .default)

    @IBAction func button(_ sender: AnyObject) 
        UIView.animate(withDuration: 1.0) 
            let isVisible = self.testInputView.isVisible
            self.testInputView.isVisible = !isVisible
            self.testInputView.layoutIfNeeded()
        
    

    override var canBecomeFirstResponder: Bool 
        return true
    

    override var inputAccessoryView: UIView? 
        return testInputView
    

    override func viewDidLoad() 
        super.viewDidLoad()
    

我希望输入附件视图在isVisible 设置为true 时从屏幕底部平滑增长,并在isVisible 设置为false 时平滑收缩到屏幕按钮。相反,当isVisibletrue 并且输入附件视图从其框架的中心 开始增长时,键盘背景覆盖显示在完整的 50 像素高度。

当缩小时,输入附件视图会立即失去一些高度,然后才能平滑地继续动画。

我创建了一个 input accessory view demonstration project 来显示这种意外行为。

【问题讨论】:

【参考方案1】:

这将为您提供正确的动画:

    UIView.animate(withDuration: 1.0) 
        let isVisible = self.testInputView.isVisible
        self.testInputView.isVisible = !isVisible
        self.testInputView.superview?.superview?.layoutIfNeeded()
    

但是,如果 Apple 更改设计,调用 superview 绝不是一个好习惯。所以可能会有更好的答案。

这就是超级视图所代表的:

print(testInputView.superview) // UIInputSetHostView

print(testInputView.superview?.superview) // UIInputSetContainerView

编辑:添加了更安全的解决方案

我对 UIInputView 不太熟悉。但是在不调用父视图的情况下解决它的一种方法是只为子视图的高度变化设置动画:

第 1 步: 将 isVisible 移到动画块之外。

@IBAction func button(_ sender: AnyObject) 
    let isVisible = self.testInputView.isVisible
    self.testInputView.isVisible = !isVisible
    UIView.animate(withDuration: 1.0) 
        self.testInputView.layoutIfNeeded()
    

第 2 步: 在 InputView 中创建一个新方法,该方法更改 InputView 的高度约束,而不是 intrinsicContentSize。

private func updateHeightConstraint(height: CGFloat) 
    for constraint in constraints 
        if constraint.firstAttribute == .height 
            constraint.constant = height
        
    
    self.layoutIfNeeded()

第 3 步: 并在 setter 中调用该方法。

if newValue == true 
     updateHeightConstraint(height: someHeight)
     hide?.isActive = false
     show?.isActive = true
 else 
     updateHeightConstraint(height: zeroHeight)
     show?.isActive = false
     hide?.isActive = true

第 4 步: 最后是 init 中的一些更改。

override init(frame: CGRect, inputViewStyle: UIInputViewStyle) 
    super.init(frame: frame, inputViewStyle: inputViewStyle)

    addSubview(subView)

    backgroundColor = .clear
    subView.backgroundColor = UIColor.purple

    subView.translatesAutoresizingMaskIntoConstraints = false

    subView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
    subView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
    subView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true

    show = subView.heightAnchor.constraint(equalToConstant: someHeight)
    hide = subView.heightAnchor.constraint(equalToConstant: zeroHeight)
    hide?.isActive = true

结论: 这导致 InputView 在动画紫色子视图的高度之前更改它的高度。唯一的缺点是 UIInputView,它默认有某种灰色背景,不能更改为清除。但是,您可以使用与 VC 相同的 backgroundColor。

但是,如果您改为使用常规 UIView 作为 InputAccessoryView,则默认为 UIColor.clear。不会注意到第一次“跳跃”。

【讨论】:

我不接受这是最好的答案希望有人揭示更安全的解决方法,但这很好用。关于如何查找超级视图的提示也非常有用。 不幸的是,高度约束方法对我来说没有正确的动画。最好的解决方案是使用superview?.superview,尽管在 ios 14 上,在键盘动画期间调用这可能会导致无限循环。在这种情况下,切换到 superview?.superview?.superview(这是层次结构中的顶视图)可以正常工作。

以上是关于如何为输入附件视图的高度设置动画?的主要内容,如果未能解决你的问题,请参考以下文章

您如何为视图填充的变化设置动画?

如何为 Grid RowDefinition 高度变化设置动画(当 Height="Auto" 时)

滚动时如何为 recyclerview 项目设置动画?

如何为图层 shadowOpacity 设置动画?

如何为自定义视图选择设置膨胀和颜色变化的动画?

如何为具有约束的 UIView 设置动画?