カテゴリー : 2011年 10月4日

UIScrollViewでズームを行ったときに常に対象を中央にする


難しいことを解決するには、手を付けやすいところから一歩ずつ。ということで、スクロールビューでズームを行ったときに、対象を常に中央に表示するサンプルコードを。

iOSでは、スクロール機能を提供するUIScrollViewがかなり便利。スクロールだけでなく、ズームも一緒に行う事ができる。ただ、UIScrollViewのズームは、そのまま使うと左上固定で拡大縮小されてしまう。これは違和感がある。画面の中心を基準に拡大縮小してほしいところ。

これをできるだけ簡単にやってみた。

ポイントは2つある。1つは、UIScrollViewのzooming viewとして、表示されているUIImageViewを使う。つまり、viewForZoomingInScrollView:の返り値として、UIImageViewのインスタンスを返す。これはまぁ、なんとなく分かるでしょ。で、気づかなかったのが、UIScrollViewはズーミングを行っているときは、zooming viewのサイズをcontentSizeとして使うらしい。つまり、zooming viewを使っている場合は、contentSizeを明示的に設定する必要は無い。つーか、設定したらまずいかも。

2つめは、UIImageViewを常に中央に表示するために、UIImageViewのframeのoriginを変更する。UIScrollViewのcontentOffsetじゃないよ。あくまでUIImageViewのframeを直接動かす。ただし、このときframeのsizeを変更しては行けない。sizeはUIScrollViewが変更してくれるので、それを使う。

ということで、それを実現してみたのが、次のコード。まずは初期化処理。UIScrollViewとUIImageViewを作成する。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Create window
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

    // Create scroll view
    _scrollView = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
    _scrollView.maximumZoomScale = 4.0f;
    _scrollView.delegate = self;
    [self.window addSubview:_scrollView];

    // Create image view
    _imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"test.png"]];
    [_scrollView addSubview:_imageView];

    // Update image view
    [self _updateImageViewSize];
    [self _updateImageViewOrigin];

    // Show window
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];

    return YES;
}

UIScrollViewをインスタンス化して設定。UIImageViewをインスタンス化して設定。そして、UIImageViewのサイズと座標を設定する。それぞれ_updateImvewViewSizeと_updateImageOriginというメソッドを用意した。_updateImageViewSizeは、UIImageViewを縦幅いっぱいまたは横幅いっぱいまで広げるためのもの。_updateImageOriginは、それを画面中央に表示するために移動するためのものだ。

- (void)_updateImageViewSize
{
    // Get image size
    CGSize  imageSize;
    imageSize = _imageView.image.size;

    // Decide image view size
    CGRect  bounds;
    CGRect  rect;
    bounds = _scrollView.bounds;
    rect.origin = CGPointZero;
    if (imageSize.width / imageSize.height > CGRectGetWidth(bounds) / CGRectGetHeight(bounds)) {
        rect.size.width = CGRectGetWidth(bounds);
        rect.size.height = floor(imageSize.height / imageSize.width * CGRectGetWidth(rect));
    }
    else {
        rect.size.height = CGRectGetHeight(bounds);
        rect.size.width = imageSize.width / imageSize.height * CGRectGetHeight(rect);
    }

    // Set image view frame
    _imageView.frame = rect;
}

- (void)_updateImageViewOrigin
{
    // Get image view frame
    CGRect  rect;
    rect = _imageView.frame;

    // Get scroll view bounds
    CGRect  bounds;
    bounds = _scrollView.bounds;

    // Compare image size and bounds
    rect.origin = CGPointZero;
    if (CGRectGetWidth(rect) < CGRectGetWidth(bounds)) {
        rect.origin.x = floor((CGRectGetWidth(bounds) - CGRectGetWidth(rect)) * 0.5f);
    }
    if (CGRectGetHeight(rect) < CGRectGetHeight(bounds)) {
        rect.origin.y = floor((CGRectGetHeight(bounds) - CGRectGetHeight(rect)) * 0.5f);
    }

    // Set image view frame
    _imageView.frame = rect;
}

あとは、UIScrollViewDelegateメソッドを実装する。viewForZoomingInScrollView:でUIImageViewを返す。そして、スクロールビューがズームされるたびに呼び出されるscrollViewDidZoom:メソッドで、_updateImageViewOriginを呼び出す。これにより、常にUIImageViewが中央に表示される事が担保される。この呼び出しが、この手法の最大のポイントだね。

- (UIView*)viewForZoomingInScrollView:(UIScrollView*)scrollView
{
    return _imageView;
}

- (void)scrollViewDidZoom:(UIScrollView*)scrollView
{
    // Update image view origin
    [self _updateImageViewOrigin];
}

これでできるよん。

ここまでのソースコード:ScrollTest.zip

ビューの回転難しい


あー、もう、ビューの回転って難しいな!UIViewControllerを使ったビューの回転のプログラミングを書いているんだけど、これが何回やっても上手にできなくて。

単純に回転するだけならいい。最も単純な回転とは、回転するとそれにあわせて中のビューの大きさが外部のビューの大きさとピッタリ同じに変わってしまう場合。

これが、回転しても比率を保とうとすると、めんどくさくなってくる。縦と横とでスケールを変えなくてはいけない。さらに、縦と横とで表示する画面を変えると、さらにめんどくさくなってくる。たとえば、縦だと1ページ、横だと2ページの見開きにする、って場合ね。

これに、スクロールビューをからめて拡大縮小も可能にすると、もう何が何だか。zoomScaleを考慮しないといけないし、更新のタイミングも回転時だけではなくてズーム時になるし。

いっつも試行錯誤してしまう。充分に検討して、パターン化できるといいんだけど。