更改默认 StackView 动画

2023-12-14

如果解释不够好,请原谅我。基本上,下面的视频显示了在堆栈视图中隐藏标签的标准动画。请注意,它看起来像标签“滑动”和“折叠在一起”。

我仍然想隐藏标签,但想要一个 Alpha 变化但标签不“滑动”的动画。相反,标签会更改 Alpha 并保持在原位。这可以通过堆栈视图实现吗?

这是我必须设置动画的代码:

    UIView.animate(withDuration: 0.5) {
      if self.isExpanded {
        self.topLabel.alpha = 1.0
        self.bottomLabel.alpha = 1.0
        self.topLabel.isHidden = false
        self.bottomLabel.isHidden = false
      } else {
        self.topLabel.alpha = 0.0
        self.bottomLabel.alpha = 0.0
        self.topLabel.isHidden = true
        self.bottomLabel.isHidden = true
      }
    } 

animation

Update 1

似乎即使没有堆栈视图,如果我对高度约束进行动画处理,您也会得到这种“挤压”效果。例子:

    UIView.animate(withDuration: 3.0) {
      self.heightConstraint.constant = 20
      self.view.layoutIfNeeded()
    }

这里有几个选项:

  1. Set .contentMode = .top在标签上。我从未找到过清楚描述使用的 Apple 文档.contentMode with UILabel,但它有效并且should work.

  2. 将标签嵌入到UIView,限制在顶部,其中Content Compression Resistance Priority set to .required,低于底部约束所需的优先级,以及.clipsToBounds = true在视图上。

示例 1 - 内容模式:

class StackAnimVC: UIViewController {
    
    let stackView: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.spacing = 0
        return v
    }()
    
    let topLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        return v
    }()
    
    let botLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 0
        return v
    }()
    
    let headerLabel = UILabel()
    let threeLabel = UILabel()
    let footerLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // label setup
        let colors: [UIColor] = [
            .systemYellow,
            .cyan,
            UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
            UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
            UIColor(white: 0.9, alpha: 1.0),
        ]
        for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
            v.backgroundColor = c
            v.font = .systemFont(ofSize: 24.0, weight: .light)
            stackView.addArrangedSubview(v)
        }
        
        headerLabel.text = "Header"
        threeLabel.text = "Three"
        footerLabel.text = "Footer"
        
        topLabel.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
        botLabel.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
        
        // we want 8-pts "padding" under the "collapsible" labels
        stackView.setCustomSpacing(8.0, after: topLabel)
        stackView.setCustomSpacing(8.0, after: botLabel)

        // let's add a label and a Switch to toggle the labels .contentMode
        let promptView = UIView()
        let hStack = UIStackView()
        hStack.spacing = 8
        let prompt = UILabel()
        prompt.text = "Content Mode Top:"
        prompt.textAlignment = .right
        let sw = UISwitch()
        sw.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
        hStack.addArrangedSubview(prompt)
        hStack.addArrangedSubview(sw)
        hStack.translatesAutoresizingMaskIntoConstraints = false
        promptView.addSubview(hStack)
        
        // add an Animate button
        let btn = UIButton(type: .system)
        btn.setTitle("Animate", for: [])
        btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
        btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
        
        let g = view.safeAreaLayoutGuide
        
        // add elements to view and give them all the same Leading and Trailing constraints
        [promptView, stackView, btn].forEach { v in
            
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            
            NSLayoutConstraint.activate([
                v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            ])
        }
        
        NSLayoutConstraint.activate([
            
            promptView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            stackView.topAnchor.constraint(equalTo: promptView.bottomAnchor, constant: 0.0),
            
            // center the hStack in the promptView
            hStack.centerXAnchor.constraint(equalTo: promptView.centerXAnchor),
            hStack.centerYAnchor.constraint(equalTo: promptView.centerYAnchor),
            promptView.heightAnchor.constraint(equalTo: hStack.heightAnchor, constant: 16.0),
            
            // put button near bottom
            btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
            
        ])
        
    }
    
    @objc func switchChanged(_ sender: UISwitch) {
        [topLabel, botLabel].forEach { v in
            v.contentMode = sender.isOn ? .top : .left
        }
    }
    @objc func btnTap(_ sender: UIButton) {
        
        UIView.animate(withDuration: 0.5) {
            
            // toggle hidden and alpha on stack view labels
            self.topLabel.alpha = self.topLabel.isHidden ? 1.0 : 0.0
            self.botLabel.alpha = self.botLabel.isHidden ? 1.0 : 0.0
            
            self.topLabel.isHidden.toggle()
            self.botLabel.isHidden.toggle()
            
        }
        
    }
}

示例 2 - 嵌入在 a 中的标签UIView:

class TopAlignedLabelView: UIView {
    
    let label = UILabel()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        self.addSubview(label)
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
            label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
            label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
        ])
        // we need bottom anchor to have
        //  less-than-required Priority
        let c = label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0)
        c.priority = .required - 1
        c.isActive = true
        
        // don't allow label to be compressed
        label.setContentCompressionResistancePriority(.required, for: .vertical)
        
        // we need to clip the label
        self.clipsToBounds = true
    }
}

class StackAnimVC: UIViewController {
    
    let stackView: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.spacing = 0
        return v
    }()
    
    let topLabel: TopAlignedLabelView = {
        let v = TopAlignedLabelView()
        return v
    }()
    
    let botLabel: TopAlignedLabelView = {
        let v = TopAlignedLabelView()
        return v
    }()
    
    let headerLabel = UILabel()
    let threeLabel = UILabel()
    let footerLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
                
        // label setup
        let colors: [UIColor] = [
            .systemYellow,
            .cyan,
            UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
            UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
            UIColor(white: 0.9, alpha: 1.0),
        ]
        for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
            v.backgroundColor = c
            if let vv = v as? UILabel {
                vv.font = .systemFont(ofSize: 24.0, weight: .light)
            }
            if let vv = v as? TopAlignedLabelView {
                vv.label.font = .systemFont(ofSize: 24.0, weight: .light)
            }
            stackView.addArrangedSubview(v)
        }
        
        headerLabel.text = "Header"
        threeLabel.text = "Three"
        footerLabel.text = "Footer"
        
        topLabel.label.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
        botLabel.label.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
        
        // we want 8-pts "padding" under the "collapsible" labels
        stackView.setCustomSpacing(8.0, after: topLabel)
        stackView.setCustomSpacing(8.0, after: botLabel)
        
        // add an Animate button
        let btn = UIButton(type: .system)
        btn.setTitle("Animate", for: [])
        btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
        btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
        
        let g = view.safeAreaLayoutGuide
        
        // add elements to view and give them all the same Leading and Trailing constraints
        [stackView, btn].forEach { v in
            
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            
            NSLayoutConstraint.activate([
                v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            ])
        }
        
        NSLayoutConstraint.activate([
            
            stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            
            // put button near bottom
            btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
            
        ])
        
    }
    
    @objc func btnTap(_ sender: UIButton) {

        UIView.animate(withDuration: 0.5) {
            
            // toggle hidden and alpha on stack view labels
            self.topLabel.alpha = self.topLabel.isHidden ? 1.0 : 0.0
            self.botLabel.alpha = self.botLabel.isHidden ? 1.0 : 0.0
            
            self.topLabel.isHidden.toggle()
            self.botLabel.isHidden.toggle()
            
        }
        
    }
}

Edit

如果您的目标是让棕色标签“向上滑动并覆盖”both蓝色和粉色标签,这两个标签都没有压缩或移动,采用了类似的方法:

  • 使用标准 UILabel 而不是 TopAlignedLabelView
  • 将蓝色和粉色标签嵌入到它们自己的堆栈视图中
  • embed that“容器”视图中的堆栈视图
  • 约束that堆栈视图“顶部对齐”,就像我们对 TopAlignedLabelView 中的标签所做的那样

“外部”堆栈视图的排列子视图现在将是:

  • 黄色的标签
  • “容器”视图
  • 棕色标签
  • 灰标

为了制作动画,我们将切换.alpha and .isHidden在“容器”视图上而不是蓝色和粉色标签上。

我编辑了控制器类——尝试一下,看看这是否是您想要的效果。

enter image description here

如果是,我强烈建议您尝试自己进行这些更改...如果您遇到问题,请使用以下示例代码作为指导:

class StackAnimVC: UIViewController {
    
    let outerStackView: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.spacing = 0
        return v
    }()
    
    // create an "inner" stack view
    //  this will hold topLabel and botLabel
    let innerStackView: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.spacing = 8
        return v
    }()

    // container for the inner stack view
    let innerStackContainer: UIView = {
        let v = UIView()
        v.clipsToBounds = true
        return v
    }()
    
    // we can use standard UILabels instead of custom views
    let topLabel = UILabel()
    let botLabel = UILabel()
    
    let headerLabel = UILabel()
    let threeLabel = UILabel()
    let footerLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // label setup
        let colors: [UIColor] = [
            .systemYellow,
            .cyan,
            UIColor(red: 1.0, green: 0.85, blue: 0.9, alpha: 1.0),
            UIColor(red: 0.7, green: 0.5, blue: 0.4, alpha: 1.0),
            UIColor(white: 0.9, alpha: 1.0),
        ]
        for (c, v) in zip(colors, [headerLabel, topLabel, botLabel, threeLabel, footerLabel]) {
            v.backgroundColor = c
            v.font = .systemFont(ofSize: 24.0, weight: .light)
            v.setContentCompressionResistancePriority(.required, for: .vertical)
        }
        
        // add top and bottom labels to inner stack view
        innerStackView.addArrangedSubview(topLabel)
        innerStackView.addArrangedSubview(botLabel)

        // add inner stack view to container
        innerStackView.translatesAutoresizingMaskIntoConstraints = false
        innerStackContainer.addSubview(innerStackView)
        
        // constraints for inner stack view
        //  bottom constraint must be less-than-required
        //  so it doesn't compress when the container compresses
        let isvBottom: NSLayoutConstraint = innerStackView.bottomAnchor.constraint(equalTo: innerStackContainer.bottomAnchor, constant: -8.0)
        isvBottom.priority = .defaultHigh
        
        NSLayoutConstraint.activate([
            innerStackView.topAnchor.constraint(equalTo: innerStackContainer.topAnchor, constant: 0.0),
            innerStackView.leadingAnchor.constraint(equalTo: innerStackContainer.leadingAnchor, constant: 0.0),
            innerStackView.trailingAnchor.constraint(equalTo: innerStackContainer.trailingAnchor, constant: 0.0),
            isvBottom,
        ])

        topLabel.numberOfLines = 0
        botLabel.numberOfLines = 0
        
        topLabel.text = "It seems that even without a stack view, if I animate the height constraint, you get this \"squeeze\" effect."
        botLabel.text = "I still want to hide the labels, but want an animation where the alpha changes but the labels don't \"slide\"."
        
        headerLabel.text = "Header"
        threeLabel.text = "Three"
        footerLabel.text = "Footer"

        // add views to outer stack view
        [headerLabel, innerStackContainer, threeLabel, footerLabel].forEach { v in
            outerStackView.addArrangedSubview(v)
        }
        
        // add an Animate button
        let btn = UIButton(type: .system)
        btn.setTitle("Animate", for: [])
        btn.titleLabel?.font = .systemFont(ofSize: 24.0, weight: .regular)
        btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
        
        let g = view.safeAreaLayoutGuide
        
        // add elements to view and give them all the same Leading and Trailing constraints
        [outerStackView, btn].forEach { v in
            
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            
            NSLayoutConstraint.activate([
                v.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                v.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            ])
        }
        
        NSLayoutConstraint.activate([
            
            outerStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            
            // put button near bottom
            btn.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
            
        ])
        
    }
    
    @objc func btnTap(_ sender: UIButton) {

        UIView.animate(withDuration: 0.5) {
            
            // toggle hidden and alpha on inner stack container
            self.innerStackContainer.alpha = self.innerStackContainer.isHidden ? 1.0 : 0.0
            self.innerStackContainer.isHidden.toggle()
            
        }
        
    }
}

Edit 2

快速解释为什么它有效......

考虑一个典型的UILabel作为 a 的子视图UIView。我们用一点“填充”将标签限制在所有 4 个侧面的视图上:

aLabel.topAnchor.constraint(equalTo: aView.topAnchor, constant: 8.0),
aLabel.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 8.0),
aLabel.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: -8.0),
aLabel.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: -8.0),

现在我们可以约束视图的顶部/前导/尾随——但不能约束底部或高度——并且标签的固有高度将控制视图的高度。

非常基本。

但是,如果我们想“让它消失”,改变视图的高度将also更改标签的高度,从而产生“挤压”效果。我们还会收到自动布局投诉,因为无法满足约束条件。

所以,我们需要改变.priority标签的底部约束,以允许其保持其固有高度,而其超级视图的高度发生变化。

这 4 个示例中的每一个都使用相同的顶部/前导/尾随约束...唯一的区别是我们对底部约束的处理方式:

enter image description here

For 实施例1,我们不设置any底部约束。因此,我们甚至从未看到它的超级视图,并且对其超级视图的高度进行动画处理对标签没有影响。

For 实施例2,我们设置“正常”底部约束,我们看到“挤压”效果。

For 实施例3,我们给出标签的底部约束.priority = .defaultHigh。标签仍然控制其超级视图的高度...直到我们激活超级视图的高度约束(为零)。超级视图折叠,但我们已经授予自动布局权限来打破底部约束。

实施例4是相同的3,但我们也设置了.clipsToBounds = true在容器视图上,因此标签高度保持不变,但不再延伸到其超级视图之外。

设置时所有这些也适用于堆栈视图中的视图.isHidden在安排的子视图上。

如果您想检查它并尝试各种变化,这里是生成该示例的代码:

class DemoVC: UIViewController {

    var containerViews: [UIView] = []
    var heightConstraints: [NSLayoutConstraint] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let g = view.safeAreaLayoutGuide

        // create 4 container views, each with a label as a subview
        let colors: [UIColor] = [
            .systemRed, .systemGreen, .systemBlue, .systemYellow,
        ]
        colors.forEach { bkgColor in
            let thisContainer = UIView()
            thisContainer.translatesAutoresizingMaskIntoConstraints = false
            
            let thisLabel = UILabel()
            thisLabel.translatesAutoresizingMaskIntoConstraints = false

            thisContainer.backgroundColor = bkgColor
            thisLabel.backgroundColor = UIColor(red: 0.75, green: 0.9, blue: 1.0, alpha: 1.0)

            thisLabel.numberOfLines = 0
            //thisLabel.font = .systemFont(ofSize: 20.0, weight: .light)
            thisLabel.font = .systemFont(ofSize: 12.0, weight: .light)
            thisLabel.text = "We want to animate compressing the \"container\" view vertically, without it squeezing or moving this label."

            // add label to container view
            thisContainer.addSubview(thisLabel)
            
            // add container view to array
            containerViews.append(thisContainer)
            
            // add container view to view
            view.addSubview(thisContainer)
            
            NSLayoutConstraint.activate([

                // each example gets the label constrained
                //  Top / Leading / Trailing to its container view
                thisLabel.topAnchor.constraint(equalTo: thisContainer.topAnchor, constant: 8.0),
                thisLabel.leadingAnchor.constraint(equalTo: thisContainer.leadingAnchor, constant: 8.0),
                thisLabel.trailingAnchor.constraint(equalTo: thisContainer.trailingAnchor, constant: -8.0),
                
                // we'll be using different bottom constraints for the examples,
                //  so don't set it here
                //thisLabel.bottomAnchor.constraint(equalTo: thisContainer.bottomAnchor, constant: -8.0),
                
                // each container view gets constrained to the top
                thisContainer.topAnchor.constraint(equalTo: g.topAnchor, constant: 60.0),

            ])

            // setup the container view height constraints, but don't activate them
            let hc = thisContainer.heightAnchor.constraint(equalToConstant: 0.0)
            
            // add the constraint to the constraints array
            heightConstraints.append(hc)

        }
        
        // couple vars to reuse
        var prevContainer: UIView!
        var aContainer: UIView!
        var itsLabel: UIView!
        var bc: NSLayoutConstraint!
        
        // -------------------------------------------------------------------
        // first example
        //  we don't add a bottom constraint for the label
        //  that means we'll never see its container view
        //  and changing its height constraint won't do anything to the label
        
        // -------------------------------------------------------------------
        // second example
        aContainer = containerViews[1]
        itsLabel = aContainer.subviews.first
        
        // we'll add a "standard" bottom constraint
        //  so now we see its container view
        bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
        bc.isActive = true
        
        // -------------------------------------------------------------------
        // third example
        aContainer = containerViews[2]
        itsLabel = aContainer.subviews.first
        
        // add the same bottom constraint, but give it a
        //  less-than-required Priority so it won't "squeeze"
        bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
        bc.priority = .defaultHigh
        bc.isActive = true
        
        // -------------------------------------------------------------------
        // fourth example
        aContainer = containerViews[3]
        itsLabel = aContainer.subviews.first
        
        // same less-than-required Priority bottom constraint,
        bc = itsLabel.bottomAnchor.constraint(equalTo: aContainer.bottomAnchor, constant: -8.0)
        bc.priority = .defaultHigh
        bc.isActive = true
        
        // we'll also set clipsToBounds on the container view
        //  so it will "hide / reveal" the label
        aContainer.clipsToBounds = true
        
        
        // now we need to layout the views
        
        // constrain first example leading
        aContainer = containerViews[0]
        aContainer.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0).isActive = true
        
        prevContainer = aContainer
        
        for i in 1..<containerViews.count {
            aContainer = containerViews[i]
            aContainer.leadingAnchor.constraint(equalTo: prevContainer.trailingAnchor, constant: 8.0).isActive = true
            aContainer.widthAnchor.constraint(equalTo: prevContainer.widthAnchor).isActive = true
            prevContainer = aContainer
        }
        
        // constrain last example trailing
        prevContainer.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0).isActive = true
    
        // and, let's add labels above the 4 examples
        for (i, v) in containerViews.enumerated() {
            let label = UILabel()
            label.translatesAutoresizingMaskIntoConstraints = false
            label.text = "Example \(i + 1)"
            label.font = .systemFont(ofSize: 14.0, weight: .light)
            view.addSubview(label)
            NSLayoutConstraint.activate([
                label.bottomAnchor.constraint(equalTo: v.topAnchor, constant: -4.0),
                label.centerXAnchor.constraint(equalTo: v.centerXAnchor),
            ])
        }
        
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        heightConstraints.forEach { c in
            c.isActive = !c.isActive
        }
        UIView.animate(withDuration: 1.0, animations: {
            self.view.layoutIfNeeded()
        })
    }
    
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

更改默认 StackView 动画 的相关文章

随机推荐