简体   繁体   中英

UISlider set thumb clear background

How can I set a clear thumb background on UISlider so that the track is not visible behind the thumb.

This is what I have so far

在此处输入图片说明

But, I want it to look like in the iOS Camera application

在此处输入图片说明

TL;DR

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.

Ideal Solution

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.

  1. 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.

在此处输入图片说明

  1. 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.

    在此处输入图片说明

Alternate Solution: Changing the Images to Match the Value

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM