如何在iPhone的滚动视图上实现非矩形滚动内容?

时间:2012-11-13 06:46:06

标签: iphone objective-c uiscrollview

通常,scrollView的内容视图是一个矩形。但我想实现那不是一个矩形....例如.... enter image description here

黄色,网格6是当前位置......以下是示例流程:

  1. 用户向左滑动。 (不能向左滚动)当前:6。
  2. 用户向右滑动。 (向右滚动)当前:7。
  3. 用户向下滑动。 (向下滚动)当前:8。
  4. 用户向下滑动。 (无法向下滚动)当前:8。
  5. 如您所见,scrollView的Content视图不是矩形。有关如何实施它的任何想法?感谢。

2 个答案:

答案 0 :(得分:1)

这是一个有趣的想法。我可以想到一些可行的方法。我尝试了一个,你可以在my github repository here找到我的实现。下载并亲自试用。

我的方法是使用普通UIScrollView,并在委托的contentOffset方法(以及其他一些委托方法)中约束其scrollViewDidScroll:

预赛

首先,我们需要页面大小的常量:

static const CGSize kPageSize = { 200, 300 };

我们需要一个数据结构来保存页面网格中的当前x / y位置:

typedef struct {
    int x;
    int y;
} MapPosition;

我们需要声明我们的视图控制器符合UIScrollViewDelegate协议:

@interface ViewController () <UIScrollViewDelegate>

@end

我们需要实例变量来保存页面的网格(地图),该网格中的当前位置以及滚动视图:

@implementation ViewController {
    NSArray *map_;
    MapPosition mapPosition_;
    UIScrollView *scrollView_;
}

初始化地图

我的地图只是一个数组数组,每个可访问页面都有一个字符串名称,在不可访问的网格位置有[NSNull null]。我将从我的视图控制器的init方法初始化地图:

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
        [self initMap];
    }
    return self;
}

- (void)initMap {
    NSNull *null = [NSNull null];
    map_ = @[
    @[ @"1", null, @"2"],
    @[ @"3", @"4", @"5" ],
    @[ null, @"6", @"7" ],
    @[ null, null, @"8" ],
    ];
    mapPosition_ = (MapPosition){ 0, 0 };
}

设置视图层次结构

我的视图层次结构如下所示:

  • 顶级视图(灰色背景)
    • 滚动视图(透明背景)
      • 内容视图(棕褐色背景)
        • 第1页视图(带阴影的白色)
        • 第2页视图(带阴影的白色)
        • 第3页视图(带阴影的白色)

通常我会在xib中设置我的一些视图,但由于在stackoverflow答案中很难显示xib,我将在代码中完成所有操作。因此,在我的loadView方法中,我首先设置了一个将在滚动视图中生效的“内容视图”。内容视图将包含每个页面的子视图:

- (void)loadView {
    UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, [map_[0] count] * kPageSize.width, map_.count * kPageSize.height)];
    contentView.backgroundColor = [UIColor colorWithHue:0.1 saturation:0.1 brightness:0.9 alpha:1];
    [self addPageViewsToContentView:contentView];

然后我将创建我的滚动视图:

    scrollView_ = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, kPageSize.width, kPageSize.height)];
    scrollView_.delegate = self;
    scrollView_.bounces = NO;
    scrollView_.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin
                                    | UIViewAutoresizingFlexibleRightMargin
                                    | UIViewAutoresizingFlexibleTopMargin
                                    | UIViewAutoresizingFlexibleBottomMargin);

我将内容视图添加为滚动视图的子视图,并设置滚动视图的内容大小和偏移量:

    scrollView_.contentSize = contentView.frame.size;
    [scrollView_ addSubview:contentView];
    scrollView_.contentOffset = [self contentOffsetForCurrentMapPosition];

最后,我创建了我的顶级视图,并将滚动视图作为子视图提供:

    UIView *myView = [[UIView alloc] initWithFrame:scrollView_.frame];
    [myView addSubview:scrollView_];
    myView.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1];
    self.view = myView;
}

以下是我如何为当前地图位置和任何地图位置计算滚动视图的内容偏移量:

- (CGPoint)contentOffsetForCurrentMapPosition {
    return [self contentOffsetForMapPosition:mapPosition_];
}

- (CGPoint)contentOffsetForMapPosition:(MapPosition)position {
    return CGPointMake(position.x * kPageSize.width, position.y * kPageSize.height);
}

要为每个可访问的页面创建内容视图的子视图,我遍历地图:

- (void)addPageViewsToContentView:(UIView *)contentView {
    for (int y = 0, yMax = map_.count; y < yMax; ++y) {
        NSArray *mapRow = map_[y];
        for (int x = 0, xMax = mapRow.count; x < xMax; ++x) {
            id page = mapRow[x];
            if (![page isKindOfClass:[NSNull class]]) {
                [self addPageViewForPage:page x:x y:y toContentView:contentView];
            }
        }
    }
}

以下是我创建每个页面视图的方法:

- (void)addPageViewForPage:(NSString *)page x:(int)x y:(int)y toContentView:(UIView *)contentView {
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectInset(CGRectMake(x * kPageSize.width, y * kPageSize.height, kPageSize.width, kPageSize.height), 10, 10)];
    label.text = page;
    label.textAlignment = NSTextAlignmentCenter;
    label.layer.shadowOffset = CGSizeMake(0, 2);
    label.layer.shadowRadius = 2;
    label.layer.shadowOpacity = 0.3;
    label.layer.shadowPath = [UIBezierPath bezierPathWithRect:label.bounds].CGPath;
    label.clipsToBounds = NO;
    [contentView addSubview:label];
}

约束滚动视图&#39; s contentOffset

当用户移动手指时,我想阻止滚动视图显示其内容中不包含页面的区域。每当滚动视图滚动(通过更新其contentOffset)时,它会向其委托发送scrollViewDidScroll:,因此如果scrollViewDidScroll:超出范围,我可以实现contentOffset重置- (void)scrollViewDidScroll:(UIScrollView *)scrollView { CGPoint contentOffset = scrollView_.contentOffset;

contentOffset

首先,我想约束 CGPoint constrainedContentOffset = [self contentOffsetByConstrainingMovementToOneDimension:contentOffset]; ,以便用户只能水平或垂直滚动​​,而不是对角滚动:

contentOffset

接下来,我想约束 constrainedContentOffset = [self contentOffsetByConstrainingToAccessiblePoint:constrainedContentOffset]; ,以便它只显示包含页面的滚动视图的部分:

contentOffset

如果我的约束修改了 if (!CGPointEqualToPoint(contentOffset, constrainedContentOffset)) { scrollView_.contentOffset = constrainedContentOffset; } ,我需要告诉滚动视图:

contentOffset

最后,我根据(约束) mapPosition_ = [self mapPositionForContentOffset:constrainedContentOffset]; } 更新了我对当前地图位置的看法:

contentOffset

以下是我如何计算给定- (MapPosition)mapPositionForContentOffset:(CGPoint)contentOffset { return (MapPosition){ roundf(contentOffset.x / kPageSize.width), roundf(contentOffset.y / kPageSize.height) }; } 的地图位置:

- (CGPoint)contentOffsetByConstrainingMovementToOneDimension:(CGPoint)contentOffset {
    CGPoint baseContentOffset = [self contentOffsetForCurrentMapPosition];
    CGFloat dx = contentOffset.x - baseContentOffset.x;
    CGFloat dy = contentOffset.y - baseContentOffset.y;
    if (fabsf(dx) < fabsf(dy)) {
        contentOffset.x = baseContentOffset.x;
    } else {
        contentOffset.y = baseContentOffset.y;
    }
    return contentOffset;
}

这里是我如何将运动限制在水平或垂直方向并防止对角线移动:

contentOffset

这是我如何限制- (CGPoint)contentOffsetByConstrainingToAccessiblePoint:(CGPoint)contentOffset { return [self isAccessiblePoint:contentOffset] ? contentOffset : [self contentOffsetForCurrentMapPosition]; } 仅限于有页面的地方:

- (BOOL)isAccessiblePoint:(CGPoint)point {
    CGFloat x = point.x / kPageSize.width;
    CGFloat y = point.y / kPageSize.height;
    return [self isAccessibleMapPosition:(MapPosition){ floorf(x), floorf(y) }]
        && [self isAccessibleMapPosition:(MapPosition){ ceilf(x), ceilf(y) }];
}

决定一个点是否可访问原来是一个棘手的问题。仅将点的坐标环绕到最近的潜在页面中心并查看该圆形点是否代表实际页面是不够的。例如,这将允许用户从第1页向左/向右拖动,显示第1页和第2页之间的空白区域,直到第1页离屏幕一半。我们需要将点向下向上舍入到潜在的页面中心,并查看两个圆点是否代表有效页面。以下是:

- (BOOL)isAccessibleMapPosition:(MapPosition)p {
    if (p.y < 0 || p.y >= map_.count)
        return NO;
    NSArray *mapRow = map_[p.y];
    if (p.x < 0 || p.x >= mapRow.count)
        return NO;
    return ![mapRow[p.x] isKindOfClass:[NSNull class]];
}

检查地图位置是否可访问意味着检查它是否在网格的范围内,并且实际上该页面位于该位置:

pagingEnabled

强制滚动视图停留在页面边界

如果您不需要强制滚动视图停留在页面边界,则可以跳过其余部分。我上面描述的所有内容都可以在没有其余部分的情况下工作。

我尝试在滚动视图上设置CGFloat以强制它停留在页面边界,但它没有可靠地工作,因此我必须通过实现更多的委托方法来强制执行它。

我们需要一些实用功能。第一个函数只需static int sign(CGFloat value) { return value > 0 ? 1 : -1; } ,如果正数则返回1,否则返回-1:

static int directionForVelocity(CGFloat velocity) {
    static const CGFloat kVelocityThreshold = 0.1;
    return fabsf(velocity) < kVelocityThreshold ? 0 : sign(velocity);
}

第二个函数需要一个速度。如果速度的绝对值低于阈值,则返回0。否则,它返回速度的符号:

targetContentOffset

现在我可以实现当用户停止拖动时滚动视图调用的一个委托方法。在此方法中,我将滚动视图的- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { if (fabsf(velocity.x) > fabsf(velocity.y)) { *targetContentOffset = [self contentOffsetForPageInHorizontalDirection:directionForVelocity(velocity.x)]; } else { *targetContentOffset = [self contentOffsetForPageInVerticalDirection:directionForVelocity(velocity.y)]; } } 设置为用户滚动方向的最近页面边界:

isAccessibleMapPosition:

这是我如何在水平方向上找到最近的页面边界。它依赖于scrollViewDidScroll:方法,我之前已经为- (CGPoint)contentOffsetForPageInHorizontalDirection:(int)direction { MapPosition newPosition = (MapPosition){ mapPosition_.x + direction, mapPosition_.y }; return [self isAccessibleMapPosition:newPosition] ? [self contentOffsetForMapPosition:newPosition] : [self contentOffsetForCurrentMapPosition]; } 使用了这个方法:

- (CGPoint)contentOffsetForPageInVerticalDirection:(int)direction {
    MapPosition newPosition = (MapPosition){ mapPosition_.x, mapPosition_.y + direction };
    return [self isAccessibleMapPosition:newPosition] ? [self contentOffsetForMapPosition:newPosition] : [self contentOffsetForCurrentMapPosition];
}

以下是我如何在垂直方向找到最近的页面边界:

targetContentOffset

我在测试中发现设置targetContentOffset没有可靠地强制滚动视图停留在页面边界上。例如,在iOS 5模拟器中,我可以从第5页向右/向左拖动,在第4页中途停止,即使我将UIScrollViewDelegate设置为第4页的边界,滚动视图也会只需在屏幕中间停止使用4/5边界滚动。

要解决此错误,我们必须再实施两个- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { if (!decelerate) { [scrollView_ setContentOffset:[self contentOffsetForCurrentMapPosition] animated:YES]; } } 方法。当触摸结束时调用这个:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    CGPoint goodContentOffset = [self contentOffsetForCurrentMapPosition];
    if (!CGPointEqualToPoint(scrollView_.contentOffset, goodContentOffset)) {
        [scrollView_ setContentOffset:goodContentOffset animated:YES];
    }
}

滚动视图停止减速时调用此项:

{{1}}

结束

正如我在开始时所说的那样,您可以从my github repository下载我的测试实现并亲自试用。

总而言之,伙计们!

答案 1 :(得分:0)

我假设您在分页模式下使用UIScrollView(滑动以显示整个新屏幕)。

有点jiggery-pokery你可以达到你想要的效果。

诀窍是确保您正在查看的任何方格,您已配置UIScrollView,以便只有可见的中央视图和您可以滚动的周围视图也被添加到滚动视图中(并且正确的偏移)。您还必须确保正确设置可滚动内容的大小(以及当前偏移量),以防止向无法向内容滚动的方向滚动。

示例:假设您当前正在查看方块6。此时,您的滚动视图将添加4个视图:4,5,6和7,在正确的相对偏移中。并且您将滚动视图的内容大小设置为等于2 x 2平方大小。这样可以防止向下滚动或向左滚动(没有切片的地方)但允许向正确的方向滚动。

您需要代表检测scrollViewDidEndDecelerating:。在这种情况下,您必须为新位置设置如上所述的视图,内容偏移和内容大小。