【问题标题】:Disable bounce effect in UIPageViewController [duplicate]在 UIPageViewController 中禁用反弹效果 [重复]
【发布时间】:2014-04-01 20:09:07
【问题描述】:

我已经实现了一个包含两个页面的UIPageViewController。在最右边的页面上,我可以向右滑动,然后将页面向后拉,这样当我释放时,它会弹回来。当我向左滑动时,左侧页面上也会发生同样的事情。 (弹跳就像您到达 safari 页面底部时发生的情况)

有没有办法禁用反弹效果?谢谢!

【问题讨论】:

  • 你的 UIPageViewController 的view 是什么类?如果是 UIScrollView(或其子类),您可以在视图上将 bounces 属性设置为 NO
  • 据我了解,UIPageViewController实现了一个UIScrollView,但是类本身还是UIPageViewController。
  • 不是控制器本身——控制器的view,它是UIViewController 上的一个属性,它是UIPageViewController 的超类。
  • 类是UIPageViewControllerContentView。

标签: ios iphone uipageviewcontroller bounce


【解决方案1】:

到目前为止,没有一个答案真正完全有效。他们都失败的边缘情况是这样的:

  1. 滚动到第 2 页。
  2. 用一根手指向第 1 页拖动。
  3. 将第二根手指放在屏幕上并向第 1 页拖动。
  4. 抬起第一根手指。
  5. 重复直到拖过第 0 页。

在这种情况下,到目前为止我看到的每个解决方案都超出了第 0 页的范围。核心问题是底层 API 已损坏,并开始报告相对于第 0 页的内容偏移量,而没有调用我们的回调让我们知道它显示的是不同的页面。在整个过程中,API 仍然声称正在显示第 1 页,即使它实际上是在第 0 页,也可以向第 0 页显示。

这个设计缺陷的解决方法非常难看,但它是:

@property (weak,nonatomic) UIPageControl *pageControl;
@property (nonatomic,assign) BOOL shouldBounce;
@property (nonatomic,assign) CGFloat lastPosition;
@property (nonatomic,assign) NSUInteger currentIndex;
@property (nonatomic,assign) NSUInteger nextIndex;

- (void)viewDidLoad {

    [super viewDidLoad];

...

    self.shouldBounce = NO;

    for (id testView in self.pageController.view.subviews) {
        UIScrollView *scrollView = (UIScrollView *)testView;
        if ([scrollView isKindOfClass:[UIScrollView class]]) {
            scrollView.delegate = self;
            // scrollView.bounces = self.shouldBounce;
        }
    }
}

- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController{

    return (NSInteger)self.currentIndex;
}

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers{

    id controller = [pendingViewControllers firstObject];
    self.nextIndex = [viewControllers indexOfObject:controller];
}

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed{

    if(completed) {
        // At this point, we can safely query the API to ensure
        // that we are fully in sync, just in case.
        self.currentIndex = [viewControllers indexOfObject:[pageViewController.viewControllers objectAtIndex:0]];
        [self.pageControl setCurrentPage:self.currentIndex];
    }

    self.nextIndex = self.currentIndex;

}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    /* The iOS page view controller API is broken.  It lies to us and tells us
       that the currently presented view hasn't changed, but under the hood, it
       starts giving the contentOffset relative to the next view.  The only
       way to detect this brain damage is to notice that the content offset is
       discontinuous, and pretend that the page changed.
     */
    if (self.nextIndex > self.currentIndex) {
        /* Scrolling forwards */

        if (scrollView.contentOffset.x < (self.lastPosition - (.9 * scrollView.bounds.size.width))) {
            self.currentIndex = self.nextIndex;
            [self.pageControl setCurrentPage:self.currentIndex];
        }
    } else {
        /* Scrolling backwards */

        if (scrollView.contentOffset.x > (self.lastPosition + (.9 * scrollView.bounds.size.width))) {
            self.currentIndex = self.nextIndex;
            [self.pageControl setCurrentPage:self.currentIndex];
        }
    }

    /* Need to calculate max/min offset for *every* page, not just the first and last. */
    CGFloat minXOffset = scrollView.bounds.size.width - (self.currentIndex * scrollView.bounds.size.width);
    CGFloat maxXOffset = (([viewControllers count] - self.currentIndex) * scrollView.bounds.size.width);

    NSLog(@"Page: %ld NextPage: %ld X: %lf MinOffset: %lf MaxOffset: %lf\n", (long)self.currentIndex, (long)self.nextIndex,
          (double)scrollView.contentOffset.x,
          (double)minXOffset, (double)maxXOffset);

    if (!self.shouldBounce) {
        CGRect scrollBounds = scrollView.bounds;
        if (scrollView.contentOffset.x <= minXOffset) {
            scrollView.contentOffset = CGPointMake(minXOffset, 0);
            // scrollBounds.origin = CGPointMake(minXOffset, 0);
        } else if (scrollView.contentOffset.x >= maxXOffset) {
            scrollView.contentOffset = CGPointMake(maxXOffset, 0);
            // scrollBounds.origin = CGPointMake(maxXOffset, 0);
        }
        [scrollView setBounds:scrollBounds];
    }
    self.lastPosition = scrollView.contentOffset.x;
}

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    /* Need to calculate max/min offset for *every* page, not just the first and last. */
    CGFloat minXOffset = scrollView.bounds.size.width - (self.currentIndex * scrollView.bounds.size.width);
    CGFloat maxXOffset = (([viewControllers count] - self.currentIndex) * scrollView.bounds.size.width);

    if (!self.shouldBounce) {
        if (scrollView.contentOffset.x <= minXOffset) {
            *targetContentOffset = CGPointMake(minXOffset, 0);
        } else if (scrollView.contentOffset.x >= maxXOffset) {
            *targetContentOffset = CGPointMake(maxXOffset, 0);
        }
    }
}

基本上,它记录每个滚动事件的偏移量。如果滚动位置在与滚动方向相反的方向上移动了一个不可能的距离(我随意选择了屏幕宽度的 90%),则代码假定 iOS 对我们说谎,并且表现得好像过渡正确完成,将偏移量视为相对于新页面而不是旧页面。

【讨论】:

  • 工作得很好。谢谢
  • 你能解释一下 Apple 是如何让它在他们自己的应用程序中运行的吗?如果他们应用了与您类似的 hack,他们应该意识到它有多么错误。
  • 我的猜测是他们要么不使用页面控制器,要么他们有一些 SPI 可以让他们正确禁用它。
  • 我已经实现了这一点,但是如果之后你让错误发生然后尝试滑动到下一页它会弹回你所在的页面而不是移动到下一页。
  • 您可能需要在 scrollViewWillBeginDragging 中重置某些状态,但我不确定是什么。
【解决方案2】:

这是一个简单的解决方案

fileprivate var currentIndex = 0
fileprivate var lastPosition: CGFloat = 0


override func viewDidLoad() {
    super.viewDidLoad()
            
    for view in view.subviews {
        if view is UIScrollView {
            (view as! UIScrollView).delegate =  self
            break
        }
    }
 }


func pageViewController(_ pageViewController: UIPageViewController,
                        didFinishAnimating finished: Bool,
                        previousViewControllers: [UIViewController],
                        transitionCompleted completed: Bool) {
    
    if completed {
        // Get current index
        let pageContentViewController = pageViewController.viewControllers![0]
        currentIndex = orderedViewControllers.index(of: pageContentViewController)!
    }
}



func scrollViewDidScroll(_ scrollView: UIScrollView) {
    self.lastPosition = scrollView.contentOffset.x
    
    if (currentIndex == orderedViewControllers.count - 1) && (lastPosition > scrollView.frame.width) {
        scrollView.contentOffset.x = scrollView.frame.width
        return
        
    } else if currentIndex == 0 && lastPosition < scrollView.frame.width {
        scrollView.contentOffset.x = scrollView.frame.width
        return
    }
}

【讨论】:

  • 这对我有用,谢谢!
  • 请给我这个Objective C的解决方案。
  • 虽然这段代码可以工作,但是在随机滑动之后......页面控制器突然发生弹跳。
【解决方案3】:

这是用 Swift 实现的 @SahilS 解决方案。

不过对我来说似乎是个问题。

 override func viewDidLoad() {
        super.viewDidLoad()


      for view in view.subviews {
        if view is UIScrollView {
          (view as! UIScrollView).delegate =  self

                  break
        }
      }



extension PageViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource {

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {

        guard let viewControllerIndex = orderedViewControllers?.index(of: viewController) else {
            return nil
        }

        let previousIndex = viewControllerIndex - 1

        guard previousIndex >= 0 else {
            return nil
        }

        guard (orderedViewControllers?.count)! > previousIndex else {
            return nil
        }
        print("in viewControllerBefore")
        return orderedViewControllers?[previousIndex]
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {

        guard let viewControllerIndex = orderedViewControllers?.index(of: viewController) else {
            return nil
        }

        let nextIndex = viewControllerIndex + 1
        let orderedViewControllersCount = orderedViewControllers?.count

        guard orderedViewControllersCount != nextIndex else {
            return nil
        }

        print("in viewControllerAfter")
        return orderedViewControllers?[nextIndex]
    }
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {


        if completed {
          // Get current index
          let pageContentViewController = pageViewController.viewControllers![0]

          currentIndex = (orderedViewControllers?.index(of: pageContentViewController))!
        }
      self.nextIndex = self.currentIndex

    }

    func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
        print("willTransitionTo")

      let controller = pendingViewControllers.first

      if let i = viewControllers?.index(of: controller!) {
        print("Jason is at index \(i)")
        self.currentIndex = i
      } else {
        print("Jason isn't in the array")
      }

    }


  func presentationIndex(for pageViewController: UIPageViewController) -> Int {
    return self.currentIndex
  }

}





extension PageViewController: UIScrollViewDelegate {

  func scrollViewDidScroll(_ scrollView: UIScrollView) {
    /* The iOS page view controller API is broken.  It lies to us and tells us
     that the currently presented view hasn't changed, but under the hood, it
     starts giving the contentOffset relative to the next view.  The only
     way to detect this brain damage is to notice that the content offset is
     discontinuous, and pretend that the page changed.
     */

    let poop = self.lastPosition + (0.9 * scrollView.bounds.size.width)
    print("poop is \(poop)")


    if (self.nextIndex > self.currentIndex) {
      /* Scrolling forwards */

      if (scrollView.contentOffset.x < (self.lastPosition - (0.9 * scrollView.bounds.size.width))) {
        self.currentIndex = self.nextIndex;
      }
    } else {
      /* Scrolling backwards */

      if (scrollView.contentOffset.x > (self.lastPosition + (0.9 * scrollView.bounds.size.width))) {
        self.currentIndex = self.nextIndex;
      }
    }

    /* Need to calculate max/min offset for *every* page, not just the first and last. */
    let minXOffset = scrollView.bounds.size.width - (CGFloat(self.currentIndex) * scrollView.bounds.size.width);
    let maxXOffset = (CGFloat(((viewControllers?.count)! - self.currentIndex)) * scrollView.bounds.size.width)

    if (!self.shouldBounce) {
      let scrollBounds = scrollView.bounds;
      if (scrollView.contentOffset.x <= minXOffset) {
        scrollView.contentOffset = CGPoint(x: minXOffset, y: 0)
      } else if (scrollView.contentOffset.x >= maxXOffset) {
        scrollView.contentOffset = CGPoint(x: maxXOffset, y: 0)
      }
      scrollView.bounds = scrollBounds
    }
    self.lastPosition = scrollView.contentOffset.x

  }
  func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    var scrollOffset = targetContentOffset.pointee


    let minXOffset = scrollView.bounds.size.width - (CGFloat(self.currentIndex) * scrollView.bounds.size.width);
    let maxXOffset = (CGFloat(((viewControllers?.count)! - self.currentIndex)) * scrollView.bounds.size.width)

    if (!self.shouldBounce) {
      if (scrollView.contentOffset.x <= minXOffset) {
        scrollOffset = CGPoint(x: minXOffset, y: 0)

      } else if (scrollView.contentOffset.x >= maxXOffset) {
        scrollOffset = CGPoint(x: maxXOffset, y: 0)

      }
    }


  }


}

【讨论】:

    【解决方案4】:
    for (UIView *view in self.pageViewController.view.subviews ) {
        if ([view isKindOfClass:[UIScrollView class]]) {
            UIScrollView *scroll = (UIScrollView *)view;
            scroll.bounces = NO;
        }
    }
    

    不过这有点 hacky —— 这可能就是 the original answer here 被否决的原因。

    【讨论】:

    • 很抱歉回复晚了,但实现这个只是完全禁用了滚动。有什么想法吗?
    • @user2551625 好吧,你真的需要只为 2 页使用 PageViewController 吗?无论如何,只有当用户想要滑动第一页和最后一页时,您才应该禁用反弹。只需检查 viewControllerBeforeViewControllerviewControllerAfterViewController 数据源方法中的当前页面索引(pageIndex = 0 和 pageIndex = pages.count 对应)
    【解决方案5】:

    我做到了。

    如果要禁用UIPageViewController对第一页(左侧弹跳)和最后一页(右侧弹跳)的弹跳效果,思路是实现底层scrollView的delegate:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
    

    要实现委托,可以

    1. 循环UIPageViewController.view的子视图,找到UIScrollView设置它的delegate
    2. 子类 UIPageViewController

    scrollViewDidScroll 的实现是在用户超出范围,如下所示:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
        if (NO == isPageToBounce) {
            if (_currentPage == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width) {
                scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
            }
            if (_currentPage == [listVCs count]-1 && scrollView.contentOffset.x > scrollView.bounds.size.width) {
                scrollView.contentOffset = CGPointMake(scrollView.bounds.size.width, 0);
            }
        }
        // more
    }
    

    scrollViewWillEndDragging 的实现是为了解决用户在第一页从左向右快速滑动时,第一页不会在左边弹跳(由于上面的函数),但会由于(可能)滑动速度而在右侧反弹。最后,当反弹回来时,UIPageViewController 将触发翻页到第二页(这当然不是预期的)。

    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
    {
        if (NO == isPageToBounce) {
            if (_currentPage == 0 && scrollView.contentOffset.x <= scrollView.bounds.size.width) {
                velocity = CGPointZero;
                *targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0);
            }
            if (_currentPage == [listVCs count]-1 && scrollView.contentOffset.x >= scrollView.bounds.size.width) {
                velocity = CGPointZero;
                *targetContentOffset = CGPointMake(scrollView.bounds.size.width, 0);
            }
        }
    }
    

    【讨论】:

    • 在 iOS 8 上试过这个 - 但它不起作用
    • 也在 iOS 上尝试过,它确实有效。 @Charpak 你为什么不解释你的问题而不仅仅是写一个笼统的声明。
    【解决方案6】:

    也是诀窍,但我认为处理 pageViewController.view.subviews 数组更好

    1) 把你的 UIPageViewController 放在 UIScrollView 上

    2) 内容宽度必须大于滚动视图宽度,例如 10.0f

    self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width + 10.0f, self.scrollView.frame.size.height);
    

    3) 设置滚动视图反弹 - 否

    4) 设置滚动视图委托,并实现

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
        scrollView.contentOffset = CGPointMake(0.0, 0.0);
    }
    

    【讨论】:

    • 您建议的方式不是解决方案。向上和向下滚动操作有时不起作用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-16
    • 2018-11-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-01
    相关资源
    最近更新 更多