UISlider and UIScrollView

2019-01-21 12:03发布

I have a UISlider as part of a view that is loaded into a UIScrollView with paging enabled. I've noticed an unexpected behavior. If the user tries to use the slider quickly (i.e. press and move) it "activates" the scroll view, causing the page to switch. However, if your press and hold for a second the slider "activates" and you can then adjust the slider value. This behavior is undesirable.

What is the best way to make the UISlider responsive when loaded into a UIScrollView? I've thought about adding a "blocker" view that just eats up touch events that is placed under the slider, but not sure if this is the best way to go about it.

7条回答
Anthone
2楼-- · 2019-01-21 12:08

most upvoted comment code in Swift 3 :)

import Foundation

class UISliderForScrollView:UISlider {
    var thumbRect:CGRect {
        let trackRect = self.trackRect(forBounds: bounds)
        return thumbRect(forBounds: bounds, trackRect: trackRect, value: value)
    }

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if thumbRect.contains(point) {
            return super.hitTest(point, with: event)
        } else {
            return superview?.hitTest(point, with: event)
        }
    }
}
查看更多
太酷不给撩
3楼-- · 2019-01-21 12:10

You can subclass UIScrollView and override the method - (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view as follows:

- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view {
    if ([view isKindOfClass:[UISlider class]]) {
        UITouch *touch = [[event allTouches] anyObject];
        CGPoint location = [touch locationInView:view];
        CGRect thumbRect;
        UISlider *mySlide = (UISlider*) view;
        CGRect trackRect = [mySlide trackRectForBounds:mySlide.bounds];
        thumbRect = [mySlide thumbRectForBounds:mySlide.bounds trackRect:trackRect value:mySlide.value];
        if (CGRectContainsPoint(thumbRect, location))
            return YES;
    }
    return NO;
}

Also Set yourScrollView.delaysContentTouches = NO; property for the scrollview.

查看更多
Anthone
4楼-- · 2019-01-21 12:12

there's no need for a hit test on the UIScrollView side. since the delay is set by the UIScrollView itself. subclassing and implementing a custom hitTest:withEvent: won't help since it's still triggered with delay.

i searched hours for an elegant solution to this, since i wanted to simulate apple's own volumeslider in the ios application switcher.

the trick:

yourScrollView.delaysContentTouches = NO;

unfortunately this disables events along the UISliders track, so for this part your UIScrollView won't trigger any touchevents because they are caught by the slider first.

to pass touchevents other than those which are in the UISliders thumb rect you have to subclass UISlider and add the following:

// get the location of the thumb
- (CGRect)thumbRect 
{
   CGRect trackRect = [self trackRectForBounds:self.bounds];
   CGRect thumbRect = [self thumbRectForBounds:self.bounds
                                  trackRect:trackRect
                                      value:self.value];
   return thumbRect;
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{   
   CGRect thumbFrame = [self thumbRect];

      // check if the point is within the thumb
   if (CGRectContainsPoint(thumbFrame, point))
   {
      // if so trigger the method of the super class
      NSLog(@"inside thumb");
      return [super hitTest:point withEvent:event];
   }
   else
   {
      // if not just pass the event on to your superview
      NSLog(@"outside thumb");
      return [[self superview] hitTest:point withEvent:event];
   }
}
查看更多
在下西门庆
5楼-- · 2019-01-21 12:17

Have your scroll view detect touches over the region occupied by your slider (override hitTest:withEvent:, you may need to subclass UIScrollView). If a touch over said region is detected, tell your scroll view to immediately pass the touch to the slider.

查看更多
孤傲高冷的网名
6楼-- · 2019-01-21 12:23

My problem was a superset of this issue - I've got UISliders inside UITableViewCells and the whole UITableView is a page inside a UIScrollView. The sliders were wreaking havoc on the interactions with the other two and the subclassing solutions did not work. Here's what I came up with that's working great: send notifications when the sliders move and have the UITableView and UIScrollView disableScrolling on during this time. Note in the picture below: my sliders are horizontal, my tableview is vertical, and my UIScrollView has horizontal pages.

UISlider in a UITableView in a UIScrollView

The UITableViewCell picks up events for the programmatically-created slider:

self.numberSlider = [[UISlider alloc] init];
[self.numberSlider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged];
[self.numberSlider addTarget:self action:@selector(sliderTouchDown:) forControlEvents:UIControlEventTouchDown];
[self.numberSlider addTarget:self action:@selector(sliderTouchUp:) forControlEvents:UIControlEventTouchUpInside];
[self.numberSlider addTarget:self action:@selector(sliderTouchUp:) forControlEvents:UIControlEventTouchUpOutside];

For the purposes of this tutorial, we only care about touchDown and Up:

- (void)sliderTouchDown:(UISlider *)sender
{
    [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFY_SLIDER_TOUCH_BEGAN object:nil];
}

- (void)sliderTouchUp:(UISlider *)sender
{
    [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFY_SLIDER_TOUCH_ENDED object:nil];
}

Now, we catch in these notifications in both the UITableView (note that the tableview is in a VC but I'm sure this would work if you subclassed):

- (void)viewDidLoad
{
    // other stuff
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sliderTouchDown:) name:NOTIFY_SLIDER_TOUCH_BEGAN object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sliderTouchUp:) name:NOTIFY_SLIDER_TOUCH_ENDED object:nil];
}

- (void)sliderTouchDown:(NSNotification *)notify
{
    self.treatmentTableView.scrollEnabled = NO;
}

- (void)sliderTouchUp:(NSNotification *)notify
{
    self.treatmentTableView.scrollEnabled = YES;
}

and the UIScrollView (same as above, enclosed in a VC):

- (void)viewDidLoad
{
    // other stuff

    // Register for slider notifications
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(disableScrolling:) name:NOTIFY_SLIDER_TOUCH_BEGAN object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(enableScrolling:) name:NOTIFY_SLIDER_TOUCH_ENDED object:nil];
}

- (void)disableScrolling:(NSNotification *)notify
{
    self.scrollView.scrollEnabled = NO;
}

- (void)enableScrolling:(NSNotification *)notify
{
    self.scrollView.scrollEnabled = YES;
}

I'd love to hear a more elegant solution, but this one definitely gets the job done. When you use a slider, the table and scrollviews hold still and when you click outside the sliders, the tableview and scrollviews move as expected. Also - notice that I could use non-subclassed instances of all 3 components in this solution. Hope this helps someone!

查看更多
劳资没心,怎么记你
7楼-- · 2019-01-21 12:25

While creating an audio player I had the exact problem with a sliderView and it is added to a scrollView , and whenever I touched the sliderView , the scrollView used to get the touch and instead of the sliderView , the scrollView moved . To avoid this , I disabled the srcolling of the scrollView when the user touched the sliderView and otherwise the scrolling is enabled .

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    UIView *result = [super hitTest:point withEvent:event] ;
    if (result == sliderView)
    {
        scrollView.scrollEnabled = NO ;
    }
    else
    {
        scrollView.scrollEnabled = YES ;
    }

    return result ;

}
查看更多
登录 后发表回答