回到前端工程師之路~
很久沒有改動 Blog 的 WUI 介面,這次改成支援 CSS 的 動畫功能 ,以及利用 JS 來做出選單物件,可以點取文章直接跳轉到相對的文章頁面。所以預計要有的功能如下:
- 滑動式文章選單。類似方塊 (cube) 的方式左右選轉文章,並且可以上下滾動文章。
- 提供有一個選單,列出目前所有的文章。支援點選單上的文章名稱,直接跳轉到相對的文章。
動畫
既然說到 CSS 動畫就不得不提到 HTML5 。HTML5 最大增進的功能在於增加 UX 上的表現。 可以利用額外提供的 TAG ,瀏覽器就可以搖身一變,成為一個 文字編輯器 甚至是 遠端桌面 。但是除了 HTML5 提供額外的 TAG 之外,CSS 以及更加豐富的 JS 也增加了不少的功能。 以 CSS 為例,過往需要利用才可以做到的動畫功能,已經可以利用 CSS 來模擬出來了。
以上是廢話:CSS 主要利用 transform、rotate 來做到變形與旋轉一個物件,並且用 transition 來做到簡單動畫的能力。transition 表示將一個物件的狀態,轉移到另一個狀態的意思。首先, 要做的事情就是撰寫一個 JavaScript 的邏輯:根據使用者的 I/O 來決定跳往下一個還是上一個文章。 一開始先實作旋轉的邏輯:根據順時針或逆時針來決定接下來的文章,該套用哪一個 CSS Style。 先掃所有的文章來獲得當下的文章 index,接下來根據方向來修改 style。
CSS 的部分跟上面提到的一樣,提供三種 Post 的樣式:前、當下、後三種樣式,分別表示將這三個文章作旋轉。
.content {
/* Support 3-D CSS animation */
-webkit-tansform-style: preserve-3d;
-ms-tansform-style: preserve-3d;
-o-tansform-style: preserve-3d;
tansform-style: preserve-3d;
-webkit-transition: transform 0.8s;
-ms-transition: transform 0.8s;
-o-transition: transform 0.8s;
transition: transform 0.8s;
}
.prevPost {
-webkit-transform: translateZ(-512px) rotateY(-90deg) translateZ(512px);
-ms-transform: translateZ(-512px) rotateY(-90deg) translateZ(512px);
-o-transform: translateZ(-512px) rotateY(-90deg) translateZ(512px);
transform: translateZ(-512px) rotateY(-90deg) translateZ(512px);
}
.currPost {
-webkit-transform: translateZ(-512px) rotateY(0deg) translateZ(512px);
-ms-transform: translateZ(-512px) rotateY(0deg) translateZ(512px);
-o-transform: translateZ(-512px) rotateY(0deg) translateZ(512px);
transform: translateZ(-512px) rotateY(0deg) translateZ(512px);
}
.nextPost {
-webkit-transform: translateZ(-512px) rotateY(90deg) translateZ(512px);
-ms-transform: translateZ(-512px) rotateY(90deg) translateZ(512px);
-o-transform: translateZ(-512px) rotateY(90deg) translateZ(512px);
transform: translateZ(-512px) rotateY(90deg) translateZ(512px);
}
等到旋轉的邏輯實作好之後就替文章上 Event Listener 。我們需要替文章上兩種不同的事件 Hook: 方向鍵以及平版的滑動事件。可以直接對 document 下的事件 hook,當使用者按下任何按鈕的時候, 都會觸發到 onkeydown 的事件並執行我們的 anonymouns function 。其中我們只需要關心左方向鍵 (37) 跟右方向鍵 (39)。
/* Key-Event Listener */
document.onkeydown = function (key) {
if (37 == key.keyCode) SlidePost(false);
else if (39 == key.keyCode) SlidePost(true);
};
另一方面,也需要支援使用者直接利用平板滑動物件選擇上/下一個文章。先判斷目前的 browser 是否有支援 ontouch 系列的 evnet hook,當有支援之後我們就監聽 ontouchstart 跟 ontouchmove 兩個事件: ontouchstart - 監控當使用者碰觸螢幕的時候,紀錄當時的位子並且開始監聽 ontouchend 事件。 ontouchend - 監聽最後使用者手指移動的位子,當移動的位置超過設定的大小的時候,取消掉 ontouchend hook 並且換成相對應的文章。
/* Touch-Pad Slide event */
if ("ontouchstart" in document.documentElement || "ontouchstart" in window) {
var posX = 0,
posY = 0,
isMove = false;
var onTouchEnd = function (ev) {
var deltaX = 0,
deltaY = 0;
if (isMove) {
deltaX = ev.changedTouches[0].screenX - posX;
deltaY = ev.changedTouches[0].screenY - posY;
/* Remove the touchend hook if left/right with angle < 15 deg */
if (0.0834 >= Math.abs(deltaY) / Math.abs(deltaX)) {
CancleTouchEvent();
if (deltaX < 0) SlidePost(true);
else SlidePost(false);
}
}
},
CancleTouchEvent = function () {
document.removeEventListener("touchend", onTouchEnd);
isMove = false;
posX = 0;
posY = 0;
};
document.addEventListener(
"touchstart",
function (ev) {
if (1 == ev.touches.length) {
posX = ev.changedTouches[0].screenX;
posY = ev.changedTouches[0].screenY;
isMove = true;
}
document.addEventListener("touchend", onTouchEnd, false);
},
false,
);
}
接著,動態的增加兩個箭頭的 Canvas 樣式,讓使用者想使用滑鼠的時候,點選箭頭來選擇上/下一個文章, 解決不想用滑鼠使用者。使用 HTML5 提供的 canvas 來畫出來簡單的箭頭樣式,這個新 TAG 可以根據要求, 畫出各種不同的幾何圖形 (現在使用兩條線來畫出箭頭符號)。然後再給這兩個 TAG 上 onclick 的 event hook, 這樣當使用者點擊之後,就可以根據方向來跳往前、後一個文章。
/* Create extra direct arrow if need, and always disable on mobile env */
if (!("ontouchstart" in document.documentElement)) {
var arrowSize = [40, 330]; /* posX, posY */
var offSize = [5, 10];
/* Make sure the space is enough */
if ($(window).width() > arrowSize[0] * 4 + $(".blog").outerWidth()) {
for (var i = 0; i < 2; ++i) {
var dom = document.createElement("canvas");
var ctx = dom.getContext("2d");
/* DOM Property */
if (0 === i) {
dom.id = "leftArrow";
} else {
dom.id = "rightArrow";
}
dom.classList.toggle("arrowWidget");
dom.width = arrowSize[0] + offSize[0] * 4;
dom.height = arrowSize[1];
dom.style.marginTop = (-1 * arrowSize[1]) / 2 + "px";
/* Canvas Property */
ctx.strokeStyle = "#CCC";
ctx.shadowColor = "#DEDEDE";
ctx.shadowBlur = 20;
ctx.shadowOffsetX = offSize[0];
/* Draw line as arrow */
ctx.moveTo(arrowSize[0] - offSize[0], offSize[1]);
ctx.lineTo(offSize[0], arrowSize[1] / 2 - offSize[1] / 2);
ctx.lineTo(arrowSize[0] - offSize[0], arrowSize[1] - offSize[1]);
ctx.lineWidth = 6;
ctx.stroke();
dom.onclick = function (ev) {
if ("leftArrow" === this.id) {
SlidePost(false);
} else if ("rightArrow" === this.id) {
SlidePost(true);
}
};
$("body")[0].appendChild(dom);
}
}
}
最後我們想要得到的功能,是將文章當成是 cube 的若干鉛直面,當下的文章為 front,前一個以及後一個文章, 分別是 left、right。當我們要跳往下一個文章的時候,left 的文章就變為 invisible,font 就變成 left, right 就變成 font,下下一個文章就轉換成 right。
function SlidePost(clockwise) {
/* First, get the post contain, and get the current Post index */
var blogs = $("#blogs")[0];
var idx = 0,
length = 0;
length = blogs.children.length;
for (idx = 0; idx < blogs.children.length; ++idx) {
if (blogs.children[idx].classList.contains("currPost")) break;
}
if (
!blogs.children[idx] ||
!blogs.children[idx].classList.contains("currPost")
) {
idx = 0;
blogs.children[(idx + length - 1) % length].classList.toggle("prevPost");
blogs.children[idx].classList.toggle("currPost");
blogs.children[(idx + 1) % length].classList.toggle("nextPost");
return;
}
if (clockwise) {
blogs.children[(idx - 1 + length) % length].classList.toggle("prevPost");
blogs.children[(idx - 1 + length) % length].classList.toggle("slideHidden");
blogs.children[idx].classList.toggle("prevPost");
blogs.children[idx].classList.toggle("currPost");
blogs.children[(idx + 1 + length) % length].classList.toggle("currPost");
blogs.children[(idx + 1 + length) % length].classList.toggle("nextPost");
blogs.children[(idx + 2 + length) % length].classList.toggle("nextPost");
blogs.children[(idx + 2 + length) % length].classList.toggle("slideHidden");
idx = (idx + 1) % length;
} else {
blogs.children[(idx - 2 + length) % length].classList.toggle("slideHidden");
blogs.children[(idx - 2 + length) % length].classList.toggle("prevPost");
blogs.children[(idx - 1 + length) % length].classList.toggle("currPost");
blogs.children[(idx - 1 + length) % length].classList.toggle("prevPost");
blogs.children[idx].classList.toggle("currPost");
blogs.children[idx].classList.toggle("nextPost");
blogs.children[(idx + 1 + length) % length].classList.toggle("nextPost");
blogs.children[(idx + 1 + length) % length].classList.toggle("slideHidden");
idx = (idx - 1 + length) % length;
}
/* Back-to the page top */
$("html, body").animate({ scrollTop: 0 }, 850);
}
利用兩個固定的 CSS class:PrevSlide、NextSlide 來表示跳往上一個還是下一個文章。而我們需要旋轉的, 就只是最外面的框架而已。
[EDIT] 在 IPAD mini / IPhone 6 測試的時候突然發現 layout 跑掉 (但是用 chrome debugger mobile mode 的時候不會有問題),原因是因為當 div 的 position 屬性是 absolute 的時候, margin 的特性會不一樣。用 CSS Box-Model 來看,margin 是元素以外的空間, 所以當下元素外面沒有其他元素的時候,margin 就沒有發生作用。解決的方式是使用兩層 div,外層設定 position: absolute,而內層用 margin 來空出所需要的空間。