これは、実装する興味深いアイデアです。うまくいくかもしれないいくつかのアプローチを考えることができます。私は 1 つを試しました。私の実装は、こちらの github リポジトリにあります。ダウンロードして、自分で試してみてください。
私のアプローチは、法線を使用し、デリゲートのメソッド (および他のいくつかのデリゲート メソッド)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];
}
スクロール ビューの制約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;
}
2 番目の関数は速度を受け取ります。速度の絶対値がしきい値を下回っている場合は 0 を返します。それ以外の場合は、速度の符号を返します。
static int directionForVelocity(CGFloat velocity) {
static const CGFloat kVelocityThreshold = 0.1;
return fabsf(velocity) < kVelocityThreshold ? 0 : sign(velocity);
}
これで、ユーザーがドラッグを停止したときにスクロール ビューが呼び出すデリゲート メソッドの 1 つを実装できるようになりました。このメソッドで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 の境界でスクロールを停止するだけでした。画面の真ん中。
このバグを回避するには、さらに 2 つの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 リポジトリからダウンロードして、自分で試すことができます。
以上です、皆さん!