使用CSS+JS幫你實現蘋果cover flow效果
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>coverflow-demo</title>
- <style>
- div.innerWrapper{
- perspective: 300px;
- width: 600px;
- height: 300px;
- margin: 100px auto;
- display: flex;
- align-items:flex-start;
- background-color: #000;
- overflow: hidden;
- padding-top: 5%;
- }
- div.cover{
- height: 50%;
- flex-grow:1;
- transition: all .5s ease;
- background-size: 100% 100%;
- background-repeat:no-repeat;
- margin: 0;
- -webkit-box-reflect:below 5% linear-gradient(transparent, white);
- border: 1px solid #fff;
- }
- div.cover:nth-child(1){
- background-image: url('covers/computergraphics-album-covers-2014-15.jpg');
- }
- div.cover:nth-child(2){
- background-image: url('covers/Funkadelic-Maggot-Brain-album-covers-billboard-1000x1000.jpg');
- }
- div.cover:nth-child(3){
- background-image: url('covers/Green-Day-American-Idiot-album-covers-billboard-1000x1000.jpg');
- }
- div.cover:nth-child(4){
- background-image: url('covers/insurgency-digital-album-cover-design.jpg');
- }
- div.cover:nth-child(5){
- background-image: url('covers/Pink-Floyd-Dark-Side-of-the-Moon-album-covers-billboard-1000x1000.jpg');
- }
- div.cover:nth-child(6){
- background-image: url('covers/sonic-quiver-time-and-space1-1000x1000.jpg');
- }
- div.cover:nth-child(7){
- background-image: url('covers/tumblr_inline_nydppi1Mp91t7tdyh_500.jpg');
- }
- button[required='required']{
- background-color: #000;
- }
- </style>
- </head>
- <body>
- <div class='container'>
- <div class="innerWrapper">
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- </div>
- </div>
- <button required='required'>222</button>
- <script>
- ;(function(parent){
- var cards = parent.querySelectorAll('div'), coverCount = cards.length, middleIndex = (coverCount-1)/2, middleCover = cards[middleIndex], parentWidth = middleCover.parentNode.clientWidth, currentIndex = middleIndex;
- var maxRotate = 42, stepper = maxRotate/middleIndex, maxZIndex = middleIndex + 1;
- var rotateReg = /rotateY\((\-?\d{1,3}\.?\d*)deg\)/, translateReg = /translateX\((\-?\d{1,3}\.?\d*)px\)/;
- // debugger;
- for(var i = 0; i<coverCount; i++){
- var elem = cards[i];
- elem.classList.add('cover');
- elem.style.transform = 'translateX(0px) rotateY(' + (maxRotate-(i*stepper).toFixed(0)) + 'deg)';
- elem.style.flexGrow = 1;
- if(i<middleIndex){
- elem.style.zIndex = i+1;
- }else if(i == middleIndex){
- elem.style.zIndex = i+1;
- elem.style.flexGrow = 2;
- }else{
- elem.style.zIndex = coverCount - i;
- }
- }
- function move(direction){
- if(currentIndex==(direction=='right'?0:coverCount-1))return;
- direction=='right'?currentIndex--:currentIndex++;
- maxZIndex++;
- [].forEach.call(cards, function(element, index) {
- var previousRotate = parseInt(element.style.transform.match(rotateReg)[1]);
- var previousTranslate = parseInt(element.style.transform.match(translateReg)[1]);
- // translateX + 80 px one right button is clicked
- var currentRotate, currentTranslate;
- if(direction=='right'){
- currentRotate = previousRotate-stepper;
- currentTranslate = previousTranslate+(parentWidth/(coverCount+1));
- }else{
- currentRotate = previousRotate+stepper;
- currentTranslate = previousTranslate-(parentWidth/(coverCount+1));
- }
- element.style.transform = 'translateX(' + currentTranslate + 'px) rotateY('+ currentRotate +'deg)'
- // element.style.zIndex =
- if(index == currentIndex){
- element.style.flexGrow = 2;
- element.style.zIndex = maxZIndex;
- }else{
- element.style.flexGrow = 1;
- }
- });
- }
- document.addEventListener('keyup', function(e){
- if(e.which == 37){
- move('right');
- }else if(e.which == 39){
- move('left');
- }
- })
- })(document.querySelector('.innerWrapper'));
- </script>
- </body>
- </html>
稍微解釋下這里用到的幾個知識點:
1. flex-box.
什么是flex-box捏, 它是為了適應當前設備屏幕大小不一而提出的一種display方法. 當一個父元素的顯示被設定為display:flex時, 它內部的子元素們會被平均分配占滿父元素的空間, 并且當父元素的尺寸變化時, 子元素的尺寸也會相應變化! 是不是很神奇呢? 不僅如此, 你還可以任意分配子元素們的排列順序, 如果覺得某個子元素需要突出顯示, 就可以給這個子元素以特殊身份, 讓它相比其他子元素大一些, 或者小一些! 由于其的自適應特性, flex是移動開發的一把利器, 我們先來看看一個小應用:
設計一個對話框函數, 當傳入一個回調函數時, 只顯示一個'確定'按鈕, 占100%寬度; 當傳入兩個回調函數(確定和取消)時, 分別顯示'確定'和'取消'按鈕, 各占50%寬度, 樣式分別如下:
怎么實現呢? 我們當然可以用JS來做, 但是(凡事就怕一個但是哈哈)! 我們作為有追求的前端, 戰斗在CSS探索的***線, 現在有了如此好用的flex屬性, 為毛不立馬用起來呢? 說走咱就走, 按鈕容器和按鈕本身的CSS如下:
關鍵是按鈕的width:100%屬性. 有了它, 當容器里只有一個按鈕時, 它的寬度會拓展為容器的100%寬度; 而當容器里有兩個按鈕時, 按鈕的寬度都為100%, 怎么辦呢? 由于兩個按鈕勢均力敵, 它們只好平分秋色, 各占50%的空間了!
有的同學要問了: 要是我不想按鈕把空間占滿怎么辦呢? 這時候, 可以設定按鈕的寬度各為45%, 然后在父元素上設置justify-content:center, 意思是兩個子元素只占了90%的橫向空間, 那怎么分配剩下的10%空間呢? 那就兩邊各分配5%吧! 除此之外, 該屬性的其它值, 可以讓子元素左右對齊, 更多flexbox的神奇應用, 請參考這篇文章~
A Complete Guide to Flexbox
回到我們的例子. 在卡片們沒有被應用transform屬性之前, 它們看起來是這個樣子的:
七個元素平均分布, 占據了父元素的全部橫向空間. 其中中間的元素應用了flex-grow:2的屬性, 使得它比其他元素高人一等, 面積是其他元素的兩倍~~
2. transform
其實有了上一幅圖, 初始頁面的雛形就已經差不多了~現在只需要給父元素設置視角(關于視角, 3D變換等內容, 請見我的這一篇文章). 為了得到比較明顯3D效果, 設置了父元素的perspective為較小的值300px, 就相當于從距離3D變換平面300px的距離看. perspective的值越大, 相當于從越遠的距離看, 3D效果越不明顯, 平面化效果越強烈~
設置好了視角, 接下來該給元素們設置3D效果了. ***步很簡單: 假設有7個元素, 沿Y軸***旋轉角度為42度, 則0,1,2號元素分別旋轉42, 28, 14度, 3號元素旋轉0度同時變大2倍,4,5,6號元素分別旋轉-14, -28, -42度. 用一個簡單的for循環就可以完成這項任務, 代碼如下:
- for(var i = 0; i<coverCount; i++){
- var elem = cards[i];
- elem.classList.add('cover');
- // 設置元素的translateX為0px, 旋轉角度為***旋轉角度-目錄值*步進值
- elem.style.transform = 'translateX(0px) rotateY(' + (maxRotate-(i*stepper).toFixed(0)) + 'deg)';
- elem.style.flexGrow = 1;
- // 設置元素的z-index以區分前后順序, 并將中間元素設置大一些
- if(i<middleIndex){
- elem.style.zIndex = i+1;
- }else if(i == middleIndex){
- elem.style.zIndex = i+1;
- elem.style.flexGrow = 2;
- }else{
- elem.style.zIndex = coverCount - i;
- }
- }
初始化完成后效果圖如下:
此時每個卡片的translateX為0, 這個值要預先寫好, 才能通過改變該值來實現卡片的左右移動效果; rotateY的值分別為42, 28, 14, 0, -14, -28, 42度; flex-grow(相對于其它子元素的大小)分別為1, 1, 1, 2, 1, 1, 1
3.-webkit-box-reflect
那么漂亮的倒影是怎么實現的呢? 哈哈其實一行CSS就能搞定, 那就是強大的-webkit-box-reflect, 值為below 5% linear-gradient(transparent, white). 相信聰明的小伙伴看到這里已經明白了大概了, 為了避免誤解, 稍稍再解釋一下~below是倒影在盒子下方, 5%表示offset, 和盒子的距離是盒子寬度的5%, linear-gradient(transparent, white)指的是倒影的顏色, 從透明到完全不透明. 漸變語法的顏色在這里起作用的只有透明度, 白色的顏色是不會顯出來的~到這里, 我們用的大部分都是CSS, 效果圖如下:
然而靜態的展示是不夠的, 我們的目標是! 要讓它動起來! 來回左右動! 到這里CSS已經無能為力, 改JS閃亮登場的時候了!~
4.JS控制
控制左右移動的函數如下, 接受一個參數left或者right表示要移動的方向~
- // 定義提取旋轉角度和translateX值的正則, 例如 -> $0.style.transform.match(/rotateY\((\-?\d{1,3}\.?\d*)deg\)/) <- ["rotateY(14deg)", "14"]
- var rotateReg = /rotateY\((\-?\d{1,3}\.?\d*)deg\)/, translateReg = /translateX\((\-?\d{1,3}\.?\d*)px\)/;
- function move(direction){
- // 當前值為0或者當前值為卡片數目時, 返回
- if(currentIndex==(direction=='right'?0:coverCount-1))return;
- // 當前值自增或者自減
- direction=='right'?currentIndex--:currentIndex++;
- // ***Z-index自增
- maxZIndex++;
- [].forEach.call(cards, function(element, index) {
- // 提取變換之前的旋轉角度
- var previousRotate = parseInt(element.style.transform.match(rotateReg)[1]);
- // 提取變換之前的translateX
- var previousTranslate = parseInt(element.style.transform.match(translateReg)[1]);
- var currentRotate, currentTranslate;
- if(direction=='right'){
- // 計算rotatey的值
- currentRotate = previousRotate-stepper;
- // 計算平移的距離
- currentTranslate = previousTranslate+(parentWidth/(coverCount+1));
- }else{
- currentRotate = previousRotate+stepper;
- currentTranslate = previousTranslate-(parentWidth/(coverCount+1));
- }
- // 寫入元素屬性
- element.style.transform = 'translateX(' + currentTranslate + 'px) rotateY('+ currentRotate +'deg)'
- // element.style.zIndex =
- if(index == currentIndex){
- element.style.flexGrow = 2;
- // 不斷寫入maxZIndex, 確保翻過的元素始終在最前面
- element.style.zIndex = maxZIndex;
- }else{
- element.style.flexGrow = 1;
- }
- });
- }
再給按鈕或者鍵盤增加事件監聽, 這樣就完成啦!
總結一下:
- flex-box可以讓你的元素變得flex, 輕松實現根據元素數目重載! 各種屬性讓你任意操作flex元素!
- transform實現漂亮的3D變換效果!
- -webkit-box-reflect實現更加酷炫的倒影效果!
- ***JS來補刀, 讓我們的卡片們動起來!