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

喜你入骨 提交于 2019-11-26 17:10:01

问题


What I want to get is a UISlider which lets the user not only slide when he starts on its thumbRect, but also when he taps elsewhere. When the user taps on the slider but outside of the thumbRect, the slider should jump to that value and then still keeping up to the user's sliding gesture.

What I have tried so far was implementing a subclass of UIGestureRecognizer like in this suggestion. It starts right then when a touch down somewhere outside the thumbRect occurs. The problem is that the slider sets its value but then further sliding gestures are ignored because the touch down recognizer has stolen the touch.

How can I implement a slider where you can tap anywhere but still slide right away?


Edit: ali59a was so kind to add an example of what I've done now. This requires to lift the finger again, after that I can touch and drag to slide (a tap is not what I want, I need a 'touch and slide' right away).


回答1:


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.




回答2:


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)
    }
}



回答3:


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




回答4:


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)
  }
}



回答5:


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)
    }
}



回答6:


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)
    }
}



回答7:


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];
        }];
    }
}
}



回答8:


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
}}



回答9:


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()
}



回答10:


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);
        }

    }

}



回答11:


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
}


来源:https://stackoverflow.com/questions/22717167/how-to-enable-tap-and-slide-in-a-uislider

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!