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

问题描述

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

enter image description here

解决方法

您可以通过使用 CAShapeLayer 作为掩码来做到这一点。

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

enter image description here

并添加一个看起来像这样的遮罩层(灰色实际上是完全透明的):

enter image description here

像这样叠加在图像上:

enter image description here

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

enter image description here

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

enter image description here

然后我们可以用黑色“随机填充网格”,循环以填充越来越多的网格点,并且图像将在您的 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 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()
    }
    
}

发布时的样子:

enter image description here

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

enter image description here

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

enter image description here