2

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?

Crocodilu
  • 43
  • 5
  • You could ask the designer to also provide you the half-rating images. – Cristik Nov 23 '20 at 17:52
  • I want to be more precise than that, it was just an example with 3.5. I want to be able to do 3.14 for example. As long as I know how to draw the opacity, the calculation won't be a problem. The problem is how to have a dynamic partial mask. – Crocodilu Nov 23 '20 at 20:37
  • I assume that you'll need a mask around the edges of the star, right? This will result in lots of calculations, not to mention running into issues with those calculations when scalling up or down. You'd be better draw the image yourself in this case :) – Cristik Nov 27 '20 at 15:29
  • Yes, it seems the level of detail I want in this is too complex :) Also none of the 3rd parties I tried that accept custom images for star do not render them correctly at all. – Crocodilu Nov 28 '20 at 20:21
  • @Crocodilu - is the shadow part of your image, or is it generated at run-time? Can you show an example of how you *want* it to look? – DonMag Dec 02 '20 at 13:14
  • Yes, the shadow is part of the image. I'll try to add the result I want, although it's pretty obvious I think. – Crocodilu Dec 05 '20 at 19:30

1 Answers1

1

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.

DonMag
  • 69,424
  • 5
  • 50
  • 86