您如何追踪 iPhone 屏幕的边框(包括 X、11 或 12 系列的缺口)?

How do you trace the border of an iPhone screen (including the notch for an X, 11, or 12 series)?

我正在尝试在主视图的外部描绘一条线 - 沿着屏幕的边缘描绘。下图显示了轮廓,下面的代码显示了它是如何动画的。

问题是,动画先绘制外边缘,然后绘制凹口周围的区域。

我需要它开始绘制外边缘,下降并围绕凹口绘制,然后继续沿着外边缘绘制直到完成。

import UIKit

class ViewController: UIViewController {
    
    var layer: CAShapeLayer = CAShapeLayer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupBorder()
        animateBorder()
    }
    
    func setupBorder() {
        let bounds = self.view.bounds
        
        if UIDevice.current.hasNotch {
        
            // FIXME: needs tweaks for:
            // 12 pro max (corners and notch)
            // 12 pro (corners)
            // 12 mini (notch)
            // the math works for all X and 11 series
            
            // border around the phone screen
            let framePath = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 40, height: 40))
            
            // Math courtesy of:
            // https://www.paintcodeapp.com/news/iphone-x-screen-demystified
            
            let devicePointWidth = UIScreen.main.bounds.size.width
            let w = devicePointWidth * 83 / 375
            let n = devicePointWidth * 209 / 375
            
            let notchBounds = CGRect(x: w, y: -10, width: n, height: 40)
            
            let notchPath = UIBezierPath(roundedRect: notchBounds, byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 20, height: 20))
            
            // This is the problem. The framePath is drawn first,
            // and then the notchPath is drawn. I need these to be
            // mathematically merged
            
            framePath.append(notchPath)
            framePath.usesEvenOddFillRule = true
            
            layer.path = framePath.cgPath
        } else {
            // if device is an 8 or lower, the border of the screen
            // is a rectangle
            
            layer.path = UIBezierPath(rect: bounds).cgPath
        }
        
        layer.strokeColor = UIColor.blue.cgColor
        layer.strokeEnd = 0.0
        layer.lineWidth = 20.0
        layer.fillColor = nil
        self.view.layer.addSublayer(layer)
    }
    
    func animateBorder() {
        CATransaction.begin()
        
        let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
        animation.timingFunction = CAMediaTimingFunction(name: .linear)
        animation.fromValue = 0.0
        animation.toValue = 1.0
        animation.duration = 6
        
        CATransaction.setCompletionBlock { [weak self] in
            self?.layer.strokeColor = UIColor.cyan.cgColor
            self?.layer.strokeEnd = 1.0
        }
        
        layer.add(animation, forKey: "stroke-screen")
        CATransaction.commit()
        
    }

}

extension UIDevice {
    var hasNotch: Bool {
        
        // FIXME: Does not work with apps that use SceneDelegate
        // Requires the window var in the AppDelegate
        
        if #available(iOS 11.0, *) {
            return UIApplication.shared.delegate?.window??.safeAreaInsets.bottom ?? 0 > 20
        }
        return false
    }

}

上面的代码可以在新项目中使用,但是SceneDelegate.swift文件需要删除,Info.plist中的Application Scene Manifest条目需要删除, var window: UIWindow? 需要添加到 AppDelegate.swift

您遇到这个问题是因为您正在创建两个单独的形状并将一个附加到下一个。所以你的路径被正确绘制了。

您需要使用精确坐标创建一个形状,以便正确绘制。
这是一个没有任何圆角的简单形状:

let framePath = UIBezierPath()
framePath.move(to: .zero)
framePath.addLine(to: CGPoint(x: w, y: 0))
framePath.addLine(to: CGPoint(x: w, y: 30))
framePath.addLine(to: CGPoint(x: w+n, y: 30))
framePath.addLine(to: CGPoint(x: w+n, y: 0))
framePath.addLine(to: CGPoint(x: devicePointWidth, y: 0))
framePath.addLine(to: CGPoint(x: devicePointWidth, y: self.view.bounds.height))
framePath.addLine(to: CGPoint(x: 0, y: self.view.bounds.height))
framePath.addLine(to: .zero)

然后你可以使用addArc(withCenter:radius:startAngle:endAngle:clockwise:)添加弯曲的部分。

这是使用您计算出的一些值的粗略草稿:

let circleTop = CGFloat(3*Double.pi / 2)
let circleRight = CGFloat(0)
let circleBottom = CGFloat(Double.pi / 2)
let circleLeft = CGFloat(Double.pi)

let framePath = UIBezierPath()
framePath.move(to: CGPoint(x: 40, y: 0))
framePath.addLine(to: CGPoint(x: w-6, y: 0))
framePath.addArc(withCenter: CGPoint(x: w-6, y: 6), radius: 6, startAngle: circleTop, endAngle: circleRight, clockwise: true)
framePath.addArc(withCenter: CGPoint(x: w+20, y: 10), radius: 20, startAngle: circleLeft, endAngle: circleBottom, clockwise: false)
framePath.addLine(to: CGPoint(x: w+n-20, y: 30))
framePath.addArc(withCenter: CGPoint(x: w+n-20, y: 10), radius: 20, startAngle: circleBottom, endAngle: circleRight, clockwise: false)
framePath.addArc(withCenter: CGPoint(x: w+n+6, y: 6), radius: 6, startAngle: CGFloat(Double.pi), endAngle: circleTop, clockwise: true)
framePath.addLine(to: CGPoint(x: devicePointWidth-40, y: 0))
framePath.addArc(withCenter: CGPoint(x: devicePointWidth-40, y: 40), radius: 40, startAngle: circleTop, endAngle: circleRight, clockwise: true)
framePath.addLine(to: CGPoint(x: devicePointWidth, y: self.view.bounds.height - 40))
framePath.addArc(withCenter: CGPoint(x: UIScreen.main.bounds.size.width-40, y: UIScreen.main.bounds.size.height-40), radius: 40, startAngle: circleRight, endAngle: circleBottom, clockwise: true)
framePath.addLine(to: CGPoint(x: 40, y: self.view.bounds.height))
framePath.addArc(withCenter: CGPoint(x: 40, y: UIScreen.main.bounds.size.height-40), radius: 40, startAngle: circleBottom, endAngle: circleLeft, clockwise: true)
framePath.addLine(to: CGPoint(x: 0, y: 40))
framePath.addArc(withCenter: CGPoint(x: 40, y: 40), radius: 40, startAngle: circleLeft, endAngle: circleTop, clockwise: true)