2010年9月9日木曜日

簡易スライドビューア [1]基本動作

スライドビューア

簡易スライドビューアを作る。スライドビューアとは画像を指でフリックしてめくる iPhoneではよくあるUI。

以前も紹介したことがあるが、前回は画像の数だけビューを生成するという実用的なものではなかった。
Cocoaの日々: UIScrollView - フリックで画像(ページ)をめくる

今回は作成するビューを最低限として、画像の数が増えてもメモリを食わないような実装としてみた。

なお紹介およびサンプルで利用している画像は下記のものを使った。
夜景 - フリー写真素材 (無料壁紙画像) Futta.NET


アーキテクチャ


3つの UIScrollView をバケツリレーよろしく入れ替えていくのが特徴。

まずベースとなる UIScrollView を1枚用意する(scrollView)。

frame は画面いっぱいにしておき、contentSize は画像数の分だけ横幅を確保しておく。こうすると frameが可視領域となり、フリックによってこの領域がスクロールすることになる。

続いて UIScrollView を3枚用意して、先程の scrollView の上に並べて配置する (previousScrollView, currentScrollView, nextScrollView)。
それぞれの UIScrollViewの上に UIImageView を配置し、そこへ画像を表示する。UIScrollView を使っているのは今後拡大縮小をやるのに都合が良い為。
この状態でユーザがフリックした時の動作を考えてみる。例えばユーザが左方向へフリックした場合。
UIScrollView上でフリックを行うと上の様に可視領域(frame)が相対的に右へ動いていく。これによって左へスクロールしているように見える。

画像1枚分スクロールが終わった時点ではこのようになる。
ここで3つの UIScrollViewを入れ替える。
その結果、3つの UIScrollViewの位置関係は最初の状態に戻る。
後はこの繰り返し。右方向へスクロールも同じ考え方。


コード解説


XcodeでViewベースの新規プロジェクトを作成する。メインとなる UIViewController を次のように書き換えていく。
@interface EasyGalleryViewController : UIViewController {

 NSMutableArray* imageFiles_;

 UIScrollView* scrollView_;

 UIScrollView* previousScrollView_;
 UIScrollView* currentScrollView_;
 UIScrollView* nextScrollView_;
 
 NSInteger currentIndex_;
}

@property (nonatomic, retain) IBOutlet  NSMutableArray* imageFiles;

@property (nonatomic, retain) IBOutlet UIScrollView* scrollView;

@property (nonatomic, retain) UIScrollView* previousScrollView;
@property (nonatomic, retain) UIScrollView* currentScrollView;
@property (nonatomic, retain) UIScrollView* nextScrollView;

@property (nonatomic, assign) NSInteger currentIndex;

@end

UIScrollView はアーキテクチャで解説した時の名称そのまま。imageFiles には表示したい画像ファイルのフルパスを入れおく。

実装。まずは viewDidLoad。
- (void)viewDidLoad {
    [super viewDidLoad];
 
 // setup scroll views
 CGRect imageScrollViewFrame = CGRectZero;
 imageScrollViewFrame.size = self.scrollView.frame.size;
 imageScrollViewFrame.origin.x = (self.currentIndex-1) * imageScrollViewFrame.size.width;
 
 CGRect imageViewFrame = CGRectZero;
 imageViewFrame.size = self.scrollView.frame.size;
 
 for (int i=0; i < 3; i++) {

  // image view
  UIImageView* imageView =
   [[UIImageView alloc] initWithFrame:imageViewFrame];

  // scroll view
  UIScrollView* imageScrollView =
  [[UIScrollView alloc] initWithFrame:imageScrollViewFrame];
  imageScrollView.minimumZoomScale = 1.0;
  imageScrollView.maximumZoomScale = 5.0;
  imageScrollView.showsHorizontalScrollIndicator = NO;
  imageScrollView.showsVerticalScrollIndicator = NO;
  imageScrollView.backgroundColor = [UIColor blackColor];
  
  // bind views
  [imageScrollView addSubview:imageView];
  [self.scrollView addSubview:imageScrollView];

  // assign to iVars
  switch (i) {
   case 0:
    self.previousScrollView = imageScrollView;
    break;
   case 1:
    self.currentScrollView = imageScrollView;
    break;
   case 2:
    self.nextScrollView = imageScrollView;
    break;    
  }
  
  // release all
  [imageView release];
  [imageScrollView release];
  
  // next image
  imageScrollViewFrame.origin.x += imageScrollViewFrame.size.width;
 }
 
 self.scrollView.pagingEnabled = YES;
 self.scrollView.showsHorizontalScrollIndicator = NO;
 self.scrollView.showsVerticalScrollIndicator = NO;
 self.scrollView.scrollsToTop = NO;
 
 [self adjustViews];
 [self scrollToIndex:self.currentIndex animated:NO]; 
}
3つの UIScrollView を作り、それぞれに UIImageViewを貼りつけている。最後にベースの UIScrollViewのプロパティを設定し(ページスクロール、スクロールバー表示制御など)、画像を表示する。 adjustViews はベース UIScrollView の contentSize を設定した後、3つの UIScrollViewに画像を表示する指示をだしている。
- (void)adjustViews
{
 CGSize contentSize = CGSizeMake(
         self.currentScrollView.frame.size.width * [self.imageFiles count], 
         self.currentScrollView.frame.size.height);
 self.scrollView.contentSize = contentSize;
 
 [self setImageAtIndex:self.currentIndex-1 toScrollView:self.previousScrollView];
 [self setImageAtIndex:self.currentIndex   toScrollView:self.currentScrollView];
 [self setImageAtIndex:self.currentIndex+1 toScrollView:self.nextScrollView];
}
画像表示はこんな感じ。
- (void)setImageAtIndex:(NSInteger)index toScrollView:(UIScrollView*)scrollView
{
 UIImageView* imageView = [scrollView.subviews objectAtIndex:0];
 if (index < 0 || [self.imageFiles count] <= index) {
  imageView.image = nil;
  scrollView.delegate = nil;
  return;
 }

 UIImage* image = [UIImage imageWithContentsOfFile:[self.imageFiles objectAtIndex:index]];
 imageView.image = image;
 imageView.contentMode = (image.size.width > image.size.height) ?
 UIViewContentModeScaleAspectFit : UIViewContentModeScaleAspectFill;
}
指定したインデックスが範囲外の場合は画像=nil としている(表示上は真っ黒になる)。 ここまでで表示ができた。 残りはフリックの制御。
#pragma mark -
#pragma mark UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
 CGFloat position = scrollView.contentOffset.x / scrollView.bounds.size.width;
 CGFloat delta = position - (CGFloat)self.currentIndex;
 
 if (fabs(delta) >= 1.0f) {
  self.currentScrollView.zoomScale = 1.0;
  self.currentScrollView.contentOffset = CGPointZero;
  
  if (delta > 0) {
   // the current page moved to right
   self.currentIndex = self.currentIndex+1; // no check (no over case)
   [self setupNextImage];
   
  } else {
   // the current page moved to left
   self.currentIndex = self.currentIndex-1; // no check (no over case)
   [self setupPreviousImage];
  }
  
 }
 
}
UIScrollViewDelegate のscrollViewDidScroll: を実装し、ここで1ページ分のスクロールが発生したかどうかを判定している。1ページ分のスクロールが発生している場合はアーキテクチャで説明した3つの UIScrollViewの入れ替えを実施する。
-(void)setupPreviousImage
{
 UIScrollView* tmpView = self.currentScrollView;
 
 self.currentScrollView = self.previousScrollView;
 self.previousScrollView = self.nextScrollView;
 self.nextScrollView = tmpView;
 
 CGRect frame = self.currentScrollView.frame;
 frame.origin.x -= frame.size.width;
 self.previousScrollView.frame = frame;
 [self setImageAtIndex:self.currentIndex-1 toScrollView:self.previousScrollView];
}

-(void)setupNextImage
{
 UIScrollView* tmpView = self.currentScrollView;
 
 self.currentScrollView = self.nextScrollView;
 self.nextScrollView = self.previousScrollView;
 self.previousScrollView = tmpView;
 
 CGRect frame = self.currentScrollView.frame;
 frame.origin.x += frame.size.width;
 self.nextScrollView.frame = frame;
 [self setImageAtIndex:self.currentIndex+1 toScrollView:self.nextScrollView];
}
これでおしまい。

ソースコード

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

参考情報

Cocoaの日々: 画像を横に並べたスクロールビューアの作成 [1] アイディア
- - - -
次回はこれに画像の拡大縮小を入れてみる。

12 件のコメント:

  1. いつも、勉強の為に
    このページを活用させて頂いております。

    解りやすい解説を公開して頂きありがとうございます。

    こちらの簡易スラードビューアは
    1 2 3 4 5 6 7

    からスタートしますが

    7 6 5 4 3 2 1
                ↑

    といった感じでも
    実装は可能なのでしょうか?

    もし、今後
    簡易スライドビューアをバージョンアップすることがありましたら
    上記のことも含めて頂けたらと思います。

    返信削除
  2. こんばんは。

    > こちらの簡易スラードビューアは
    > 1 2 3 4 5 6 7
    > ↑
    > からスタートしますが
    >
    7 6 5 4 3 2 1
    >             ↑

    可能だと思います。ただ今は左=>右方向固定なので修正が少し必要です。


    > もし、今後
    > 簡易スライドビューアをバージョンアップすることがありましたら
    > 上記のことも含めて頂けたらと思います。

    考えてみますね。ところでその場合はどんな用途を考えていますか?
    もし良ければ教えて下さい。
    (特に他意はありません> 好奇心からです)

    では。

    返信削除
  3. 特には、コレといった実用方法は無いのですが

    日本人なので右から左にスクロール出来た方が気持ち良く感じました。

    並びに、textFieldでも縦書き表示が出来なかったり、など
    日本の文化が皆無のような気がしてなりません。

    なので、簡易スライドビューアを”写真集”だと認識したときには

    日本人として
    左→右のめくり方に違和感を感じました。

    私も調べてみますので、もし解りましたら私の方からも
    報告させて頂きます。

    返信削除
  4. 追記で度々申し訳ございません。

    何か、ヒントになればと思いまして

    逆から出す為には3つの UIScrollViewの動きを変えるのであれば
    面倒ですので

    1 2 3 4 5 6 7 8
                  ↑

    からスタートさせれば
    逆っぽく見えるかとも思います。

    返信削除
  5. こんにちは。橋口です。

    > 日本人なので右から左にスクロール出来た方が気持ち良く感じました。

    なるほど。これは全然思ってもみませんでした。

    > 逆から出す為には3つの UIScrollViewの動きを変えるのであれば
    > 面倒ですので
    > 1 2 3 4 5 6 7 8
    >               ↑
    > からスタートさせれば
    > 逆っぽく見えるかとも思います。

    なるほど。

    コメントありがとうございました。

    返信削除
  6. はじめまして^^)

    一週間ほど前にこちらを知り
    勉強させていただいております。

    SDK超初心者の新田と申します。

    簡易スライドビューア、
    私には難解ですが、少しずつ理解できるようにしています。

    写真集のようなアプリを作りたいのですが
    さっそく壁にぶちあたり、まいっています(涙)

    もしよろしければご教授お願いいたします。



    写真集アプリですが。

    1ページ目(簡易スライドビューアにTopViewControllerを追加)
      ボタン(UIButton)を追加して、そこから画面遷移して2ページ目へ

    2ページ目(簡易スライドビューアにIndexViewControllerを追加)
      写真のサムネイル(UIButton)を表示してそこから3ページめへ

    3ページ目(簡易スライドビューア本体)
      前後写真ボタン(UIButton)を追加、シンプルな構成です。

    TopViewControllerを表示までは出来たのですが、

    問題がいくつか有ります。
    一つ目はInterfaceBuilderではUIButtonが表示されているのですが
    ビルドするとボタンは消えてしまいます。
    二つ目は画面遷移すると落ちてしまいます。

    三つ目はDelegateの「viewController.imageFiles = array;」
    がエラーとなってしまいます。

    画面遷移はこちらを参項に参考にしています。
    http://japan.internet.com/developer/20091113/26.html


    簡易スライドビューアに上記の画面遷移を組み込むこと自体がおかしいのでしょうか
    もしお時間がありましたら、
    よろしくお願いしますm(_ _)m


    、、追記、Twitterフォローさせていただきました。
    @kazufumi_nitta

    返信削除
  7. 新田さん、こんにちは。
    連絡が遅くなりました。

    > 簡易スライドビューアに上記の画面遷移を組み込むこと自体がおかしいのでしょうか

    考えは間違ってません。原因はなんでしょうね。
    ソースを見ないとなんとも言えないところです。
    もし良ければソースを見せてくれませんか?
    何かわかるかもしれません。

    プログラミング頑張って下さいね!
    では。

    返信削除
  8. xcatsanさん

    おはようございます^^)
    年末のお忙しい所、お返事ありがとうございます。

    >もし良ければソースを見せてくれませんか?
    ぜひ、そうさせていただきたいのですが
    あまりにも超初心者なもので

    もう少し勉強後、まとまってからご指導いただきたいと思います。

    その時はよろしくお願いします。

    ありがとうございました。

    新田

    返信削除
  9. 新田さん、おはようございます。
    橋口です。

    後で一つ気がついたのですが、Xcodeでプロジェクトを作るとそのアプリは終了しても直前の状態を保持していて、次回起動時にその状態を復帰する設定になっています。開発時はこの設定が効くと困ることがあって、プログラムを修正してもそれがシミュレータで反映されない場合があります。今回症状の出ている Interface Buidler のボタンの追加ももしかしたらこの設定が有効になっているのかもしれません。

    対処方法としてはシミュレータ上のアプリを実行毎に削除するか、設定を変えます。設定については過去に紹介記事があるのでそちらを参照してみて下さい。

    「iOS 4.0 でアプリを一時停止しない設定 - UIApplicationExitsOnSuspend」
    http://cocoadays.blogspot.com/2010/06/ios-40-uiapplicationexitsonsuspend.html

    では。

    返信削除
  10. 橋口さん

    おはようございます。
    追加のご説明ありがとうございます。
    年末の忙しさで、しばらくxcodeをさわっていないのですが
    年明けにでも、本格的に勉強してみます。

    また、初歩的な質問をしてしまうかと思いますが
    よろしくお願いします。

    良いお年をお迎えください。
    ありがとうございました^^)

    返信削除
  11. コメントどうも。新田さんも良いお年を ^^)

    返信削除
  12. このコードの場合、orientationが変わった際のリサイズに対応していないのでは

    返信削除