芝生やDIY等のライフハックやWeb制作情報を発信するメディア

【プラグイン不要!】レスポンシブWebデザインに対応したカルーセルjsを自作してみた

Web2017年6月23日

レスポンシブWebデザインに対応したカルーセルjsを自作してみました。プラグインを使えば手っ取り早く実装できるカルーセルですが、要件によっては不要な機能も多々あり、機能変更などに柔軟に対応できないケースもあることから、自作できるに越したことはありません。本記事では私が自作したレスポンシブWebデザイン対応カルーセルjsをご紹介させていただきたいと思います。

今回実装した機能

本記事でご紹介させていただくカルーセルjsには、以下の機能を実装しました。基本的に必要となってくる機能は大体盛り込んでいるかと思います。

  • レスポンシブWebデザイン対応
  • フリック対応(スマホ対応)
  • インジケーター(現在地)表示
  • ループ対応(1枚の場合も考慮)
  • リサイズ対応
  • 自動再生
  • マウスオーバーで自動再生を停止

サンプルコード

html

<div id="carousel">
<div class="carousel-view">
<div class="carousel-contents">
<div class="item"><a href="https://yahoo.co.jp/" target="_blank"><img src="http://placehold.jp/333333/ffffff/960x450.png?text=1" alt=""></a></div>
<div class="item"><a href="https://yahoo.co.jp/" target="_blank"><img src="http://placehold.jp/333333/ffffff/960x450.png?text=2" alt=""></a></div>
<div class="item"><a href="https://yahoo.co.jp/" target="_blank"><img src="http://placehold.jp/333333/ffffff/960x450.png?text=3" alt=""></a></div>
</div>
<p class="prev"><a href="#"><img src="https://placehold.jp/80x80.png?text=前" alt="前"></a></p>
<p class="next"><a href="#"><img src="https://placehold.jp/80x80.png?text=次" alt="次"></a></p>
</div>
<ul class="list-indicator">
</ul>
</div>

css

*{margin:0;padding:0;}

img{
    max-width:100%;
    border:none;
}

#carousel{
    position:relative;
    max-width:960px;
    margin:0 auto;
    font-size:0;
    line-height:0;
}

#carousel .carousel-view {
    overflow:hidden;
    position:relative;
}

#carousel .carousel-view .carousel-contents{
    position:relative;
    top:0;
}

#carousel .carousel-view .carousel-contents:after{
    clear:both;
    content:"";
    display:table;
}

#carousel .carousel-view .carousel-contents .item {
    float:left;
    display:inline;
}

#carousel .carousel-view p.prev{
    position:absolute;
    top:50%;
    left:0;
    transform: translateY(-50%);
}

#carousel .carousel-view p.next{
    position:absolute;
    top:50%;
    right:0;
    transform: translateY(-50%);
}

#carousel ul.list-indicator{
    text-align:center;
}

#carousel ul.list-indicator li{
    display:inline-block;
    margin-right:20px;
    margin-top:20px;
}

#carousel ul.list-indicator li a{
    display:block;
    height:20px;
    width:50px;
    background:#ccc;
}

#carousel ul.list-indicator li.current a{
    background:#ff0000;
}

js

$(function(){
    'use scrict';
        
    //アニメーションスピード
    var $speed = 500;
    
    //自動再生までの時間
    var $interval = 3000;
        
    //カウント初期値
    var $currentNum = 1;
    
    //セレクタを変数に格納
    var $view = $('.carousel-view'),
        $viewContents = $view.find('.carousel-contents'),
        $viewContentsItem = $viewContents.find('.item'),
        $viewContentsItemImg = $viewContentsItem.find('img'),
        $indicator = $('#carousel ul.list-indicator'),
        $btnPrev = $('p.prev a'),
        $btnNext = $('p.next a');
    
    //要素の数を変数に格納
    var $viewContentsItemLength = $viewContentsItem.length;
    
    //カルーセルのwidthを変数に格納
    var $viewContentsItemWidth = $viewContentsItem.width();
    
    //画像のwidthを変数に格納
    var $viewContentsItemImgWidth = $viewContentsItemImg.width();
    
    //処理実行
    $viewContents.each(function() {
        
        var autoTimer;
        
        //自動スライド関数
        var autoLoad = function(){
            autoTimer = setInterval(function(){
                rollNext();
            }, $interval);
        };
                                
        //1個以上の場合のみ
        if($viewContentsItemLength > 1){
            
            //アイテムのdivにwidthを設定
            $viewContentsItem.css('width', $viewContentsItemImgWidth);
            
            //インジケーター生成
            for(var i = 1;i <= $viewContentsItemLength;i++){
                $indicator.append('<li><a href="#">'+ i +'枚目</a></li>');
            }
            $indicator.find('li').first().addClass("current");
            
            //ループ用のクローンを前後に複製
            $viewContentsItem.first().clone().addClass("clone-f").appendTo($viewContents);
            $viewContentsItem.last().clone().addClass("clone-l").prependTo($viewContents);
            
            //初期カレントclassを付与
            $viewContents.find('.clone-l').next().addClass("current");
            
            //アイテムの数で全体のwidthを設定(クローン分を含む)
            $viewContents.css('width', $viewContentsItemWidth * ($viewContentsItemLength + 2));
            
            //leftをクローン分ずらして1個目の位置に
            $viewContents.css('left', - $viewContentsItemWidth);
            
            //自動再生
            autoLoad();
        }
        
        //1個の場合(インジケーター削除、クローン削除、cssリセット)
        if($viewContentsItemLength === 1){
            $btnPrev.parent().remove();
            $btnNext.parent().remove();
            $indicator.remove();
        }
        
        //右回転
        var rollNext = function(){
            clearInterval(autoTimer);
            if(!$viewContents.is(":animated")){
                $currentNum++;
                $viewContents.find('.current').removeClass('current').next().addClass('current');
                $indicator.find('.current').removeClass('current');
                $indicator.find('li').eq($currentNum - 1).addClass('current');
                if($currentNum > $viewContentsItemLength){
                    $indicator.find('li').eq(0).addClass('current');    
                }
                $viewContents.animate({ 'left': - $viewContentsItemWidth * $currentNum,
                }, $speed, function() {
                    if($currentNum > $viewContentsItemLength){
                        $viewContents.find('.current').removeClass('current');
                        $viewContents.find('.clone-l').next().addClass("current");
                        $viewContents.css('left', - $viewContentsItemWidth);
                        $currentNum = 1;
                    }
                });
            }
            autoLoad();
        };
            
        //左回転
        var rollPrev = function(){
            clearInterval(autoTimer);
            if(!$viewContents.is(":animated")){
                $currentNum--;
                $viewContents.find('.current').removeClass('current').prev().addClass('current');
                $indicator.find('.current').removeClass('current');
                $indicator.find('li').eq($currentNum - 1).addClass('current');
                if($currentNum > $viewContentsItemLength){
                    $indicator.find('li').eq(0).addClass('current');    
                }
                $viewContents.animate({ 'left': - $viewContentsItemWidth * $currentNum,
                }, $speed, function() {
                    if($currentNum < 1){
                        $viewContents.find('.current').removeClass('current');
                        $viewContents.find('.clone-f').prev().addClass("current");
                        $viewContents.css('left', - $viewContentsItemWidth * $viewContentsItemLength);
                        $currentNum = $viewContentsItemLength;
                    }
                });
            }
        };
        
        //右クリック
        $btnNext.click(function(){
            rollNext();
            clearInterval(autoTimer);
            return false;
        });
        
        //左クリック
        $btnPrev.click(function(){

            rollPrev();
            return false;
        });
        
        //インジケータークリック
        $indicator.find('li a').click(function(){
            
            //自動再生タイマーをクリア
            clearInterval(autoTimer);
            
            if(!$viewContents.is(":animated")){
                
                //インジケーターのインデックス番号を取得
                var $indicatorIndex = $(this).parent().index() + 1;
                
                //カウントを更新
                $currentNum = $indicatorIndex;
                
                //インジケーターのカレントのclassを付け替え
                $indicator.find('.current').removeClass('current');
                $(this).parent().addClass('current');
                
                //アイテムのカレントのclassを付け替え
                $viewContents.find('.current').removeClass('current');
                $viewContentsItem.eq($currentNum - 1).addClass('current');
                
                //アニメーション
                $viewContents.animate({ 'left': - $viewContentsItemWidth * $currentNum,
                }, $speed, function() {
                    
                    /*完了時の処理は不要*/
                    
                });
            }
            
            return false;
            e.preventDefault();
        });
        
        //リサイズ対応
        var $resizeTimer = false;
        $(window).resize(function() {
            if ($resizeTimer !== false) {
                clearTimeout($resizeTimer);
            }
            $resizeTimer = setTimeout(function() {
                
                //widthをリセット
                $viewContents.find('div.item').css('width','auto');
                $viewContents.css('width','auto').css('left','auto');
                
                //カルーセルのwidthを変数に格納し直す
                $viewContentsItemWidth = $viewContentsItem.width();
                
                //画像のwidthを変数に格納し直す
                $viewContentsItemImgWidth = $viewContentsItemImg.width();
                
                //アイテムのdivにwidthを設定
                $viewContents.find('div.item').css('width', $viewContentsItemImgWidth);
                
                //アイテムの数で全体のwidthを設定(クローン分を含む)
                $viewContents.css('width', $viewContentsItemWidth * ($viewContentsItemLength + 2));
                
            }, 200);
        });
        
        //マウスオーバー時は自動再生タイマー停止
        $view.hover(function(){
            clearInterval(autoTimer);
        }, function(){
            autoLoad();
        });
        $indicator.find('li a').hover(function(){
            clearInterval(autoTimer);
        }, function(){
                autoLoad();
        });
        
        //フリック対応
        $viewContents.on({
            
            //タッチ開始
            'touchstart': function(e) {
                this.startX = e.originalEvent.changedTouches[0].pageX;
                this.startY = e.originalEvent.changedTouches[0].pageY;
                this.touchX = e.originalEvent.changedTouches[0].pageX;
                this.slideX = $(this).position().left;
                
                //自動再生タイマーをクリア
                clearInterval(autoTimer);
            },
            
            //タッチ移動
            'touchmove': function(e) {
                var $moveX = this.startX - e.originalEvent.changedTouches[0].pageX,
                    $moveY = this.startY - e.originalEvent.changedTouches[0].pageY;
                
                //縦スクロール対応
                var $moveRate = $moveX / $moveY;
                if($moveRate > Math.tan(15 * Math.PI/180)) {
                    e.originalEvent.preventDefault();
                }

                this.slideX = this.slideX - (this.touchX - e.originalEvent.changedTouches[0].pageX);
                $(this).css({left:this.slideX});
                this.touchX = e.originalEvent.changedTouches[0].pageX;
            },
            
            //タッチ終了
            'touchend': function(e) {
                var $diff = this.startX - this.touchX;
                if ($diff < -50) {
                    rollPrev();
                }else if(50 < $diff){
                    rollNext();
                }else{
                    $(this).animate({left:this.slideX + $diff});
                }
            }    
        });
        
    });
    
});
  • jQueryを読み込んでいる場合のスクリプトとなります

解説

かなり長くなってしまいましたが。。。頑張って解説してみたいと思います。スクリプトにもなるべく丁寧にコメントを記載していますので、あわせてご参考にしてください。

まず、変数「$speed」と「$interval」にそれぞれアニメーションのスピードと自動再生されるまでの秒数を指定しています。次の変数「$currentNum」では初期値を1とし、自動再生やインジケーター、「次」「前」を操作によってこの値を増減し、現在値や移動幅を示すのに使用しています。

次に、あらかじめ頻繁に使用するセレクタやcss設定に必要な画像の幅、要素の数、画像を括っている.itemの幅を変数に格納しておきます。

ここから処理実行($ViewContents.each~)に入ります。要素が1個以上ある場合のみインジケーターの生成や前後のループ用クローン生成を行います。あわせてここに自動再生用の関数も書いておきます。1個の場合はこれらを行わず、不要な要素をremove()で取り除いています。

次に、「次」「前」のクリック時の処理を記述しています。自動再生と競合しないようclearIntervalで一旦再生タイマーをクリアします。左の場合はクリック毎に$currentNumの値を減らし、右クリックの場合は増やします。この値を使ってカレント用class(.current)を付け替えたり、アニメーション移動する位置の計算に使います。
また、アニメーション完了時に最後もしくは最初の要素(.clone-l、.clone-f)に到達していた場合は同じ要素の位置にずらし、かつカレント用classを付け替えて$curentNumの値も元に戻します。例えば最後のアイテム(.clone-f)到達したらleftの値を.clone-lの後ろに移動させ、かつその要素に.currentを付け替え、$currentNumの値を1(初期値)に戻す、といった感じです。また、最後にautoLoad()で自動再生を再スタートさせます。

次はインジケーターの処理です。やはりclearIntervalで自動再生タイマーをクリアします。クリックしたインジケーターのインデックス番号を参照し、$currentNumを更新します。あとは例によって.currentを付け替えて、leftの値を「$ViewContentsItemWidth * $currentNum」と計算してアニメーションさせます。特にアニメーション完了時の処理は必要ありません。「次」「前」と同じように、最後にautoLoad()で自動再生を再スタートさせます。

次にリサイズ対応です。以前ご紹介した記事の応用で、ウインドウリサイズが終わった時にだけ処理を実行します。一旦最初に設定したcssリセットして、改めて幅を取得し設定し直します。全体の幅については初期読み込み時同様、クローン分も含めて幅設定($ViewContentsItemLength + 2)するのがポイントです。

ここまで来たらあと少しです。マウスオーバー時に自動再生されないようclearIntervalします。function~にはマウスオーバーが外れた際自動再生が再スタートされるよう、autoLoad();を記述しておきます。

最後にフリック対応です。恥ずかしながらあまりここの処理は書いたことがなく、一番四苦八苦しました。タッチ開始、タッチ移動、タッチ終了それぞれのイベントで処理を行います。タッチ開始(touchstart~)では、タッチした場所のX座標とY座標を取得します。やはりここでclearIntervalで自動再生タイマーをクリアします。タッチ移動(touchmove~)ではまず変数$moveXと$moveYに移動先のX座標とY座標を格納します。縦スクロール対応の記述はおまじないのようなものなので、あまり深く考えなくてよいです。とにかく書いておけば斜めにフリックしてもちゃんと動くようになります。$(this).css~の記述でフリックした分カルーセルがグリグリ動くようになります。タッチ終了(touchend~)では移動した距離を変数$diffに格納し、50px以上であれば次もしくは前の画像へアニメーションするようにしています。50px以下の場合は元の位置に戻るようアニメーションします。この50pxはお好みで変えてもOKです。

デモ

実際に動作を確認できるデモページも公開しておりますのであわせてご確認ください。

まとめ

プラグインを使用したものや、もっと簡単なカルーセルはこれまでも実装したことはありますが、ここまで機能を盛り込んだカルーセルjsをフルスクラッチで自作したのは初めてでしたので、とても勉強になりました。基本的な機能は大体網羅できたと思うので、手前味噌ですが結構実用的なものが作れたと思います。なるべく機能毎に処理はまとめたので、不要なものがあれば適宜記述を削除することで必要最小限のスクリプトにすることも可能です。

時間が無い場合はプラグインに頼るのもアリかと思いますが、自作できるといざ修正やメンテナンスを行う際とてもやりやすくなりますし、マークアップも自由自在に操れるので、結果よいものが作れると思います。レスポンシブWebデザインのカルーセルjsを自作される際は、ぜひ本記事をご参考にしていただければ幸いです。

追記(2017年6月28日)

ご本記事にてご紹介させていただいたレスポンシブWebデザインに対応したカルーセルjsを、プラグイン化してみました。手前味噌ですがかなり実用的なものに仕上がったと思いますので、どうぞあわせてお読みください。

追記(2017年7月5日)

さらに、jsonファイルのデータ読み込みに対応したものも作ってみました。どうぞあわせてお読みください。

追記(2017年7月14日)

本記事のスクリプトをさらに改良し、クロスドメイン対応しました。どうぞあわせてお読みください。

「Web」の他の記事を読む

ページの先頭に戻る