Warm tip: This article is reproduced from serverfault.com, please click

Adding a partial mask over an UIImageView

发布于 2020-11-22 09:23:08

I want to add a 0.5 alpha mask over just one part of an image (that I will calculate in code). Basically, it's a 5-star rating control, but the stars are not one color, but some nice images like this:

Star image

The image has a transparent background that I need to respect. So I'd like to be able to add a mask or to somehow set the alpha of just half of the image for example, when your rating is 3.5. (2 full stars and one with half of it with less alpha)

I can't just put a UIView over it with 0.5 alpha, because that will also impact with the background where the stars are displayed.

Any ideas?

Questioner
Crocodilu
Viewed
0
DonMag 2020-12-04 23:25:22

You can use a CAGradientLayer as a mask:

    gLayer.startPoint = CGPoint.zero
    gLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
    gLayer.locations = [
        0.0, 0.5, 0.5, 1.0,
    ]
    gLayer.colors = [
        UIColor.black.cgColor,
        UIColor.black.cgColor,
        UIColor.black.withAlphaComponent(0.5).cgColor,
        UIColor.black.withAlphaComponent(0.5).cgColor,
    ]

This would create a horizontal gradient, with the left half full alpha and the right half 50% alpha.

So, a white view with this as a mask would look like this:

enter image description here

If we set the image to your star, it looks like this:

enter image description here

If we want the star to be "75% filled" we change the locations:

    gLayer.locations = [
        0.0, 0.75, 0.75, 1.0,
    ]

resulting in:

enter image description here

Here is an example implementation for a "Five Star" rating view:

@IBDesignable
class FiveStarRatingView: UIView {
    
    @IBInspectable
    public var rating: CGFloat = 0.0 {
        didSet {
            var r = rating
            stack.arrangedSubviews.forEach {
                if let v = $0 as? PercentImageView {
                    v.percent = min(1.0, r)
                    r -= 1.0
                }
            }
        }
    }
    
    @IBInspectable
    public var ratingImage: UIImage = UIImage() {
        didSet {
            stack.arrangedSubviews.forEach {
                if let v = $0 as? PercentImageView {
                    v.image = ratingImage
                }
            }
        }
    }
    
    @IBInspectable
    public var tranparency: CGFloat = 0.5 {
        didSet {
            stack.arrangedSubviews.forEach {
                if let v = $0 as? PercentImageView {
                    v.tranparency = tranparency
                }
            }
        }
    }
    
    override var intrinsicContentSize: CGSize {
        return CGSize(width: 100.0, height: 20.0)
    }
    
    private let stack: UIStackView = {
        let v = UIStackView()
        v.axis = .horizontal
        v.alignment = .center
        v.distribution = .fillEqually
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    private func commonInit() -> Void {
        addSubview(stack)
        // constrain stack view to all 4 sides
        NSLayoutConstraint.activate([
            stack.topAnchor.constraint(equalTo: topAnchor),
            stack.leadingAnchor.constraint(equalTo: leadingAnchor),
            stack.trailingAnchor.constraint(equalTo: trailingAnchor),
            stack.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])
        // add 5 Percent Image Views to the stack view
        for _ in 1...5 {
            let v = PercentImageView(frame: .zero)
            stack.addArrangedSubview(v)
            v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
        }
    }
    
    private class PercentImageView: UIImageView {
        
        var percent: CGFloat = 0.0 {
            didSet {
                setNeedsLayout()
            }
        }
        
        var tranparency: CGFloat = 0.5 {
            didSet {
                setNeedsLayout()
            }
        }
        
        private let gLayer = CAGradientLayer()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            gLayer.startPoint = CGPoint.zero
            gLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
            layer.mask = gLayer
        }
        override func layoutSubviews() {
            super.layoutSubviews()

            // we don't want the layer's intrinsic animation
            CATransaction.begin()
            CATransaction.setDisableActions(true)

            gLayer.frame = bounds
            gLayer.locations = [
                0.0, percent as NSNumber, percent as NSNumber, 1.0,
            ]
            gLayer.colors = [
                UIColor.black.cgColor,
                UIColor.black.cgColor,
                UIColor.black.withAlphaComponent(tranparency).cgColor,
                UIColor.black.withAlphaComponent(tranparency).cgColor,
            ]
            
            CATransaction.commit()
        }
    }

}


class StarRatingViewController: UIViewController {

    let ratingView = FiveStarRatingView()
    
    let slider = UISlider()
    let valueLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        guard let starImage = UIImage(named: "star") else {
            fatalError("Could not load image named \"star\"")
        }
        
        // add a slider and a couple labels so we can change the rating
        let minLabel = UILabel()
        let maxLabel = UILabel()
        [slider, valueLabel, minLabel, maxLabel].forEach {
            view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
            if let v = $0 as? UILabel {
                v.textAlignment = .center
            }
        }
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            valueLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            valueLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            slider.topAnchor.constraint(equalTo: valueLabel.bottomAnchor, constant: 8.0),
            slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 32.0),
            slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -32.0),
            
            minLabel.topAnchor.constraint(equalTo: slider.bottomAnchor, constant: 8.0),
            minLabel.centerXAnchor.constraint(equalTo: slider.leadingAnchor, constant: 0.0),
            
            maxLabel.topAnchor.constraint(equalTo: slider.bottomAnchor, constant: 8.0),
            maxLabel.centerXAnchor.constraint(equalTo: slider.trailingAnchor, constant: 0.0),
        ])
        minLabel.text = "0"
        maxLabel.text = "5"
        
        ratingView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(ratingView)

        NSLayoutConstraint.activate([
            // constrain the rating view centered in the view
            //  300-pts wide
            //  height will be auto-set by the rating view
            ratingView.topAnchor.constraint(equalTo: minLabel.bottomAnchor, constant: 20.0),
            ratingView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            ratingView.widthAnchor.constraint(equalToConstant: 240.0),
        ])
        
        // use the star image
        ratingView.ratingImage = starImage
        
        // start at rating of 0 stars
        updateValue(0.0)
        slider.value = 0
        
        slider.addTarget(self, action: #selector(self.sliderChanged(_:)), for: .valueChanged)
    }
    
    @objc func sliderChanged(_ sender: UISlider) {
        // round the slider value to 2 decimal places
        updateValue((sender.value * 5.0).rounded(digits: 2))
    }
    
    func updateValue(_ v: Float) -> Void {
        valueLabel.text = String(format: "%.2f", v)
        ratingView.rating = CGFloat(v)
    }
    
}

extension Float {
    func rounded(digits: Int) -> Float {
        let multiplier = Float(pow(10.0, Double(digits)))
        return (self * multiplier).rounded() / multiplier
    }
}

Result:

enter image description here

Note that the FiveStarRatingView class is marked @IBDesignable so you can add it in Storyboard / IB and set image, amount of transparency and rating at design-time.