How to implement UIScrollView with 1000+ subviews?


I am struggling with writing portion of an app which should behave like the native iphone photo app. Looked at iphone sdk app development book from Orielly which gave an example code for implementing this so-called page-flicking. The code there first created all subviews and then hide/unhide them. At a given time only 3 subviews are visible rest are hidden. After much effort I got it working with app which at that time had only around 15 pages.

我正在努力编写应用程序的部分应该像本机iphone照片应用程序。看看Orielly的iphone sdk app开发书,它给出了实现这个所谓的页面轻弹的示例代码。那里的代码首先创建所有子视图,然后隐藏/取消隐藏它们。在给定时间,只有3个子视图可见隐藏。经过很多努力,我得到了它与应用程序,当时只有大约15页。

As soon as I added 300 pages, it became clear that there are performance/memory issues with that approach of pre-allocating so many subviews. Then I thought may be for my case I should just allocate 3 subviews and instead of hide/unhide them. May be I should just remove/add subviews at runtime. But can't figure out whether UIScrollView can dynamically update contents. For example, at the start there are 3 frames at different x-offsets ( 0, 320, 640 ) from the screen as understood by UIScrollView. Once user moves to 3rd page how do I make sure I am able to add 4th page and remove 1st page and yet UIScrollView doesn't get confused ?

一旦我添加了300页,就会发现预分配这么多子视图的方法存在性能/内存问题。然后我想可能是我的情况我应该只分配3个子视图而不是隐藏/取消隐藏它们。可能是我应该在运行时删除/添加子视图。但无法弄清楚UIScrollView是否可以动态更新内容。例如,在开始时,UIScrollView可以理解,屏幕上有不同x偏移(0,320,640)的3帧。一旦用户移动到第3页,我如何确保我能够添加第4页并删除第1页但是UIScrollView不会混淆?

Hoping there is a standard solution to this kind of problem...can someone guide ?

希望有这种问题的标准解决方案......有人可以指导吗?

3 个解决方案

#1


7  

UIScrollView is just a subclass of UIView so it's possible to add and remove subviews at runtime. Assuming you have fixed width photos (320px) and there are 300 of them, then your main view would be 300 * 320 pixels wide. When creating the scroll view, initialize the frame to be that wide.

UIScrollView只是UIView的子类,因此可以在运行时添加和删除子视图。假设您有固定宽度的照片(320像素)并且有300张,那么您的主视图将是300 * 320像素宽。创建滚动视图时,将帧初始化为该宽度。

So the scroll view's frame would have the dimensions (0, 0) to (96000, 480). Whenever you are adding a subview, you will have to change it's frame so it fits in the correct position in its parent view.

因此滚动视图的框架将具有尺寸(0,0)到(96000,480)。无论何时添加子视图,都必须更改其框架,使其适合父视图中的正确位置。

So let's say, we are adding the 4th photo to the scroll view. It's frame would be from (960, 480) to (1280, 480). That is easily to calculate, if you can somehow associate an index with each picture. Then use this to calculate the picture's frame where indexes start at 0:

所以我们假设我们将第4张照片添加到滚动视图中。它的帧是从(960,480)到(1280,480)。如果你能以某种方式将索引与每张图片相关联,这很容易计算。然后用它来计算索引从0开始的图片框架:

Top-Left -- (320 * (index - 1), 0)

to

Bottom-Right -- (320 * index, 480)

Removing the first picture/subview should be easy. Keep an array of the 3 subviews currently on-screen. Whenever you are adding a new subview to the screen, also add it to the end of this array, and then remove the first subview in this array from the screen too.

删除第一张图片/子视图应该很容易。保持当前屏幕上的3个子视图的数组。每当您向屏幕添加新的子视图时,也将其添加到此数组的末尾,然后从屏幕中删除此数组中的第一个子视图。

#2


15  

Following what has been said, you can show thousand of elements using only a limited amount of resources (and yes, it's a bit of a Flyweight pattern indeed). Here's some code that might help you do what you want.

按照上述说法,你可以只使用有限的资源来展示数千个元素(是的,它确实是一个Flyweight模式)。这里有一些代码可以帮助你做你想做的事情。

The UntitledViewController class just contains a UIScroll and sets itself as its delegate. We have an NSArray with NSString instances inside as data model (there could be potentially thousands of NSStrings in it), and we want to show each one in a UILabel, using horizontal scrolling. When the user scrolls, we shift the UILabels to put one on the left, another on the right, so that everything is ready for the next scroll event.

UntitledViewController类只包含一个UIScroll并将自己设置为其委托。我们有一个NSArray,里面有NSString实例作为数据模型(其中可能有数千个NSStrings),我们希望使用水平滚动在UILabel中显示每个。当用户滚动时,我们移动UILabels将一个放在左边,另一个放在右边,这样一切都准备好进行下一个滚动事件。

Here's the interface, rather straightforward:

这是界面,相当简单:

@interface UntitledViewController : UIViewController <UIScrollViewDelegate>
{
@private
    UIScrollView *_scrollView;

    NSArray *_objects;

    UILabel *_detailLabel1;
    UILabel *_detailLabel2;
    UILabel *_detailLabel3;
}

@end

And here's the implementation for that class:

这是该类的实现:

@interface UntitledViewController ()
- (void)replaceHiddenLabels;
- (void)displayLabelsAroundIndex:(NSInteger)index;
@end

@implementation UntitledViewController

- (void)dealloc 
{
    [_objects release];
    [_scrollView release];
    [_detailLabel1 release];
    [_detailLabel2 release];
    [_detailLabel3 release];
    [super dealloc];
}

- (void)viewDidLoad 
{
    [super viewDidLoad];

    _objects = [[NSArray alloc] initWithObjects:@"first", @"second", @"third", 
                @"fourth", @"fifth", @"sixth", @"seventh", @"eight", @"ninth", @"tenth", nil];

    _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 460.0)];
    _scrollView.contentSize = CGSizeMake(320.0 * [_objects count], 460.0);
    _scrollView.showsVerticalScrollIndicator = NO;
    _scrollView.showsHorizontalScrollIndicator = YES;
    _scrollView.alwaysBounceHorizontal = YES;
    _scrollView.alwaysBounceVertical = NO;
    _scrollView.pagingEnabled = YES;
    _scrollView.delegate = self;

    _detailLabel1 = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 460.0)];
    _detailLabel1.textAlignment = UITextAlignmentCenter;
    _detailLabel1.font = [UIFont boldSystemFontOfSize:30.0];
    _detailLabel2 = [[UILabel alloc] initWithFrame:CGRectMake(320.0, 0.0, 320.0, 460.0)];
    _detailLabel2.textAlignment = UITextAlignmentCenter;
    _detailLabel2.font = [UIFont boldSystemFontOfSize:30.0];
    _detailLabel3 = [[UILabel alloc] initWithFrame:CGRectMake(640.0, 0.0, 320.0, 460.0)];
    _detailLabel3.textAlignment = UITextAlignmentCenter;
    _detailLabel3.font = [UIFont boldSystemFontOfSize:30.0];

    // We are going to show all the contents of the _objects array
    // using only these three UILabel instances, making them jump 
    // right and left, replacing them as required:
    [_scrollView addSubview:_detailLabel1];
    [_scrollView addSubview:_detailLabel2];
    [_scrollView addSubview:_detailLabel3];

    [self.view addSubview:_scrollView];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [_scrollView flashScrollIndicators];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self displayLabelsAroundIndex:0];
}

- (void)didReceiveMemoryWarning 
{
    // Here you could release the data source, but make sure
    // you rebuild it in a lazy-loading way as soon as you need it again...
    [super didReceiveMemoryWarning];
}

#pragma mark -
#pragma mark UIScrollViewDelegate methods

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    // Do some initialization here, before the scroll view starts moving!
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self replaceHiddenLabels];
}

- (void)displayLabelsAroundIndex:(NSInteger)index
{
    NSInteger count = [_objects count];
    if (index >= 0 && index < count)
    {
        NSString *text = [_objects objectAtIndex:index];
        _detailLabel1.frame = CGRectMake(320.0 * index, 0.0, 320.0, 460.0);
        _detailLabel1.text = text;
        [_scrollView scrollRectToVisible:CGRectMake(320.0 * index, 0.0, 320.0, 460.0) animated:NO];

        if (index < (count - 1))
        {
            text = [_objects objectAtIndex:(index + 1)];
            _detailLabel2.frame = CGRectMake(320.0 * (index + 1), 0.0, 320.0, 460.0);
            _detailLabel2.text = text;
        }

        if (index > 0)
        {
            text = [_objects objectAtIndex:(index - 1)];
            _detailLabel3.frame = CGRectMake(320.0 * (index - 1), 0.0, 320.0, 460.0);
            _detailLabel3.text = text;
        }
    }
}

- (void)replaceHiddenLabels
{
    static const double pageWidth = 320.0;
    NSInteger currentIndex = ((_scrollView.contentOffset.x - pageWidth) / pageWidth) + 1;

    UILabel *currentLabel = nil;
    UILabel *previousLabel = nil;
    UILabel *nextLabel = nil;

    if (CGRectContainsPoint(_detailLabel1.frame, _scrollView.contentOffset))
    {
        currentLabel = _detailLabel1;
        previousLabel = _detailLabel2;
        nextLabel = _detailLabel3;
    }
    else if (CGRectContainsPoint(_detailLabel2.frame, _scrollView.contentOffset))
    {
        currentLabel = _detailLabel2;
        previousLabel = _detailLabel1;
        nextLabel = _detailLabel3;
    }
    else
    {
        currentLabel = _detailLabel3;
        previousLabel = _detailLabel1;
        nextLabel = _detailLabel2;
    }

    currentLabel.frame = CGRectMake(320.0 * currentIndex, 0.0, 320.0, 460.0);
    currentLabel.text = [_objects objectAtIndex:currentIndex];

    // Now move the other ones around
    // and set them ready for the next scroll
    if (currentIndex < [_objects count] - 1)
    {
        nextLabel.frame = CGRectMake(320.0 * (currentIndex + 1), 0.0, 320.0, 460.0);
        nextLabel.text = [_objects objectAtIndex:(currentIndex + 1)];
    }

    if (currentIndex >= 1)
    {
        previousLabel.frame = CGRectMake(320.0 * (currentIndex - 1), 0.0, 320.0, 460.0);
        previousLabel.text = [_objects objectAtIndex:(currentIndex - 1)];
    }
}

@end

Hope this helps!

希望这可以帮助!

#3


6  

Many thanks to Adrian for his very simple and powerfull code sample. There was just one issue with this code : when the user made a "double scroll" (I.E. when he did not wait for the animation to stop and rescroll the scrollview again and again).

非常感谢Adrian的非常简单和强大的代码示例。这段代码只有一个问题:当用户进行“双滚动”时(I.E.,当他没有等待动画停止并一次又一次地重新滚动滚动视图时)。

In this case, the refresh for the position of the 3 subviews is effective only when the "scrollViewDidEndDecelerating" method is invoked, and the result is a delay before the apparition of the subviews on the screen.

在这种情况下,只有在调用“scrollViewDidEndDecelerating”方法时,3个子视图位置的刷新才有效,结果是在屏幕上显示子视图之前的延迟。

This can be easily avoided by adding few lines of code :

通过添加几行代码可以轻松避免这种情况:

in the interface, just add this :

在界面中,只需添加:

int refPage, currentPage;

in the implementation, initialize refPage and currentPage in the "viewDidLoad" method like this :

在实现中,在“viewDidLoad”方法中初始化refPage和currentPage,如下所示:

refpage = 0; 
curentPage = 0;

in the implementation, just add the "scrollViewDidScroll" method, like this :

在实现中,只需添加“scrollViewDidScroll”方法,如下所示:

- (void)scrollViewDidScroll:(UIScrollView *)sender{
    int currentPosition = floor(_scrollView.contentOffset.x);
    currentPage = MAX(0,floor(currentPosition / 340)); 
//340 is the width of the scrollview...
    if(currentPage != refPage)  { 
        refPage = currentPage;
        [self replaceHiddenLabels];
        }
    }

et voilà !

etvoilà!

Now, the subviews are correctly replaced in the correct positions, even if the user never stop the animation and if the "scrollViewDidEndDecelerating" method is never invoked !

现在,即使用户从未停止动画并且从不调用“scrollViewDidEndDecelerating”方法,也会在正确的位置正确替换子视图!


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
© 2014-2018 ITdaan.com 粤ICP备14056181号