Reputation: 53
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:
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?
Upvotes: 3
Views: 688
Reputation: 77690
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:
If we set the image to your star, it looks like this:
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:
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:
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.
Upvotes: 2