【问题标题】:How to enable "tap and slide" in a UISlider?如何在 UISlider 中启用“点击和滑动”?
【发布时间】:2014-05-08 04:06:00
【问题描述】:

我想要的是一个UISlider,它让用户不仅可以在从thumbRect 开始时滑动,还可以在他点击其他地方时滑动。当用户在thumbRect 之外点击滑块时,滑块应该跳到那个值然后仍然跟上用户的滑动手势

到目前为止,我尝试的是实现 UIGestureRecognizer 的子类,例如 in this suggestion。当在thumbRect 之外的某个地方发生触地时,它就开始了。问题是滑块设置了它的值但随后进一步的滑动手势被忽略了,因为触摸向下识别器窃取了触摸

如何实现一个滑块,您可以在其中点击任意位置但仍会立即滑动?


编辑:ali59a 非常乐意添加我现在所做的an example。这需要再次抬起手指,然后我可以触摸并拖动以滑动(我不想要点击,我需要立即“触摸并滑动”)。

【问题讨论】:

标签: ios uigesturerecognizer uislider uitapgesturerecognizer


【解决方案1】:

我不确定您是否仍在为此寻找答案,但我今天自己只是在看这个;我设法让它为我工作。

它的关键是使用UILongPressGestureRecognizer 而不仅仅是UITapGestureRecognizer,然后我们可以将识别器的minimumPressDuration 设置为0;让它充当点击识别器,但您现在可以实际检查它的状态。

只需将UITapGestureRecognizer 替换为UILongPressGestureRecognizer,就可以使用ali59a 的建议。但是,我发现这似乎并没有完全将 thumbRect 放在我的拇指之下。对我来说似乎有点不对劲。

我为我的项目创建了自己的 UISlider 子类,下面是我为我实现“点击和滑动功能”的方法。

在我的init 方法中:

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

然后是我的tapAndSlide: 方法:

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

我还使用了一种方法来返回自定义 thumbRect 的框架:

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

我还让我的滑块动画到用户第一次点击的位置,超过 0.35 秒。我认为这看起来很可爱,所以我将它包含在该代码中。 如果你不想这样,只需试试这个:

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

我希望这是有道理的,并且对你有帮助。

【讨论】:

    【解决方案2】:

    我将 DWilliames 提供的答案转换为 Swift

    在你的 viewDidAppear() 中

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

    类文件

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

    【讨论】:

    • 对上述内容的一个小修改对我有用。你调用 self.setValue 的两个地方,我还需要启动 super.sendActionsForControlEvents(UIControlEvents.ValueChanged)
    【解决方案3】:

    您应该在UISlider 上添加一个点击手势。

    示例:

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

    在sliderTapped中,您可以获取位置并更新滑块的值:

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

    我在这里创建一个示例:https://github.com/ali59a/tap-and-slide-in-a-UISlider

    【讨论】:

    • 感谢您编辑我的问题的标题,但它与问题不匹配:正如我所写,我希望能够触摸任何地方,然后能够在不(!)抬起手指的情况下滚动(不是水龙头)!这是我已经做过的,但不幸的是我必须再次抬起手指才能使用滑动来更改值。
    • 似乎不起作用。 UISlider 没有看到任何识别器,但是如果我将它添加到具有 UIView 类的超级视图中 - 一切都很好。有什么想法吗?
    【解决方案4】:

    这是我对上面的修改:

    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】:

      添加 Ali AB. 答案的 swift 版本,

      @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】:

        我没有检查 David Williams answer,但我会发布我的解决方案,以防有人正在寻找其他方法。

        斯威夫特 4

        首先创建一个自定义 UISlider,以便它也能检测到栏上的触摸:

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

        (不要忘记在故事板上将您的滑块设置为这个 CustomSlider)

        显示滑块的视图控制器的 on viewDidLoad:

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

        (这仅用于在移动滑块时暂停播放器)

        然后,在您的 UISlider 操作中:

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

        在你的“sliderTap”选择器方法上:

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

        建议:开始播放前设置播放器“currentTime”:

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

        【讨论】:

          【解决方案7】:

          更新了 tsji10draSwift 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)
              }
          }
          

          【讨论】:

            【解决方案8】:

            我的解决方法很简单:

            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】:

              我为包含minimummaximumValueImagesUISlider 子类完成了@DWilliames 解决方案。

              此外,我在trackArea 之外的区域(即minimummaximumValueImage 周围的区域)实现了用户触摸功能。触摸这些区域会移动滑块/以间隔更改值。

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

              【讨论】:

                【解决方案10】:

                要扩展 Khang Azun-for swift 5 的答案,请将以下内容放入 UISlider 自定义类中:

                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
                }
                

                【讨论】:

                • 这不考虑可能存在的任何最小或最大值图像,这意味着如果您确实有最小/最大图像,则不会点击滑块的开头或结尾一直跳到媒体的开头或结尾。您可能会认为 UISlider.trackRect(forBounds:) 可用于获取滑块的边界而无需其最小/最大图像,但 Apple 的文档明确指出不应直接调用它。我想您可以获得最小/最大图像并明确计算滑块边界。
                【解决方案11】:

                冒着被 iOS 纯社区责骂的风险……

                这是从 David Williams Answer 转换而来的 Xamarin iOS C# 解决方案。

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

                【讨论】:

                  【解决方案12】:

                  来自苹果,

                  https://developer.apple.com/forums/thread/108317

                  现在这在 iOS 10 和 iOS 11 上运行良好。您可以像往常一样滑动,感谢上面的代码,您可以点击滑块,它会自动滑动。但是在 iOS 12 中这不起作用。您必须强制触摸它才能让 Tap 工作

                  【讨论】:

                    【解决方案13】:

                    这是我的有效解决方案:

                    import UIKit
                    
                    class CustomSlider: UISlider {
                        
                        override init(frame: CGRect) {
                            super.init(frame: frame)
                            setupView()
                        }
                        
                        required init?(coder aDecoder: NSCoder) {
                            super.init(coder: aDecoder)
                            setupView()
                        }
                        
                        
                        private func setupView() {
                            addTapGesture()
                        }
                        
                        private func addTapGesture() {
                            let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
                            addGestureRecognizer(tap)
                        }
                        
                        @objc private func handleTap(_ sender: UITapGestureRecognizer) {
                            let location = sender.location(in: self)
                            let percent = minimumValue + Float(location.x / bounds.width) * maximumValue
                            setValue(percent, animated: true)
                            sendActions(for: .valueChanged)
                        }
                    }
                    

                    【讨论】:

                      【解决方案14】:

                      这适用于 iOS 13.6 和 14.0
                      无需添加手势,只需像这样覆盖 beginTracking 函数:

                      @objc
                      private func sliderTapped(touch: UITouch) {
                          let point = touch.location(in: self)
                          let percentage = Float(point.x / self.bounds.width)
                          let delta = percentage * (self.maximumValue - self.minimumValue)
                          let newValue = self.minimumValue + delta
                          if newValue != self.value {
                              value = newValue
                              sendActions(for: .valueChanged)
                          }
                      }
                      
                      override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
                          sliderTapped(touch: touch)
                          return true
                      }
                      

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2020-02-29
                        • 1970-01-01
                        • 2020-09-20
                        • 2017-10-22
                        相关资源
                        最近更新 更多