0

通常、scrollView のコンテンツ ビューは長方形です。しかし、長方形ではないことを実装したいと思います....たとえば.... ここに画像の説明を入力

黄色のグリッド 6 が現在の位置です...フローの例を次に示します。

  1. ユーザーが左にスワイプします。(左にスクロールできません) 現在: 6.
  2. ユーザーが右にスワイプします。(右にスクロール) 現在: 7.
  3. ユーザーが下にスワイプします。(下にスクロール) 現在: 8.
  4. ユーザーが下にスワイプします。(下にスクロールできません) 現在: 8.

ご覧のとおり、scrollView の Content ビューは四角形ではありません。それを実装する方法についてのアイデアはありますか?ありがとう。

4

2 に答える 2

1

これは、実装する興味深いアイデアです。うまくいくかもしれないいくつかのアプローチを考えることができます。私は 1 つを試しました。私の実装は、こちらの github リポジトリにあります。ダウンロードして、自分で試してみてください。

私のアプローチは、法線を使用し、デリゲートのメソッド (および他のいくつかのデリゲート メソッド)UIScrollViewでそれを制約することです。contentOffsetscrollViewDidScroll:

予選

まず、ページ サイズの定数が必要になります。

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 ページの途中で停止することができ、targetContentOffset4 ページの境界に設定していたにもかかわらず、スクロール ビューは 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 リポジトリからダウンロードして、自分で試すことができます。

以上です、皆さん!

于 2012-11-14T07:06:36.403 に答える
0

UIScrollViewをページモードで使用していることを前提としています(スワイプして新しい画面全体を表示します)。

少しジゲリーポケリーであなたはあなたが望む効果を達成することができます。

秘訣は、現在表示している正方形が何であれ、UIScrollViewを構成して、表示されている中央のビューと、スクロールできる周囲のビューのみがスクロールビューに(正しいオフセットで)追加されるようにすることです。 。また、スクロール可能なコンテンツのサイズ(および現在のオフセット)が正しく設定されていることを確認して、コンテンツがない方向にスクロールしないようにする必要があります。

例:現在、正方形6を表示しているとします。その時点で、スクロールビューには、正しい相対オフセットで4、5、6、および7の4つのビューが追加されます。また、スクロールビューのコンテンツサイズを2x2の正方形のサイズに相当するように設定します。これにより、下または左へのスクロール(タイルがない場合)は防止されますが、正しい方向へのスクロールは可能になります。

を検出するには、代理人が必要ですscrollViewDidEndDecelerating:。その場合、新しい場所について、上記のようにビュー、コンテンツオフセット、およびコンテンツサイズを設定する必要があります。

于 2012-11-13T10:59:39.460 に答える