How to enable “tap and slide” in a UISlider?

陌路散爱 提交于 2019-11-27 15:28:05

I'm not sure if you are still looking for an answer for this, but I was just looking at this myself today; and I managed to get it to work for me.

The key to it, is using a UILongPressGestureRecognizer instead of just a UITapGestureRecognizer, we can then set the minimumPressDuration of the recognizer to 0; making it act as a tap recognizer, except you can now actually check its state.

Putting what ali59a suggested will work for you, just by replacing the UITapGestureRecognizer with a UILongPressGestureRecognizer. However, I found that this didn't seem to quite put the thumbRect directly under my thumb. It appeared a bit off to me.

I created my own UISlider subclass for my project, and here is how I implemented the "tap and slide feature" for me.

In my init method:

UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(tapAndSlide:)];
longPress.minimumPressDuration = 0;
[self addGestureRecognizer:longPress];

Then my tapAndSlide: method:

- (void)tapAndSlide:(UILongPressGestureRecognizer*)gesture
{
    CGPoint pt = [gesture locationInView: self];
    CGFloat thumbWidth = [self thumbRect].size.width;
    CGFloat value;

    if(pt.x <= [self thumbRect].size.width/2.0)
        value = self.minimumValue;
    else if(pt.x >= self.bounds.size.width - thumbWidth/2.0)
        value = self.maximumValue;
    else {
        CGFloat percentage = (pt.x - thumbWidth/2.0)/(self.bounds.size.width - thumbWidth);
        CGFloat delta = percentage * (self.maximumValue - self.minimumValue);
        value = self.minimumValue + delta;
    }

    if(gesture.state == UIGestureRecognizerStateBegan){
        [UIView animateWithDuration:0.35 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
            [self setValue:value animated:YES];
            [super sendActionsForControlEvents:UIControlEventValueChanged];
        } completion:nil];
    }
    else [self setValue:value];

    if(gesture.state == UIGestureRecognizerStateChanged)
        [super sendActionsForControlEvents:UIControlEventValueChanged];
}

Where I also used a method to return the frame of my custom thumbRect:

- (CGRect)thumbRect {
    CGRect trackRect = [self trackRectForBounds:self.bounds];
    return [self thumbRectForBounds:self.bounds trackRect:trackRect value:self.value];
}

I also have my slider animate to the position where the user first taps, over 0.35 seconds. Which I reckon looks pretty sweet, so I included that in that code. If you don't want that, simply try this:

- (void)tapAndSlide:(UILongPressGestureRecognizer*)gesture
{
    CGPoint pt = [gesture locationInView: self];
    CGFloat thumbWidth = [self thumbRect].size.width;
    CGFloat value;

    if(pt.x <= [self thumbRect].size.width/2.0)
        value = self.minimumValue;
    else if(pt.x >= self.bounds.size.width - thumbWidth/2.0)
        value = self.maximumValue;
    else {
        CGFloat percentage = (pt.x - thumbWidth/2.0)/(self.bounds.size.width - thumbWidth);
        CGFloat delta = percentage * (self.maximumValue - self.minimumValue);
        value = self.minimumValue + delta;
    }

    [self setValue:value];

    if(gesture.state == UIGestureRecognizerStateChanged)
        [super sendActionsForControlEvents:UIControlEventValueChanged];
}

I hope that makes sense, and helps you.

I converted the answer provided by DWilliames to Swift

Inside your viewDidAppear()

let longPress                  = UILongPressGestureRecognizer(target: self.slider, action: Selector("tapAndSlide:"))
longPress.minimumPressDuration = 0
self.addGestureRecognizer(longPress)

Class file

class TapUISlider: UISlider
{
    func tapAndSlide(gesture: UILongPressGestureRecognizer)
    {
        let pt           = gesture.locationInView(self)
        let thumbWidth   = self.thumbRect().size.width
        var value: Float = 0

        if (pt.x <= self.thumbRect().size.width / 2)
        {
            value = self.minimumValue
        }
        else if (pt.x >= self.bounds.size.width - thumbWidth / 2)
        {
            value = self.maximumValue
        }
        else
        {
            let percentage = Float((pt.x - thumbWidth / 2) / (self.bounds.size.width - thumbWidth))
            let delta      = percentage * (self.maximumValue - self.minimumValue)

            value          = self.minimumValue + delta
        }

        if (gesture.state == UIGestureRecognizerState.Began)
        {
            UIView.animateWithDuration(0.35, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut,
            animations:
            {
                self.setValue(value, animated: true)
                super.sendActionsForControlEvents(UIControlEvents.ValueChanged)
            },
            completion: nil)
        }
        else
        {
            self.setValue(value, animated: false)
        }
    }

    func thumbRect() -> CGRect
    {
        return self.thumbRectForBounds(self.bounds, trackRect: self.bounds, value: self.value)
    }
}

You should add a tap gesture on your UISlider.

Exemple :

 UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(sliderTapped:)];
    [_slider addGestureRecognizer:tapGestureRecognizer];

In sliderTapped you can get the location and update the value of the slider :

- (void)sliderTapped:(UIGestureRecognizer *)gestureRecognizer {
    CGPoint  pointTaped = [gestureRecognizer locationInView:gestureRecognizer.view];
    CGPoint positionOfSlider = _slider.frame.origin;
    float widthOfSlider = _slider.frame.size.width;
    float newValue = ((pointTaped.x - positionOfSlider.x) * _slider.maximumValue) / widthOfSlider;
    [_slider setValue:newValue];
}

I create an example here : https://github.com/ali59a/tap-and-slide-in-a-UISlider

Here's my modification to the above:

class TapUISlider: UISlider {

  func tapAndSlide(gesture: UILongPressGestureRecognizer) {
    let pt = gesture.locationInView(self)
    let thumbWidth = self.thumbRect().size.width
    var value: Float = 0

    if (pt.x <= self.thumbRect().size.width / 2) {
      value = self.minimumValue
    } else if (pt.x >= self.bounds.size.width - thumbWidth / 2) {
      value = self.maximumValue
    } else {
      let percentage = Float((pt.x - thumbWidth / 2) / (self.bounds.size.width - thumbWidth))
      let delta = percentage * (self.maximumValue - self.minimumValue)
      value = self.minimumValue + delta
    }

    if (gesture.state == UIGestureRecognizerState.Began) {
      UIView.animateWithDuration(0.35, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut,
        animations: {
          self.setValue(value, animated: true)
          super.sendActionsForControlEvents(UIControlEvents.ValueChanged)
        }, completion: nil)
    } else {
      self.setValue(value, animated: false)
      super.sendActionsForControlEvents(UIControlEvents.ValueChanged)
    }
  }

  func thumbRect() -> CGRect {
    return self.thumbRectForBounds(self.bounds, trackRect: self.bounds, value: self.value)
  }
}

Adding swift version of Ali AB.'s answer,

@IBAction func sliderTappedAction(sender: UITapGestureRecognizer)
{
    if let slider = sender.view as? UISlider {

        if slider.highlighted { return }

        let point = sender.locationInView(slider)
        let percentage = Float(point.x / CGRectGetWidth(slider.bounds))
        let delta = percentage * (slider.maximumValue - slider.minimumValue)
        let value = slider.minimumValue + delta
        slider.setValue(value, animated: true)
    }
}

Updated tsji10dra's answer to Swift 4:

@IBAction func sliderTappedAction(sender: UITapGestureRecognizer) {

    if let slider = sender.view as? UISlider {
        if slider.isHighlighted { return }

        let point = sender.location(in: slider)
        let percentage = Float(point.x / slider.bounds.size.width)
        let delta = percentage * (slider.maximumValue - slider.minimumValue)
        let value = slider.minimumValue + delta
        slider.setValue(value, animated: true)

        // also remember to call valueChanged if there's any
        // custom behaviour going on there and pass the slider
        // variable as the parameter, as indicated below
        self.sliderValueChanged(slider)
    }
}

I completed @DWilliames solution for a UISlider subclass containing minimum and maximumValueImages.

Additionally I implemented a functionality for user touches in the areas outside the trackArea (means either the area around the minimum or the maximumValueImage). Touching these areas moves the slider/changes the value in intervals.

- (void) tapAndSlide: (UILongPressGestureRecognizer*) gesture {
    CGPoint touchPoint = [gesture locationInView: self];
    CGRect trackRect = [self trackRectForBounds: self.bounds];
    CGFloat thumbWidth = [self thumbRectForBounds: self.bounds trackRect: trackRect value: self.value].size.width;
    CGRect trackArea = CGRectMake(trackRect.origin.x, 0, trackRect.size.width, self.bounds.size.height);
    CGFloat value;

if (CGRectContainsPoint(trackArea, touchPoint)) {
    if (touchPoint.x <= trackArea.origin.x + thumbWidth/2.0) {
        value = self.minimumValue;
    }
    else if (touchPoint.x >= trackArea.origin.x + trackArea.size.width - thumbWidth/2.0) {
        value = self.maximumValue;
    }
    else {
        CGFloat percentage = (touchPoint.x - trackArea.origin.x - thumbWidth/2.0)/(trackArea.size.width - thumbWidth);
        CGFloat delta = percentage*(self.maximumValue - self.minimumValue);
        value = self.minimumValue + delta;
    }

    if (value != self.value) {
        if (gesture.state == UIGestureRecognizerStateBegan) {
            [UIView animateWithDuration: 0.2 delay: 0 options: UIViewAnimationOptionCurveEaseInOut animations: ^{
                [self setValue: value animated: YES];

            } completion: ^(BOOL finished) {
                [self sendActionsForControlEvents: UIControlEventValueChanged];
            }];
        }
        else {
            [self setValue: value animated: YES];
            [self sendActionsForControlEvents: UIControlEventValueChanged];
        }
    }
}
else {
    if (gesture.state == UIGestureRecognizerStateBegan) {
        if (touchPoint.x <= trackArea.origin.x) {
            if (self.value == self.minimumValue) return;
            value = self.value - 1.5;
        }
        else {
            if (self.value == self.maximumValue) return;
            value = self.value + 1.5;
        }
        CGFloat duration = 0.1;
        [UIView animateWithDuration: duration delay: 0 options: UIViewAnimationOptionCurveEaseInOut animations: ^{
            [self setValue: value animated: YES];
        } completion: ^(BOOL finished) {
            [self sendActionsForControlEvents: UIControlEventValueChanged];
        }];
    }
}
}

My solution is quite simple:

class CustomSlider: UISlider {
    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        let newValue = <calculated_value>
        self.setValue(newValue, animated: false)
        super.sendActions(for: UIControlEvents.valueChanged)
        return true
}}

I didn't check David Williames answer, but I'll post my solution in case someone is looking for another way to do it.

Swift 4

First create a custom UISlider so that it will detect touches on the bar as well :

class CustomSlider: UISlider {
    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        return true
    }
}

(don't forget to set your slider to be this CustomSlider, on storyboard)

The on viewDidLoad of the view controller that is displaying the slider:

self.slider.addTarget(self, action: #selector(sliderTap), for: .touchDown)

(this is only used to pause the player when moving the slider)

Then, on your UISlider action:

@IBAction func moveSlider(_ sender: CustomSlider, forEvent event: UIEvent) {
    if let touchEvent = event.allTouches?.first {
        switch touchEvent.phase {
            case .ended, .cancelled, .stationary:
                //here, start playing if needed
                startPlaying()                    
            default:
                break
        }
    }
}

And on your "sliderTap" selector method:

@objc func sliderTap() {
    //pause the player, if you want
    audioPlayer?.pause()
}

Suggestion: set the player "currentTime" before starting to play:

private func startPlaying() {
    audioPlayer?.currentTime = Double(slider.value)
    audioPlayer?.play()
}

At the risk of being chastised by the iOS pure community...

Here is a solution for Xamarin iOS C# converted from David Williames Answer.

Sub class UISlider:

[Register(nameof(UISliderCustom))]
[DesignTimeVisible(true)]
public class UISliderCustom : UISlider
{

    public UISliderCustom(IntPtr handle) : base(handle) { }

    public UISliderCustom()
    {
        // Called when created from code.
        Initialize();
    }

    public override void AwakeFromNib()
    {
        // Called when loaded from xib or storyboard.
        Initialize();
    }

    void Initialize()
    {
        // Common initialization code here.

        var longPress = new UILongPressGestureRecognizer(tapAndSlide);
        longPress.MinimumPressDuration = 0;
        //longPress.CancelsTouchesInView = false;
        this.AddGestureRecognizer(longPress);
        this.UserInteractionEnabled = true;

    }

    private void tapAndSlide(UILongPressGestureRecognizer gesture)
    {
        System.Diagnostics.Debug.WriteLine($"{nameof(UISliderCustom)} RecognizerState {gesture.State}");

        // need to propagate events down the chain
        // I imagine iOS does something similar
        // for whatever recogniser on the thumb control
        // It's not enough to set CancelsTouchesInView because
        // if clicking on the track away from the thumb control
        // the thumb gesture recogniser won't pick it up anyway
        switch (gesture.State)
        {
            case UIGestureRecognizerState.Cancelled:
                this.SendActionForControlEvents(UIControlEvent.TouchCancel);
                break;

            case UIGestureRecognizerState.Began:
                this.SendActionForControlEvents(UIControlEvent.TouchDown);
                break;

            case UIGestureRecognizerState.Changed:
                this.SendActionForControlEvents(UIControlEvent.ValueChanged);                    
                break;

            case UIGestureRecognizerState.Ended:
                this.SendActionForControlEvents(UIControlEvent.TouchUpInside);
                break;

            case UIGestureRecognizerState.Failed:
                //?
                break;

            case UIGestureRecognizerState.Possible:
                //?
                break;

        }

        var pt = gesture.LocationInView(this);
        var thumbWidth = CurrentThumbImage.Size.Width;
        var value = 0f;

        if (pt.X <= thumbWidth / 2)
        {
            value = this.MinValue;
        }
        else if (pt.X >= this.Bounds.Size.Width - thumbWidth / 2)
        {
            value = this.MaxValue;
        }
        else
        {
            var percentage = ((pt.X - thumbWidth / 2) / (this.Bounds.Size.Width - thumbWidth));
            var delta = percentage * (this.MaxValue - this.MinValue);
            value = this.MinValue + (float)delta;
        }

        if (gesture.State == UIGestureRecognizerState.Began)
        {               
            UIView.Animate(0.35, 0, UIViewAnimationOptions.CurveEaseInOut,
                () =>
                {
                    this.SetValue(value, true);
                },
                null);
        }
        else
        {
            this.SetValue(value, animated: false);
        }

    }

}

To expand on the answer of Khang Azun- for swift 5 put the following in a UISlider custom class:

override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    let percent = Float(touch.location(in: self).x / bounds.size.width)
    let delta = percent * (maximumValue - minimumValue)

    let newValue = minimumValue + delta
    self.setValue(newValue, animated: false)
    super.sendActions(for: UIControl.Event.valueChanged)
    return true
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!