Handling touch events within a child UIScrollView

前端 未结 4 674
青春惊慌失措
青春惊慌失措 2021-02-01 11:27

I\'m displaying a series of images in a UIScrollView. I pretty much want to replicate the Photos application.

My current architecture is:

  • A parent UIScrollView with
  • 4条回答
    •  悲哀的现实
      2021-02-01 11:36

      Yay! I tried to approach the problem with just one UIScrollView, and I think I found a solution.

      Before the user starts zooming (in viewForZoomingInScrollView:), I switch the scroll view into zooming mode (remove all additional pages, reset content size and offset). When the user zooms out to scale 1.00 (in scrollViewDidEndZooming:withView:atScale:), I switch back to paging view (add all pages back, adjust content size and offset).

      Here's the code of a simple view controller that does just that. This sample switches between, zooms and pans three large UIImageViews.

      Note that a single view controller with a handful of functions is all it takes, no need to subclass UIScrollView or something.

      typedef enum {
          ScrollViewModeNotInitialized,           // view has just been loaded
          ScrollViewModePaging,                   // fully zoomed out, swiping enabled
          ScrollViewModeZooming,                  // zoomed in, panning enabled
          ScrollViewModeAnimatingFullZoomOut,     // fully zoomed out, animations not yet finished
          ScrollViewModeInTransition,             // during the call to setPagingMode to ignore scrollViewDidScroll events
      } ScrollViewMode;
      
      @interface ScrollingMadnessViewController : UIViewController  {
          UIScrollView *scrollView;
          NSArray *pageViews;
          NSUInteger currentPage;
          ScrollViewMode scrollViewMode;
      }
      
      @end
      
      @implementation ScrollingMadnessViewController
      
      - (void)setPagingMode {
          NSLog(@"setPagingMode");
          if (scrollViewMode != ScrollViewModeAnimatingFullZoomOut && scrollViewMode != ScrollViewModeNotInitialized)
              return; // setPagingMode is called after a delay, so something might have changed since it was scheduled
          scrollViewMode = ScrollViewModeInTransition; // to ignore scrollViewDidScroll when setting contentOffset
      
          // reposition pages side by side, add them back to the view
          CGSize pageSize = scrollView.frame.size;
      
          NSUInteger page = 0;
          for (UIView *view in pageViews) {
              if (!view.superview)
                  [scrollView addSubview:view];
              view.frame = CGRectMake(pageSize.width * page++, 0, pageSize.width, pageSize.height);
          }
      
          scrollView.pagingEnabled = YES;
          scrollView.showsVerticalScrollIndicator = scrollView.showsHorizontalScrollIndicator = NO;
          scrollView.contentSize = CGSizeMake(pageSize.width * [pageViews count], pageSize.height);
          scrollView.contentOffset = CGPointMake(pageSize.width * currentPage, 0);
      
          scrollViewMode = ScrollViewModePaging;
      }
      
      - (void)setZoomingMode {
          NSLog(@"setZoomingMode");
          scrollViewMode = ScrollViewModeInTransition; // to ignore scrollViewDidScroll when setting contentOffset
      
          CGSize pageSize = scrollView.frame.size;
      
          // hide all pages besides the current one
          NSUInteger page = 0;
          for (UIView *view in pageViews)
              if (currentPage != page++)
                  [view removeFromSuperview];
      
          // move the current page to (0, 0), as if no other pages ever existed
          [[pageViews objectAtIndex:currentPage] setFrame:CGRectMake(0, 0, pageSize.width, pageSize.height)];
      
          scrollView.pagingEnabled = NO;
          scrollView.showsVerticalScrollIndicator = scrollView.showsHorizontalScrollIndicator = YES;
          scrollView.contentSize = pageSize;
          scrollView.contentOffset = CGPointZero;
      
          scrollViewMode = ScrollViewModeZooming;
      }
      
      - (void)loadView {
          CGRect frame = [UIScreen mainScreen].applicationFrame;
          scrollView = [[UIScrollView alloc] initWithFrame:frame];
          scrollView.delegate = self;
          scrollView.maximumZoomScale = 2.0f;
          scrollView.minimumZoomScale = 1.0f;
      
          UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"red.png"]];
          UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"green.png"]];
          UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"yellow-blue.png"]];
      
          // in a real app, you most likely want to have an array of view controllers, not views;
          // also should be instantiating those views and view controllers lazily
          pageViews = [[NSArray alloc] initWithObjects:imageView1, imageView2, imageView3, nil];
      
          self.view = scrollView;
      }
      
      - (void)setCurrentPage:(NSUInteger)page {
          if (page == currentPage)
              return;
          currentPage = page;
          // in a real app, this would be a good place to instantiate more view controllers -- see SDK examples
      }
      
      - (void)viewDidLoad {
          scrollViewMode = ScrollViewModeNotInitialized;
          [self setPagingMode];
      }
      
      - (void)viewDidUnload {
          [pageViews release]; // need to release all page views here; our array is created in loadView, so just releasing it
          pageViews = nil;
      }
      
      - (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
          [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(setPagingMode) object:nil];
          CGPoint offset = scrollView.contentOffset;
          NSLog(@"scrollViewDidScroll: (%f, %f)", offset.x, offset.y);
          if (scrollViewMode == ScrollViewModeAnimatingFullZoomOut && ABS(offset.x) < 1e-5 && ABS(offset.y) < 1e-5)
              // bouncing is still possible (and actually happened for me), so wait a bit more to be sure
              [self performSelector:@selector(setPagingMode) withObject:nil afterDelay:0.1];
          else if (scrollViewMode == ScrollViewModePaging)
              [self setCurrentPage:roundf(scrollView.contentOffset.x / scrollView.frame.size.width)];
      }
      
      - (UIView *)viewForZoomingInScrollView:(UIScrollView *)aScrollView {
          if (scrollViewMode != ScrollViewModeZooming)
              [self setZoomingMode];
          return [pageViews objectAtIndex:currentPage];
      }
      
      - (void)scrollViewDidEndZooming:(UIScrollView *)aScrollView withView:(UIView *)view atScale:(float)scale {
          NSLog(@"scrollViewDidEndZooming: scale = %f", scale);
          if (fabsf(scale - 1.0) < 1e-5) {
              if (scrollView.zoomBouncing)
                  NSLog(@"scrollViewDidEndZooming, but zoomBouncing is still true!");
      
              // cannot call setPagingMode now because scrollView will bounce after a call to this method, resetting contentOffset to (0, 0)
              scrollViewMode = ScrollViewModeAnimatingFullZoomOut;
              // however sometimes bouncing will not take place
              [self performSelector:@selector(setPagingMode) withObject:nil afterDelay:0.2];
          }
      }
      
      @end
      

      Runnable sample project is available at http://github.com/andreyvit/ScrollingMadness/ (if you don't use Git, just click Download button there). A README is available there, explaining why the code was written the way it is.

      (The sample project also illustrates how to zoom a scroll view programmatically, and has ZoomScrollView class that encapsulates a solution to that. It is a neat class, but is not required for this trick. If you want an example that does not use ZoomScrollView, go back a few commits in commit history.)

      P.S. For the sake of completeness, there's TTScrollView — UIScrollView reimplemented from scratch. It's part of the great and famous Three20 library. I don't like how it feels to the user, but it does make implementing paging/scrolling/zooming dead simple.

      P.P.S. The real Photo app by Apple has pre-SDK code and uses pre-SDK classes. One can spot two classes derived from pre-SDK variant of UIScrollView inside PhotoLibrary framework, however it is not immediately clear what they do (and they do quite a lot). I can easily believe this effect used to be harder to achieve in pre-SDK times.

    提交回复
    热议问题