通常,scrollView 的内容视图是一个矩形。但我想实现的不是矩形......例如......
黄色的 Grid 6 是当前位置...这是示例流程:
- 用户向左滑动。(无法向左滚动) 当前:6。
- 用户向右滑动。(向右滚动)当前:7。
- 用户向下滑动。(向下滚动)当前:8。
- 用户向下滑动。(无法向下滚动) 当前:8。
如您所见,scrollView 的 Content 视图不是矩形。关于如何实施它的任何想法?谢谢。
通常,scrollView 的内容视图是一个矩形。但我想实现的不是矩形......例如......
黄色的 Grid 6 是当前位置...这是示例流程:
如您所见,scrollView 的 Content 视图不是矩形。关于如何实施它的任何想法?谢谢。
这是一个有趣的想法。我能想到一些可行的方法。我尝试了一个,你可以在我的 github 存储库中找到我的实现。下载并亲自试用。
我的方法是使用 normal 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 };
}
我的视图层次结构将如下所示:
通常我会在 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];
}
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;
}
最后,我根据 (constrained) 更新我对当前地图位置的想法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];
}
决定一个点是否可访问是一个棘手的问题。仅将点的坐标四舍五入到最近的潜在页面中心并查看该四舍五入的点是否代表实际页面是不够的。例如,让用户从第 1 页向左/向右滚动,显示第 1 页和第 2 页之间的空白区域,直到第 1 页离开屏幕的一半。我们需要将该点向下和向上舍入到潜在的页面中心,并查看两个舍入点是否代表有效页面。就是这样:
- (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) }];
}
检查地图位置是否可访问意味着检查它是否在网格的边界内,并且该位置实际上有一个页面:
- (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
滚动视图以强制它在页面边界处停止,但它不能可靠地工作,所以我必须通过实现更多委托方法来强制它。
我们需要几个实用函数。第一个函数只接受 a CGFloat
,如果它是正数则返回 1 ,否则返回 -1 :
static int sign(CGFloat value) {
return value > 0 ? 1 : -1;
}
第二个函数采用速度。如果速度的绝对值低于阈值,则返回 0。否则,它返回速度的符号:
static int directionForVelocity(CGFloat velocity) {
static const CGFloat kVelocityThreshold = 0.1;
return fabsf(velocity) < kVelocityThreshold ? 0 : sign(velocity);
}
现在我可以实现当用户停止拖动时滚动视图调用的委托方法之一。在这个方法中,我将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
并不能可靠地强制滚动视图停留在页面边界上。例如,在 iOS 5 模拟器中,我可以从第 5 页向右/向左拖动,在中途停止到第 4 页,即使我设置targetContentOffset
到第 4 页的边界,滚动视图也会停止滚动到 4/5 边界在屏幕中间。
为了解决这个错误,我们必须实现另外两个UIScrollViewDelegate
方法。当触摸结束时调用这个:
- (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];
}
}
正如我一开始所说,您可以从我的 github 存储库下载我的测试实现并自己尝试一下。
就是这样,伙计们!
我假设您在分页模式下使用 UIScrollView(滑动以显示整个新屏幕)。
稍微玩一下,你就可以达到你想要的效果。
诀窍是确保您当前正在查看的任何正方形都配置了 UIScrollView 以便只有可见的中央视图和您也可以滚动的周围视图被添加到滚动视图(并且在正确的偏移量) . 您还必须确保正确设置可滚动内容的大小(和当前偏移量),以防止滚动到没有内容的方向。
示例:假设您当前正在查看广场 6。那时,您的滚动视图将只添加 4 个视图:4、5、6 和 7,在正确的相对偏移量中。并且您将滚动视图的内容大小设置为 2 x 2 正方形大小。这将防止向下或向左滚动(没有图块),但允许以正确的方向滚动。
您需要您的委托来检测scrollViewDidEndDecelerating:
. 在这种情况下,您必须如上所述为新位置设置视图、内容偏移量和内容大小。