18
votes

Thumb image does not move to the edge even when it's value is max or min.

Does anyone know how to make it move all the way to the edge of the slider?

enter image description here

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UISlider *slider;
@end

@implementation ViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
UIImage* sliderBarMinImage = [[UIImage imageNamed:@"slider_bar_3_min"] stretchableImageWithLeftCapWidth:8.0 topCapHeight:0.0];
UIImage* sliderBarImage = [[UIImage imageNamed:@"slider_bar_3_max"] stretchableImageWithLeftCapWidth:8.0 topCapHeight:0.0];
UIImage* sliderThumbImage= [[UIImage imageNamed:@"slider_thumb_3"] stretchableImageWithLeftCapWidth:8.0 topCapHeight:0.0];

[self.slider setMinimumTrackImage:sliderBarMinImage forState:UIControlStateNormal];
[self.slider setMaximumTrackImage:sliderBarImage forState:UIControlStateNormal];
[self.slider setThumbImage:sliderThumbImage forState:UIControlStateNormal];
}
@end
9

9 Answers

24
votes

What you see is the UISlider working as designed, apparently.

By default, at the extreme values, the thumb's edge is aligned with the edges of the track, rather than the thumb's center being positioned on the edge of the track. So this is a problem if you provide a thumb image whose left and right edges are not opaque, since then the track underneath becomes visible.

One partial fix is to override thumbRectForBounds(_:trackRect:value:), so that the thumbs' centers range across the width of the slider. The code below shows this:

class CenteredThumbSlider : UISlider {
  override func thumbRectForBounds(bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect
  {
    let unadjustedThumbrect = super.thumbRectForBounds(bounds, trackRect: rect, value: value)
    let thumbOffsetToApplyOnEachSide:CGFloat = unadjustedThumbrect.size.width / 2.0
    let minOffsetToAdd = -thumbOffsetToApplyOnEachSide
    let maxOffsetToAdd = thumbOffsetToApplyOnEachSide
    let offsetForValue = minOffsetToAdd + (maxOffsetToAdd - minOffsetToAdd) * CGFloat(value / (self.maximumValue - self.minimumValue))
    var origin = unadjustedThumbrect.origin
    origin.x += offsetForValue
    return CGRect(origin: origin, size: unadjustedThumbrect.size)
  }
}

However, I would say this is only a partial fix since I do not believe this also modifies the position of the thumb's tap region, so this fix above has the unfortunate side-effect of making the thumbs even less easy to touch than they are by default, since the slider still listens for touches more toward the center of the slider.

2
votes

I recently had to deal with customization of the slider (subclassed), including adding tickmarks on the slider. Thumb image is not centered on the value and since you replaced image it is positioned where it is supposed to be according to implementation. In order to do what you are trying to do I had to adjust position of the thumb image using

- (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value
2
votes

Most upvoted answer didn't work for me without changes. Here's what did -- it's a drop in replacement for UISlider:

class Slider: UISlider {

    override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float)->CGRect {

            var thumbRect = super.thumbRect(forBounds: bounds, trackRect: rect, value: value)

            //determine value dependent offset
            var offsetForValue: CGFloat = 0
            if value != 0 {offsetForValue = thumbRect.size.width * CGFloat(value / (self.maximumValue - self.minimumValue)) - ((value > 0) ? 2 : -2)}

            //apply offset to thumb rect
            thumbRect.origin.x += offsetForValue

            return thumbRect
    }
}
1
votes

Instead of

[self.slider setMinimumTrackImage:sliderBarMinImage forState:UIControlStateNormal];
[self.slider setMaximumTrackImage:sliderBarImage forState:UIControlStateNormal];
[self.slider setThumbImage:sliderThumbImage forState:UIControlStateNormal];

Try setting the appearance proxy like this

[[UISlider appearance] setMinimumTrackImage:sliderBarMinImage forState:UIControlStateNormal];
[[UISlider appearance] setMaximumTrackImage:sliderBarImage forState:UIControlStateNormal];
[[UISlider appearance] setThumbImage:sliderThumbImage forState:UIControlStateNormal];
0
votes

It you see it already moves to the edge of the Slider. Its because thumb image slider have such corner so you feel like its not moving to the edge.

If you want to confirm this that try out the same code with Square image in the thumb image.

I hope it will help you.

0
votes

It may not be the most efficient, but it seems to work fine. This is another simple way to do it in Objective-C. Basically, you'll play around with the "pixelAdjustment" variable to get the slider where you want it.

- (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:   (float)value {
    float multValue = value - 0.5;
    float pixelAdjustment = 30; //This is the value you need to change depending on the size of the thumb image of your slider.
    float xOriginDelta = (multValue * (bounds.size.width - pixelAdjustment));

    CGRect adjustedRect = CGRectMake(bounds.origin.x + xOriginDelta, bounds.origin.y, bounds.size.width, bounds.size.height);
    return adjustedRect;
}
0
votes

I have created the similar slider by subclassing UISlider. In storyboard you can set a height constraint to increase height at runtime if slider thumb not gets touch events.

static float const SLIDER_OFFSET = 10.0;

//Get thumb rect for larger track rect than actual to move slider to edges
    -(CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value {
        UIImage *image = self.currentThumbImage;

    rect.origin.x -= SLIDER_OFFSET;
    rect.size.width += (SLIDER_OFFSET*2);
    CGRect thumbRect = [super thumbRectForBounds:bounds trackRect:rect value:value];
    return CGRectMake(thumbRect.origin.x, rect.origin.y+2, image.size.width, image.size.height);
}

//Make track rect smaller than bounds
-(CGRect)trackRectForBounds:(CGRect)bounds  {
    bounds.origin.x += SLIDER_OFFSET;
    bounds.size.width -= (SLIDER_OFFSET*2);
    CGRect trackRect = [super trackRectForBounds:bounds];

    return CGRectMake(trackRect.origin.x, trackRect.origin.y, trackRect.size.width, trackRect.size.height);
}
0
votes

I found a simpler solution which also has a quite smooth UX.

I basically just set the .highlighted image to the same value as that of the .normal. Here's the code.

    seekBar.setThumbImage(thumbImage, for: .normal)
    seekBar.setThumbImage(thumbImage, for: .highlighted)
0
votes

So my solution to this problem was to make the colour of the slider Min Track and Max Track transparent and then draw a new track in the initWithCoder and update the size of the track in the continueTrackingWithTouch.

-(AnalogueSlider *)initWithCoder:(NSCoder *)coder :(NSArray *)labels {
    self = [super initWithCoder:coder];

    trackRect = [self trackRectForBounds:self.bounds];
    thumbRect = [self thumbRectForBounds:self.bounds trackRect:trackRect value:self.value];

    // drawn a new track
    leftTrack = [[UIView alloc] initWithFrame:CGRectMake(trackRect.origin.x+thumbRect.size.width/2.0, trackRect.origin.y, thumbRect.origin.x-thumbRect.size.width, trackRect.size.height)];
    leftTrack.backgroundColor = [UIColor blueColor];
    [self insertSubview:leftTrack belowSubview:self];

    rightTrack = [[UIView alloc] initWithFrame:CGRectMake(thumbRect.origin.x+thumbRect.size.width/2.0, trackRect.origin.y, trackRect.size.width-thumbRect.origin.x, trackRect.size.height)];
    rightTrack.backgroundColor = [UIColor grayColor];
    [self insertSubview:rightTrack belowSubview:self];

    return self;
}

-(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    thumbRect = [self thumbRectForBounds:self.bounds trackRect:trackRect value:self.value];
    leftTrack.frame = CGRectMake(trackRect.origin.x+thumbRect.size.width/2.0, trackRect.origin.y, thumbRect.origin.x-thumbRect.size.width, trackRect.size.height);
    rightTrack.frame = CGRectMake(thumbRect.origin.x+thumbRect.size.width/2.0, trackRect.origin.y, trackRect.size.width-thumbRect.origin.x, trackRect.size.height);

    return [super continueTrackingWithTouch:touch withEvent:event];
}

This allows the thumb image to travel further than the track so the centre of the thumb is at the end of the track.

near end

And at the end:

at end