響應式設計中百分比 % 的問題

問題

為了要能夠解釋得更清楚我們需要實作一小段跟我們會遇到的問題相關的程式碼

1
2
3
4
.list-item {
float: left;
width: 33%;
}

現在您可能會想知道關於上面這段程式碼有什麼問題,看起來這樣並不明顯,好!假設這是一個三欄(column)的網格,就算你知道 33% + 33% + 33% = 99% 並不是 100%。但在大多數的情況下並不會有什麼問題,不過這誤差的 1% 如果遇到容器像是 1400px 時就是 14px,那就是一個蠻大的誤差了。那為什麼我們不直接調整百分比的精度呢? 我們是可以將它降低到 1.4px 甚至是 0.14px 那麼一來就不會有問題啦

1
2
3
4
.list-item {
float: left;
width: 33.33%;
}

實際上…

關於流質網格(Fluid Grid)破版的問題,…其實就只是誤差造成縫隙或偏移。
所謂響應式設計(RWD)依照 Ethan Marcotte 所定義: 就是流質網格,響應式圖片,加上 Media Query 所構成。但關於流質網格卻有一個比較麻煩的問題,就是計算捨入的部分是錯誤的。當我們使用百分比設定欄位時,瀏覽器必須要根據螢幕,viewport 及可視區域將其轉換為實際的像素(pixel)。在這個轉換的過程中 Chrome, Safari, Opera 等瀏覽器全部產出錯誤的值。

Fluid:英文為液態,液體,流質定義為一種物質可持續性的改變形狀以適應周圍的壓力或容器
流質網格:網格尺寸會根據父元素尺寸自動調整,簡單說即我們定義最外圍的尺寸後就像液體一樣格子會自動變形去適應尺寸

所謂的錯誤主要是因為這是需要被定義在 CSS 規範的問題。但由於 CSS 並沒有規範瀏覽器對於百分比計算精度應該要到小數第幾位,舉例來說如果 6 個欄位的網格 100% ÷ 6 = 16.666667% ,那麼在一個 1000px 的可視區域 viewport 中一欄(column)的寬就是 166.66667px,但因為沒有規範,所以瀏覽器廠商各自使用自己的規則,如果瀏覽器使用四捨五入那麼在我們這個範例我們就會得到 167px 結果就是 167 x 6 = 1002 就會超出 viewport 範圍。如果捨去變成 166px 那最終我們會少 4px。

IE6 7 採用前者就是進位,這也意味著常常會超出我們想要的尺寸,WebKit 採用後者以避免破版,Opera 甚至直接在百分比動手腳把 16.66667% 直接換成 16%,結果就是一個欄位(column)的寬跟我們要的整整誤差 6px。好!在你開始罵這些開發商之前,請先想想這是因為 CSS 規範沒有定義規則啊!

更糟糕的狀況?

不幸的是,如果您同時也使用百分比來設定這些間隔(gutters)尺寸如 pedding 之類的,那麼這問題真的會非常糟糕。過去(約 2012 年)大部分佈局的方式都是採用 float 的方式,即網格中格子位置必須仰賴大量計算來完成。
假設我們有一列 12 欄的網格透過百分比設定 width, margin, padding,那麼第 12 欄的位置需要前面 11 欄的 padding, margin, width 來計算,也就是說包含自己在內會有 56 次機會計算產生誤差2(padding) + 2(margin) + 1(width) = 5; 11 * 5 + 1(margin itself) = 56,假如每一次計算都誤差 1px 那麼就會有 56px 的誤差。

在你知道這點之後也就不奇怪為什麼 mediaqueri.es 網站上這麼多設計都沒有凸顯區塊邊緣的設計,因為如此一來就這個計算的誤差就比較不明顯一點。

您可以檢視範例來看看各個瀏覽器誤差的情況

那有什麼辦法?

首先,我得先對那些自適應(adaptive)網頁設計的提倡者說,我明白你看到這邊非常開心。的確! 採用自適應的方法能良好的運作,因為 Adaptive Web Design 預先對 viewport 尺寸定義然後當遇到那些不符合的尺寸時則採用小一級例如:預先設計了 1024 和 960 寬的 viewport 如果遇到 1000 就採用 960 的設計。因為不使用流質網格所以完全避開了關於百分比誤差的問題。但這裡並不是要說 AWD 就是比較好的做法,只是稍微提一下採用 AWD 的話不會遇到這個問題。

響應式(Responsive) vs 自適應(Adaptive)

響應式和自適應設計共同點; 都是要處理在不同裝置下瀏覽網頁的問題,可讀性,版型等等。

最大的部分在於 RWD 是透過 Fluid Grid 和元素使其自動符合視窗或父元素尺寸,而自適應 AWD 則是預先定義可視區域的尺寸然後透過 JS CSS 等方式去套用版型樣式

最佳解決方案

您可能注意到我剛剛並沒有提到 Firefox 計算的問題。Firefox 實作了一個較為先進的方式稱為 sub-pixel 渲染取代捨入計算的方式,Firefox 會替所有 CSS 屬性保留 sub-pixel 值,當元素的位置需要相依其他元素時就會把 sub-pixel 拿出來計算。這個效果相對接近設計師的期望。

IE8 也採用了 sub-pixel 顯然是為了補救 IE7 非常糟糕的計算方式。WebKit 與 Opera 以及那些使用捨去值策略的瀏覽器還沒有採用 sub-pixel 的方式。

而其中一個解決方式就是我們可以等到所有瀏覽器都採取 sub-pixel 的方式渲染,不過這可能需要等待非常久的時間。
這篇文章試圖要找出解決方案而不是被動的等待,幸運的是下面有一些方式是我們今天可以採用的。

關於 CSS3 Flexbox?

由於我們仍會在 Flexbox 的設計中使用百分比,如此一來或多或少還是會受到進位誤差的影響,因此 Flexbox 並不是佈局的萬能藥。

盡可能移除使用百分比的部分

首先,我們必須要認同將所有佈局 Layout 每個屬性例如間隔的 padding 等都用百分比處理只是那些龜毛,強迫症開發者的樂趣,我們並不需要完全採用百分比。

第一個問題是因為在 CSS2 時盒子模型(Box model)中 paddingborder 並不包含在元素寬內,意思是如果我們設定一個元素的寬為百分比,我們也必須使用百分比去設定 padding margin,否則計算上一定會出現不符合 100% 的狀況,但如果把 box-model 換成 box-sizing: border-box ,padding 和 border 就會包含在 width 裡,意思是我們就不需要在被迫在這些屬性上使用百分比,就可以使用固定的值 em, rem, px 等。針對網格邊界之間的間隔空間比較好的做法是使用 padding 而不是 margin。所有內容與間隔都套用保持一致比例,這樣一來比起當要顯示出邊界的樣式時才個別因為對齊的關係加上容器元素(wrapper),前者的優點大於後者。

所以結論就是當使用 border-box 時,間距的部分 margin padding 就不會再遇到數學計算捨入的誤差問題。但在 width 方面仍然會有這個問題,不過以 12 欄來說我們把最大誤差從 56px 降低到 11px 。雖然很不錯了,但對使用者來說還是會被注意到這樣的瑕疵。

不讓捨入誤差累加

捨入誤差真正的問題是因為在設計佈局時這些誤差常常是會累加的,為了要取得一個格子的左邊界定位我們必須要依賴前面同層的格子來計算,因為 float 需要依據上一個元素來排位置。
那假如我們有辦法直接指定左邊界呢? 就是說如果可以就直接設定從父元素左邊界到本身的距離,然後其他元素遵循一樣 float 的排版呢?

事實證明是可以這樣做的。大約從 2004 年這個技術就已經被使用了稱為 container relative floats,這也是 Drupal 的 Zen 樣板使用的核心技術。
雖然乍看之下這招不怎麼高明,但事實證明這個做法還蠻牢靠的。
這個方法主要是透過在每個網格套用下面的 CSS

1
2
float: left;
margin-right: -100%;

接著每一個格子設定 margin-left 值是從父容器左邊界到其定位的距離。下面是一個簡單的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.item1 {
float: left;
width 40%;
margin-left: 0;
margin-right: -100%;
}
.item2 {
float: left;
width: 40%;
margin-left: 40%;
margin-right: -100%;
}
.item3 {
float: left;
width: 20%;
margin-left: 80%;
margin-right: -100%;
}

注意您也可以反過來設定網格對應右邊界的距離 float: right 然後 margin-left: -100%;
不過這個原理到底是啥?首先思考一下 margin-right: -100% 這個 -100% 就是外層容器的寬,接著再想想一個 float 元素 的 margin-right 是會影響緊鄰地下一個 float 元素,假如我們設定 margin-right: -10px 那麼它右邊的元素(格子)就會從原本的位置往左偏移 10px 就是減 10px。

邏輯上 -100% 意味著下一個元素應該要偏移減去容器寬的距離,不過有個重點就是如果減掉過大的值讓元素超過父容器邊界時結果並不會超出左邊界。
簡言之就是 float 元素在排列位置時不用在管前面元素右邊界的位置(原本是從上一個元素的右邊界開始計算,現在不是),只要看自己和父容器左邊界的距離就好。

試試這個範例

如果你對這個做法有一種好像 absolute 定位的感覺,覺得很不可靠,那是因為你應該沒注意到關鍵的不同點,absolute 的方式會使元素完全脫離文件排版的一個規則順序之中,且 absolute 不會影響周圍其他元素的位置,但 container-relative 的設定是可以透過 clear 移除的。
這是關鍵,因為當 float 項目搭配這樣的用法就可以無視其他元素的右邊界位置,但設定 clear 時下邊界仍可以產生影響換行。
這表示我們可以透過設定上一個網格或項目 clear 來換行產生新 row ,這是 absolute 辦不到的。

由於我們的網格不再被其他周圍的兄弟元素影響,我們就不再受到 HTML tag 的順序限制。如此一來我們就可以很簡單地把 HTML 中排序的第一個元素放到列的中間或者隨意把 row 中的網格任意調整位置順序。

但是這麼做我們還是沒有完全修好誤差的問題!不過我們已經減少定位相關的計算過程,現在只剩一個值就是和父元素的距離。不過這個值仍然受到捨入誤差的影響,也就是或多或少還是會遇到 1px 的誤差。不過幸運的是因為大部分的網站使用者並沒有強迫症,這樣微小的誤差並不太容易被注意到。

如果內容本身是有質量的使用者不會特別去注意那 1px 設計上的誤差,不過還有一個替代的解決方案可以幫助我們實作甚至減少這 1px 的誤差。

真的可以處理 1px 的誤差?

如果您的網格 float 是往左靠,因為所有的元素都是向左對齊,所以最可能的狀況那 1px 的誤差都會顯示在最右邊的那格。即便所有列 row都對齊,這 1px 也蠻容易被發現,但如果我們把這些很明顯的地方換成對齊右邊,如此一來誤差的 1px 會被放到頁面的中間就比較不會被注意到。

使用 Zen Grids

關於為什麼需要使用 CSS 預編譯器實作 RWD,那是因為例如使用 Sass 可以簡化一些我們需要的設計同時處理那些用純 CSS 會比較為複雜的地方。那麼 Zen Grids 是如何處理關於捨入誤差呢?透過使用 Zen Grids 預設就透過 border-box 來處理網格的間隔搭配 container-relative 方式,提供一些輔助方法(Methods)讓我們可以簡單的改變對齊的方向。

使用 Sass 與 calc() 處理

除了 Zen Grids 的做法,透過 Sass 我們還有一些簡單的方式可以協助我們

使用除法

1
2
3
4
5
6
7
8
9
10
11
.list-item {
float: left;
width: (100%/3);
}

// or

.list-item {
float: left;
width: percentage(1/3);
}

透過這兩種方式 Sass 會自動幫我們把精度輸出到小數以下第五位,雖然沒有完全解決問題但是有幫助的。

使用 CSS 的 calc()

現在最新的做法則是使用 calc() 這麼做是把問題交還給瀏覽器去處理,而對於那些還沒支援的瀏覽器則交給 Sass

1
2
3
4
5
.list-item {
float: left;
width: (100% / 3);
width: calc(100%/3);
}

結論

  • 使用 Sass 計算方式 e.g 100%/3
  • 使用 calc(100%/3)
  • float 的情況下用 border-box 搭配 padding 以及 margin-right: -100% Container relative float 技術,將誤差往中間移

資源

作者

andyyou(YOU,ZONGYAN)

發表於

2016-05-20

更新於

2021-12-12

許可協議