簡易スライドビューア [6] 拡大中の回転に対応する

2010年9月19日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

拡大中に回転させるといろいろ問題があることがわかった。

拡大画像を回転させた時の問題


例えばこんな画像を拡大させたとする。
拡大中はこんな感じ。
この状態で回転させるとこうなる。
位置が左上にリセットされている。またこの状態で拡大画像をスクロールすることができない。これは回転に合わせて UIScrollView.contentSize を変更している為。

内部的にはこんなふうになっているのだろう。下図は画像が拡大された状態で白い点線内が表示されている(十字は中心がわかるように描いてある)。

これを回転させた時、表示領域が左上にリセットされている。
こうではなくて、画面の回転に合わせて画像の中心で回転させてやる必要がある。
するとこうなる。
UIとしてはこっちの方が感覚的で正しい。


ロジック


拡大中の画像を回転後もきちんと表示するには中心座標を押さえておくのがポイント。回転後に中心座標から contentOffset を決めることができて、それによって意図した位置で拡大画像を表示することができる。
contentOffset.x = center.x - w/2
contentOffset.y = center.y - h/2
なので回転前の中心座標をとっておいて、それを回転後に使えばいい。

ただ今回の場合、座標系自体が回転しているので話が複雑になる。
例えば左回転した場合、ビューの原点はこんな感じになる。
つまり回転前の中心座標を回転後の座標に変換する必要がある。

この回転は変換行列を使って求められる(と思う)が、今回のケースではもっと簡単(原始的な?)な方法がある。これは回転前の中心位置を座標としてではなく表示ビュー内の相対位置(比)として取っておき、回転後はこの比を使って新しい中心位置を割り出す。

以下に手順を示す。

1. 回転前

UIScrollView.contentOffsetより回転前のオフセット座標がわかる(oldContentOffset)。ここから回転前の中心座標が求められる。
oldCenter.x = oldContentOffset.x + oldContentSize.width/2.0
oldCenter.y = oldContentOffset.y + oldContentSize.height/2.0
さらに全体の大きさから相対位置(比)が求められる。
ratioX = oldCenter.x / oldSize.width
ratioY = oldCenter.y / oldSize.height

2. 回転後
先に求めた比を元に最終的に設定すべきUIScrollView.contentOffsetを割り出す。
まず回転後の中心位置を求める。
newCenter.x = ratioX * newSize.width
newCenter.y = ratioY * newSize.height
中心位置が分かればオフセット座標は簡単に求められる。
newContentOffset.x = newCenter.x - newContentSize.width/2.0
newContentOffset.y = newCenter.y - newContentSize.height/2.0
これを UIScrollView.contentOffetへ入れてやれば回転前の中心が中心にくるような表示となる。

この方法の利点は回転方向と無関係に計算ができること。一見ややこしいが分岐が無いのでコードはすっきりするはず。


実装


上記のロジックをプログラムに落としこんでみる。こんな感じ。
- (void)layoutScrollViews
{
 CGSize newSize = self.view.bounds.size;
 CGSize oldSize = previousScrollSize_;
 previousScrollSize_ = newSize;

 // save previous contentSize
 //--
 ImageScrollView* currentScrollView =
  [self.imageScrollViews objectAtIndex:kIndexOfCurrentScrollView];
 CGSize oldContentSize = currentScrollView.contentSize;
 CGPoint oldContentOffset = currentScrollView.contentOffset;

 CGFloat zoomScale = currentScrollView.zoomScale;

 // [A] calculate ratio (center / size)
 CGPoint oldCenter;
 oldCenter.x = oldContentOffset.x + oldSize.width/2.0;
 oldCenter.y = oldContentOffset.y + oldSize.height/2.0;

 CGFloat ratioW = oldCenter.x / oldContentSize.width;
 CGFloat ratioH = oldCenter.y / oldContentSize.height;

 
 // set new origin and size to imageScrollViews
 //--
 CGFloat x = (self.contentOffsetIndex-1) * newSize.width;
 for (ImageScrollView* scrollView in self.imageScrollViews) {
  scrollView.frame = CGRectMake(x, 0, newSize.width, newSize.height);
  CGSize contentSize;
  if (scrollView == currentScrollView) {
   contentSize.width  = newSize.width  * scrollView.zoomScale;
   contentSize.height = newSize.height * scrollView.zoomScale;
  } else {
   contentSize = newSize;
  }
  scrollView.contentSize = contentSize;
  x += newSize.width;
 }
 
  
 // [B] adjust current scroll view for zooming
 //--
 if (zoomScale > 1.0) {
  CGSize newContentSize = currentScrollView.contentSize;

  CGPoint newCenter;
  newCenter.x = ratioW * newContentSize.width;
  newCenter.y = ratioH * newContentSize.height;

  CGPoint newOffset;
  newContentOffset.x = newCenter.x - newSize.width /2.0;
  newContentOffset.y = newCenter.y - newSize.height/2.0;
  currentScrollView.contentOffset = newContentOffset;
 }
 
 // adjust content size and offset of base scrollView
  :
}
[A] で ratioX, ratioY を求め、[B]で newOffsetを求めている。なお回転中の処理では oldSize のみ求めることができないのでこれはメンバ変数を用意してとっておく。
@interface EasyGalleryViewController : UIViewController {

  :
 CGSize previousScrollSize_;
}
回転処理中に UIScrollView.frame.size で取得できるような記述をどこかでみたのだが、試してみるとシミュレーターでは横⇒縦のケースでは取得できなかった(回転後のサイズとなっていた)。


サンプル


さて実行してみよう。
拡大して
回転する。

出た。そのまま回転を続けても中心は変わらない(※端の方は表示領域の関係で補正される)。


ソースコード


GitHubからどうぞ。
EasyGallery at 2010-09-19 from xcatsan's iOS-Sample-Code - GitHub


- - - - -
たかが拡大時の位置補正、なんて高を括っていたら見事にハマってしまった。方法はともかく動いてよかった。


(追記) なおこのブログを書き終えた後で気がついたのだが実はこんなに大げさな計算はいらない。拡大したままの回転でのポイントは zoomScaleの扱い。
CGFloat x = (self.contentOffsetIndex-1) * newSize.width;
 for (ImageScrollView* scrollView in self.imageScrollViews) {
  scrollView.frame = CGRectMake(x, 0, newSize.width, newSize.height);
  CGSize contentSize;
  if (scrollView == currentScrollView) {
   contentSize.width  = newSize.width  * scrollView.zoomScale;
   contentSize.height = newSize.height * scrollView.zoomScale;
  } else {
   contentSize = newSize;
  }
  scrollView.contentSize = contentSize;
  x += newSize.width;
 }
回転処理中に contentSize を計算しているが、ここで *scrollView.zoomScale をいれればこれだけで済んでしまう(以前は入っていなかった)。これに気がつかないが為に延々と時間を費やしてしまった...。ただこの方法だと中心位置は微妙にずれる。計算した場合は中心位置がずれないのでそれを慰めに?良しとしよう。

座標変換(回転)

2010年9月18日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

覚え書き。

P0 を原点として、P1 を αだけ回転させた時の P2の座標は次の数式で求められる。
x2 = x0 + (x1-x0)cosα - (y1-y0)sinα
y2 = y0 + (x1-x0)sinα + (y1-y0)cosα

[参考情報]回転した座標値を求める

UIScrollView.contentSize も zoomScale倍される

2010年9月17日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: UIScrollView.contentOffset は zoomScale倍される

うっかりしていたのだが前回 contentSize を調査に含めていなかった。こちらも zoomScale倍される。まあ contentOffsetが zoomScale倍されるので当然といえば当然か。

以下、サンプルに contentSize を追加した結果。





ソースコード (GitHub)
xcatsan's iOS-Sample-Code at 2010-09-17 - GitHub

UIScrollView.contentOffset は zoomScale倍される

2010年9月16日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

UIScrollView を使った画像の拡大を処理していてどうもうまくいかないので調べてみるとこんなことがわかった。

UIScrollView.contentOffset は zoomScale倍される

例えば等倍(zoomScale==1)の状態で contentOffset=={100, 100} だったとすると、拡大して zoomScale==2 の場合、contentOffset=={200, 200}となる。つまり2倍になっている。


サンプル


こんな画像を用意して {100,100}を左上に来るように表示した場合、x1, x2, x3のそれぞれの倍率で contentOffset がどう変わるかを見てみた。


これを UIScrollView内に表示して「x1」「x2」「x3」を押すとそれぞれの倍率で拡大し、{100,100}が左上に来るようにしてみた。


コードはこんな感じ。
-(void)setZoomScale:(CGFloat)scale
{
    CGRect zoomRect;
    zoomRect.size.height = self.scrollView.frame.size.height / scale;
    zoomRect.size.width  = self.scrollView.frame.size.width  / scale;
 zoomRect.origin.x = 100.0;
    zoomRect.origin.y = 100.0;
 [self.scrollView zoomToRect:zoomRect animated:YES];
 
 self.contentOffsetLabel.text = NSStringFromCGPoint(self.scrollView.contentOffset);
}

- (IBAction)x1:(id)sender
{
 [self.scrollView setZoomScale:1.0 animated:YES];
 self.scrollView.contentOffset = CGPointMake(100, 100);
 self.contentOffsetLabel.text = NSStringFromCGPoint(self.scrollView.contentOffset);
}

- (IBAction)x2:(id)sender
{
 [self setZoomScale:2.0];
}

- (IBAction)x3:(id)sender
{
 [self setZoomScale:3.0];

}


実行してみよう。

「x1」を押すと等倍のまま表示。contentOffsetは {100,100}。
次に「x2」。contentOffsetが {200,200}になった。
同様に「x3」も contentOffsetが {300,300}と zoomScale倍されたものになった。


ソースコード


GitHubからどうぞ。

ScrollViewZooming at 2010-09-16 from xcatsan's iOS-Sample-Code - GitHub


参考情報


UIScrollView Class Reference

-[UITouch locationInView:] の座標

2010年9月15日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

locationInView: の引数に安易に nil を渡していたら画面回転時に痛い目にあったのでメモしておく。

タッチ位置の座標系

-[UITouch locationInView:] の引数を nil とすると Windowの座標系でタッチ位置が取得できる。

[参考情報] UITouch Class Reference より引用:

Parameters
view
The view object in whose coordinate system you want the touch located. A custom view that is handling the touch may specify self to get the touch location in its own coordinate system. Pass nil to get the touch location in the window’s coordinates.


Windowの座標系はデバイスの向きに依存する。こんな感じ。
横向きにすると...
左下が原点になる(デバイスとして見ればあくまでも左上)。


明示的にビューを指定すれば原点は常に左上となる。

画面を回転しても原点位置は変わらない。


結論


-[UITouch locationInView:]の引数は nil とせず、通常はきちんとタッチ対象のビューを指定するのが良い。

簡易スライドビューア [5] リファクタリング

2010年9月14日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

ズーム処理をUIImageのサブクラスを作って実装していたのをやめて、それが載る UIScrollViewをサブクラス化して実装することにした。
こんなのを
こんな感じに


[変更前]
@interface CustomImageView : UIImage <uiscrollviewdelegate> {

}
@end

[変更後]
@interface ImageScrollView : UIScrollView <uiscrollviewdelegate> {

}
@end

※CustomImageViewは廃止。

理由は後々の再利用を考えた時、画像を表示するビューの方がコンテナとなる UIScrollViewよりもカスタマイズする可能性が高いから(反対に言うと、UIScrollViewの方がカスタマイズ要求は少ない)。


ソースは GitHubからどうぞ
EasyGallery at 2010-09-14 from xcatsan's iOS-Sample-Code - GitHub

UIMenuController - メニューのカスタマイズ

2010年9月13日月曜日 | Published in | 1 コメント

このエントリーをはてなブックマークに追加

iOS3.2 からメニューのカスタマイズが可能になった。少し試してみた。


UIMenuController


メニューは UIMenuController を使ってカスタマイズする。
UIMenuController Class Reference

使い方は次の通り。

1. setTargetRect:inView: でメニュー出現の位置を決める
2. 表示するメニューを配列にして menuItemsに格納する
3. setMenuVisible:animated: で表示する
4. canPerformAction:withSender: をオーバーライドして表示したいセレクタを選別する

表示内容は menuItemsプロパティに設定する。これは UIMenuItem の配列を指定する。
UIMenuItem Class Reference

使い方については下記サイトがわかりやすくて参考になる。
UIMenuControllerをカスタマイズする | iPad Techfirm Lab


サンプル


簡単なサンプルを作った。UITextFieldのサブクラスを作り、タッチされた時に3つのメニューアイテムを出すようにしてみた。
@interface CustomTextField : UITextField {

}

@end

@implementation CustomTextField

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
 UIMenuController* menuController = [UIMenuController sharedMenuController];
 [menuController setTargetRect:CGRectZero inView:self];
 menuController.arrowDirection = UIMenuControllerArrowDown;
 
 NSMutableArray* menuItems = [NSMutableArray array];
 
 [menuItems addObject:
  [[[UIMenuItem alloc] initWithTitle:@"メニュー1"
         action:@selector(menu1:)] autorelease]];
 [menuItems addObject:
  [[[UIMenuItem alloc] initWithTitle:@"メニュー2"
         action:@selector(menu2:)] autorelease]];
 [menuItems addObject:
  [[[UIMenuItem alloc] initWithTitle:@"メニュー3"
         action:@selector(menu3:)] autorelease]];
 menuController.menuItems = menuItems;
 [menuController setMenuVisible:YES animated:YES];
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {

 if (action == @selector(menu1:) ||
  action == @selector(menu2:) ||
  action == @selector(menu3:)) {
  return YES;
 }
 return NO;
}
- (void)menu1:(id)sender
{
 NSLog(@"menu1: %@", sender);
}

- (void)menu2:(id)sender
{
 NSLog(@"menu2: %@", sender);
}

- (void)menu3:(id)sender
{
 NSLog(@"menu3: %@", sender);
}

@end

実行結果は冒頭の図のようになる。


標準メニュー


UITextFieldの beganTouhces:でカスタムメニューを表示させた場合、標準のコピー&ペーストが使えなくなる。これらを表示するには canPerformAction:withEvent: で許可する必要がある。たとえばペーストの場合:
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {

 if (
  action == @selector(paste:) ||
  action == @selector(menu1:) ||
  action == @selector(menu2:) ||
  action == @selector(menu3:)
  ) {
  return YES;
 }
 return NO;
}
するとこうなる。

ただカスタムメニューは "More..." に押し込められてしまった。"More..."を押すとメニューが表示される。


考察


もともとメニューカスタマイズに興味を持ったのはテキストフィールドの履歴表示に使えないかと思ったから。例えばテキストフィールドにフォーカスが当たった時に過去に入力した履歴が表示されて選ぶことができる(このインターフェイスは遥か昔 NewtonOSで実装されていた)。この用途で UIMenuController を使って実現するにはいくつかの課題があることがわかった。

1. 標準メニューとの共存
先に見たように標準メニューを表示するとカスタムメニューは "More..."へ追いやられてしまう。これでは使えない。同じメニューでの共存は難しいので、ハンドリングするイベントを変えることで使い分けで回避できるかが鍵。

2. 動的に内容が変化するメニューへの対応
どのメニューアイテムが選択されたかはセレクタでしか判断できない。なので履歴のように表示内容が変わる場合、あらかじめ3〜5の固定数のメニューアイテムを用意しておいて呼び出されたメソッドによってどの履歴が選択されたかを判断させる必要がある。

3. その他
そもそも可変内容の選択に UIMenuControllerを使うのは UIガイドライン的に良いのかどうか(でも、そういうUIを見たことがあるような気もする)。


ソースコード


GitHubからどうぞ
MenuSample at 2010-09-13 from xcatsan's iOS-Sample-Code - GitHub


参考情報


UIMenuControllerをカスタマイズする | iPad Techfirm Lab
分かりやすくて参考になる。

人気の投稿(過去 30日間)