【Swift4】UICollectionViewを使ってカルーセルを実装してみた。【セルの拡大縮小編】

こんにちは、Reoです。前回の続きです。

前回「【Swift4】UICollectionViewを使ってカルーセルを実装してみた。【無限スクロール編】

第1回:無限スクロール編
第2回:コレ
第3回:ページング編
第4回:セルの装飾編

目指す形はコレです。

前回の記事では、無限スクロールまでを実装しました。

今回は、「中心のセルは大きく表示し、左右のセルは小さく表示する」といった実装をしていきます!

次 > 【Swift4】UICollectionViewを使ってカルーセルを実装してみた。【ページング編】

 

前回作ったCarouselViewを用いて作っていきます。

どういう実装にする?

まず、どうすれば完成形のようなUIが作れるかを考えていきます。

目指す形は、真ん中が大きくドーンと表示され、左右が小さいといった形です。

UIScrollViewDelegateプロトコルを用いて、スクロールされたときにCGAffineTransformを用いてセルの大きさを変更するという方法で実装しようと思います。

手順は

  1. CarouselViewにUIScrollViewDelegateプロトコルを用いる。
  2. scrollViewDidScroll(_:)で表示されているセルを取得する。
  3. セルの現在位置からScaleを決定する。
  4. CGAffineTransformでセルのScaleを変更する。

という感じです。

今回は3番目のどうやってそのスケールを割り出す?ということが重要になります。

1-2の部分は、一番最後にも書きましたが、scrollViewDidScroll(_:)でなく、layoutSubviews()をoverrideして、その中に同様のコードを書く実装でも上手くいきます。(全部書ききってから気づいたので、この先はUIScrollViewDelegateプロトコルの方に書いています。)

 

まずは手順2までを書いておきましょう。

スクロール中の処理を書こう!

前回のコードでは既にCarouselViewでUIScrollViewDelegateプロトコルを用いてるので、その下にコードを書いていきます。

class CarouselView: UICollectionView {
    // 略
    func transformScale(cell: UICollectionViewCell) {
        // 計算してスケールを変更する
    }
}

extension CarouselView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        if isInfinity {
            // 略
        }
        
        // 画面内に表示されているセルを取得
        let cells = self.visibleCells
        for cell in cells {
            // ここでセルのScaleを変更する
            transformScale(cell: cell)
        }
    }
    
}

visibleCellsで現在画面内で実際に目に見えているセルを取得することができます。これで、UICollectionViewCellの配列が取得できます。

for-in文でセルを1つずつ取り出してスケールの変更をする、という実装です。

 

セルのScaleを計算しよう!

まず、どんな計算をすれば実装できるかを考えます。

これは、「セルが画面の中心からどれだけ離れているかを考えて、その距離に応じてスケールを変更させる」という実装でできそうです。

 

Scaleは最大でも1.0で、負の値にはならない必要があります。(0 < scale <= 1.0)

この理由は、Scaleが1.0より大きくなった場合、実際にitemSizeで設定したセルサイズを超えることになるので、UICollectionViewのframeからはみ出してしまう可能性が出てくるからです。

またScaleが負の値になると反転してむしろ大きくなってしまいます。

 

まとめると、セルが画面の中心にいるときに最大値(1.0)をとり、中心からずれた分だけ縮小率に応じて縮小させるという計算をします。(下図)

 

実際の実装手順は

  1. セルの中心座標xを取得する。
  2. 画面の中心座標xを取得する。
  3. 1,2を用いて中心までの距離を計算する。
  4. 3と縮小率を用いて、セルのScaleを決定する。
  5. CGAffineTransformを用いて4のScaleを変更する。

こんな感じです。

 

実装しよう!

手順にそって実装していきます。この処理は先ほど用意したtransformScale(cell:)の中に書いていきます。

先に全体像を貼っておきますね。

func transformScale(cell: UICollectionViewCell) {
    let cellCenter:CGPoint = self.convert(cell.center, to: nil) //セルの中心座標
    let screenCenterX:CGFloat = UIScreen.main.bounds.width / 2  //画面の中心座標x
    let reductionRatio:CGFloat = -0.0009                        //縮小率
    let maxScale:CGFloat = 1                                    //最大値
    let cellCenterDisX:CGFloat = screenCenterX - cellCenter.x   //中心までの距離
    let newScale = reductionRatio * cellCenterDisX + maxScale   //新しいスケール
    cell.transform = CGAffineTransform(scaleX:newScale, y:newScale)
}

 

セルの中心座標を取得する

中心座標は


let cellCenter:CGPoint = self.convert(cell.center, to: nil)

で取得します。

これは、実際のスクリーンに対してセルの真ん中がどの座標かを取得しています。

 

単にcell.centerを取得した場合、スクロールビュー内の座標が返されます。

なので実際のスクリーンからの座標にconvertする必要があります。

convertのto:(UIView)の部分はnilにするとスクリーン上での座標が取得できます。(今回初めて知った)

座標のconvertはムズカシイので、一度自分の頭を整理するためにもそのうち記事にしたいです。

今回だと、self.convertのselfの部分を例えばcellにすると、ヤバイものができます。ムズカシイね。

 

画面の中心座標xを取得する

これは簡単。


let screenCenterX:CGFloat = UIScreen.main.bounds.width / 2

UIScreenを使ってその半分の位置を取得します。

 

中心までの距離を計算する

中心からどれだけ離れているか、です。

let cellCenter:CGPoint = self.convert(cell.center, to: nil)
let screenCenterX:CGFloat = UIScreen.main.bounds.width / 2
//中心までの距離
let cellCenterDisX:CGFloat = abs(screenCenterX - cellCenter.x)

先ほどのセルの中心座標xと画面の中心座標xの差を計算します。

距離が欲しいので、差の絶対値です。

中心に近いと値は小さく、中心から離れるほど値は大きくなります。

絶対値でない場合は、こんなのを作ることもできます。

これはこれで使い道はありそう。ただ重なり順がおかしくなるときがあるのでそこを調整するのがちょっと面倒そうではあります。

 

セルのScaleを決定する

縮小率を決めます。

// 縮小率
let reductionRatio:CGFloat = -0.0009

この値で左右のセルがどれくらいの大きさまで縮小するかが決まります。

Scaleの最大値を決めます。

// 最大値
let maxScale:CGFloat = 1

実際のスケールを計算します。

// スケール
let newScale = reductionRatio * cellCenterDisX + maxScale

 

reductionRatio * cellCenterDisXの部分が-1~0の間におさまる値でないと上手くいきません。

それがうまくいくように、reductionRatioの値を決定してください。

 

CGAffineTransformを用いてScaleを変更する

あとはCGAffineTransformを用いてScaleの変更をするだけ!

// transform
cell.transform = CGAffineTransform(scaleX:newScale, y:newScale)

うえぇぇい

ここまでできていればこんな感じのものができているはずです。

ここまでの全体コード

ここまでの全体はこんな感じです。

今回追加したコードは結構少ないですが、convertの方法だったり縮小率の調整がうまくいかなかったりで一番時間がかかってます。そんなもんだよね…

 

次回、セルを中心で止める!

今回はこの辺で、次回は「セルを中心で止める方法」を紹介していこうと思います。

あと、初期表示の際にもセルが縮小されて、中心に配置されている状態に調整していきます。

次 > 【Swift4】UICollectionViewを使ってカルーセルを実装してみた。【ページング編】

 

今回はstackoverflowの回答を参考に作成しました。

実のところ、実装しているときはここまできちんと考えずに値をとりあえず変えて作ってました。今こうやって記事を書いて、ようやくちゃんと理解したって感じです。

計算はあんまり得意ではないので適当にやってたらこんなのになってたり。

 

別の方法

今回、scrollViewDidScroll(_:)内に処理を書いていますが、実のところlayoutSubviewsに書いてもうまくいきます。というかそっちのが多分キレイに動いてます。あと初期表示でも縮小された状態ででてきます。

次回の記事にも書くつもりですが、ここでもさらっと紹介しておきます。

    override func layoutSubviews() {
        super.layoutSubviews()
        
        // 画面内に表示されているセルを取得
        let cells = self.visibleCells
        for cell in cells {
            // ここでセルのScaleを変更する
            transformScale(cell: cell)
        }
        
    }

を追加して、scrollViewDidScroll(_:)内の同じ処理を書いている部分を消しても上手くいきます。

それではまた次回!

第1回:無限スクロール編
第2回:コレ
第3回:ページング編
第4回:セルの装飾編

Comments...

コメントは認証制です。詳しくは下記の注意をお読みください。お気軽にコメントお願いします!

Write a Comment

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

Related Memo...

UITableView.RowAnimation の .none はアニメーションするよ

UITableView.RowAnimation の .none はアニメーションがnoneなわけじゃなく、デフォルトの設定を使うよという意味らしい。

The inserted or deleted rows use the default animations.

なのでアニメーションしちゃう。今更の気づき。

 

iOS

記事を書くほどでもないけれどメモっておきたいこと

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS

UINavigationController + UIScrollView の組み合わせで使っている時に謎の余白ができる時

UINavigationController + UIScrollView の組み合わせで使っていて、UIScrollView 上に AutoLayout で上下左右0で View を設置しているのに、30px程度上にずれてしまうとき。

`navigationController.navigationBar.isTranslucent = false` にすると直るかもしれない。

ScrollView上のコンテンツとNavigationBarの重なっているところが透過していたら多分これで直せるはず。

通常のターゲットではちゃんと動いているのに、iOSSnapshotTestCase を用いたテストでだけこの対応が必要なのよくわからないけれど。。。

iOS
more