Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
了控制篇幅,本打算分3篇文章來講述輪播圖實現的,后來想了想,JS代碼量其實很少,完全沒必要,所以這篇文章就是收尾。
昨天,我們完成了結構和布局設計,今天我們來完成JavaScript邏輯部分。我這人比較有代碼潔癖,不喜歡過多的變量污染全局作用域,習慣使用自執行的匿名函數包裹一個功能塊。我們所有的JS代碼都在如下函數內部編寫:
(function () {
// ... ... 我們的代碼都寫到這兒
})();
我們的程序中需要用到多個DOM元素,所以我們先拿到它們的引用存儲到變量中。
/** @type {HTMLDivElement} 輪播圖容器 */
const carouselWrapper=document.querySelector('.carousel-wrapper')
/** @type {HTMLUListElement} banner列表元素 */
const slides=carouselWrapper.querySelector('.slides')
/** @type {HTMLDivElement} 指示器 */
const indicator=carouselWrapper.querySelector('.indicators')
/** @type {NodeListOf<HTMLLIElement>} 指示器節點列表 */
const indicatorItems=indicator.querySelectorAll('.indicator-item')
/** @type {HTMLUListElement} 導航按鈕列表元素 */
const nav=carouselWrapper.querySelector('.nav')
接下來,我們定義一些常量和變量,建議大家定義常量名使用全大寫字母下劃線分割。
/** 活動類名 */
const ACTIVE_CLS_NAME='active'
/** 左導航按鈕類名 */
const CLS_NAME_NAV_LEFT='nav-left'
/** 右導航按鈕類名 */
const CLS_NAME_NAV_RIGHT='nav-right'
/** 取消過渡的類名 */
const CLS_NAME_NO_TRANSITION='no-transition'
/** 定時器間隔 */
const TIMER_DELAY=3000
/** 定時器id */
let timerId
/** 當前指示器索引 */
let currentIndex=0
/** 當前banner索引 */
let currentslideItemIndex=0
/** 過渡結束標識 */
let transitionendFlag=true
要做到左右滾動的無縫切換,我們必須至少拷貝banner列表中的第一和最后一個元素,將其追加到banner列表尾部,并更新banner列表的寬度。為了節省代碼,我完全拷貝了一份banner列表的子元素。我們用一個函數來做這件事,代碼如下:
function updateSlides() {
slides.innerHTML +=slides.innerHTML
slides.style.width=`${slides.childElementCount * 100}%`
}
當用戶單擊指示器項的時候,我們需要切換當前顯示的banner圖。首先需要確定被單擊指示器項的索引。我們用一個函數來實現,代碼如下:
function getTargetIndex(el) {
for (let i=0, len=indicatorItems.length; i < len; i++) {
if (indicatorItems[i]===el) {
return i
}
}
return -1
}
索引確定了,我們需要根據索引來更新X軸的平移值。當然,要實現左右滾動,我們還有其它實現方案,例如:改變可滾動元素的滾動條位置,改變元素的左右定位等。那么,為什么使用3D平移變換呢?原因之一就是,瀏覽器會對3D平移變換開啟GPU硬件加速渲染,以獲得更好的性能,直觀感受就是動畫非常絲滑,毫無拖泥帶水之感。我們用一個函數來更新X軸平移,代碼如下:
function updateTranslateX(index, targetIndex=-1) {
// 如果過渡沒有結束,直接返回
if (!transitionendFlag) return
// 當前指示器移除活動類
indicatorItems[currentIndex].classList.remove(ACTIVE_CLS_NAME)
// 如果沒有提供目標索引,就把index賦給它
if (targetIndex < 0) {
targetIndex=index
}
// 目標指示器添加活動類
indicatorItems[targetIndex].classList.add(ACTIVE_CLS_NAME)
// 將當前索引設為目標索引
currentIndex=targetIndex
// 將當前banner索引設為index
currentslideItemIndex=index
// 改變banner列表元素的X軸平移變換
slides.style.transform=`translateX(-${index / slides.childElementCount * 100}%)`
}
以下是指示器單擊處理函數的實現。這個代碼很簡單,首先獲取目標指示器項的索引,如果找到了,就根據該索引更新banner列表的X軸平移。
/** @param {MouseEvent} e */
function indicatorClickHandler(e) {
const index=getTargetIndex(e.target)
index >=0 && updateTranslateX(index)
}
接下來,我們實現左右導航按鈕的單擊。首先,我們需要一個向左和向右滾動的函數,代碼如下:
function toNext() {
// 指示器項的數量
const len=indicatorItems.length
if (currentIndex < len) {
if (currentIndex===len - 1) {
// 繼續滾動到下一個索引位置,并將目標索引設為0
updateTranslateX(currentIndex + 1, 0)
} else {
updateTranslateX(currentIndex + 1)
}
} else {
updateTranslateX(0)
}
}
function toPrev() {
const len=indicatorItems.length
if (currentIndex < 1) {
// 去掉過渡
slides.classList.add(CLS_NAME_NO_TRANSITION)
// 讓索引為len的banner圖可見
updateTranslateX(len, len - 1)
setTimeout(()=> {
// 添加過渡
slides.classList.remove(CLS_NAME_NO_TRANSITION)
// 讓索引為len-1的banner圖可見,此時有過渡
updateTranslateX(len - 1)
}, 0)
} else {
updateTranslateX(currentIndex - 1)
}
}
以下是左右導航按鈕單擊事件處理函數的實現。我們根據是否包含指定的類名,來判斷單擊的哪個按鈕,進而執行不同的導航函數。
/** @param {MouseEvent} e */
function navClickHandler(e) {
/** @type {{target: HTMLElement}} */
const { target }=e
if (target.classList.contains(CLS_NAME_NAV_RIGHT)) {
toNext()
} else if (target.classList.contains(CLS_NAME_NAV_LEFT)) {
toPrev()
}
}
我們還需要監聽banner列表的過渡開始和結束事件。過渡開始事件處理函數非常簡單,只是將過渡結束標志設為了false,代碼如下:
function slidesTransitionstartHandler(e) {
transitionendFlag=false
}
下面是過渡結束事件處理函數定義,其實也很簡單。
function slidesTransitionendHandler(e) {
transitionendFlag=true
if (currentslideItemIndex >=indicatorItems.length) {
// 取消過渡
slides.classList.add(CLS_NAME_NO_TRANSITION)
updateTranslateX(0)
setTimeout(()=> {
// 再添加回過渡
slides.classList.remove(CLS_NAME_NO_TRANSITION)
}, 0)
}
}
我們還需要讓輪播圖可以間隔一定時間自動切換。所以,我們需要實現開始和結束定時器的函數。這兩個函數非常簡單,代碼如下:
function startTimer() {
timerId=setInterval(toNext, TIMER_DELAY)
}
function stopTimer() {
clearInterval(timerId)
timerId=null
}
現在,是時候讓我們的輪播圖動起來了。我們首先調用更新banner列表函數,然后開始定時器,最后注冊事件監聽器。
updateSlides()
startTimer()
// 指針進入容器的時候停止定時器
carouselWrapper.addEventListener('mouseenter', stopTimer)
// 指針離開容器的時候開始定時器
carouselWrapper.addEventListener('mouseleave', startTimer)
nav.addEventListener('click', navClickHandler)
indicator.addEventListener("click", indicatorClickHandler)
slides.addEventListener('transitionstart', slidesTransitionstartHandler)
slides.addEventListener('transitionend', slidesTransitionendHandler)
最終實現的效果圖如下:
童鞋們都學廢了嗎?是不是很簡單?感謝閱讀!
先讓我們回顧一下系列內容。
從零開始學Bootstrap(1)介紹了BootStrap最簡單的模板,逐條解釋了每行代碼的含義。
從零開始學Bootstrap(2)強調了邊學邊做,通過實際的例子,講解了如何一步一步的實現自己想要的效果。
寫到這里,這篇從零開始學Bootstrap(3)我想寫以下幾個內容:
1. 基于我對Bootstrap的理解,做一個小小的總結。
2. 對從零開始學Bootstrap(2)例子進行UI美化和代碼優化,主要是說說我是怎么做出自己想要的效果的。
3. 授人以魚不如授人以漁,以自己的例子(因為自己也是新手,寫出來的東西可能更適合初學者),講講代碼編寫過程中遇到的坑和需要注意的點。
廢話不多說,下面開始:
一、 Bootstrap的小小總結
基于以Bootstrap的官方文檔(http://v3.bootcss.com/)的說明,Bootstrap分為三個大塊:CSS樣式,組件,Javascript插件。
首先要明確一點:Bootstrap是一個框架,意思就是別人造好了輪子,你可以直接拿來用,免去了自己去造輪子。所以咱們需要明確兩點:這些輪子是什么樣的輪子,怎么樣去使用這些輪子。
1. CSS樣式:主要介紹了柵格系統和Bootstrap的全局樣式。通過設定Class的值實現。
1.1柵格系統:
讓我們可以很方便的實現HTML頁面的布局(http://v3.bootcss.com/css/#grid)。
我覺得柵格系統很重要。因為HTML頁面的布局是很重要也很繁瑣的一項任務(你看一下W3School里關于布局的介紹http://www.w3school.com.cn/html/html_layout.asp,看一下例子里的代碼,就明白了),并且需要考慮到不同瀏覽器、不同設備的兼容性。
柵格系統把這一切大大簡化了。打開上面關于柵格系統的連接,你會發現只需要根據你想要實現的效果,給HTML元素Class屬性賦相應的值,就可以實現,并且還能設置針對不同屏幕大小的設備展現不同的效果。
1.2 Bootstrap全局樣式:
也就是Bootstrap對常用HTML元素(eg: DIV、Button、 P、 Table、 Img等等)是怎樣美化的。通過給HTML元素的Class屬性賦相應的值,就可以得到自己想要的效果。
舉一個最簡單的例子:
如上圖所示,Bootstrap可以讓你僅僅改變Button元素的class的值就是改變按鈕的大小,而不用很麻煩的去修改css文件,或者給元素內嵌style的值。
2. 組件:我認為組件就是Bootstrap把一些元素(HTML元素和Javascript代碼)組合起來,就變成了組件,并且提供了很多很好看很實用的圖標。這些組件基本都是HTML開發過程中常用的。通過設定Class的值實現。(http://v3.bootcss.com/components/)
舉一個最簡單的例子:
如上圖所示,當我們需要實現導航功能的時候。找到Boostrap里相應的組件,依照其給的代碼模板,根據自己的需求,賦相應的class和ul、li值就可以了
3. Javascript插件:我認為Bootstrap的Javascript插件就是封裝了常用網頁交互功能的”輪子”。只需要設定class屬性和data屬性就可以實現常用的網頁交互功能,而不用自己寫一大堆javascript代碼。
首先說一個小插曲,新手可能誤以為”javascript”和”java”有著很深的聯系,甚至會認為javascript是java的變種。其實并不是這樣,javascript是網景(Netscape)公司開發的應用于互聯網的腳本語言,其實它最先的名字”是livescript”,后來網景公司與Sun公司(也就是發明Java的公司,后來被Oracle收購)達成合作,當時Java語言如日中天,名氣很大,為了搭順風車,就把livesript改名為javascript。以至于有人開玩笑:”Java”和”Javascrip”的區別就好比”雷鋒”和”雷峰塔”的區別一樣。
言歸正傳,我們知道,Javascript是為了賦予網頁交互功能而存在的。所以,Bootsript上豐富的Javascript插件能夠讓你很方便的實現常用的網頁交互功能,而不用把精力放在”造輪子”上。
如上圖所示,利用Bootstrap的輪播插件(http://www.runoob.com/bootstrap/bootstrap-carousel-plugin.html, Bootstrap的官方文檔這里沒有翻譯成中文,runoob上卻有很詳細的中文翻譯,并且可以在線修改代碼提交觀察效果,強烈推薦),你可以很方便的實現現在很多網站都采用的圖片輪播功能。這里只需要根據上述鏈接里面的教程,賦相應的class和圖片src值就可以了,連data值都不用設。
二、 對從零開始學Bootstrap(2)例子進行UI美化和代碼優化
下圖是上期教程中實現的效果:
我們可以看到有以下幾個缺點需要改正:
(1) 點擊具體同學,顯示其信息之后,沒有處于被選中的狀態。(你一開始點擊,會處于被選中狀態,但那只是button的點擊效果,你點一下空白處,被選中的狀態就消失了)
(2) 色彩單調,不太美觀。
(3) 布局方面需要調整,信息框是呈現我們需要的信息的地方,應該盡可能大,最好其顯示的時候能把不需要的框給隱藏起來。
(4) 對于代碼方面,對從零開始學Bootstrap(2)中的Javascript代碼都寫在HTML頁面里了,并且有重復的代碼段,應該把重復的代碼段寫成函數,這樣可以減少代碼體積,再一個修改代碼的時候,我直接修改相應的函數就可以了,而不用一個地方一個地方的找。Javascript代碼可以寫到JS文件里,實現HTML頁面和Javascript代碼的分離。
技術選型(直白的說就是思考怎樣利用Bootstrap框架里已有的東西,實現想要的效果):
1、首先調整布局,把信息框和同學框調整在一起,班級框顯示在最上面。
顯然,我們只需要把信息框所在的DIV和同學框的DIV放在同一個row的DIV里,并把修改跟柵格系統相關的class屬性值”col-md”就可以實現。比如我們想讓他們2:1的比例顯示,那么信息框的class屬性應該有col-md-8,而同學框的應該是col-md-4了。
值得注意的是,前端永遠不可能是一步到位的開發,往往最開始的代碼都不是我們想要的完美結果,需要細細調整。以下面為例,大家要學會自己搜,去嘗試,去不斷的調整(后面的調整,具體過程就不交代了):
我們修改完代碼后,呈現的效果是下面這樣的:
很明顯的,上面每行只顯示一個Class,有點浪費空間。下面的兩個部分沒有對齊。
把上面DIV組件的class屬性里的”btn-group-vertical”刪掉,把col-md-6添加到js代碼里的tmp_button的class屬性中。另外觀察到這樣設定了之后,第一行與第二行相比有奇怪的縮進:
毫無疑問,這種外觀、布局類的變化跟CSS有關。這時候我們可以看一下元素具體的CSS。
以Chrome為例:
在這個元素上點鼠標右鍵,選擇Inspect,即審查,你會在右邊的框里查到相應的代碼。通過比對,我們發現是margin-left的問題,這個屬性是bootstrap框架里默認的,從上層元素繼承而來,有的為 -1px,有的為0px,我們只需要改成一樣就可以了,比如都改成0px:
在js代碼里的tmp_button,修改style屬性,添加”margin-left:0px;”,有的人覺得字體居中不好看,可以添加text-align:left,設定按鈕上的文字從左邊起。
改正后的效果:
2、添加折疊按鈕,讓用戶可以通過這個按鈕隱藏/打開class框,點擊classmate顯示詳細信息的時候,自動隱藏,以把大量的空間省給信息框來顯示。
另外,我們可以給折疊按鈕加一個好看的圖標,參考http://v3.bootcss.com/components/#glyphicons的教程就可以輕松實現。
實現點擊classmate顯示詳細信息的時候,自動隱藏,需要修改classmate按鈕的點擊事件,即相應的js代碼。
我們查看Bootstrap折疊插件的用法http://www.runoob.com/bootstrap/bootstrap-collapse-plugin.html(這個更好,官方的這部分還沒翻譯好),查到下面的內容:
原來Class屬性里的cllapse和in值控制了隱藏和顯示功能,那我們只需要在按鈕的click事件js代碼里修改相應的要進行”顯示/隱藏”操作的HTML元素的class屬性就可以了,于是在classmate按鈕的click函數里添加以下語句就可以了:
$("#collapseOne").attr("class","panel-collapse collapse");
3、修正”點擊具體同學,顯示其信息之后,沒有處于被選中的狀態”的bug。
我們通過查閱文檔,http://www.runoob.com/bootstrap/bootstrap-button-plugin.html, 給button的data-toggle屬性設置為”button”,可以使其變成點擊后自動呈現被選中的狀態。
所以我們給classmate的button添加屬性data-toggle=”button”。
此時又出現了另一個問題,我想接下去點擊其他classmate的時候,原來點的還是處于激活狀態,怎么辦?
通過查閱http://www.runoob.com/bootstrap/bootstrap-buttons.html, button的class賦值有active時,才會呈現被選中的狀態,也就是說,上面的設定,也就是bootstrap做了這么一件事情:當data-toggle=”button”的按鈕被點擊時,自動把active添加到class里,此時呈現被選中狀態,當再次被點擊時,自動把active從class里移除,就呈現未被選中的狀態。
也就是說,我們只要自己去做這個操作,比如在點擊classmate時,我可以把所有的classmate按鈕的active都從class屬性里移除,這樣點擊完成后,只有我最新點擊的Button處于active狀態。
因此,只需要在classmate按鈕的click函數里加入這樣的語句:
$(".btn-classmate").attr("class","btn btn-default btn-classmate btn-info");
效果如下圖:
4、美化按鈕外觀
找到bootstrap里CSS關于按鈕的部分:
按照教程修改,我這里只是簡單的修改了一下按鈕的顏色,大家可以按照需求自己改。效果如下圖:
5、 HTML頁面和Javascript代碼的分離
其實分為兩步:
第一步:把javascript代碼放到js文件里,并在HTML文件里鏈接上。
第二步:把javascript里復用,或者有明確功能的代碼塊,寫進一個函數里,直接調用函數。
由于這兩步都比較簡單,任何學過編程語言的應該都會。我就不展開寫了。
值得注意的是,在鏈接JS文件時,要注意順序。
比如Bootstrap的JS文件,里面的代碼是基于Jquery寫的,用了很多Jquery的函數,所以Jquery的JS文件要在Bootstrap的JS文件之前聲明鏈接。
同理,咱們的JS文件時基于Bootstrap,所以要在Bootstrap之后,不然代碼就不起作用了。
最后國際慣例,貼一下相關的代碼:
getClassmate.html:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>getClassmate</title> <!-- Bootstrap --> <link href="css/bootstrap.min.css" rel="stylesheet"> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="http://cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script> <script src="http://cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script> <![endif]--> <style type="text/css"> #btn-group-vertical-classes { overflow-y:auto; overflow-x:auto; height:100px; } #btn-group-vertical-classmates { overflow-y:auto; overflow-x:auto; height:500px; } button { text-overflow: ellipsis; overflow: hidden; border-radius: 0px; } #context_div { overflow-y:auto; overflow-x:auto; height:500px; } </style> </head> <body> <div class="container"> <div class="panel panel-default"> <div class="panel-heading"> <h4 class="panel-title" style="text-align:right;"> <a id="collapse_a" data-toggle="collapse" data-parent="#accordion" href="#collapseOne"> <span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span> Click this to show/hide class </a> </h4> </div> <div id="collapseOne" class="panel-collapse collapse in"> <div class="panel-body"> <div class="row" id="div1-classes-classmates"> <div class="btn-group-lg col-md-12" role="group" aria-label="..." id="btn-group-vertical-classes"> <!-- where classes show--> </div> </div> </div> </div> </div> <br></br> <div class="row"> <div class="btn-group-vertical btn-group-lg col-md-4 " id="btn-group-vertical-classmates" role="group" aria-label="..."> <!-- where classmates show--> <button type="button" style="border-radius: 0px;" class="btn btn-default">Click class to show classmate.</button> </div> <div class="form-group"> <div class="col-md-8"> <div class="jumbotron" id="context_div"> <p>Click classmate to show details.</p> </div> </div> </div> </div> </div> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <script src="js/jquery-3.0.0.min.js"></script> <!-- Include all compiled plugins (below), or include individual files as needed --> <script src="js/bootstrap.min.js"></script> <script src="js/script_getClassmate.js"></script> </body> </html>
script_getClassmate.js:
$(document).ready(function{ $.getJSON("resource/classmates.json",function(result){ $.each(result, function(i, field){ var tmp_button=$('<button type="button" style="border-radius: 0px; text-align:left; margin-left:0px" class="btn btn-default btn-class col-md-6 btn-success" data-toggle="button" chosen_state=0></button>').text(i); tmp_button.attr("title",i); $("#btn-group-vertical-classes").append(tmp_button); }); $(".btn.btn-default.btn-class").click(function{ var tmp_chosen=Number($(this).attr("chosen_state"))^1; $(this).attr("chosen_state",String(tmp_chosen)); showClassmates(result); $(".btn.btn-default.btn-classmate").click(function{ $(".btn-classmate").attr("class","btn btn-default btn-classmate btn-info"); $("#collapseOne").attr("class","panel-collapse collapse"); var selected_classmate=$(this).text; showClassmateDetail(result,selected_classmate); }); }); }); }) function showClassmates(result){ $("#btn-group-vertical-classmates").empty; var chosen_list=new Array; $(".btn.btn-default.btn-class").filter(function{ judgeflag=false; if($(this).attr("chosen_state")=="1"){ judgeflag=true; chosen_list.push($(this).text); } return judgeflag; }); $.each(result,function(i,field){ var chosen_list_x; for (chosen_list_x in chosen_list){ if(chosen_list[chosen_list_x]==i){ $.each(field,function(j,field2){ var tmp_button=$('<button type="button" style="border-radius: 0px;" class="btn btn-default btn-classmate btn-info" data-toggle="button" chosen_state=0></button>').text(j); tmp_button.attr("title",j); $("#btn-group-vertical-classmates").append(tmp_button); }); } } }); } function showClassmateDetail(result,selected_classmate){ var classmate_context_item; $("#context_div").empty; $.each(result,function(i,field){ $.each(field,function(j,field2){ if(j==selected_classmate){ $.each(field2,function(k,field3){ //alert(k); //alert(field3); classmate_context_item=k + ": " + field3; var tmp_p=$('<p></p>').text(classmate_context_item); $("#context_div").append(tmp_p); }); } }); }); }
classmates.json:
{ "Class 001": { "Xiao Wang": { "Gender": "Male", "Age": "18", "Interest": "Football", "Hometown": "Shanghai", "Chinese": "78", "Math": "90", "English": "66", "Physics": "81", "Chemistry": "88", "History": "69", "Geography": "92" }, "Xiao Li": { "Gender": "Male", "Age": "20", "Interest": "Basketball", "Hometown": "Beijing", "Chinese": "98", "Math": "77", "English": "97", "Physics": "72", "Chemistry": "73", "History": "88", "Geography": "81" } }, "Class 002": { "Xiao Cai": { "Gender": "Female", "Age": "19", "Interest": "Dance", "Hometown": "Gaoxiong", "Chinese": "93", "Math": "80", "English": "92", "Physics": "82", "Chemistry": "77", "History": "89", "Geography": "83" } }, "Class 003": { "Xiao Ma": { "Gender": "Male", "Age": "18", "Interest": "Reading", "Hometown": "Taibei", "Chinese": "91", "Math": "93", "English": "96", "Physics": "97", "Chemistry": "100", "History": "94", "Geography": "92" } }, "Class 005": { "Xiao Zhang": { "Gender": "Male", "Age": "20", "Interest": "Running", "Hometown": "Guangzhou", "Chinese": "67", "Math": "70", "English": "66", "Physics": "80", "Chemistry": 68, "History": "79", "Geography": "93" } } }
主要參考鏈接:
[1].Bootstrap中文官方文檔
http://v3.bootcss.com/
[2].Runoob.com的Bootstrap教程
http://www.runoob.com/bootstrap/bootstrap-tutorial.html
章來自iCSS前端趣聞,https://mp.weixin.qq.com/s/Ct-QCJ02K7rhsYCy6kPiOQ
在 CSS 規范 Scroll-linked Animations[1] 中,推出了一個劃時代的 CSS 功能。也就是 -- The @scroll-timeline[2] at-rule,直譯過來就是滾動時間線。
本文,就將帶大家一探究竟,從入門到學會使用 CSS @scroll-timeline。
什么是 @scroll-timeline 滾動時間線呢?
@scroll-timeline 能夠設定一個動畫的開始和結束由滾動容器內的滾動進度決定,而不是由時間決定。
意思是,我們可以定義一個動畫效果,該動畫的開始和結束可以通過容器的滾動來進行控制。
再系統性學習語法之前,我們通過一個 DEMO,簡單了解一下它的用法:
我們首先實現一個簡單的字體 F 旋轉動畫:
<div id="g-box">F</div>
#g-box {
animation-name: rotate;
animation-duration: 3s;
animation-direction: alternate;
animation-easing-function: linear;
}
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
正常而言,它是這樣一個簡單的動畫:
接下來,我們把這個動畫和 @scroll-timeline 相結合,需要把它放置到一個可滾動的容器中:
<div id="g-content">
<div id="g-box">F</div>
</div>
#g-content {
width: 300px;
height: 170vh;
background: #999;
}
#g-box {
font-size: 150px;
margin: 70vh auto 0;
animation-name: rotate;
animation-duration: 3s;
animation-direction: alternate;
animation-easing-function: linear;
animation-timeline: box-rotate;
}
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
@scroll-timeline box-rotate {
source: selector("#g-content");
}
這里,我們實現了一個可滾動容器 #g-content,它的高度是 170vh,也就是可視界面高度的 1.7 倍,并且把 #g-box 容器放置在一個距離頂部 70vh 高度的地方:
有意思的來了,我們設置的旋轉動畫不會自動開始,只有當我們向下滾動的時候,動畫才會開始進行,實際效果 Gif:
CodePen Demo -- @scroll-timeline Demo[3]
看到這里,大家應該能夠理解 @scroll-timeline 的作用及含義了,它賦予了 CSS 能夠基于滾動條的滾動去控制動畫行進的能力! Amazing!!
接下來,我們先緩一緩,簡單看一看 @scroll-timeline 的語法。
使用 @scroll-timeline,最核心的就是需要定義一個 @scroll-timeline 規則:
@scroll-timeline moveTimeline {
source: selector("#g-content");
orientation: vertical;
scroll-offsets: 0px, 500px;
}
其中:
scroll-offsets 的理解會比較困難,我們稍后詳述。
在設定了一個 @scroll-timeline 之后,我們只需要將它和動畫綁定起來即可,通過 animation-timeline:
@scroll-timeline moveTimeline {
source: selector("#g-content");
orientation: vertical;
scroll-offsets: 0px, 500px;
}
div {
animation-name: move;
animation-duration: 3s;
animation-timeline: moveTimeline;
}
@keyframes move{
0% {
transform: translate(0, 0);
}
100% {
transform: translate(100%, 0);
}
}
之前在 不可思議的純 CSS 滾動進度條效果[6] 一文中,我們介紹了一種使用漸變實現的純 CSS 滾動進度指示器效果:
該方法有些小小的瑕疵。其中一個就是當滾動距離太短的時候,進度條右側會有明顯的斜邊效果。
而有了 @scroll-timeline 之后,我們終于可以將滾動和動畫這兩個元素綁定起來,再實現滾動進度指示器,就已經非常輕松了:
<div id="g-container">
<p>...文本內容...</p>
</div>
#g-container {
width: 100vw;
}
#g-container::before {
content: "";
position: fixed;
height: 5px;
left: 0;
top: 0;
right: 0;
background: #ffc107;
animation-name: scale;
animation-duration: 1s;
animation-fill-mode: forwards;
animation-timeline: box-rotate;
transform-origin: 0 50%;
}
@keyframes scale {
0% {
transform: scaleX(0);
}
100% {
transform: scaleX(1);
}
}
@scroll-timeline box-rotate {
source: auto;
orientation: vertical;
}
完整的代碼,你可以戳這里:CodePen Demo - 使用 @scroll-timeline 實現滾動進度條[7]
大家可以再看看上面的 Gif 圖,都有一個問題,就是動畫的開始時間都是從滾動一開始就開始了,剛好在滾動結束時結束。那么如果我希望動畫在滾動的特定階段觸發,那該怎么辦呢?
這里,就需要借助 scroll-offsets,去更加精確的控制我們的動畫。
在滾動過程中,我們可以將一個元素,劃分為 3 個區域:
在這里,我們就可以得到兩個邊界,上方邊界,下方邊界:
而對于上下兩個邊界,又會有兩種狀態。以上邊界為例子,會有:
對于這兩種狀態,我們用 start 0 和 start 1 表示,同理,下方的邊界也可以用 end 0 和 end 1 表示:
這里的 0 和 1 實際表示的是,元素滾動中預期可見的百分比。
有了這些狀態值,配合 scroll-offsets,我們就可以精確控制滾動動畫的觸發時間。
我們設定一個從左向右并且伴隨透明度變化的動畫,的看看下面幾種情況:
動畫運行范圍:end 0 --> end 1:
@keyframes move {
0% {
transform: translate(-100%, 0);
opacity: 0;
}
100% {
transform: translate(0, 0);
opacity: 1;
}
}
@scroll-timeline box-move {
source: auto;
orientation: "vertical";
scroll-offsets:
selector(#g-box) end 0,
selector(#g-box) end 1;
/* Legacy Descriptors Below: */
start: selector(#g-box) end 0;
end: selector(#g-box) end 1;
time-range: 1s;
}
#g-box {
animation-name: move;
animation-duration: 3s;
animation-fill-mode: both;
animation-timeline: box-move;
}
效果如下:
動畫運行范圍:end 1 --> start 1:
// ...
@scroll-timeline box-move {
source: auto;
orientation: "vertical";
scroll-offsets:
selector(#g-box) end 1,
selector(#g-box) start 1;
/* Legacy Descriptors Below: */
start: selector(#g-box) end 1;
end: selector(#g-box) start 1;
time-range: 1s;
}
// ...
效果如下:
動畫運行范圍:start 1 --> start 0:
// ...
@scroll-timeline box-move {
source: auto;
orientation: "vertical";
scroll-offsets:
selector(#g-box) start 1,
selector(#g-box) start 0;
/* Legacy Descriptors Below: */
start: selector(#g-box) start 1;
end: selector(#g-box) start 0;
time-range: 1s;
}
// ...
效果如下:
掌握 scroll-offsets 的用法是靈活運用滾動時間線的關鍵,當然,在上面你還會看到 start: selector(#g-box) start 1 和 end: selector(#g-box) start 0 這種寫法,這是規范歷史遺留問題,最新的規范已經使用了 scroll-offsets 去替代 start: 和 end: 的寫法。
把上述 3 種情況放在一起,再比較比較:
完整的代碼,你可以戳這里:CodePen Demo - @scroll-timeline Demo | element-based offset[8]
在能夠掌握 @scroll-timeline 的各個語法之后,我們就可以開始使用它創造各種動畫效果了。
譬如如下的,滾動內容不斷劃入:
代碼較長,可以戳這里,來自 bramus 的 Codepen CodePen Demo -- Fly-in Contact List (CSS @scroll-timeline version)[9]
甚至可以結合 scroll-snap-type 制作一些全屏滾動的大屏特效動畫:
要知道,這在以前,是完全不可能利用純 CSS 實現的。完整的代碼你可以戳這里:CodePen Demo -- CSS Scroll-Timeline Split Screen Carousel[10]
簡而言之,任何動畫效果,如今,都可以和滾動相結合起來,甚至乎是配合 SVG 元素也不例外,這里我還簡單改造了一下之前的一個 SVG 線條動畫:
完整的代碼你可以戳這里:CodePen Demo -- SVG Text Line Effect | Scroll Timeline[11]
@scroll-timeline 雖好,目前仍處于實驗室特性時間,Chrome 從 85 版本開始支持,但是默認是關閉的。
開啟該特性需要:
基于目前的兼容性問題,我們可以通過瀏覽器的特性檢測 @supports 語法,來漸進增強使用該功能。
特性檢測的語法也非常簡單:
@supports (animation-timeline: works) {
@scroll-timeline list-item-1 {
source: selector(#list-view);
start: selector(#list-item-1) end 0;
end: selector(#list-item-1) end 1;
scroll-offsets:
selector(#list-item-1) end 0,
selector(#list-item-1) end 1
;
time-range: 1s;
}
// ...
}
通過 @supports (animation-timeline: works) {} 可以判斷瀏覽器是否支持 @scroll-timeline。
目前關于 @scroll-timeline 的相關介紹還非常少,但是它確是能夠改變 CSS 動畫的一個非常大的革新。隨著兼容性的逐漸普及,未來勢必會在 CSS 中占據一席之地。
本文到此結束,希望對你有幫助 :)
如果還有什么疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。
[1]Scroll-linked Animations: https://drafts.csswg.org/scroll-animations-1/
[2]@scroll-timeline: https://drafts.csswg.org/scroll-animations/#at-ruledef-scroll-timeline
[3]CodePen Demo -- @scroll-timeline Demo: https://codepen.io/Chokcoco/pen/JjOZMaQ
[4]: https://developer.mozilla.org/en-US/docs/Web/CSS/length-percentage
[5]animation-duration: https://developer.mozilla.org/en-US/docs/Web/CSS/animation-duration
[6]不可思議的純 CSS 滾動進度條效果: https://www.cnblogs.com/coco1s/p/10244168.html
[7]CodePen Demo - 使用 @scroll-timeline 實現滾動進度條: https://codepen.io/Chokcoco/pen/eYeKLMj
[8]CodePen Demo - @scroll-timeline Demo | element-based offset: https://codepen.io/Chokcoco/pen/qBVyqob
[9]CodePen Demo -- Fly-in Contact List (CSS @scroll-timeline version): https://codepen.io/bramus/pen/bGwJVzg
[10]CodePen Demo -- CSS Scroll-Timeline Split Screen Carousel: https://codepen.io/Chokcoco/pen/QWOrPdM
[11]CodePen Demo -- SVG Text Line Effect | Scroll Timeline: https://codepen.io/Chokcoco/pen/wvPxbRm
*請認真填寫需求信息,我們會在24小時內與您取得聯系。