65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (3投票s)

2023 年 2 月 2 日

CPOL

5分钟阅读

viewsIcon

10045

显示照片在地图上位置的应用程序示例。

在俄乌冲突全面爆发之前的疯狂日子里,我曾在国内和国际上旅行过很多次。那时,我还有一部诺基亚 Lumia 手机,带有一个很棒的窗口小部件,可以在地图上显示我拍摄照片的位置。我喜欢看它,意识到世界是多么广阔,我还有多少地方需要去发现。

虽然我的 Android 手机上也有一个类似的窗口小部件,但我对其外观和感觉并不满意。另一个问题是,我上次接触前端开发还是在4 年前,之后我也没有多少现代单页应用框架的经验。渴望弥合这一差距并拥有我想要的应用程序,这激励我创建了自己的应用。

最终的结果如下所示:

如果您想尝试该应用程序,可以从Play 商店下载。源代码可在Github上找到,如果您想跟随文章中的代码一起学习。

约束和权衡

选择我的技术栈的原因是为了更好地了解现代单页应用框架。因此,我自然选择了React Native。作为移动开发新手,我决定从Expo开始。这是一个很好的项目启动方式,因为它包含了一些模板以及一套不错的调试工具。

我学到的是,Expo 的便利性并非没有代价。以下是一些缺点: - 与Facebook SDK的隐式集成 - 一些关键库的弃用,例如expo-ads-admob - 经典构建的弃用。

然而,这些缺点可以通过 EAS 构建来弥补。

可视化算法

为了提供最佳的外观和感觉,我们需要让用户在应用程序枚举照片并获取其地理数据时,感受到正在发生的事情。因此,渲染顺序如下所示:

  1. 显示启动屏幕
  2. 用一个看起来像启动屏幕但还提供加载进度信息的屏幕替换它。
  3. 照片处理完成后,显示一个带有标记的地图。

让我们简要看一下这些点。

显示进度信息

根组件的代码如下所示:

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 日 - 初始版本
© . All rights reserved.