Rounded Border with Animated Gradient Color

詹小魚
OurSong
Published in
9 min readFeb 13, 2020

--

這次的目標是要在 Web 以及 React Native 實作出以下效果:

Rounded Animated Gradient-color Border

效果的重點包含:
1. 圓角邊框(Rounded Border)
2. 漸層邊框(Gradient Border)
3. 動態變色(Animated Color)

Web

首先,先想辦法把基本的邊框樣子用 CSS 定義出來。CSS 對於邊框(border)已經有許多屬性可以使用,而在 CSS 要實作漸層,最簡單的方式就是使用 linear-gradient()。初步想到的設定如下:

#animated-border{
...
height: 40px;
border-image: linear-gradient(to right, red, blue) 30;
border-radius: 20px;
border-width: 2px;
border-style: solid;
...
}

這時會注意到一件事 … border-radius 在這裡似乎沒有作用?

開啟瀏覽器的開發者工具檢查,會發現當把 border-image 關閉的時候,實際上 border-radius 是有作用的:

沒有 border-image 時的狀況

看來 border-image border-radius 似乎是不能同時使用的,這是怎麼回事呢?Google 了一下之後發現原來在 W3C 的 Spec 中有如下規範:

A box’s backgrounds, but not its border-image, are clipped to the appropriate curve (as determined by background-clip). Other effects that clip to the border or padding edge (such as overflow other than visible) also must clip to the curve. The content of replaced elements is always trimmed to the content edge curve.
https://www.w3.org/TR/css-backgrounds-3/#corner-clipping

原來 border-radius 是不會去裁切 border-image 的!這麼看來,目前是無法使用 border-image + border-radius 這樣直觀的方式,直接操作 border 去實現我們想要的效果。

這下該怎麼辦?

Workaround:

為了實現圓角,border-radius 必須要有。而 CSS 的 linear-gradient 產出的是一種特殊的 image 元素,除了 border-image 之外,也可以放在 background 上。如果我們把漸層效果實作在 background 屬性,那看來必須疊 element ,藉由遮住背景的一部分,讓它看起來像是個邊框,才能達成我們想要的效果。如此一來,可行的方法大致上有兩種。

方法一:Parent + child element
方法二:將 background 畫在 Pseudo-elements (:after) 上

我們選擇實作方法一:

<div id="gradient-border-background">
<div id="join-party-button-container">
...
</div>
</div>
---#gradient-border-background {
height: 40px;
border-radius: 20px;
background-image: linear-gradient(to right, red, blue);
...
}
#join-party-button-container {
display: flex;
margin: 2px;
border-radius: 19px;
background-color: 'black';
...
}

這樣一來外觀就完成了!接著繼續做變色的效果。

Animation

因為我們的漸層色是畫在 background 上,所以我們的動畫效果便是要做在 background 上。

實作方法是先將所有要使用到的漸層顏色,通通畫在 background

#gradient-border-background {
background: linear-gradient(101deg, #38b8f2, #843cf6, #f030c1, #6094ea, #fd8041, #ff4ca1, #ffa7e7, #ea6362, #4dd0e1, #6078ea, #38b8f2, #843cf6);
background-size: 1200% 100%;
...
}

這時,雖然外觀上看起來還是跟剛才一樣,但其實 background 已經是之前的好幾倍長,我們只顯示了 background 的一小部分而已。

接著,我們定義 animation,讓 background 自動滑動,並且無限循環,來形成一個持續變化顏色的漸層效果。

#gradient-border-background {
...
animation: animatedgradient 12s linear infinite;
}
@keyframes animatedgradient {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 0%;
}
}

從上面的 code 可以看到,其實我們只是把已經畫好漸層的 background,由左至右滑動。而我們的漸層背景就會用 12 秒從頭移動到尾,並且不斷重複這個動畫。

如此,我們就完成網頁上的「動態漸層變色圓角外框」了!(名字好長)

參考資料:
- https://css-tricks.com/gradient-borders-in-css/

React Native

RN 的部分,要實作漸層色本身就需要用到 RN Community 做的 LinearGraident 這個 Component,因此實作方式不做他想,一定是用元素疊元素的方式去達成。

比較麻煩的部分是要做出動態的效果。

一開始先參考文件上的 AnimatedGradient Example 用 RN 的 Animated API 做了一個簡單的 Animated.LinearGradient component:

<Animated.LinearGradient
colors={[animatedStartColor, animatedEndColor]}>
...
</Animated.LinearGradient>

看起來相當直觀且好用。但當我們 build 在模擬器上時,RN 隨即就噴出 Error:

嗯 … what? 看來應該是跟傳入的 colors array 有關的問題 …

萬事問 Google 之後,發現原來 Animated Value 並不能直接用 Array 的方式當作 Props 往下傳。因此在這裡另外做了一個 GradientHelper,讓 LinearGradient 需要的 colors 分開傳入:

class GradientHelper extends React.PureComponent {
render() {
const { colorStart, colorEnd } = this.props;
return (
<LinearGradient
colors={[
colorStart,
colorEnd
]}
{...this.props}
/>
);
}
}

然後用 Animated API 做一個新的 AnimatedLinearGradient Component:

const AnimatedLinearGradient = Animated.createAnimatedComponent(GradientHelper);

如此一來,我們就可以透過變動傳入的 colorStartcolorEnd 來達到 Animation 的效果:

<AnimatedLinearGradient
colorStart={AnimatedStartColor}
colorEnd={AnimatedButtonEndColor}
>
...
</AnimatedLinearGradient>

至此 React Native 的部分也完成了!

結語

目前無論是在 Web 還是 RN 的部分,都是用元素相疊來達成效果。 border-image 不能與 border-radius 共用真的蠻可惜的。

目前筆者想到可能可行的還有透過 clip-path 去畫出 border 的部分,但 clip-path 不能動態調整大小,不然就必須再搭配 calc() 去實作,想來有些複雜,程式碼也會變成不易理解,因此筆者就沒有嘗試這個方法。

不知道還有沒有其他可行的做法?歡迎留下留言來一起討論、分享想法。

圖文不符

--

--