React Native 動態調整樣式
這陣子工作需求開始在寫 React Native,基本上對 React 有概念寫起來還蠻容易上手的,只是對於排版的使用不太習慣,所以想要記錄一下遇到並解決的問題,如果文章內容有誤,歡迎提出,因為剛開始寫 React Native 才不久,所以可能會有許多遺漏該注意的部分。
🎨 建立樣式
在 Web 開發中,可以透過自訂 css
調整樣式,藉由 id
以及 class
這些 keyword 來指定該元素(Element)的樣式是什麼樣子:
<!DOCTYPE html><html><head><meta charset="utf-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge"><title>Page Title</title><meta name="viewport" content="width=device-width, initial-scale=1"><!-- Your Custom Style --><link rel="stylesheet" type="text/css" media="screen" href="main.css" /><script src="main.js"></script></head><body><!-- Add Some Elements --></body></html>
但在 react-native 中不允許引入額外的樣式檔案,只能透過 Inline CSS 來方式來自訂樣式:
<View style={{ backgroundColor: '#cbf35c' }}><Text style={{ color: '#4d3398' }}>Hello, World</Text></View>
如果今天的 Layout 設計的相對複雜,包含許多不同的 Component 以及各式各樣的樣式的微調,如果寫成 inline css 閱讀起來會相當的困難,所以在 react-native 中,提供了一個 StyleSheet 的 function 讓你可以建立一個抽象的 CSS:
import { Stylesheet } from 'react-native';const styles = StyleSheet.create({container: {backgroundColor: '#cbf35c'},title: {fontSize: 20,fontWeight: '500',color: '#4d3398'}});const Component = () => (<View style={styles.container}><Text style={styles.title} /></View>);
在 react-native 的 Style 官方文件也提到了:
As a component grows in complexity, it is often cleaner to use StyleSheet.create to define several styles in one place.
我的理解是,每個 Component 的 Style 都應該一起與 View 被放在一起,也就是都寫在同一隻 JavaScript 裡。
💅🏻 react 和 react-native 自訂樣式
React 讓你可以定義元件的狀態(State),讓這些 state
可以被渲染(Render)在你的視圖(View)中,所以當某個 state
改變時,view 當中的某個部份會自動的重新被 render。
在 react-native 中的概念也是如此,你的 view 可能會因為 state 的改變而 render 不同的畫面或者是更新樣式,例如有一個登入畫面,每個輸入的 input 後面會有一個狀態的 icon:
當你輸入完成後,後面可能會有個 icon 圖示告訴你登入成功與否,又或者是當你登入之後,發現帳號密碼有誤,讓輸入框都變成紅色,這些都是 state 的改變,讓部分的 view 被重新 render。
使用 react 開發 Web 時,可以透過以下的方式來更新 style,例如以下的 Counter:CodeSandbox
當按下超過五次後,count
文字的顏色會改變;在 Counter 內有預設樣式,但你可以提供一個 style
的 props 來調整 Counter 的樣式。
預設樣式:
{bg: { backgroundColor: '#cbf35c', height: '20vh' }}
加入 dashed
border:
<Counter style={{ borderStyle: 'dashed' }} />
可以基於原本的樣式再做調整多虧了 JavaScript 的 Spread Syntax,讓我們可以 clone 原本的 Object(或使用 Object.assign()
),再更新 Object 原有的 property:
function Counter({ style }) {// ...render() {return (<div style={{ ...styles.bg, ...style }}>{/* ... */}</div>);}}
如果想要 overwrite 預設樣式的 property 也是可以的,例如 overwrite background
的顏色:
<Counter style={{ background: 'red' }} />
那,在 react-native 也可以使用相同的方式嗎?
在閱讀 react-native 文件後,我注意到了 Performance 的部分:
Making a stylesheet from a style object makes it possible to refer to it by ID instead of creating a new style object every time.
官方建議使用 StyleSheet
來建立樣式,而不是建立一個新的 Object,透過 StyleSheet.create
建立樣式會得到一個 ID。
看完這段的當下,突然覺得在 react-native 要使用動態的樣式調整好像不像寫 Web 一樣那麼容易,後來研究了一下找到了解法,所以把這個方法記錄下來。
Dynamic Styles
回顧一下前面提到在 react-native 透過 StyleSheet.create
來建立樣式:
import { Stylesheet } from 'react-native';const styles = StyleSheet.create({container: {backgroundColor: '#cbf35c'},title: {fontSize: 20,fontWeight: '500',color: '#4d3398'}});
在 element 中使用自訂的樣式:
const Component = () => (<View style={styles.container}><Text style={styles.title} /></View>);
因為一開始樣式就被定義了,所以要如何像在 Web 中一樣,而外的傳入新的參數來 overwrite 預設的樣式?
在 react-native 中,style
這個參數可以接受如下:
export type StyleValue = {[key: string]: Object} | number | false | null;export type StyleProp = StyleValue | Array<StyleValue>;
Reference: Standard Flow type for style prop
一般的 inline style 就是 StyleValue
類型的,不過在原始碼可以了解到,style 的 prop 是可以接受陣列的 StyleValue (Array<StyleValue>
)。
以上面的 Counter 來繼續作為 react-native 範例,若 Counter Component 一開始內部就自訂了原始樣式,如何在 react-native 中將後來自訂的 style
overwrite 原始的樣式呢?每個 Component 都可以接收 props,而 style
也是作為 prop 傳入到 Component 中,所以讓我們來撰寫 react-native 的 Counter:React Native Counter
如果想要動態的改變 Counter 的樣式,可以傳遞一個 style
:
const App = () => (<Counter style={{ backgroundColor: 'red' }} />);
style
會作為 props 傳到 Counter Component,我們可以從 props 將 style
取出,讓我們來改寫 Counter 的 render
function:
原始寫法:
<View style={[styles.bg]}>{/* ... */}</View>
修改寫法:
<View style={[styles.bg, { ...this.props.style }]}>{/* ... */}</View>
又或者可以透過傳入一些額外的 props 狀態,來決定是否要不要 render,例如:
<View style={[styles.bg, { this.props.isActive && ...this.props.style }]}>{/* ... */}</View>
完整範例
import React from 'react';import { View, StyleSheet, Text, TouchableOpacity } from 'react-native';const styles = StyleSheet.create({bg: { flex:1, paddingTop: 150, alignItems: 'center', backgroundColor: '#cbf35c' },less: { fontSize: 25, color: '#4d3398', fontWeight: 'bold' },greater: { fontSize: 25, color: '#f3845c', fontWeight: 'bold' },button: {width: 150,height: 50,alignItems: 'center',paddingTop: 10,borderRadius: 10,backgroundColor: '#3498db'},buttonText: {fontSize: 25,color: '#fff'}});class Counter extends React.Component {state = { count: 0 };setCount = () => this.setState(prevState => ({ ...prevState, count: this.state.count + 1 }))render() {const { count } = this.state;return (<View style={[styles.bg, { ...this.props.style }]}><View style={{ height: 100 }}><Text style={count < 5 ? styles.less : styles.greater}>You clicked {count} times</Text></View><View style={{ height: 100 }}><TouchableOpacity style={styles.button} onPress={this.setCount}><Text style={styles.buttonText}>Click</Text></TouchableOpacity></View></View>);}}const App = () => (<Counter style={{ backgroundColor: 'red' }}/>);export default App;