React Native 動態調整樣式

November 16, 2018

這陣子工作需求開始在寫 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 Only Updates What’s Necessary

在 react-native 中的概念也是如此,你的 view 可能會因為 state 的改變而 render 不同的畫面或者是更新樣式,例如有一個登入畫面,每個輸入的 input 後面會有一個狀態的 icon:

Login flow interactions - Photo by Jakub Antalík on Dribble

當你輸入完成後,後面可能會有個 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;

Reference