Обработка сенсорных событий в дочернем UIScrollView

Я показываю серию изображений в UIScrollView. Я очень хочу повторить приложение Фотографии.

Моя текущая архитектура:

  • Родительский UIScrollView с размером контента, который достаточно широк для количества страниц x + некоторое дополнительное пространство для полей между изображениями.
  • Каждое изображение содержится в UIImageView.
  • Каждый UIImageView содержится в своем собственном UIScrollview, которые затем являются подпредставлениями родительского UIScrollView.

    Таким образом, у меня есть ряд UIScrollViews в родительском UIScrollView.

    В родительском UIScrollView включена подкачка страниц, поэтому я могу без проблем прокручивать страницу на страницу.

    Проблема в том, как плавно перемещаться по увеличенному изображению. Я переопределил viewForZoomingInScrollView метод для возврата соответствующего UIImageView, когда пользователь вводит / выводит. Я переопределил scrollViewDidEndZooming метод для установки родительского представления canCancelContentTouches собственность на NO если масштаб увеличения больше 1.

    Таким образом, пользователи могут перемещаться по изображению. Однако они должны удерживать палец на мгновение, чтобы преодолеть небольшую задержку, которую имеет родительский вид прокрутки, прежде чем отправлять события касания в подпредставления. Кроме того, как только пользователь выполняет панорамирование одного изображения, следующие / предыдущие изображения не попадают в видимую область, когда пользователь достиг границы текущего изображения.

    Есть идеи?

    Благодарю.

  • 4 ответа

    Решение

    Мне пришлось сделать аналогичную настройку, но я в основном все написал на заказ. Я не уверен, как вы собираетесь обойти проблему "передачи" сенсорных событий от дочернего UIScrollView к родительскому UISscrollView, когда вы достигнете края. Вы можете попытаться переопределить UITouchesBegan:withEvent: в родительском UIScrollView и создать дамп непосредственно для дочернего элемента. Удачи!

    Ура! Я попытался подойти к проблеме только с одним UIScrollView, и я думаю, что нашел решение.

    Прежде чем пользователь начнет масштабирование (в viewForZoomingInScrollView:), Я переключаю представление прокрутки в режим масштабирования (удаляет все дополнительные страницы, сбрасывает размер содержимого и смещение). Когда пользователь уменьшает масштаб 1,00 (в scrollViewDidEndZooming:withView:atScale:), Я переключаюсь обратно на просмотр страниц (добавление всех страниц назад, настройка размера и смещения контента).

    Вот код простого контроллера представления, который делает именно это. Этот пример переключает, увеличивает и перемещает три больших UIImageViews.

    Обратите внимание, что все, что нужно, - это один контроллер представления с несколькими функциями, нет необходимости создавать подкласс UIScrollView или что-то в этом роде.

    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 <UIScrollViewDelegate> {
        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
    

    Пример запускаемого проекта доступен по адресу http://github.com/andreyvit/ScrollingMadness/ (если вы не используете Git, просто нажмите здесь кнопку "Загрузить"). README доступен там, объясняя, почему код был написан таким, какой он есть.

    (Пример проекта также иллюстрирует, как масштабировать представление прокрутки программно, и имеет класс ZoomScrollView, который инкапсулирует решение для этого. Это классный класс, но не требуется для этого трюка. Если вам нужен пример, который не использует ZoomScrollView, вернуться на несколько коммитов в истории коммитов.)

    PS Для полноты картины есть TTScrollView - UIScrollView, переопределенный с нуля. Это часть великой и знаменитой библиотеки Three20. Мне не нравится, что он чувствует к пользователю, но он делает реализацию пейджинга / прокрутки / масштабирования очень простой.

    PPS Настоящее приложение Photo от Apple имеет код pre-SDK и использует классы pre-SDK. Можно обнаружить два класса, производных от UIScrollView до SDK-варианта внутри фреймворка PhotoLibrary, однако не сразу понятно, что они делают (и они делают довольно много). Я легко могу поверить, что этот эффект раньше было труднее достичь в период до SDK.

    Это код Андрея с переводом C# для разработчиков monotouch... Сначала вам нужно отредактировать свой файл xib. Вставьте 3 контроллера вида и создайте розетки, такие как _page1, _page2, _mainpage.. и свяжите эти розетки с представлениями. обратите внимание, что вы должны ссылаться на просмотр представления контроллера на _mainpage view. (Извините за мой английский)

        public partial class Test_Details_Controller : UIViewController
    {
        private UIPageControl _pageCont;
    
        private UIScrollView _scView;
        private Object[] _pageViews;
        private int _currentPageIndex;        
        private bool _rotationInProgress;
    
    
        void InitializeAfterLoad ()
        { 
    
            this.Title = "Test";
    
            this._pageCont = CreatePageControll();
    
        }
    
        private UIPageControl CreatePageControll()
        {
            UIPageControl pageControll = new UIPageControl( new RectangleF( 146,348, 38, 20 ) );
            pageControll.BackgroundColor = UIColor.Red;
            pageControll.Alpha = 0.7f;
    
            return pageControll;
        }
    
        private void UpdatePageControll(UIPageControl cont, int current, int pages, UIView showed)
        {
            cont.CurrentPage = current;
            cont.Pages = pages;
            cont.UpdateCurrentPageDisplay();
    
            UIPageControl.AnimationsEnabled = true;
            UIPageControl.BeginAnimations(string.Empty, this.Handle);
            cont.Frame = new RectangleF(showed.Frame.Location.X 
                                            , cont.Frame.Location.Y , pageSize().Width, cont.Frame.Height);         
            UIPageControl.CommitAnimations();
    
        }
    
        private UIView loadViewForPage(int pageIndex){
            UIView _view = null;
            switch ( pageIndex ) {
            case 1:
                _view = this._page1;
            break;
            case 2:
                _view = this._page2;
            break;
            default:
                _view = this._page1;
            break;
            }
            return _view;
        }
    
        private int numberOfPages(){
            return (int)this._pageViews.Count();
        }
    
        private UIView viewForPage( int pageIndex ){
            UIView pageView;
            if(this._pageViews.ElementAt( pageIndex ) == null)
            {
                pageView = loadViewForPage( pageIndex );
                _pageViews[ pageIndex ] = pageView;
            }
            else{
                pageView = (UIView)_pageViews[ pageIndex ];
            }
    
            _scView.AddSubview( pageView );
    
            return pageView;                
        }
    
        private SizeF pageSize(){
            return this._scView.Frame.Size;
        }
    
        private bool isPageLoaded( int pageIndex ){
            return this._pageViews.ElementAt( pageIndex ) != null;
        }
    
        private void layoutPage( int pageIndex ){
    
            SizeF pageSize = this.pageSize();
            ((UIView)this._pageViews[pageIndex]).Frame = new RectangleF( pageIndex * pageSize.Width,0, pageSize.Width, pageSize.Height );
            this.viewForPage( pageIndex );  
    
        }
    
        private void loadView(){
            this._scView = new UIScrollView();
            this._scView.Delegate = new ScViewDelegate( this );
            this._scView.PagingEnabled = true;
            this._scView.ShowsHorizontalScrollIndicator = false;
            this._scView.ShowsVerticalScrollIndicator = false;
            this._scView.Layer.BorderWidth = 2;
            this._scView.AddSubview( _pageCont );
            this.View = this._scView;
    
        }
    
        public override void ViewDidLoad ()
        {
            base.ViewDidLoad ();
            InitializeAfterLoad ();
            this._pageViews = new Object[]{ _page1, _page2 };
            this.loadView();    
        }           
    
        private void currentPageIndexDidChange(){
            this.layoutPage( _currentPageIndex );
    
            if(_currentPageIndex+1 < this.numberOfPages()){
                this.layoutPage( _currentPageIndex + 1 );
            }
            if(_currentPageIndex >0){
                this.layoutPage( _currentPageIndex - 1 );
            }
    
            this.UpdatePageControll( _pageCont, _currentPageIndex, this.numberOfPages(), ((UIView)this._pageViews[_currentPageIndex]) );
            this._scView.BringSubviewToFront( _pageCont );
    
    
            this.NavigationController.Title = string.Format( "{0} of {1}", _currentPageIndex + 1, this.numberOfPages() );
        }
    
        private void layoutPages(){
            SizeF pageSize = this.pageSize();
            this._scView.ContentSize = new SizeF( this.numberOfPages() * pageSize.Width, pageSize.Height );
            // move all visible pages to their places, because otherwise they may overlap
            for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
                if(this.isPageLoaded( pageIndex ))
                    this.layoutPage( pageIndex );
            }
        }
    
        public override void ViewWillAppear (bool animated)
        {
            this.layoutPages();
            this.currentPageIndexDidChange();
            this._scView.ContentOffset = new PointF( _currentPageIndex * this.pageSize().Width, 0 );
        }
    
        class ScViewDelegate : UIScrollViewDelegate
        {
            Test_Details_Controller id;
            public ScViewDelegate ( Test_Details_Controller id )
            {
                this.id = id;
            }
            public override void Scrolled (UIScrollView scrollView)
            {
    
                if(id._rotationInProgress)
                    return;// UIScrollView layoutSubviews code adjusts contentOffset, breaking our logic
    
                SizeF pageSize = id.pageSize();
                int newPageIndex = ((int)id._scView.ContentOffset.X + (int)pageSize.Width / 2) / (int)pageSize.Width;
                if( newPageIndex == id._currentPageIndex )
                    return;
    
                id._currentPageIndex = newPageIndex;
                id.currentPageIndexDidChange();
            }
        }
    
        public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation)
        {
            return toInterfaceOrientation != UIInterfaceOrientation.PortraitUpsideDown;
        }
    
        public override void WillRotate (UIInterfaceOrientation toInterfaceOrientation, double duration)
        {           
            _rotationInProgress = true;
            // hide other page views because they may overlap the current page during animation
            for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
                if(this.isPageLoaded( pageIndex ))
                    this.viewForPage( pageIndex ).Hidden = ( pageIndex != _currentPageIndex );
            }
        }
        public override void WillAnimateRotation (UIInterfaceOrientation toInterfaceOrientation, double duration)
        {
        // resize and reposition the page view, but use the current contentOffset as page origin
        // (note that the scrollview has already been resized by the time this method is called)
            SizeF pageSize = this.pageSize();
            UIView pageView = this.viewForPage( _currentPageIndex );
            this.viewForPage( _currentPageIndex ).Frame = new RectangleF( this._scView.ContentOffset.X, 0, pageSize.Width, pageSize.Height );           
        }
        public override void DidRotate (UIInterfaceOrientation fromInterfaceOrientation)
        {
            base.DidRotate (fromInterfaceOrientation);
    
            // adjust frames according to the new page size - this does not cause any visible changes
            this.layoutPages();
            this._scView.ContentOffset = new PointF( _currentPageIndex * this.pageSize().Width, 0 );
    
            //unhide
            for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
                if( this.isPageLoaded( pageIndex ) )
                    this.viewForPage( pageIndex ).Hidden = false;
            }
    
            _rotationInProgress = false;
        }               
    
        public override void DidReceiveMemoryWarning ()
        {
            //SuperHandle = DidReceiveMemoryWarning();
            if(this._pageViews != null)
            {
                // unload non-visible pages in case the memory is scarse
                for (int pageIndex = 0; pageIndex < this.numberOfPages(); pageIndex++) {
                    if( pageIndex < _currentPageIndex - 1 || pageIndex > _currentPageIndex + 1 )
                        if( this.isPageLoaded(pageIndex) ){
                            UIView pageview = (UIView)this._pageViews[ pageIndex ];
                            this._pageViews[ pageIndex ] = null;
                            pageview.RemoveFromSuperview();
                        }
                }
            }
        }
        public override void ViewDidUnload ()
        {
            this._pageViews = null;
            this._scView = null;
        }       
    }
    

    У меня есть пример проекта, и он отлично работает.

    https://code.google.com/p/uiscrollview-touch-events/

    Другие вопросы по тегам