今回は、いよいよSymmetryView上での対称(シンメトリー)表示処理にチャレンジしてみます。編集用のユーザインターフェースとしてタッチイベントにも対応させます。
まず手始めに、SymmetryViewクラスのdrawRect:メソッドを手直し、ビューの上半分にノーマルな画像を、下半分に上下反転した画像を表示してみます。この処理は以下のようなソースコードで実現できます。ビューフレーム枠への画像の合わせ込み(フィット)には、サムネイルを作成する時に用いた自作ルーチンfitBounds()を利用しています。
- (void)drawRect:(CGRect)rect
{
CGRect srt,frt,drt;
CGContextRef context;
CGImageRef cgimage;
context=UIGraphicsGetCurrentContext();
cgimage=sy_image.CGImage;
srt=CGRectZero;
srt.size=sy_image.size;
frt=self.frame;
frt.size.height/=2.0; // フレームサイズは半分に
frt.origin.y=frt.size.height; // フレーム原点を移動
fitBounds( 1,&frt,&srt,&drt ); // フレーム枠にフィット(自作ルーチン)
CGContextDrawImage( context,drt,cgimage ); // 下の画像描画
CGContextTranslateCTM( context,0.0,rect.size.height );
CGContextScaleCTM( context,1.0,-1.0 ); // 座標系の反転
CGContextDrawImage( context,drt,cgimage ); // 上の画像描画
}

何となくシンメトリーの雰囲気が出てきました(笑)。続いて、この2画像の表示オフセットをずらしてシンメトリーの状態が変化するようにしてみます。表示オフセット値の変更は、画面にタッチした指を上下にを移動させることで実現します。そして、その値はSymmetryViewクラスのプロパティsy_offset(CGFloat)へ保存するようにします。
@interface SymmetryView : UIView
{
UIImage *sy_image; // 対称処理を実行する画像
CGFloat sy_offset; // 対称オフセット値
}
@property(nonatomic,retain)UIImage *sy_image;
@property(nonatomic,assign)CGFloat sy_offset;
タッチイベントのハンドラ(受け取る処理)は、ImageViewControllerクラスに実装します。今回は、タッチ開始を処理するtouchesBegan:withEvent:と、タッチ位置の移動を処理するtouchesMoved:withEvent:の2つをオーバライドし実装します。クラスのプロパティには、オフセット値を計算するための基準点を保存するim_point(CGPoint)を用意しておきます。
CGPoint im_point; // タッチ開始座標(基準点)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event // タッチ開始
{
NSUInteger ct;
ct=[[touches anyObject] tapCount];
if( ct==2 )
{
im_view.sy_offset=0.0; // ダブルタッチで初期位置に戻す
[im_view setNeedsDisplay]; // ビューの再描画
}
else
{
for( UITouch *touch in touches )
{
im_point=[touch locationInView:self.view]; // タッチ位置(基準点)
break;
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event // 移動中
{
CGFloat off;
CGPoint vpt;
for( UITouch *touch in touches )
{
vpt=[touch locationInView:self.view]; // 新しいタッチ位置
break;
}
off=im_view.sy_offset+vpt.y-im_point.y; // タッチのY方向のオフセット値
if( off >=0.0 )
{
im_view.sy_offset=off; // ビューへオフセット値を設定
[im_view setNeedsDisplay]; // ビューの再描画
}
im_point.y=vpt.y; // 基準点を更新する
}
以下はオフセット値の変動を考慮した新しいdrawRect:メソッドです。重なり部分をなくすために、上の画像の表示時にクリッピング処理を施し、画像の一部分を表示しないように工夫しています。
- (void)drawRect:(CGRect)rect
{
CGRect srt,frt,drt;
CGContextRef context;
CGImageRef cgimage;
context=UIGraphicsGetCurrentContext();
cgimage=sy_image.CGImage;
srt=CGRectZero;
srt.size=sy_image.size;
frt=self.frame;
frt.size.height/=2.0; // フレームサイズは半分に
frt.origin.y=frt.size.height; // フレーム原点を移動
fitBounds( 1,&frt,&srt,&drt ); // フレーム枠にフィット(自作ルーチン)
crt=drt; // クリッピング用矩形枠
drt.origin.y=frt.origin.y;
drt.origin.y-=sy_offset; // 表示位置をオフセット値分ずらす
CGContextDrawImage( context,drt,cgimage ); // 下の画像描画
CGContextTranslateCTM( context,0.0,rect.size.height );
CGContextScaleCTM( context,1.0,-1.0 ); // 座標系の反転
CGContextClipToRect( context,crt ); // 画像のクリッピング
CGContextDrawImage( context,drt,cgimage ); // 上の画像描画
}

この改良で思った通りのシンメトリー処理がかなり可能になりました。しかし、画像の幅が狭くなってくるとクリッピング領域が足りずに、下の画像が上の画像の下側から飛び出してしまいます(涙)。

新しくクリッピング矩形領域を追加すれば何とかなりそうですが、今後のことを考えると処理が複雑になりそうです。ここは作戦変更、画面の上下に2つのSymmetryViewを配置して、それぞれに画像を表示することにしてみます。そのため、いままで1つだけ用意していたOutletのim_view(SymmetryView)を、 im_view0と im_view1の2つに変更します(次回は4つにする予定)。それぞれをInterfacu Builderでコネクトします。

どちら向きに画像を配置するのかを決定するために、SymmetryViewクラスに新しいプロパティsy_type(NSUInteger)を用意し、これに1の場合には上向き画像(上部)の描画であることを指示します(ゼロは下向き)。新しい仕組みに対応したdrawRect:メソッドは以下の様になります。こちらの方法では特別クリッピング処理は必要ありません。
NSUInteger sy_type; // 対称方向タイプ
- (void)drawRect:(CGRect)rect
{
CGRect srt,frt,drt;
CGContextRef context;
CGImageRef cgimage;
context=UIGraphicsGetCurrentContext();
cgimage=sy_image.CGImage;
srt.size=sy_image.size;
frt=self.bounds;
fitBounds( 1,&frt,&srt,&drt );
drt.origin.y=frt.origin.y;
drt.origin.y-=sy_offset;
if( sy_type==1 )
{
CGContextTranslateCTM( context,0.0,rect.size.height );
CGContextScaleCTM( context,1.0,-1.0 ); // 座標系の反転
}
CGContextDrawImage( context,drt,cgimage );
}
この変更に合わせて、ImageViewControllerクラスのviewDidLoadメソッドで実行しているSymmetryViewクラスの初期化を、以下のように変更します。
- (void)viewDidLoad
{
[super viewDidLoad];
im_view0.sy_type=0; // 下部の画像表示用ビュー
im_view0.sy_image=[im_model loadImage]; // 画像を読み込む
im_view1.sy_type=1; // 上部の画像表示用ビュー
im_view1.sy_image=im_view0.sy_image;
self.navigationItem.rightBarButtonItem=im_save; // 保存ボタン追加
im_type.tintColor=[UIColor grayColor]; // タイプセグメントカラー
}
最後は新しいタッチイベントハンドラの用意です。対応するSymmetryViewが同時に2つになっただけで、それほど大きな変更はありません。これで、シンメトリー画像の編集がそこそこ楽しめる機能が実装できました。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event // タッチ開始
{
NSUInteger ct;
ct=[[touches anyObject] tapCount];
if( ct==2 )
{
im_view0.sy_offset=im_view1.sy_offset=0.0; // ダブルタッチで初期位へ
[im_view0 setNeedsDisplay]; // 下のビューの再描画
[im_view1 setNeedsDisplay]; // 上のビューの再描画
}
else
{
for( UITouch *touch in touches )
{
im_point=[touch locationInView:self.view]; // タッチ位置(基準点)
break;
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event // 移動中
{
CGFloat off;
CGPoint vpt;
for( UITouch *touch in touches )
{
vpt=[touch locationInView:self.view]; // 新しいタッチ位置
break;
}
off=im_view0.sy_offset+vpt.y-im_point.y; // タッチのY方向のオフセット値
if( off >=0.0 )
{
im_view0.sy_offset=im_view1.sy_offset=off; // オフセット値代入
[im_view0 setNeedsDisplay]; // 下のビューの再描画
[im_view1 setNeedsDisplay]; // 上のビューの再描画
}
im_point.y=vpt.y; // 基準点を更新する
}

次回は、縦方向だけのシンメトリー表示だけでなく、横方向のシンメトリー表示にもチャレンジしてみます。また、画像の上下や左右の関係を逆転する機能も追加してみたいと思います。