はじめに
Webページでカルーセルを実装するときにSwiper.jsって便利ですよね(唐突)。
最近も、とある事情でやっかいなカルーセルの実装があったのですがこのライブラリのおかげでなんとか実装できました。感謝感謝。
個人的にも便利だなぁと思ったのと、なぜか急にSwiper.jsのページネーションをInstagram(スマホアプリ版)風に出来ないかなぁと思い立ち実装してみたので、実装の経緯などを残しておこうと思います。
ソース
https://github.com/shimabox/insta-like-swiper-pagination/
デモ
https://shimabox.github.io/insta-like-swiper-pagination/
注意(まだ未完成です)
実装してみたといいつつも、全然完成形ではありません。
めちゃんこTODOがあります。
中でもページネーションの動きにアニメーションをつけないとページが変わったのかどうかが全くわからん状態なので、そこはなる早で対応したいなぁと思っています。
(例えば総数が10ページあるときに、7ページ目まで移動してきたときと7ページ目から8ページ目まで移動したときなど)
未完成なのに公開することにしたのは、TODO対応まですると絶対に公開まで時間がかかると思ったからであります。
ただ、以下にずらずらと記載した通り独特な?動きはとりあえず実装できているではないのかなと思っています。
完璧を目指すよりまず終わらせろ
Done is better than perfect.
-マーク・ザッカーバーグ
というわけで、まず実装手順からいってみたいと思います。
実装手順
- まずインスタグラムの動きを見てみる
- 動作にパターンがあるか考える
- パターンが見えたら実装に落とし込む
上記の流れで実装を行いました。
では、実装までの手順をつれづれと書いていきたいと思います。
インスタグラム上での動き
実際にインスタグラムのスマホアプリに画像をアップ(数年ぶりに使った!)して動作を確認しました。
※ ちなみにスマホはAndroid(Pixel3)です
5ページ(5枚)
次に進んでいった場合
総数 | キャプチャ (By Instagram.) |
移動過程 | 移動回数 ( → の数) |
現在ページ | 前ページ | ドットの数 | ドットパターン ※ パターンx.は後述 |
|
---|---|---|---|---|---|---|---|---|
5 | 初期表示 | 0 | 1 | 0 | 5 | パターン1. | ・デフォルト (1〜5ページ目まで一緒) |
|
1 → 2 → 3 → 4 → 5 | 4 | 5 | 4 | 5 | パターン1. | ・デフォルト (カレントが動くのみ) |
前に戻っていった場合 (移動回数を – とする)
総数 | キャプチャ (By Instagram.) |
移動過程 | 移動回数 ( → の数) |
現在ページ | 前ページ | ドットの数 | ドットパターン | |
---|---|---|---|---|---|---|---|---|
5 | 5 → 4 → 3 → 2 → 1 | -4 | 1 | 2 | 5 | パターン1. | ・デフォルト (カレントが動くのみ) |
6ページ(6枚)
次に進んでいった場合
前に戻っていった場合 (移動回数を – とする)
7ページ(7枚)
次に進んでいった場合
前に戻っていった場合 (移動回数を – とする)
8ページ(8枚)
次に進んでいった場合
前に戻っていった場合 (移動回数を – とする)
9ページ(9枚)
次に進んでいった場合
前に戻っていった場合 (移動回数を – とする)
10ページ(10枚)
次に進んでいった場合
前に戻っていった場合 (移動回数を – とする)
ここまででわかったこと(その1)
- スライド対象オブジェクトが5個まではカレントが移動するのみ
- 初期に表示されるドットの数はMaxで7個まで
- 同じ方向(次へ, 前へ)への移動回数が(+-)5回以上になってくるとドットパターンが変わるケースが出てくる
- 次へのドットは少し小さいドットと微小のドット、最大で2個表示される
- 前へのドットは少し小さいドットと微小のドット、最大で2個表示される
- 通常サイズのドットは最大で5個表示される
- ドットパターンは以下に分けられる
ドットパターン
パターン1.
パターン2.
パターン3.
パターン4.
パターン5.
パターン6.
パターン7.
パターン8.
パターン9.
少し複雑なパターンで移動した場合
今までのパターンだと、ある程度次へ進んでから戻るを実施しただけの単純なパターンのみなので、以下のパターンでスライドを移動させてみます。
※ スライド対象オブジェクト数は 9 とします
- 1ページ目から8ページ目まで移動
- 8ページ目から2ページ目まで移動
- 2ページ目から9ページ目まで移動
- 9ページ目から5ページ目まで移動
- 5ページ目から9ページ目まで移動
- 9ページ目から1ページ目まで移動
- 1ページ目から8ページ目まで移動
- 8ページ目から7ページ目まで移動
- 7ページ目から9ページ目まで移動
1ページ目から8ページ目まで移動
現在ページ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
ドットパターン |
↓ カレントが移動するのみ |
|||||||
パターン4. | パターン7. | パターン9. | パターン8. |
8ページ目から2ページ目まで移動
現在ページ | 8 | 7 | 6 | 5 | 4 | 3 | 2 |
ドットパターン |
↓ カレントが移動するのみ |
||||||
パターン8. | パターン9. | パターン7. |
2ページ目から9ページ目まで移動
現在ページ | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
ドットパターン |
↓ カレントが移動するのみ |
|||||||
パターン7. | パターン9. | パターン8. | パターン6. |
9ページ目から5ページ目まで移動
現在ページ | 9 | 8 | 7 | 6 | 5 |
ドットパターン |
↓ カレントが移動するのみ |
||||
パターン6. |
5ページ目から9ページ目まで移動
現在ページ | 5 | 6 | 7 | 8 | 9 |
ドットパターン |
↓ カレントが移動するのみ |
||||
パターン6. |
9ページ目から1ページ目まで移動
現在ページ | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
ドットパターン |
↓ カレントが移動するのみ |
||||||||
パターン6. | パターン8. | パターン9. | パターン7. | パターン4. |
1ページ目から8ページ目まで移動
現在ページ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
ドットパターン |
↓ カレントが移動するのみ |
|||||||
パターン4. | パターン7. | パターン9. | パターン8. |
8ページ目から7ページ目まで移動
現在ページ | 8 | 7 |
ドットパターン | ||
パターン8. | パターン8. |
7ページ目から9ページ目まで移動
現在ページ | 7 | 8 | 9 |
ドットパターン | |||
パターン8. | パターン8. | パターン6. |
ここまででわかったこと(その2)
- 同じ方向への移動回数が5回以上になったタイミングで進む方向を変えた場合、再度その方向(変えたほうへの方向)へ5回以上進まないとドットパターンは変わらない
- 上にある通り1ページ目から8ページ目へ移動(次へ)したあとは、前へ5回(8ページ目から3ページ目まで)進まないとドットパターンは変化しない
- ただし、進む方向を変えた場合でも、その後に再び進む方向を戻して進んでいった結果、
もともと進んでいた方向の移動回数 + 再び進んだ方向への移動回数が5回以上
の移動回数になればドットパターンが変わる- 1ページ目から8ページ目まで移動、8ページ目から7ページ目まで移動、7ページ目から9ページ目まで移動したときのパターンをみると明白
- 次への移動回数と前への移動回数を保持しておく必要がある
- 次への移動なのか前への移動なのかを判別するために現在のページと前にいたページを知っておく必要がある
- 次への移動は
現在のページ >= 前にいたページ
でわかりそう
- 前への移動は
現在のページ < 前にいたページ
でわかりそう
- 次への移動が5回以上、前への移動が-5回以上のタイミングを取ることを考えると以下のようにやるのがよさそう
- 次へ移動した場合は次への移動回数を取得して保持しておく
- 前回保持しておいた次への移動回数に加えて保持する
- 5回以上の回数は特に考えなくていいので5を超えたら5にまるめておく
- ページが飛ばされることも考える(1 → 4ページ目)
- となると、
現在のページ - 前にいたページ
が次への移動回数
- 前へ移動した場合は前への移動回数を取得して保持しておく
- 前回保持しておいた前への移動回数から減算して保持する
- -5回以上の回数は特に考えなくていいので-5より小さくなれば-5にまるめておく
- ページが飛ばされることも考える(4 → 1ページ目)
- となると、
前にいたページ - 現在のページ
が前への移動回数
- 移動回数が
+5
と-5
のときにドットパターンの変化を考えたいので - 次へ移動した場合、前への移動回数に次への移動回数をプラスする
- その結果、前への移動回数が0以上になった場合0にまるめておく
- 前へ移動した場合、次への移動回数から前への移動回数をマイナスする
- その結果、次への移動回数が0以下になった場合0にまるめておく
- 上記を考えると次への移動回数と前への移動回数の初期値(1ページ目での)は以下でよさそう
※ 1ページより前のページは無い && 前への移動回数は-5以下にしないことから- 次への移動回数初期値 …
0
- 前への移動回数初期値 …
-5
- 次への移動回数初期値 …
これらを加味して、もう一度移動回数を整理したのが以下になります。
移動回数の整理
1ページ目から8ページ目まで移動
現在ページ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
次への移動回数 | 0 | 1 | 2 | 3 | 4 | 5 | 5 | 5 |
前への移動回数 | -5 | -4 | -3 | -2 | -1 | 0 | 0 | 0 |
ドットパターンの変化 | なし (パターン4.) |
なし (パターン4.) |
なし (パターン4.) |
なし (パターン4.) |
なし (パターン4.) |
あり (パターン7.) |
あり (パターン9.) |
あり (パターン8.) |
8ページ目から2ページ目まで移動
現在ページ | 8 | 7 | 6 | 5 | 4 | 3 | 2 |
次への移動回数 | 5 | 4 | 3 | 2 | 1 | 0 | 0 |
前への移動回数 | 0 | -1 | -2 | -3 | -4 | -5 | -5 |
ドットパターンの変化 | なし (パターン8.) |
なし (パターン8.) |
なし (パターン8.) |
なし (パターン8.) |
なし (パターン8.) |
あり (パターン9.) |
あり (パターン7.) |
2ページ目から9ページ目まで移動
現在ページ | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
次への移動回数 | 0 | 1 | 2 | 3 | 4 | 5 | 5 | 5 |
前への移動回数 | -5 | -4 | -3 | -2 | -1 | 0 | 0 | 0 |
ドットパターンの変化 | なし (パターン7.) |
なし (パターン7.) |
なし (パターン7.) |
なし (パターン7.) |
なし (パターン7.) |
あり (パターン9.) |
あり (パターン8.) |
あり (パターン6.) |
9ページ目から5ページ目まで移動
現在ページ | 9 | 8 | 7 | 6 | 5 |
次への移動回数 | 5 | 4 | 3 | 2 | 1 |
前への移動回数 | 0 | -1 | -2 | -3 | -4 |
ドットパターンの変化 | なし (パターン6.) |
なし (パターン6.) |
なし (パターン6.) |
なし (パターン6.) |
なし (パターン6.) |
5ページ目から9ページ目まで移動
現在ページ | 5 | 6 | 7 | 8 | 9 |
次への移動回数 | 1 | 2 | 3 | 4 | 5 |
前への移動回数 | -4 | -3 | -2 | -1 | 0 |
ドットパターンの変化 | なし (パターン6.) |
なし (パターン6.) |
なし (パターン6.) |
なし (パターン6.) |
なし(パターン6.) ※ ドットパターン切り替えの対象となるが変化はしない |
9ページ目から1ページ目まで移動
現在ページ | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
次への移動回数 | 5 | 4 | 3 | 2 | 1 | 0 | 0 | 0 | 0 |
前への移動回数 | 0 | -1 | -2 | -3 | -4 | -5 | -5 | -5 | -5 |
ドットパターンの変化 | なし (パターン6.) |
なし (パターン6.) |
なし (パターン6.) |
なし (パターン6.) |
なし (パターン6.) |
あり (パターン8.) |
あり (パターン9.) |
あり (パターン7.) |
あり (パターン4.) |
1ページ目から8ページ目まで移動
現在ページ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
次への移動回数 | 0 | 1 | 2 | 3 | 4 | 5 | 5 | 5 |
前への移動回数 | -5 | -4 | -3 | -2 | -1 | 0 | 0 | 0 |
ドットパターンの変化 | なし (パターン4.) |
なし (パターン4.) |
なし (パターン4.) |
なし (パターン4.) |
なし (パターン4.) |
あり (パターン7.) |
あり (パターン9.) |
あり (パターン8.) |
8ページ目から7ページ目まで移動
現在ページ | 8 | 7 |
次への移動回数 | 5 | 4 |
前への移動回数 | 0 | -1 |
ドットパターンの変化 | なし (パターン8.) |
なし (パターン8.) |
7ページ目から9ページ目まで移動
現在ページ | 7 | 8 | 9 |
次への移動回数 | 4 | 5 | 5 |
前への移動回数 | -1 | 0 | 0 |
ドットパターンの変化 | なし (パターン8.) |
なし(パターン8.) ※ ドットパターン切り替えの対象となるが変化はしない |
あり (パターン6.) |
なんか、なんとなくいけそうな気がする..?
わかったこと
これまでに書いたもろもろを考慮すると、以下のパターンも見えてきたような気がします。
初期表示
総数 <= 5
- そもそも総数が5ページ以下の場合、ドットパターンはパターン1に収まる
- よって、パターン1の描画のみ行えばいいので考慮しない
総数 - 5 == 1
の場合後ろに1つ
ドットを追加- ドットパターンはパターン2の状態
総数 - 5 >= 2
の場合後ろに2つ
ドットを追加- ドットパターンはパターン4の状態
(最大2つまで描画される)前へのドットの描画条件
次へ
進んでいて
ドットパターンの変化なし
、または、現在ページ - 1 < 5
の場合- 前にドットなし
現在ページ - 1 == 5
の場合前に1つ
ドットを追加- ドットパターンはパターン3の状態
現在ページ - 1 >= 6
の場合前に2つ
ドットを追加- ドットパターンはパターン6の状態
前へ
戻っていて
ドットパターンの変化なし
、または、現在ページ == 1
の場合- 前にドットなし
現在ページ == 2
の場合前に1つ
ドットを追加
現在ページ >= 3
の場合前に2つ
ドットを追加
(最大2つまで描画される)次へのドットの描画条件
- 前へのページドットの中で一番大きいページの数を取得
前へのページドットの各ページが 2, 3 となっていれば3
が対象 -
- で取得した値と
5
(通常ドット最大表示数)を足す
- で取得した値と
- ドットの表示終了ページ※後述 から2.で取得した値を引く
これで求めた値が次へのドットの描画数となります。
例えば、総数が10で9ページ目まで移動した場合は1
となります。
※ 上の式に当てはめると、10 - (4 + 5) = 1
となります
※ 4
は前へのページドットの中で一番大きいページの数です
※ 10
はドットの表示終了ページ※後述 です
ドットの表示開始ページ
(現在ページ + 1) - 7
で求める- 求めた結果、
1以下
になれば、1
でまるめる 7
は、2
(前へのドット最大表示数) +5
(通常ドット最大表示数)
- 求めた結果、
例えば、総数が10で10ページ目まで移動した場合、ドットのパターンはパターン6.
で、一番先頭に描画される微小ドットのページは4を指します。
この4
が開始ページとなります。
※ 上の式に当てはめると、(10 + 1) - 7 = 4
となります
前に移動している場合のドットの表示開始ページ
現在ページ - 前へのドットの表示数
で求める
例えば、総数が10で10ページ目まで移動 -> 5ページ目まで戻った場合、ドットのパターンはパターン8.
で、一番先頭に描画される微小ドットのページは3を指します。
この3
が開始ページとなります。
※ 上の式に当てはめると、5 - 2 = 3
となります
ドットの表示終了ページ
- 前へのページドットの中で一番大きいページの数を取得
前へのページドットの各ページが 2, 3 となっていれば3
が対象 -
- で取得した値と
7
を足す
7
は、2
(前へのドット最大表示数) +5
(通常ドット最大表示数)
- で取得した値と
2.で求めた値 > 総数
であれば総数
がドットの終了ページ2.で求めた値 <= 総数
であれば2.で求めた値
がドットの終了ページ
例えば、総数が10で7ページ目まで移動した場合、ドットのパターンはパターン9.
で、一番最後に描画される微小ドットのページは9を指します。
この9
が終了ページとなります。
※ 上の式に当てはめると、2(前へのページドットの中で一番大きいページの数) + 7 = 9
となります
なんとなくルールはよめてきたので、あとは実装するのみです。
実装の前に
実装の前に、上記の動きが再現できそうか確認をしてみます。
まず、ページネーションのカスタムをするには
pagination.type
にcustom
を指定renderCustom(swiper, current, total)
を利用- Swiper API #param-pagination-renderCustom
- swiperにはSwiperオブジェクトが入っています。こいつが状態を知っているようです。
- currentは現在ページ
- totalは総ページ数
これらを活用すれば良さそうな気がします。
お試し実装
試しに、5枚画像を用意して、偶数のページ(2枚目と4枚目)は無視する実装を行ってみます。
※ 画像はこちらです
<html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.min.css"> <style> * { margin: 0; padding: 0; } .container { display: flex; height: 100vh; width: 100vw; background-color: #202020; } .my-swiper { width: 100%; max-width: 960px; height: 250px; align-self: center; padding-bottom: 36px; } .slide { width: 100%; height: 100%; background-color: #ffffff; } .slide-image { width: 100%; height: 100%; object-fit: contain; } .swiper-pagination-bullet { margin-right: 10px; } .swiper-pagination-bullet:hover { cursor: pointer; } </style> </head> <body> <div class="container"> <div class="swiper my-swiper"> <div class="swiper-wrapper"> <div class="swiper-slide"> <div class="slide"> <img class="slide-image" src="./img/example_1.png" alt="example_1"> </div> </div> <div class="swiper-slide"> <div class="slide"> <img class="slide-image" src="./img/example_2.png" alt="example_2"> </div> </div> <div class="swiper-slide"> <div class="slide"> <img class="slide-image" src="./img/example_3.png" alt="example_3"> </div> </div> <div class="swiper-slide"> <div class="slide"> <img class="slide-image" src="./img/example_4.png" alt="example_4"> </div> </div> <div class="swiper-slide"> <div class="slide"> <img class="slide-image" src="./img/example_5.png" alt="example_5"> </div> </div> </div> <div class="swiper-pagination"></div> </div> </div> <script src="https://unpkg.com/swiper/swiper-bundle.min.js"></script> <script> // Make Swiper. new Swiper('.my-swiper', { pagination: { el: '.swiper-pagination', clickable: true, type: 'custom', renderCustom: function (swiper, current, total) { let html = ''; let className = ''; swiper.slides.forEach((_, index) => { if (index % 2 !== 0) { // 偶数ページであればdisplay:noneで隠す html += '<span style="display:none;"></span>'; return; } if (index + 1 === current) { className = 'swiper-pagination-bullet swiper-pagination-bullet-active'; // カレントページ } else { className = 'swiper-pagination-bullet'; } html += `<span class="${className}"></span>`; }); return html; }, } }); </script> </body>
一番左のドット(ページネーション)クリックで1枚目を表示、真ん中のドットクリックで3枚目を表示、一番右のドットクリックで5枚目が表示されるようになりました。
このように、無視したいページはdisplay:none
で隠してあげて、表示したいページは.swiper-pagination-bullet
をつけて返してあげると良さそうです。
(なお、この表現だと画像のスワイプをしてしまうと偶数のページも見れてしまいますので、そこはあしからず)
うん、なんとなくこのへんをうまいことやれば出来そうですね。
あとはひたすら実装するのみです。
※ これ以上はソースを見ていただければと思います
おわりに
なにかのUI実装を真似ようと思ったら、パターンを地味に丁寧に1つずつ洗い出してみると(正確では無いかもしれませんが)アルゴリズムが見えてくる気がしたこの頃でした。
※ ただし相当疲れるぞ!
お仕事としてアニメーションを付けたバージョンを作っていただくことは可能でしょうか?その場合の料金(概算)はどの程度でしょうか?
@canehaco
こんにちは。あまり、ブログを見ないので返信遅れました。
お仕事として受けるのは難しそうですが、相談には乗れるかもしれません。