memoirする画面の共有機能を実装したので解説
Pull Request
実装
内容としては、memoirする画面(1週間分のタスクを表示する)をスクリーンショットして、共有できるような感じで実装していきます。
まず、画面スクリーンショットはreact-native-view-shotを使用して実装
以下みたいな感じで実装できます
import React, { useRef } from 'react'; import ViewShot from 'react-native-view-shot'; import { View, Text } from 'react-native'; export default () => { const viewShot = useRef(null); const onCapture = () => { const url = await viewShot.current.capture(); console.log(url); } return ( <ViewShot onCapture={onCapture} ref={viewShot} options={{ format: 'jpg' }}> <Text>テスト</Text> </ViewShot> ) }
ViewShot
でラップした部分をキャプチャーすることが可能です。
この画像を、expo-sharingを使用して画像を共有します
先程の例に追記すると、こんな感じで実装できます
import React, { useRef } from 'react'; import ViewShot from 'react-native-view-shot'; import { View, Text } from 'react-native'; import * as Sharing from 'expo-sharing'; export default () => { const viewShot = useRef(null); const onCapture = async() => { const url = await viewShot.current.capture(); const ok = await Sharing.isAvailableAsync(); if (ok) { await Sharing.shareAsync('file://' + url); } } return ( <ViewShot onCapture={onCapture} ref={viewShot} options={{ format: 'jpg' }}> <Text>テスト</Text> </ViewShot> ) }
この2つのライブラリを使用して実装していきます。 実装する中で以下の対応を行いました。
- ①.元々がインフィニティスクロールしている画面だったので、スクリーンショット用に、全項目表示の画面を作成
- ②.レンダリングから画像が表示されるまでのタイムラグがあるので、全ての画像表示が完了したらキャプチャーするように実装
①の方は、サクッと別画面として全項目表示用の画面を実装
■ src/components/organisms/Memoir/ScreenShot.tsx
import React, { memo, useRef, useCallback, useState } from 'react'; import { StyleSheet, ScrollView, useWindowDimensions, Alert, } from 'react-native'; import * as Sharing from 'expo-sharing'; import ViewShot from 'react-native-view-shot'; import View from 'components/atoms/View'; import { useNavigation } from '@react-navigation/native'; import dayjs from 'lib/dayjs'; import theme from 'config/theme'; import { Props as PlainProps } from 'components/pages/Memoir/ScreenShot/Plain'; import { User as TUser } from 'store/atoms'; import Header from 'components/molecules/Memoir/Header'; import { getModeCountMax } from 'lib/utility'; import DateText from 'components/molecules/Memoir/DateText'; import Divider from 'components/atoms/Divider'; import Loading from 'components/molecules/Overlay/Loading'; import { Item } from 'hooks/useItemsInPeriodPaging'; import Card from './Card'; export type Props = Pick<PlainProps, 'users'> & { startDate: string; endDate: string; items: Item[]; }; type User = Omit<TUser, 'userID'> & { id: string; }; type Card = Item & { user: User; }; type RenderedItem = { date: string | null; contents?: Card; categoryID?: number; last?: boolean; width: number; onLoadEnd: () => void; }; const RenderItem: React.FC<RenderedItem> = (props) => { if (props.date) { return <DateText date={props.date} categoryID={Number(props.categoryID)} />; } return ( <View> <View mb={3} mx={3}> <Card title={props?.contents?.title || ''} categoryID={props?.contents?.categoryID || 0} user={props?.contents?.user as User} onPress={() => null} onLoadEnd={props.onLoadEnd} /> {!props?.last && <Divider />} </View> </View> ); }; const ScreenShot: React.FC<Props> = (props) => { const viewShot = useRef<ViewShot>(null); const navigation = useNavigation(); const count = useRef(0); const [loading, setLoading] = useState(true); const windowWidth = useWindowDimensions().width; const onShare = useCallback( async (uri: string) => { const ok = await Sharing.isAvailableAsync(); if (!ok) { Alert.alert('エラー', '共有機能を利用できませんでした', [ { text: '戻る', onPress: () => { navigation.goBack(); }, }, ]); return; } await Sharing.shareAsync(uri); navigation.goBack(); }, [navigation] ); const onCapture = useCallback(async () => { const url = await viewShot.current?.capture?.(); if (url) onShare('file://' + url); }, [onShare]); const onLoadEnd = useCallback(() => { count.current = count.current + 1; if (props.items.length === count.current) { setLoading(false); setTimeout(() => onCapture(), 100); } }, [props.items, onCapture]); const dates = Array.from( new Set(props.items.map((v) => dayjs(v.date).format('YYYY-MM-DD'))) ); const dateItems = dates.sort().map((date) => { const contents = date; return { date, contents, }; }); const data = dateItems .map((v1) => { const sameDateItems = props.items.filter( (v2) => dayjs(v2.date).format('YYYY-MM-DD') === v1.date ); const item: RenderedItem[] = sameDateItems.map((v2, index) => { const user: User | undefined = props.users.find( (v) => v.id === v2.userID ); return { date: null, contents: { ...v2, user: user || { id: '', displayName: '', image: '', }, }, last: sameDateItems.length === index + 1, width: windowWidth, onLoadEnd: onLoadEnd, }; }); const categoryID = item.map((v) => Number(v.contents?.categoryID)); const dateItem: RenderedItem = { date: v1.date, categoryID: getModeCountMax(categoryID), width: windowWidth, onLoadEnd: () => null, }; return [dateItem, ...item]; }) .flat(); return ( <> <ScrollView style={styles.root}> <ViewShot ref={viewShot} options={{ format: 'jpg' }}> <Header startDate={props.startDate} endDate={props.endDate} /> {data.map((v, index) => ( <RenderItem {...v} key={index} /> ))} <View /> </ViewShot> </ScrollView> {loading && <Loading text="作成中" />} </> ); }; const styles = StyleSheet.create({ root: { backgroundColor: theme().color.background.main, flex: 1, width: '100%', paddingTop: theme().space(4), }, }); export default memo(ScreenShot);
①はReact NativeのImageのonLoadEndから表示完了タイミングを取得できるので、こちらをハンドリングして、全ての画像が表示されたタイミングでキャプチャーするように実装
これで諸々の実装が完了、動作は以下のようになりました