In short, there is no simple API of UISlider
that recreates this behavior.
Precondition: Per the question, I am only demonstrating how to make the thumb appear transparent and not show the track. I am not attempting to recreate the camera's slider exactly.
The ideal solution would be to create minimum and maximum track images that take advantage of cap insets to keep a portion of the track from being filled in. I say this is ideal because it is straightforward and relatively performant. Unfortunately, UISlider
does 2 things that prevents us from using this method.
The maximum track image is actually the same width as the entire track, but it is contained in view that has clipsToBounds
set to true. The maximum track's super view is resized and clips the maximum track to ensure it is never visible on the leading side of the thumb.
Disabling clipsToBounds
on the entire hierarchy lets us see this in the view debugger.
The minimum track UIImageView
is resized, but when one calls setMinimumTrackImage
on UISlider
it actually creates a different resizable image. This one took a while to figure out, but it can be demonstrated by inspecting the memory addresses.
Taking what I learned from my initial investigation, we can generate the images as the value changes. This is not the most efficient approach because it requires creating many images over and over. But, it fits within the original question's parameters. Note, you can only do it effectively with "continuous" sliders since it relies on value updates. On the plus side, this can easily be extended to draw more interesting track images, (like the one in the camera).
Here's the result, I made the thumb quite large so you can easily see that it is transparent.
Here's the class:
class TransparentThumbSlider: UISlider {
let thumbWidth: CGFloat = 40
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
func configure() {
setThumbImage(TransparentThumbSlider.thumbImage(diameter: thumbWidth, color: .blue), for: .normal)
addTarget(self, action: #selector(updateTrackImages), for: .valueChanged)
updateTrackImages()
}
static func thumbImage(diameter: CGFloat, color: UIColor) -> UIImage {
let strokeWidth: CGFloat = 4
let halfStrokeWidth = strokeWidth / 2
let totalRect = CGRect(x: 0, y: 0, width: diameter, height: diameter)
let strokeRect = CGRect(x: halfStrokeWidth,
y: halfStrokeWidth,
width: diameter - strokeWidth,
height: diameter - strokeWidth)
let renderer = UIGraphicsImageRenderer(size: totalRect.size)
let image = renderer.image { context in
context.cgContext.setStrokeColor(color.cgColor)
context.cgContext.setLineWidth(strokeWidth)
context.cgContext.strokeEllipse(in: strokeRect)
}
return image
}
static func trackImage(colored color: UIColor,
thumbWidth: CGFloat,
trackWidth: CGFloat,
value: CGFloat,
flipped: Bool) -> UIImage {
let fraction = flipped ? 1 - value : value
let adjustableWidth = (trackWidth - thumbWidth)
let halfThumbWidth = thumbWidth / 2
let fudgeFactor: CGFloat = 2
var totalRect: CGRect
var coloredRect: CGRect
if flipped {
totalRect = CGRect(x: 0, y: 0, width: trackWidth, height: 2)
coloredRect = CGRect(
x: adjustableWidth * value + thumbWidth - fudgeFactor,
y: 0,
width: adjustableWidth * fraction + (halfThumbWidth - fudgeFactor),
height: 2)
} else {
totalRect = CGRect(x: 0, y: 0, width: adjustableWidth * fraction + halfThumbWidth, height: 2)
coloredRect = CGRect(x: 0, y: 0, width: adjustableWidth * fraction, height: 2)
}
let renderer = UIGraphicsImageRenderer(size: totalRect.size)
let image = renderer.image { context in
context.cgContext.setFillColor(color.cgColor)
context.fill(coloredRect)
}
return image
}
@objc func updateTrackImages() {
let trackWidth = trackRect(forBounds: bounds).width
let minimumImage = TransparentThumbSlider.trackImage(
colored: .green,
thumbWidth: thumbWidth,
trackWidth: trackWidth,
value: CGFloat(value),
flipped: false)
setMinimumTrackImage(minimumImage, for: .normal)
let maximumImage = TransparentThumbSlider.trackImage(
colored: .yellow,
thumbWidth: thumbWidth,
trackWidth: trackWidth,
value: CGFloat(value),
flipped: true)
setMaximumTrackImage(maximumImage, for: .normal)
}
var previousBounds: CGRect = .zero
override func layoutSubviews() {
super.layoutSubviews()
// Prevent layout loop
if previousBounds != bounds {
previousBounds = bounds
updateTrackImages()
}
}
}
Let's start with it in action to show that this should accomplish OP's goal:
The short version is that I use a UIImage of the background, with the following extension applied to it to add a border.
extension UIImage {
func addRoundBorder(width: CGFloat,
color: UIColor) -> UIImage {
let square = CGSize(width: min(size.width, size.height) + width * 2,
height: min(size.width, size.height) + width * 2)
let imageView = UIImageView(frame: CGRect(origin: .zero, size: square))
imageView.contentMode = .center
imageView.image = self
imageView.layer.cornerRadius = square.width / 2
imageView.layer.masksToBounds = true
imageView.layer.borderWidth = width
imageView.layer.borderColor = color.cgColor
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
guard let context = UIGraphicsGetCurrentContext() else { return self }
imageView.layer.render(in: context)
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result ?? self
}
}
Then, I bring in a UIImage
of my background and call the extension, using the result as my new thumbnail image.
let image = UIImage(named: "circle")!.addRoundBorder(width: 3.0, color: .yellow)
slider.setThumbImage(image, for: .normal)
While this is just for demonstrative purposes, I would recommend using an #imageLiteral
to avoid the force cast.
The following screenshot is comparing the sample image provided by OP via the iOS camera application against the one I implemented as seen below. Due to the size of each, the quality of the Apple one allows for better detail in the comparison.
Regarding the comment by @Alladinian, the issue I see is that the background has a light texture to it, and the use of the border provides a visual way to break up where the button merges with the background. By using the OP's background, it appeared to provide a similar visual result. However, a more challenging background such as an image would definitely expose my design and produce an undesirable result.
据我所知,您必须添加仅具有圆形边框并且透明的缩略图图像
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.