繁体   English   中英

Swift 5 和 UIKit 在 2 到 3 个 UIView 之间绘制、动画和分割线

[英]Swift 5 and UIKit draw, animate and split lines between 2 to 3 UIViews

有一个可能包含 2 或 3 个 UIViews 的视图。

我想绘制(并且可能是动画)动画一条从较高视图的底部 MidX 到底部的线。

如果我有 3 个视图,我希望线条分割并为它们两个设置动画。

所有这一切都考虑到屏幕高度(4.7" -> 6.2"),我附上了图片来说明我想要实现的目标。

谢谢您的帮助。 在此处输入图像描述 在此处输入图像描述

经过一番研究,我为 Swift 5 提出了这个解决方案:

class ViewController: UIViewController {
    
    @IBOutlet weak var someView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let start = CGPoint(x: self.someView.bounds.midX, y: self.someView.bounds.maxY)
        let end = CGPoint(x: self.someView.layer.bounds.midX, y: (UIScreen.main.bounds.height / 2) - 100)
        
        let linePath = UIBezierPath()
        linePath.move(to: start)
        linePath.addLine(to: end)
        
        linePath.addLine(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
        linePath.move(to: end)
        linePath.addLine(to: CGPoint(x: 250, y: (UIScreen.main.bounds.height / 2) - 100))

        linePath.addLine(to: CGPoint(x: lowerViewA.x, y: lowerViewA.y))
        linePath.move(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
        linePath.addLine(to: CGPoint(x: lowerViewB.x, y: lowerViewB.y))

        let shapeLayer = CAShapeLayer()
        shapeLayer.path = linePath.cgPath
        
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = UIColor.green.cgColor
        shapeLayer.lineWidth = 2 
        shapeLayer.lineJoin = CAShapeLayerLineJoin.bevel

        self.someView.layer.addSublayer(shapeLayer)
        
        //Basic animation if you want to animate the line drawing.
        let pathAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
        pathAnimation.duration = 4.0
        pathAnimation.fromValue = 0.0
        pathAnimation.toValue = 1.0
        //Animation will happen right away
        shapeLayer.add(pathAnimation, forKey: "strokeEnd")
    }
    
}

你在正确的轨道上...

绘制“分割”线的问题在于有一个起点和两个终点。 因此,生成的 animation 可能不是您真正想要的。

另一种方法是使用两个图层 - 一个带有“左侧”分割线,一个带有“右侧”分割线,然后将它们一起制作动画。

这是将事物包装到“连接”视图子类中的示例。

我们将使用 3 层:1 层用于单个垂直连接线,1 层用于右侧和左侧线。

我们还可以将路径点设置为视图的中心,以及左右边缘。 这样我们就可以将前沿限制在左框的中心,将后沿限制在右框的中心。

这个视图本身看起来像这样(带有黄色背景,因此我们可以看到它的框架):

在此处输入图像描述

或者:

在此处输入图像描述

随着线条将从顶部开始动画。

class ConnectView: UIView {
    
    // determines whether we want a single box-to-box line, or
    //  left and right split / stepped lines to two boxes
    public var single: Bool = true
    
    private let singleLineLayer = CAShapeLayer()
    private let leftLineLayer = CAShapeLayer()
    private let rightLineLayer = CAShapeLayer()

    private var durationFactor: CGFloat = 0

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    
    func commonInit() -> Void {
        
        // add and configure sublayers
        [singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
            layer.addSublayer(lay)
            lay.lineWidth = 4
            lay.strokeColor = UIColor.blue.cgColor
            lay.fillColor = UIColor.clear.cgColor
        }
        
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // for readablility, define the points for our lines
        let topCenter = CGPoint(x: bounds.midX, y: 0)
        let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
        let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
        let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
        let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
        let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
        let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)

        let singleBez = UIBezierPath()
        let leftBez = UIBezierPath()
        let rightBez = UIBezierPath()

        // vertical line
        singleBez.move(to: topCenter)
        singleBez.addLine(to: botCenter)
        
        // split / stepped line to the left
        leftBez.move(to: topCenter)
        leftBez.addLine(to: midCenter)
        leftBez.addLine(to: midLeft)
        leftBez.addLine(to: botLeft)

        // split / stepped line to the right
        rightBez.move(to: topCenter)
        rightBez.addLine(to: midCenter)
        rightBez.addLine(to: midRight)
        rightBez.addLine(to: botRight)
        
        // set the layer paths
        //  initializing strokeEnd to 0 for all three
        
        singleLineLayer.path = singleBez.cgPath
        singleLineLayer.strokeEnd = 0

        leftLineLayer.path = leftBez.cgPath
        leftLineLayer.strokeEnd = 0
        
        rightLineLayer.path = rightBez.cgPath
        rightLineLayer.strokeEnd = 0
        
        // calculate total line lengths (in points)
        //  so we can adjust the "draw speed" in the animation
        let singleLength = botCenter.y - topCenter.y
        let doubleLength = singleLength + (midCenter.x - midLeft.x)
        durationFactor = singleLength / doubleLength
    }
    
    public func doAnim() -> Void {

        // reset the animations
        [singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
            lay.removeAllAnimations()
            lay.strokeEnd = 0
        }
        
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        
        animation.fromValue = 0.0
        animation.toValue = 1.0
        animation.duration = 2.0
        animation.fillMode = .forwards
        animation.isRemovedOnCompletion = false
        
        if self.single {
            // we want the apparent drawing speed to be the same
            //  for a single line as for a split / stepped line
            //  so change the animation duration
            animation.duration *= durationFactor
            // animate the single line layer
            self.singleLineLayer.add(animation, forKey: animation.keyPath)
        } else {
            // animate the both left and right line layers
            self.leftLineLayer.add(animation, forKey: animation.keyPath)
            self.rightLineLayer.add(animation, forKey: animation.keyPath)
        }

    }
    
}

和一个示例视图 controller 显示它的运行情况:

class ConnectTestViewController: UIViewController {
    
    let vTop = UIView()
    let vLeft = UIView()
    let vCenter = UIView()
    let vRight = UIView()

    let testConnectView = ConnectView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // give the 4 views different background colors
        //  add them as subviews
        //  make them all 100x100 points
        let colors: [UIColor] = [
            .systemYellow,
            .systemRed, .systemGreen, .systemBlue,
        ]
        for (v, c) in zip([vTop, vLeft, vCenter, vRight], colors) {
            v.backgroundColor = c
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            v.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
            v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
        }

        // add the clear-background Connect View
        testConnectView.backgroundColor = .clear
        testConnectView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(testConnectView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            // horizontally center the top box near the top
            vTop.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            vTop.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            // horizontally center the center box, 200-pts below the top box
            vCenter.topAnchor.constraint(equalTo: vTop.bottomAnchor, constant: 200.0),
            vCenter.centerXAnchor.constraint(equalTo: g.centerXAnchor),

            // align tops of left and right boxes with center box
            vLeft.topAnchor.constraint(equalTo: vCenter.topAnchor),
            vRight.topAnchor.constraint(equalTo: vCenter.topAnchor),

            // position left and right boxes to left and right of center box
            vLeft.trailingAnchor.constraint(equalTo: vCenter.leadingAnchor, constant: -20.0),
            vRight.leadingAnchor.constraint(equalTo: vCenter.trailingAnchor, constant: 20.0),
            
            // constrain Connect View
            //  Top to Bottom of Top box
            testConnectView.topAnchor.constraint(equalTo: vTop.bottomAnchor),
            //  Bottom to Top of the row of 3 boxes
            testConnectView.bottomAnchor.constraint(equalTo: vCenter.topAnchor),
            //  Leading to CenterX of Left box
            testConnectView.leadingAnchor.constraint(equalTo: vLeft.centerXAnchor),
            //  Trailing to CenterX of Right box
            testConnectView.trailingAnchor.constraint(equalTo: vRight.centerXAnchor),

        ])
        
        // add a couple buttons at the bottom
        let stack = UIStackView()
        stack.spacing = 20
        stack.distribution = .fillEqually
        stack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stack)
        
        ["Run Anim", "Show/Hide"].forEach { str in
            let b = UIButton()
            b.setTitle(str, for: [])
            b.backgroundColor = .red
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.lightGray, for: .highlighted)
            b.addTarget(self, action: #selector(buttonTap(_:)), for: .touchUpInside)
            stack.addArrangedSubview(b)
        }
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            stack.heightAnchor.constraint(equalToConstant: 50.0),
        ])
        
    }
    
    @objc func buttonTap(_ sender: Any?) -> Void {
        guard let b = sender as? UIButton,
              let t = b.currentTitle
        else {
            return
        }
        if t == "Run Anim" {
            // tap button to toggle between
            //  Top-to-Middle box line or
            //  Top-to-SideBoxes split / stepped line
            testConnectView.single.toggle()
            
            // run the animation
            testConnectView.doAnim()
        } else {
            // toggle background of Connect View between
            //  clear and yellow
            testConnectView.backgroundColor = testConnectView.backgroundColor == .clear ? .yellow : .clear
        }
    }
    
}

运行将给出以下结果:

在此处输入图像描述

在此处输入图像描述

底部的第一个按钮将切换 Top-Center 和 Top-Left-Right 之间的连接(每次重新运行 animation)。 第二个按钮将在透明和黄色之间切换视图的背景颜色,以便我们可以看到它的框架。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM