🪗
React Native でアコーディオンを実装
この記事は React Native Advent Calendar 2023 の15日目の記事です。
ラベルタップで表示・非表示を切り替える View を作る
"アコーディオン” と言っていますが一般的にはなんと呼ばれているんでしょうか。ググると Expandable や Collapsible といった名称でも呼ばれているようです。この記事では簡易的な名称で Expand といった呼び方で実装を進めたいと思います。
まずは表示・非表示を切り替えるだけの仕組みを実装
クリックするたびにフラグを反転させ children
の表示・非表示を切り替えるようにします。
Expand.js
import { useState } from 'react';
import { Text, View, Pressable, StyleSheet } from 'react-native';
export const Expand = ({ title = '', children}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<View style={styles.frame}>
<Pressable
onPress={() => {
setIsOpen(!isOpen);
}}>
<Text style={styles.title}>{title}</Text>
</Pressable>
<View style={isOpen ? {} : styles.hide}>
{children}
</View>
</View>
)
}
const styles = StyleSheet.create({
frame: {
width: "100%",
overflow: 'hidden',
borderColor: "black",
borderWidth: 1,
borderRadius: 8,
padding: 12
},
title: {
marginTop: 12,
marginBottom: 12,
color: "black",
fontSize: 24
},
hide: {
height: 0,
overflow: "scroll"
}
});
App.js
import { SafeAreaView, StyleSheet, Text } from 'react-native';
import { Expand } from './Expand';
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Expand title='React Native'>
<Text>React Native combines the best parts of native development with React, a best-in-class JavaScript library for building user interfaces.</Text>
</Expand>
<Expand title='Flutter'>
<Text>Flutter is an open source framework by Google for building beautiful, natively compiled, multi-platform applications from a single codebase.</Text>
</Expand>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#fff',
margin: 16,
gap: 20
}
});
現時点ではただ表示・非表示が切り替わっているのみです。
開閉アニメーションを追加
開閉時のアニメーションは LayoutAnimation
を使用します。
Animated
より自由度は少ないですが、ビフォーアフターの状態変化を自動でアニメーション化してくれるので View にちょっとした動きを追加したい場合に便利です。
status を更新する直前で configureNext
を設定します。
Expand.js
- import { Text, View, Pressable, StyleSheet } from 'react-native';
+ import { Text, View, Pressable, StyleSheet, LayoutAnimation, UIManager, Platform } from 'react-native';
+ // Android 用の設定
+ if (Platform.OS === 'android') {
+ if (UIManager.setLayoutAnimationEnabledExperimental) {
+ UIManager.setLayoutAnimationEnabledExperimental(true);
+ }
+ }
export const Expand = ({ title = '', children}) => {
・・・
<Pressable
onPress={() => {
+ LayoutAnimation.configureNext(LayoutAnimation.create(300, 'easeInEaseOut', 'opacity'));
setIsOpen(!isOpen);
}}>
動作確認
開閉がいい感じでアニメーションされるようになりました。
複数のアコーディオンを連動させる
一つのアコーディオンを開いたら他のアコーディオンが閉じるようにしてみたいと思います。
アコーディオン間で状態を共有するため Context を使用します。
Expand.js
- import { useState } from 'react';
+ import { useState, createContext, useRef } from 'react';
+ const ExpandGroupContext = createContext(null);
+ export const ExpandGroup = ({ children }) => {
+ let context = useRef([]).current;
+
+ return (
+ <ExpandGroupContext.Provider value={context}>
+ {children}
+ </ExpandGroupContext.Provider>
+ )
+ }
Expand 側で Context が取得できれば ExpandGroup の子要素と判断し Context に setIsOpen を追加し、開閉時に他の Expand をすべて閉じるようにします。
Expand.js
- import { useState, createContext, useRef } from 'react';
+ import { useState, createContext, useRef, useContext, useEffect } from 'react';
export const Expand = ({ title = '', children}) => {
const [isOpen, setIsOpen] = useState(false);
+ const context = useContext(ExpandGroupContext);
+
+ useEffect(() => {
+ // stateを登録
+ if (context != null && !context.includes(setIsOpen)) {
+ context.push(setIsOpen);
+ }
+ return () => {
+ // stateを解除
+ if (context != null && context.includes(setIsOpen)) {
+ context.splice(context.indexOf(setIsOpen), 1);
+ }
+ }
+ }, []);
return (
<View style={styles.frame}>
<Pressable
onPress={() => {
LayoutAnimation.configureNext(LayoutAnimation.create(300, 'easeInEaseOut', 'opacity'));
+ // 他のアコーディオンを閉じる
+ if (context != null) {
+ context.filter(s => s != setIsOpen).forEach(setter => setter(false));
+ }
setIsOpen(!isOpen);
}}>
連動させたい Expand
を ExpandGroup
で囲みます。
App.js
- import { Expand } from './Expand';
+ import { Expand, ExpandGroup } from './Expand';
・・・
+ <ExpandGroup>
<Expand title='React Native'>
<Text>React Native combines the best parts of native development with React, a best-in-class JavaScript library for building user interfaces.</Text>
</Expand>
<Expand title='Flutter'>
<Text>Flutter is an open source framework by Google for building beautiful, natively compiled, multi-platform applications from a single codebase.</Text>
</Expand>
+ </ExpandGroup>
動作確認
一方を開くともう一方が閉じるようになりました👍
Discussion