以前、iPhone5sのプロダクトページがリリースした頃に『簡単にパララックス実装可能な軽量スクリプト「skrollr」&コンテンツごとにページスクロールするjQueryプラグイン「fullPage.js」「One Page Scroll」』と題して1ページをフルスクリーン表示にして、画面全体をスクロールさせることで、1ページ分(1コンテンツ分)を画面遷移させるUIを実装できるjQueryプラグインを紹介しましたが、自分なりにもっと使いやすい形で実現できないかやってみた実験をご紹介してみます。
【2014/08/05 追記】
各ブロック内での横スライド機能を追加しました。
jQueryで1ページごとにスクロールして画面遷移させるUIを実装する方法
まずは動作サンプルから。
下記のサンプル画面でマウスホイールスクロールもしくは画面右側のナビゲーションボタン等を使って画面(ページ)を切り替えてみてください。
「jQueryで1ページごとにスクロールして画面遷移させるUIを実装する方法」サンプルを別枠で表示
以前話題になっていたiPhone5sのプロダクトページのように、1つのコンテンツを1画面内でフルスクリーン表示として、スクロールやボタンクリックで画面全体をスクロールさせて、次の画面を表示(遷移)する構成になっています。
ブラウザのウィンドウサイズをリサイズしても、1コンテンツは1画面内で展開する様になっています。
【2014/08/05 追記】
サンプル内では3~5ブロック目に横スライド機能を付けてあります。
設置する横スライドブロックの個数は自由に増減でき開始位置の設定も可能になっています。
このサンプルファイルの全体構成について、まずはHTMLから。
◆HTML <div id="container"> <section id="stage1" class="stageBase"> <div class="fieldWrap"> <h1>jQuery OnePage Scroll</h1> <p>ここは1ページ目</p> </div><!--/.fieldWrap --> </section> <section id="stage2" class="stageBase"> <div class="fieldWrap"> <p>ここは2ページ目</p> </div><!--/.fieldWrap --> </section> <section id="stage3" class="stageBase"> <div class="stageSlide"> <div class="slidePanel"> <div class="fieldWrap"> <p>ここは3ページ目<br> スライドエリア【1】</p> </div><!--/.fieldWrap --> </div><!--/.slidePanel --> <div class="slidePanel"> <div class="fieldWrap"> <p>スライドエリア【2】</p> </div><!--/.fieldWrap --> </div><!--/.slidePanel --> <div class="slidePanel"> <div class="fieldWrap"> <p>スライドエリア【3】</p> </div><!--/.fieldWrap --> </div><!--/.slidePanel --> </div><!--/.stageSlide --> </section> <section id="stage4" class="stageBase"> <div class="stageSlide"> <div class="slidePanel"> <div class="fieldWrap"> <p>スライドエリア【1】</p> </div><!--/.fieldWrap --> </div><!--/.slidePanel --> <div class="slidePanel slideInitial"> <div class="fieldWrap"> <p>ここは4ページ目<br> スライドエリア【2】</p> </div><!--/.fieldWrap --> </div><!--/.slidePanel --> <div class="slidePanel"> <div class="fieldWrap"> <p>スライドエリア【3】</p> </div><!--/.fieldWrap --> </div><!--/.slidePanel --> </div><!--/.stageSlide --> </section> <section id="stage5" class="stageBase"> <div class="stageSlide"> <div class="slidePanel"> <div class="fieldWrap"> <p>スライドエリア【1】</p> </div><!--/.fieldWrap --> </div><!--/.slidePanel --> <div class="slidePanel"> <div class="fieldWrap"> <p>スライドエリア【2】</p> </div><!--/.fieldWrap --> </div><!--/.slidePanel --> <div class="slidePanel"> <div class="fieldWrap"> <p>スライドエリア【3】</p> </div><!--/.fieldWrap --> </div><!--/.slidePanel --> <div class="slidePanel slideInitial"> <div class="fieldWrap"> <p>ここは5ページ目<br> スライドエリア【4】</p> </div><!--/.fieldWrap --> </div><!--/.slidePanel --> <h2>COPYRIGHT © <a href="https://black-flag.net/">BLACKFLAG.NET</a> ALL RIGHTS RESERVED.</h2> </div><!--/.stageSlide --> </section> </div><!--/#container-->
まず、すべてを囲う大枠ブロック要素を用意し(サンプルでは「#container」)、その中に1画面分のベースとなる任意のブロック要素(サンプルでは「.stageBase」)を作成する画面数分、設置します。
※サンプルでは5画面設定にしてあるのでブロック要素「.stageBase」を5つ設置
◆HTML(縦スクロールブロック枠組) <div id="container"> <section class="stageBase"></section> <section class="stageBase"></section> <section class="stageBase"></section> <section class="stageBase"></section> <section class="stageBase"></section> </div><!--/#container-->
サンプルでは各画面を囲うタグを<section>にしていますが、ここは<div>など別のタグで構成することも可能です。
サンプル内3~5ブロック目の横スライド機能については、以下の様に「.stageSlide」のクラスで囲ったブロック要素を配置し、その中に横スライドさせる画面数分「.slidePanel」のクラスでブロック要素を配置します。
◆HTML(横スライドブロック枠組) <div class="stageSlide"> <div class="slidePanel"></div> <div class="slidePanel"></div> <div class="slidePanel"></div> </div><!--/.stageSlide -->
デフォルトの状態ではページロード時、パネル1枚目(一番左)が最初に表示されるようになっていますが、任意の番号を最初に表示させるには「.slidePanel」に、さらに「.slideInitial」のクラスを追加します。
※上記サンプルHTMLソース参照
サンプル動作内では、横スライドの開始位置を
・3ブロック目は全3スライド中、1つ目のスライド(一番左)
・4ブロック目は全3スライド中、2つ目のスライド(真ん中)
・5ブロック目は全4スライド中、4つ目のスライド(一番右)
といったように開始位置を変えてあります。
【2014/08/31 追記】
現在地のスライドパネル(.slidePanel)には、別途クラス「.activeClass」が付くようになっています。
これに対してCSSは以下の様になっています。
◆CSS body { position: relative; overflow: hidden; visibility: hidden; } /* #container --------------------------- */ #container { top: 0; left: 0; width: 100%; position: absolute; z-index: 1; } /* .stageBase --------------------------- */ .stageBase { width: 100%; position: relative; overflow: hidden; } .stageBase .fieldWrap { padding: 100px 0 0 0; text-align: center; } #stage1 {background:#fff;} #stage2 {background:#eee;} #stage3 {background:#ddd;} #stage4 {background:#ccc;} #stage5 {background:#bbb;} /* .stageSlide --------------------------- */ .stageSlide { position: relative; overflow: hidden; visibility: hidden; } .stageSlide .slideWrap { top: 0; left: 0; position: absolute; overflow: hidden; } .stageSlide .slideWrap:before, .stageSlide .slideWrap:after { content: " "; display: table; } .stageSlide .slideWrap:after {clear: both;} .stageSlide .slideWrap {*zoom: 1;} .stageSlide .slidePanel { float: left; overflow: hidden; } .stageSlide .sdPrev, .stageSlide .sdNext { margin-top: -25px; top: 50%; width: 50px; height: 50px; display: block; position: absolute; z-index: 99; } .stageSlide .sdPrev { left: 80px; background: transparent url(../img/slide_prev.png) no-repeat left top; } .stageSlide .sdNext { right: 80px; background: transparent url(../img/slide_next.png) no-repeat left top; } .stageSlide .slideNav { bottom: 70px; left: 0; width: 100%; height: 15px; text-align: center; position: absolute; z-index: 98; } .stageSlide .slideNav a { margin: 0 5px; width: 15px; height: 15px; background: transparent url(../img/nav.png) no-repeat center center; display: inline-block; overflow: hidden; } .stageSlide .slideNav a.pnActive { background: transparent url(../img/nav_acv.png) no-repeat center center; } /* #pageNav --------------------------- */ #pageNav { top: 0; right: 25px; width: 15px; text-align: center; position: fixed; z-index: 2; } #pageNav ul { width: 15px; display: block; } #pageNav ul li { padding-bottom: 5px; width: 15px; height: 15px; display: block; overflow: hidden; } #pageNav ul li a { width: 15px; height: 15px; background: transparent url(../img/nav.png) no-repeat center center; display: block; } #pageNav ul li.activeStage a { background: transparent url(../img/nav_acv.png) no-repeat center center; } /* #pageDown --------------------------- */ #pageDown { bottom: 0; left: 0; width: 100%; height: 40px; text-align: center; position: fixed; overflow: hidden; z-index: 3; } #pageDown a { margin: 0 auto; width: 30px; height: 30px; background: transparent url(../img/next_arw.png) no-repeat center center; display: block; }
bodyに対して「position: relative;」「overflow: hidden;」「visibility: hidden;」は必須となっております。
全体を囲うブロック要素「#container」を「position: absolute;」にして、この要素を上下移動させて画面遷移をしている構成になります。
縦に並ぶブロック要素のレイアウト等を指定する部分は「.stageBase」、横スライドのブロック要素のレイアウト等を指定する部分は「.stageSlide」になりますので、ページネーション部分含め、装飾部分をCSSで調整をしてください。
そして実際の動作スクリプトは以下の様になります。
◆SCRIPT <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script> $(function(){ var setWrap = $('#container'), setBase = $('.stageBase'), scrollSpeed = 1000, scrollEasing = 'swing', slideSpeed = 500, slideEasing = 'linear', downBtn = 'show', // 'show' or 'hide' urlHash = 'on', // 'on' or 'off' setHash = '!page'; var url = document.URL, stageSlide = $('.stageSlide'); setWrap.append('<nav id="pageNav"><ul></ul></nav>'); setBase.each(function(i){ $('#pageNav ul').append('<li class="pagePn'+(i+1)+'"><a href="javascript:void(0);"></a></li>'); }); if(downBtn == 'show'){ setWrap.append('<div id="pageDown"><a href="javascript:void(0);"></a></div>'); } var coreNav = $('#pageNav'), setNav = coreNav.find('ul'), navList = setNav.find('li'), navLength = navList.length; setNav.find('li:first').addClass('activeStage'); $('body').attr('data-page','1'); $(window).load(function(){ // StageHeight $(window).resize(function(){ var wdHeight = $(window).height(); setBase.css({height:wdHeight}); var resizeContTop = parseInt(setWrap.css('top')); if(resizeContTop === 0){ setWrap.css({top:'0'}); } else { var activeStagePos = setNav.find('li.activeStage'); activeStagePos.each(function(){ var posIndex = navList.index(this); setWrap.css({top:-(wdHeight*posIndex)}); }); } coreNav.each(function(){ var navHeight = $(this).height(); $(this).css({top:((wdHeight)-(navHeight))/2}); }); }).resize(); // StageSlide stageSlide.each(function(){ var thisSlide = $(this), chdPanel = thisSlide.find('.slidePanel'), chdPanelLength = chdPanel.length; chdPanel.eq('0').addClass('activePanel').end().wrapAll('<div class="slideWrap"></div>'); thisSlide.append('<a href="javascript:void(0);" class="sdPrev"></a><a href="javascript:void(0);" class="sdNext"></a><div class="slideNav"></div>'); var thisWrap = thisSlide.find('.slideWrap'), thisPrev = thisSlide.find('.sdPrev'), thisNext = thisSlide.find('.sdNext'), thisPn = thisSlide.find('.slideNav'); chdPanel.each(function(i){ thisPn.append('<a href="javascript:void(0);" class="slidePn'+(i+1)+'"></a>'); }); var pnPoint = thisPn.find('a'), pnFirst = thisPn.find('a:first'), pnLast = thisPn.find('a:last'), pnCount = thisPn.find('a').length; pnFirst.addClass('pnActive'); pnPoint.click(function(){ var pnNum = pnPoint.index(this), mvWidth = chdPanel.width(), wpWidth = thisWrap.width(), moveLeft = mvWidth*pnNum; thisWrap.stop().animate({left: -(moveLeft)},slideSpeed,slideEasing); pnPoint.removeClass('pnActive'); $(this).addClass('pnActive'); pnAcvCheck(); }); thisPrev.click(function(){ thisWrap.not(':animated').each(function(){ thisPn.find('.pnActive').prev().click(); pnAcvCheck(); }); }); thisNext.click(function(){ thisWrap.not(':animated').each(function(){ thisPn.find('.pnActive').next().click(); pnAcvCheck(); }); }); function pnAcvCheck(){ var pnAcvNum = thisPn.find('.pnActive'); pnAcvNum.each(function(){ var acvIndex = pnPoint.index(this); acvCount = acvIndex+1; if(1 == acvCount){ thisPrev.css({display:'none'}); thisNext.css({display:'block'}); } else if(pnCount == acvCount){ thisPrev.css({display:'block'}); thisNext.css({display:'none'}); } else { thisPrev.css({display:'block'}); thisNext.css({display:'block'}); } chdPanel.removeClass('activePanel').eq(acvIndex).addClass('activePanel'); }); } pnAcvCheck(); $(window).resize(function(){ var setWrapLeft = parseInt(thisWrap.css('left')), setPanelWidth = chdPanel.width(), setLeft = setWrapLeft / setPanelWidth; var sdWidth = $(window).width(), sdHeight = $(window).height(); thisSlide.css({width:sdWidth,height:sdHeight}); thisWrap.css({width:(sdWidth*chdPanelLength),height:sdHeight}); chdPanel.css({width:sdWidth,height:sdHeight}); var setWidth = chdPanel.width(), adjLeft = setWidth * setLeft; thisWrap.css({left:(adjLeft)}); }).resize(); var thisInt = thisWrap.find('.slideInitial'); thisInt.each(function(){ var pnlInt = thisWrap.find('.slideInitial'); pnlInt.each(function(){ var intIndex = chdPanel.index(this); pnPoint.eq(intIndex).click(); }); }); setTimeout(function(){ thisSlide.css({visibility:'visible',opacity:'0'}).animate({opacity:'1'},slideSpeed); },slideSpeed); }); // MouseWheelEvent var mousewheelevent = 'onwheel' in document ? 'wheel' : 'onmousewheel' in document ? 'mousewheel' : 'DOMMouseScroll'; $(document).on(mousewheelevent,function(e){ if(!(setWrap.is(':animated'))){ e.preventDefault(); var delta = e.originalEvent.deltaY ? -(e.originalEvent.deltaY) : e.originalEvent.wheelDelta ? e.originalEvent.wheelDelta : -(e.originalEvent.detail); if (delta < 0){ motionDown(); } else { motionUp(); } } }); // KeyEvent $('html').keydown(function(e){ if(setWrap.is(':animated') || setWrap.find('*').is(':animated')){ e.preventDefault(); } else { var acvStgSwP = parseInt($('body').attr('data-page')); switch(e.which){ case 33: // Key[PgUp] e.preventDefault(); motionUp(); break; case 34: // Key[PgDn] e.preventDefault(); motionDown(); break; case 38: // Key[↑] e.preventDefault(); motionUp(); break; case 40: // Key[↓] e.preventDefault(); motionDown(); break; case 37: // Key[←] e.preventDefault(); var dsChkP = $('#stage' + acvStgSwP + ' .sdPrev').css('display'); if (!(dsChkP == 'none')){ $('#stage' + acvStgSwP + ' .sdPrev').click(); } break; case 39: // Key[→] e.preventDefault(); var dsChkN = $('#stage' + acvStgSwP + ' .sdNext').css('display'); if (!(dsChkN == 'none')){ $('#stage' + acvStgSwP + ' .sdNext').click(); } break; } } }); // FlickEvent var isTouch = ('ontouchstart' in window); setWrap.on( {'touchstart': function(e){ if(setWrap.is(':animated')){ e.preventDefault(); } else { this.pageY = (isTouch ? event.changedTouches[0].pageY : e.pageY); this.topBegin = parseInt($(this).css('top')); this.top = parseInt($(this).css('top')); this.touched = true; } },'touchmove': function(e){ if(!this.touched){return;} e.preventDefault(); this.top = this.top - (this.pageY - (isTouch ? event.changedTouches[0].pageY : e.pageY)); this.pageY = (isTouch ? event.changedTouches[0].pageY : e.pageY); },'touchend': function(e){ if (!this.touched) {return;} this.touched = false; if(((this.topBegin)-30) > this.top){ motionDown(); } else if(((this.topBegin)+30) < this.top){ motionUp(); } } }); // ScrollUpEvent function motionUp(){ var stageHeightU = setBase.height(), contTopUp = parseInt(setWrap.css('top')), moveTopUp = contTopUp + stageHeightU; $('input,textarea').blur(); if(!(contTopUp === 0)){ setWrap.stop().animate({top:moveTopUp},scrollSpeed,scrollEasing); setNav.find('li.activeStage').removeClass('activeStage').prev().addClass('activeStage'); var acvStageP = parseInt($('body').attr('data-page')), setPrev = acvStageP-1; $('body').attr('data-page',setPrev); if(downBtn == 'show'){ pagePos(); } } if(urlHash == 'on'){ replaceHash(); } } // ScrollDownEvent function motionDown(){ var stageHeightD = setBase.height(), contTopDown = parseInt(setWrap.css('top')), moveTopDown = contTopDown - stageHeightD; $('input,textarea').blur(); var contHeight = setWrap.height(), maxHeightAdj = -(contHeight - stageHeightD); if(!(contTopDown == maxHeightAdj)){ setWrap.stop().animate({top:moveTopDown},scrollSpeed,scrollEasing); setNav.find('li.activeStage').removeClass('activeStage').next().addClass('activeStage'); var acvStageN = parseInt($('body').attr('data-page')), setNext = acvStageN+1; $('body').attr('data-page',setNext); if(downBtn == 'show'){ pagePos(); } } if(urlHash == 'on'){ replaceHash(); } } // SideNaviClick navList.click(function(){ if(!(setWrap.is(':animated'))){ var crtIndex = navList.index(this), crtHeight = $(window).height(); setWrap.stop().animate({top:-(crtHeight*crtIndex)},scrollSpeed,scrollEasing); setNav.find('li.activeStage').removeClass('activeStage'); $(this).addClass('activeStage'); $('body').attr('data-page',crtIndex+1); if(downBtn == 'show'){ pagePos(); } if(urlHash == 'on'){ replaceHash(); } } }); // PageDownBtnClick $('#pageDown a').click(function(){ if(!(setWrap.is(':animated'))){ var navActive = setNav.find('li.activeStage'); navActive.each(function(){ var navIndex = navList.index(this), setNav = navIndex+1; if(!(setNav == navLength)){ $(this).next().click(); } }); if(urlHash == 'on'){ replaceHash(); } } }); function pagePos(){ var pnAcv = coreNav.find('li.activeStage'); pnAcv.each(function(){ var pnIndexN = navList.index(this), pnCountN = pnIndexN+1; if(pnCountN == navLength){ $('#pageDown').css({display:'none'}); } else { $('#pageDown').css({display:'block'}); } }); } // HashReplace function replaceHash(){ var pnAcv = coreNav.find('li.activeStage'); pnAcv.each(function(){ var pnIndexN = navList.index(this), pnCountN = pnIndexN+1; location.hash = setHash + pnCountN; }); } if(urlHash == 'on'){ replaceHash(); } // OpeningFade $('body').css({visibility:'visible',opacity:'0'}).animate({opacity:'1'},1000); // LoadPageMove if(url.indexOf(setHash) !== -1){ var numSplit = ((url.split(setHash)[1])-1); navList.eq(numSplit).click(); } }); // HashChangeEvent if(urlHash == 'on'){ $(window).on('hashchange',function(){ var stateUrl = document.URL, hashSplit = ((stateUrl.split(setHash)[1])-1); navList.eq(hashSplit).click(); }); } }); </script>
スクリプト開始部分にある設定値の内容は以下の様になっています。
var setWrap = $(‘#container’) | すべてを囲うブロック要素(クラスでも可) |
---|---|
setBase = $(‘.stageBase’) | 1画面を囲うブロック要素 |
scrollSpeed = 1000 | スクロール(縦)移動スピード |
scrollEasing = ‘swing’ | スクロール(縦)移動する際のイージング |
slideSpeed = 500 | 横スライド時の移動スピード |
slideEasing = ‘linear’ | 横スライド時のイージング |
downBtn = ‘show’ | 画面下にNEXTボタンの設置有無(表示 = show, 非表示 = hide) |
urlHash = ‘on’ | ハッシュをつけてのURL操作の有無(有 = on, 無 = off) |
setHash = ‘!page’ | URLの後ろに付けるハッシュ用テキスト |
これらの設定値を変更することで微調整が可能になっています。
サンプル画面で表示している画面下の▼ボタンは「downBtn」の指定を変えてもらうことで、表示/非表示を切り替えることができます。
※見た目の装飾についてはCSS側で「#pageDown、#pageDown a」を調整します。
画面右側のナビゲーションボタンについては、作成する画面数によって自動でリストを生成するようになっており、見た目等を変える場合はCSS側で「#pageNav」の部分を調整します。
マウスホイールでのスクロールと別にフリック(スワイプ?)動作での制御も入れてありますので、iPhone/iPadなどのスマホ・タブレットでも動作すると思います。
【2017/01/09 追記】
キーボードの上下矢印キー操作で画面移動するほかに、左右の矢印キーでも横スライド機能が動作するように追加しました。
「urlHash = ‘on’」ここを“on”にすることで、URLの最後に各画面を個別に判別する為の「#(ハッシュ)」が付与されるようになります。
付与するテキストは「#」だけでなく識別名を入れられるように「setHash = ‘!page’」の値で設定することが可能です。
※「!」は必須ではないので「#」だけにしたい時は「!」を付けずにテキストのみで記述してください。
ここで設定している「#(ハッシュ)」をhasheventを使うことで、ページ内で画面遷移をした後、ブラウザでの「戻る」「進む」ボタンでも画面が行き来(スクロール移動)するようになっています。
このページ内でのブラウザの「戻る」「進む」ボタン動作が必要ない場合には「urlHash」の指定を“off”にしてください。
固有のURLから特定画面(○番目の画面)を指定する場合は、このハッシュの後ろに表示する画面番号を付けたURLを使って指定することが可能です。
※画面表示後、スクロールアニメーションが実行されます。
※この動作に関しては「urlHash」の指定を“off”にしていても実行されます。
サンプルページの3ページ目を表示【https://black-flag.net/devel/jQueryOnePageScroll/#!page3】
スクリプト全体は画面スクロール遷移動作の枠組みのみとなっているので、それぞれ各画面内のレイアウト等についてはCSSや別途JSなどのスクリプトを使って形成する形になります。
スクリプト自体はシンプルではないかもしれませんが、HTMLとCSS構成に関してはカスタマイズしやすい形にはなっているかな、、と思っております。
jQueryを使って1ページごとにスクロールして画面遷移させるUIを実装する際にぜひ。。。