Expo/ReactNativeで特定のコンポーネントを表示する時に画面の明るさを変更する


Expo/ReactNativeで画面の明るさを変更するためのexpo-brightnessを使う機会があったので、コンポーネント化してみました。QRコードを表示する時に画面を明るくするなどの場面に使えそうです。

expo-brightnessのドキュメントはこちら

Androidではシステム設定の明るさとアプリ単位での明るさと2種類あり、ここではパーミッションが必要ない後者を使います。
iOSではアプリ単位での明るさ設定が使えないので、アプリがforegroundかbackgroundかを監視して、backgroundになった時に明るさを元に戻すようにしました。

また、明るさを変更した時にAndroidではデフォルトでアニメーションされるのに対してiOSではアニメーションされずパッと切り替わるので、durationIOSを渡すとiOSでもアニメーションされるようにしてみました。

BrightnessControl.js
import React, { PureComponent } from "react";
import * as PropTypes from "prop-types";
import * as Brightness from "expo-brightness";
import { AppState, Animated, Easing, Platform } from "react-native";

/**
 * 明るさを制御する
 */
class BrightnessControl extends PureComponent {
  appState = null;
  originalBrightness = null;
  brightness = null;

  componentDidMount() {
    this._init();
  }

  /**
   * 明るさを元に戻し、リスナーなどを解除
   */
  componentWillUnmount() {
    if (this.brightness) {
      const { durationIOS } = this.props;
      if (durationIOS && Platform.OS === "ios") {
        Animated.timing(this.brightness, {
          toValue: this.originalBrightness,
          duration: durationIOS,
          easing: Easing.in(Easing.out(Easing.ease))
        }).start(() => {
          this.brightness.removeListener(this._updateBrightness);
        });
      } else {
        this.brightness.setValue(this.originalBrightness);
        this.brightness.removeListener(this._updateBrightness);
      }
      AppState.removeEventListener("change", this._handleAppStateChange);
    }
  }

  /**
   * propsのbrightnessの変更に対応
   * @param {object} prevProps
   */
  componentDidUpdate(prevProps) {
    if (
      this.props.brightness !== prevProps.brightness &&
      (this.appState === "active" || Platform.OS === "android")
    ) {
      this.brightness.setValue(this.props.brightness);
      this._updateBrightness();
    }
  }

  /**
   * 初期処理
   * @returns {Promise<void>}
   * @private
   */
  _init = async () => {
    this.originalBrightness = await Brightness.getBrightnessAsync(); // 元の明るさを保持しておく

    // 明るさはAnimatedで変更
    this.brightness = new Animated.Value(this.originalBrightness);
    this.brightness.addListener(this._updateBrightness);

    if (Platform.OS === "ios") {
      // iOSの場合はAppStateでbackground/foregroundを監視
      this._handleAppStateChange(AppState.currentState);
      AppState.addEventListener("change", this._handleAppStateChange);
    } else {
      const { brightness } = this.props;
      this.brightness.setValue(brightness);
    }
  };

  /**
   * 明るさを更新
   * @private
   */
  _updateBrightness = () => {
    Brightness.setBrightnessAsync(this.brightness._value);
  };

  /**
   * AppStateの変更に合わせて制御
   * @param {string} nextAppState
   * @returns {Promise<void>}
   * @private
   */
  _handleAppStateChange = async nextAppState => {
    if (nextAppState !== this.appState) {
      this.appState = nextAppState;
      if (this.appState === "active") {
        this.originalBrightness = await Brightness.getBrightnessAsync();
      }
      const { brightness, durationIOS } = this.props;
      if (durationIOS) {
        // アニメーションさせる場合
        Animated.timing(this.brightness, {
          toValue:
            this.appState === "active" ? brightness : this.originalBrightness,
          duration: durationIOS,
          easing: Easing.in(Easing.out(Easing.ease))
        }).start();
      } else {
        // アニメーションさせない場合
        this.brightness.setValue(
          this.appState === "active" ? brightness : this.originalBrightness
        );
      }
    }
  };

  render() {
    const { children } = this.props;
    return <>{children}</>;
  }
}

BrightnessControl.defaultProps = {
  brightness: 1,
  durationIOS: 0
};

BrightnessControl.propTypes = {
  brightness: PropTypes.number, // 明るさ(0~1)
  durationIOS: PropTypes.number // アニメーションの長さ(ms)
};

export default BrightnessControl;

下記のような感じで使うと、BrightnessControlコンポーネントの表示・非表示によって明るさが変化するようになります。

使用例
<BrightnessControl brightness={1} durationIOS={500}>
  {...}
</BrightnessControl>