react-native-draggable-flatlist
を使って、ドラッグ・アンド・ドロップで並べ替えできるリストを作ってみます。
react-native-draggable-flatlist
の公式ドキュメントによると、 react-native-reanimated
と react-native-gesture-handler
を先にインストールしろ、とのことなので、まずはこの2つをインストールしていきます。
react-native-reanimated をインストール
npm i react-native-reanimated
プロジェクトのルートディレクトリにある babel.config.js
に 'react-native-reanimated/plugin'
を追加します。
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
};
};
react-native-gesture-handler をインストール
npx expo install react-native-gesture-handler
react-native-draggable-flatlist をインストール
npm i react-native-draggable-flatlist
実装
サンプルデータ用の util
を作ります。これは実際のプロダクトでは不要です。
export function getColor(i: number, numItems: number = 25) {
const multiplier = 255 / (numItems - 1);
const colorVal = i * multiplier;
return `rgb(${colorVal}, ${Math.abs(128 - colorVal)}, ${255 - colorVal})`;
}
export const mapIndexToData = (_d: any, index: number, arr: any[]) => {
const backgroundColor = getColor(index, arr.length);
return {
text: `${index}`,
key: `key-${index}`,
backgroundColor,
height: 75,
};
};
export type Item = ReturnType<typeof mapIndexToData>;
上のサンプルデータを読み込んで表示させるコンポーネントです。
import DraggableItem from '../uiParts/DraggableItem';
import { type Item, mapIndexToData } from './draggableUtil';
import * as Haptics from 'expo-haptics';
import { useCallback, useState } from 'react';
import { SafeAreaView, View } from 'react-native';
import DraggableFlatList, {
OpacityDecorator,
type RenderItemParams,
ScaleDecorator,
ShadowDecorator,
} from 'react-native-draggable-flatlist';
const NUM_ITEMS = 5;
const initialData: Item[] = [...Array(NUM_ITEMS)].map(mapIndexToData);
export default function TopTabSettingScreen() {
const [data, setData] = useState(initialData);
const renderItem = useCallback(({ item, drag, isActive }: RenderItemParams<Item>) => {
return (
<ShadowDecorator>
<ScaleDecorator>
<OpacityDecorator>
<DraggableItem item={item} drag={drag} isActive={isActive} />
</OpacityDecorator>
</ScaleDecorator>
</ShadowDecorator>
);
}, []);
return (
<SafeAreaView>
<DraggableFlatList
data={data}
onDragBegin={() => {
console.log('onDragBegin');
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
onDragEnd={({ data }) => {
console.log('onDragEnd', data);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setData(data);
}}
keyExtractor={(item) => item.key}
renderItem={renderItem}
renderPlaceholder={() => <View style={{ flex: 1, backgroundColor: '#7F8C8D' }} />}
/>
</SafeAreaView>
);
}
一つ一つの行に相当するコンポーネントは以下です。
import { AntDesign, EvilIcons, MaterialIcons } from '@expo/vector-icons';
import * as Haptics from 'expo-haptics';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
export interface DraggableItemProps {
item: {
text: string;
backgroundColor: string;
};
drag: () => void;
isActive: boolean;
}
export default function DraggableItem({ item, drag, isActive }: DraggableItemProps) {
return (
<TouchableOpacity
activeOpacity={1}
onLongPress={drag}
disabled={isActive}
style={[styles.rowItem, { backgroundColor: isActive ? '#ECF0F1' : '#fff' }]}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons
name="drag-handle"
style={styles.draggableHandler}
size={24}
color="#BDC3C7"
/>
<Text style={styles.text}>{item.text}</Text>
</View>
<View style={{ flexDirection: 'row' }}>
<AntDesign
name="edit"
style={styles.editIcon}
size={24}
color="gray"
onPress={async () => {
console.log('edit', item);
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
/>
<EvilIcons
name="trash"
style={styles.trashIcon}
size={24}
color="gray"
onPress={async () => {
console.log('trash', item);
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}}
/>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
rowItem: {
flexDirection: 'row',
height: 70,
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ECF0F1',
},
draggableHandler: {
marginHorizontal: 16,
},
editIcon: {
alignItems: 'flex-end',
marginHorizontal: 12,
},
trashIcon: {
alignItems: 'flex-end',
marginHorizontal: 12,
},
text: {
color: '#2C3E50',
fontSize: 18,
},
});
以下のように動作します。
サンプル
上の例は最もシンプルなサンプルを使っていますが、 swipeableList
のような、スワイプできそうなサンプルもありました。ただ、ちょっと複雑に見えました。
エラー対応
Error: [Reanimated] Mismatch between JavaScript part and native part of Reanimated (3.3.0 vs. 2.14.4). Did you forget to re-build the app after upgrading react-native-reanimated?
ERROR Error: [Reanimated] Mismatch between JavaScript part and native part of Reanimated (3.3.0 vs. 2.14.4). Did you forget to re-build the app after upgrading react-native-reanimated? If you use Expo Go, you must downgrade to 2.14.4 which is bundled into Expo SDK., js engine: hermes
ERROR Invariant Violation: "main" has not been registered. This can happen if:
* Metro (the local dev server) is run from the wrong folder. Check if Metro is running, stop it and restart it in the current project.
* A module failed to load due to an error and `AppRegistry.registerComponent` wasn't called., js engine: hermes
うまくいった解決策
react-native-reanimated
のバージョンを 3.3.0
から 2.14.4
に変更します。
"react-native-reanimated": "^2.14.4",
で、再度 npm i
を実行すれば、エラーが出なくなりました。
npm uninstall react-native-reanimated
npm install react-native-reanimated@2.14.4
再インストールした後は、 --clear
をつけて起動します。
npx expo start --tunnel --clear
うまくいかなかったパターン(expo-doctorを実行)
npx expo-doctor
Error: GestureDetector must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized
2023年7月に新規でインストールして動かそうとすると、以下のようなエラーが出ました。
ERROR Error: GestureDetector must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/installation for more details.
This error is located at:
in GestureDetector (created by DraggableFlatListInner)
in DraggableFlatListProvider (created by DraggableFlatListInner)
"react-native-reanimated": "^2.14.4"
にしても動きません。
"expo": "^49.0.3",
"react-native-draggable-flatlist": "^4.0.1",
"react-native-gesture-handler": "~2.12.0",
"react-native-reanimated": "^2.14.4"
expo
が2023年7月にバージョンアップされたニュースを見たので、とりあえず "expo": "~48.0.18",
まで下げてみました。 React native Gesture Handler の公式ページにも、最新 Expo では動かないかもしれん、みたいなことが書いてあるためです。
The Expo SDK incorporates the latest version of react-native-gesture-handler available at the time of each SDK release, so managed Expo apps might not always support all our latest features as soon as they are available.
Expo SDKは、各SDKのリリース時に利用可能なreact-native-gesture-handlerの最新バージョンを組み込んでいるため、管理されたExpoアプリは、最新の機能が利用可能になり次第、必ずしもすべてをサポートするとは限りません。
https://docs.swmansion.com/react-native-gesture-handler/docs/installation/
node_modules
と package-lock.json
を削除して、以下の package.json
で再度 npm i
を実行したら動きました。
react-native-reanimated
は 2.14.1
まで downgrade しています。原因は「ライブラリのバージョン齟齬」で、結局、何のライブラリが原因なのかはよくわからん。
{
"name": "shoppinglist",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"expo": "~48.0.18",
"expo-haptics": "~12.4.0",
"expo-status-bar": "~1.4.4",
"react": "18.2.0",
"react-native": "0.71.8",
"react-native-draggable-flatlist": "^4.0.1",
"react-native-gesture-handler": "~2.9.0",
"react-native-reanimated": "2.14.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/react": "~18.0.14",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard-with-typescript": "^36.1.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^16.0.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.32.2",
"prettier": "^2.8.0",
"typescript": "^4.9.5"
},
"private": true
}