使用 React Native 在地图上可视化您的照片






4.83/5 (3投票s)
显示照片在地图上位置的应用程序示例。
在俄乌冲突全面爆发之前的疯狂日子里,我曾在国内和国际上旅行过很多次。那时,我还有一部诺基亚 Lumia 手机,带有一个很棒的窗口小部件,可以在地图上显示我拍摄照片的位置。我喜欢看它,意识到世界是多么广阔,我还有多少地方需要去发现。
虽然我的 Android 手机上也有一个类似的窗口小部件,但我对其外观和感觉并不满意。另一个问题是,我上次接触前端开发还是在4 年前,之后我也没有多少现代单页应用框架的经验。渴望弥合这一差距并拥有我想要的应用程序,这激励我创建了自己的应用。
最终的结果如下所示:
如果您想尝试该应用程序,可以从Play 商店下载。源代码可在Github上找到,如果您想跟随文章中的代码一起学习。
约束和权衡
选择我的技术栈的原因是为了更好地了解现代单页应用框架。因此,我自然选择了React Native。作为移动开发新手,我决定从Expo开始。这是一个很好的项目启动方式,因为它包含了一些模板以及一套不错的调试工具。
我学到的是,Expo 的便利性并非没有代价。以下是一些缺点: - 与Facebook SDK的隐式集成 - 一些关键库的弃用,例如expo-ads-admob - 经典构建的弃用。
然而,这些缺点可以通过 EAS 构建来弥补。
可视化算法
为了提供最佳的外观和感觉,我们需要让用户在应用程序枚举照片并获取其地理数据时,感受到正在发生的事情。因此,渲染顺序如下所示:
- 显示启动屏幕
- 用一个看起来像启动屏幕但还提供加载进度信息的屏幕替换它。
- 照片处理完成后,显示一个带有标记的地图。
让我们简要看一下这些点。
显示进度信息
根组件的代码如下所示:
SplashScreen.preventAutoHideAsync().catch(() => {
/* reloading the app might trigger some race conditions, ignore them */
});
const App = () => {
return (
<AnimatedAppLoader/>
);
}
非常直接。我们阻止启动屏幕自动隐藏,并渲染一个单一组件。让我们更深入地研究该组件的代码。
return (
<View style={{ flex: 1 }}>
<AnimatedSplashScreen/>
</View>
)
我们感兴趣的点是 `AnimatedSplashScreen` 组件。
const AnimatedSplashScreen = () => {
const textAnimation = useMemo(() => new Animated.Value(0), []);
const [isAppReady, setAppReady] = useState(false);
const [isTextAnimationIsReady, setTextAnimationIsReady] = useState(false);
const [loadingText, setLoadingText] = useState("");
useEffect(() => {
if (!isAppReady && isTextAnimationIsReady) {
Animated.timing(textAnimation, {
toValue: 1,
duration: 200,
easing: Easing.inOut(Easing.exp),
useNativeDriver: true,
}).start();
}
}, [isAppReady, isTextAnimationIsReady]);
const loadLocations = async () => {
let markersArray : MediaLibrary.Location[] = [];
let hasMoreData = true;
try {
while (hasMoreData) {
//loading imahe locations is omitted by now
let now = Date.now();
let delta = now - timeStart;
if (delta > 9000) {
setLoadingText(`We've processed ${cursor.endCursor} items.
There's more work though...`)
} else if (delta > 5000) {
setLoadingText("Working on it...")
} else if (delta > 2000) {
if (!isTextAnimationIsReady) {
setTextAnimationIsReady(true);
}
setLoadingText("Hold on! We're doing some magic just for you...")
}
}
} catch (e) {
console.log(e)
} finally {
setAppReady(!hasMoreData);
}
}
const onImageLoaded = useCallback(async () => {
await SplashScreen.hideAsync();
await loadLocations();
}, []);
return (
<View style={{ flex: 1 }}>
{isAppReady && (<MainScreen/>)}
{!isAppReady && (<Animated.View
pointerEvents="none"
style={[
{
backgroundColor: Constants.manifest?.splash?.backgroundColor
},
]}>
<Animated.Text>
{loadingText}
</Animated.Text>
<Animated.Image
source={require("../assets/splash.png")}
onLoadEnd={onImageLoaded}
fadeDuration={0}
/>
</Animated.View>)}
</View>
);
}
这里发生的是,我们用一个看起来完全像启动屏幕的 `Animated.Image` 替换了我们的启动屏幕。一旦图像准备好,我们就会调用 `onImageLoaded` 回调。现在我们的图像已准备好,我们可以隐藏启动屏幕并调用 `loadLocations`。此方法执行枚举照片位置所需的工作,同时负责动画以使用户保持参与。
目前,我们关于 `loadLocations` 方法需要知道的一切是,它按批次处理图像,这由 `while (hasMoreData)` 指示。这意味着在每次循环迭代中,我们可以检查经过了多少时间并相应地触发文本动画。一旦到了显示文本动画的时候,我们就用 `setTextAnimationIsReady(true);` 更新状态。这反过来又触发 `Animated.timing`。它改变了 `Animated.Text` 的不透明度。
完成后,我们用 `setAppReady` 更新状态,并将我们的动画组件替换为 `MainScreen`。
加载图片位置
为了枚举我们图库的内容,我们将使用Expo MediaLibrary。
let medialibraryRequest : MediaLibrary.AssetsOptions = {}
const loadLocations = async () => {
let markersArray : MediaLibrary.Location[] = [];
let hasMoreData = true;
try {
let { status } = await MediaLibrary.requestPermissionsAsync();
let markersSet : Set<MediaLibrary.Location> = new Set();
const albums = await MediaLibrary.getAlbumsAsync();
const cameraAlbum = albums.find(p => p.title === "Camera");
medialibraryRequest.album = cameraAlbum;
while (hasMoreData) {
let cursor = await MediaLibrary.getAssetsAsync(medialibraryRequest);
await populateLocationsIntoSet(cursor, markersSet);
hasMoreData = cursor.hasNextPage;
medialibraryRequest.after = cursor.endCursor
}
markersArray = [...markersSet]
setMarkers(markersArray);
} catch (e) {
console.log(e)
} finally {
setAppReady(!hasMoreData);
}
}
const populateLocationsIntoSet = async (
cursor : MediaLibrary.PagedInfo<MediaLibrary.Asset>,
markersSet : Set<MediaLibrary.Location>) => {
const allowedTypes : MediaLibrary.MediaTypeValue[] = [
MediaLibrary.MediaType.photo,
MediaLibrary.MediaType.video
]
const markersArray = await Promise.all(cursor.assets.map(async element => {
if (!allowedTypes.includes(element.mediaType)) {
return;
}
let image = await MediaLibrary.getAssetInfoAsync(element);
return image.location;
}));
if (markersArray.length === 0) {
return;
}
let nonNullLocations = markersArray.filter(p => p != undefined)
as MediaLibrary.Location[];
nonNullLocations.forEach(markersSet.add, markersSet);
}
我们首先使用 `requestPermissionsAsync` 请求访问媒体库的权限。然后,我们使用 `getAssetsAsync` 方法迭代“相机”相册的内容,该方法充当光标,按批次获取项目。由于生成的项目仅包含项目的基本信息,因此我们需要执行其他请求来获取地理位置。我们在 `populateLocationsIntoSet` 中使用 `getAssetInfoAsync` 方法执行此操作。
在地图上显示位置
要在地图上显示位置,我使用了react-native-maps。
const MainScreen = ({markers}: MainScreenProps) => {
let map = useRef<MapView>(null);
const fitAllMarkers = () => {
const boundingBox = markers
if (markers.length === 1) {
markers.push({latitude: markers[0].latitude+0.1, longitude: markers[0].longitude+0.1})
}
map.current?.fitToCoordinates(boundingBox, {
edgePadding: DEFAULT_PADDING,
animated: true,
})};
return (
<View style={styles.container}>
<MapView ref={map}
style={styles.map}
onMapLoaded={fitAllMarkers}>
{markers.map((item : MediaLibrary.Location) => (
<Marker
key={Math.random()}
coordinate={{
latitude: item.latitude,
longitude: item.longitude,
}}
icon={require('../assets/marker.png')}>
</Marker>
))}
</MapView>
);
}
在上面的代码中,我们使用 `MapView` 组件,并且对于 `markers` 数组中的每个项目,我们在地图上放置一个 `Marker`。地图加载后,我们使用 `fitToCoordinates` 方法将其聚焦在标记所在区域。
分享您的地图
我想您永远无法想象没有人们在社交网络上分享东西的现代世界。因此,我希望在我的应用程序中嵌入的关键功能是 **分享** 按钮。
要分享结果,我们将需要结合使用react-native-view-shot(在地图渲染后捕获屏幕状态)和expo-sharing来分享我们捕获的屏幕截图。
const captureAndShareScreenshot = async () => {
const uri = await captureRef(map, {
format: "png",
quality: 1
})
await Sharing.shareAsync("file://" + uri);
};
检查连接状态
react native maps 的缺点是它需要互联网连接才能加载图块。这就是为什么我们希望在没有互联网连接时优雅地通知用户,而不是显示损坏的地图。
为了探测互联网连接,我们将需要NetInfo。
让我们回顾一下 `AnimatedAppLoader` 组件。
const AnimatedAppLoader = () => {
const [isConnected, setConnected] = useState(false)
const [isConnectionProbeFinished, setConnectionProbeFinished] = useState(false)
const fetchConnection = async () => {
const connectionStaus = await NetInfo.fetch();
setConnected(connectionStaus.isConnected === true)
if (!connectionStaus.isConnected) {
await SplashScreen.hideAsync();
}
setConnectionProbeFinished(true)
}
useEffect(() => {
fetchConnection()
}, [])
const refresh = useCallback(async () => {
await fetchConnection()
},[])
return (
<View style={{ flex: 1 }}>
{isConnected && isConnectionProbeFinished && <AnimatedSplashScreen/>}
{!isConnected && isConnectionProbeFinished && <OfflineScreen refresh={refresh}/>}
</View>
)
}
我想强调的是在 `OfflineScreen` 组件中传递 `refresh`。这样做允许我们在子组件内部执行另一次连接探测。
const OfflineScreen = ({refresh}: any) => {
return (
<View style={styles.container}>
<Text style={styles.text}>
Locate! needs an internet connection to function properly. {'\n'}
Refresh this page once you enable it.
</Text>
<Button
icon="refresh"
mode="contained"
buttonColor="#E81E25"
textColor="#FFF"
theme={CustomTheme}
style={styles.button}
onPress={() => refresh()}>
Refresh
</Button>
</View>
)
}
结论
总结我的旅程,我想说 React Native 和 Expo 特别是,是非常方便的工具,它允许您在无需精通 Android/iOS 特定技术的情况下开发移动应用程序。通过使用广受欢迎的 React 框架,我设法开发了我长期以来一直想要的应用程序。
根据我的个人经验,发布应用程序是一个非常繁琐的过程,我不确定我是否愿意再次尝试,但这部分我将在本文中省略。
历史
- 2023 年 2 月 2 日 - 初始版本