如何逐像素缓慢渲染完整图像?

How to render full image slowly pixel by pixel?

我想慢慢渲染图像的各个部分,如附图所示。如何使用 Swift 在 iOS 中实现此目的?

您可以使用 CAShapeLayer 作为 遮罩

如果我们从这张图片开始:

并添加如下所示的遮罩层(灰色实际上将完全透明):

像这样叠加在图像上:

只会显示蒙版的黑色部分(同样,灰色部分将完全透明):

因此,如果我们计算图像的矩形网格:

然后我们可以用黑色“随机填充网格”,循环填充越来越多的网格点,图像将像您的 gif 中一样“显露出来”。

这是一个简单的示例,使用 0.75 秒的重复计时器:

class RevealImageView: UIView {
    
    public var image: UIImage? {
        didSet {
            imgView.image = image
        }
    }

    private let imgView = UIImageView()
    
    private let maskLayer = CAShapeLayer()
    
    // array of rectangles that make up our grid
    private var gridOfRects: [CGRect] = []
    
    // we're going to use a 20x20 grid
    private var numRows: Int = 20
    private var numCols: Int = 20
    
    // counter for how many rectangles to reveal
    private var gridCounter: Int = 0
    
    // how many more rects each time
    private var gridIncrement: Int = 40
    
    // how long between steps (in seconds)
    private var stepTime: CFTimeInterval = 0.75
    
    // timer for updating the grid
    private var timer: Timer?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        backgroundColor = .white
        
        // add the image view and constrain all 4 sides
        addSubview(imgView)
        imgView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            imgView.topAnchor.constraint(equalTo: topAnchor),
            imgView.leadingAnchor.constraint(equalTo: leadingAnchor),
            imgView.trailingAnchor.constraint(equalTo: trailingAnchor),
            imgView.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])

        // set the layer mask for the image view
        imgView.layer.mask = maskLayer
        
        // black rects will show that part of the image
        maskLayer.fillColor = UIColor.black.cgColor
        
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        // this is when we know auto-layout has set the frame / bounds
        maskLayer.frame = bounds
    }
    
    @objc private func updateMask() -> Void {
        
        // increment number of rectangles to reveal
        gridCounter += gridIncrement
        
        let pth = UIBezierPath()
        
        for i in 0..<gridCounter {
            // don't go past the end of the array
            if i < gridOfRects.count {
                pth.append(UIBezierPath(rect: gridOfRects[i]))
            }
        }
        
        maskLayer.path = pth.cgPath
        
        // if image is fully revealed
        if gridCounter >= gridOfRects.count {
            // stop the repeating timer
            timer?.invalidate()
        }

    }
    
    // call this from the controller to start the reveal
    func startReveal() -> Void {

        // stop the timer if it's running
        timer?.invalidate()
        
        // empty grid array if it's being called again
        gridOfRects.removeAll()

        let rowHeight = bounds.height / CGFloat(numRows)
        let colWidth = bounds.width / CGFloat(numCols)
        
        var r: CGRect = CGRect(x: 0, y: 0, width: colWidth, height: rowHeight)
        
        // build array of grid rectangles
        for _ in 1...numRows {
            for _ in 1...numCols {
                gridOfRects.append(r)
                r.origin.x += colWidth
            }
            r.origin.x = 0
            r.origin.y += rowHeight
        }
        
        // shuffle the array so the rectangles are randomized
        gridOfRects.shuffle()

        // reset grid counter
        gridCounter = 0
        
        // do the first update
        updateMask()
        
        // set a repeating timer to continue the reveal
        timer = Timer.scheduledTimer(timeInterval: stepTime, target: self, selector: #selector(self.updateMask), userInfo: nil, repeats: true)
    }
    
    override func willMove(toSuperview newSuperview: UIView?) {
        if newSuperview == nil {
            // stop the timer if it's running
            timer?.invalidate()
        }
    }
    
}

这是一个示例视图控制器 class 以查看它的实际效果:

class RevealImageViewController: UIViewController {

    var revealImageView: RevealImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBlue
        
        // make sure we can load an image
        guard let img = UIImage(named: "testing") else {
            print("Could not load image!!!")
            return
        }
        
        // create the view
        revealImageView = RevealImageView()
        
        // set the image
        revealImageView.image = img
        
        revealImageView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(revealImageView)
        
        // respect safe area
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            revealImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
            revealImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
            // use image size to keep proportions
            revealImageView.heightAnchor.constraint(equalTo:revealImageView.widthAnchor, multiplier: img.size.height / img.size.width),
            revealImageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
        ])
        
        // tap anywhere in the view to begin the reveal
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.didTap))
        view.addGestureRecognizer(tapGesture)
    }
    
    @objc func didTap() -> Void {
        revealImageView.startReveal()
    }
    
}

发布时的样子:

点击任意位置,几秒钟后看起来像这样:

当它完全“显示”时,计时器停止,看起来像这样: