在 React Native 中用 Expo Camera 和 MaskedView 实现抠图相机效果

了解如何使用 Expo Camera 打造模板镂空效果,把实时画面变成磨砂玻璃背景。

中文
复制
How to build cutout camera effects in React Native with Expo Camera and MaskedView

本文是 Mehdi Davoodi 的客座文章——他做 React Native 和 Expo 的 UI 实验,并在 𝕏 上分享自己的心得:@mehdi_made

...

我想做点东西,让相机画面透过文字和 UI 元素显示出来,做出一种镂空的效果。思路很简单:把实时相机画面当作动态背景,但只让它从 UI 的特定部分透出来。

下面就是我用 Expo Camera、MaskedView 和 Expo Blur 实现的方法。

我们要做的东西

本质上就是一个显示用户列表的界面。但背景不是纯色,而是让相机画面透过文字和头像显示出来。上面再叠一层模糊,做出磨砂玻璃的质感。整体效果就是一个有生命力、独特、让人记得住的 UI。

几个关键部分:

配置 Expo Camera

先安装这个包:

npx expo install expo-camera

expo-camera 这个包主要给你两样东西:一个 CameraView 组件和一个 useCameraPermissions hook。hook 才是重点;不先申请权限,你没法直接渲染相机。

基本配置如下:

import { CameraView, useCameraPermissions } from 'expo-camera';

const App = () => {
  const [permission, requestPermission] = useCameraPermissions();

  if (!permission) {
    // Still loading
    return <View />;
  }

  if (!permission.granted) {
    // Need to ask
    return (
      <Button title="Grant Access" onPress={requestPermission} />
    );
  }

  return <CameraView style={{ flex: 1 }} />;
};

这是最低限度的写法。但有个问题:权限对象的状态不止「已授权」和「未授权」两种。

理解相机权限

来自 useCameraPermissionspermission 对象有两个重要属性:

  • granted——你是否拥有权限

  • canAskAgain——你是否还能再次向用户弹出申请

这样就有了四种实际状态:

permissionnull → 还在检查中

permission.grantedtrue → 相机已就绪

permission.grantedfalsecanAskAgaintrue → 用户拒绝了,但你可以再问一次

permission.grantedfalsecanAskAgainfalse → 用户在设置里彻底禁掉了

最后一种很关键。如果用户拒绝了权限并勾选了「不再询问」,你就没法再弹出申请了。你只能让他们自己去设置里手动开启。

我把这套逻辑封装进了一个 hook:

import { useEffect, useCallback } from 'react';
import { useCameraPermissions } from 'expo-camera';

export const useCamera = (autoRequest = true) => {
  const [permission, requestCameraPermission] = useCameraPermissions();

  const getStatus = () => {
    if (!permission) return 'loading';
    if (permission.granted) return 'granted';
    if (!permission.canAskAgain) return 'blocked';
    return 'denied';
  };

  const status = getStatus();

  const requestPermission = useCallback(async () => {
    try {
      await requestCameraPermission();
    } catch (error) {
      console.error('Failed to request camera permission:', error);
    }
  }, [requestCameraPermission]);

  // Auto-request on mount
  useEffect(() => {
    if (autoRequest && status === 'denied') {
      requestPermission();
    }
  }, [autoRequest, status, requestPermission]);

  return {
    status,
    isReady: status === 'granted',
    requestPermission,
  };
};

现在我的组件只要检查 isReady 就行,不用操心那些细节。

和 MaskedView 结合

MaskedView 是一个组件,可以让你拿另一个组件当遮罩。把它想成一块模板——你抠出一些形状,后面的东西就从这些形状里透出来。

工作原理:

MaskedView 由两部分组成:

maskElement —— 模板(定义形状的部分)

children —— 透过模板显示的内容

MaskedView

遮罩依据不透明度工作。遮罩中完全不透明的部分(比如黑色)会显示子视图,透明的部分则将其隐藏。灰色值对应部分透明。

import MaskedView from '@react-native-masked-view/masked-view';

<MaskedView
  maskElement={
    <View style={{ backgroundColor: 'transparent', flex: 1 }}>
      <Text style={{ color: 'black', fontSize: 60 }}>Hello</Text>
    </View>
  }
>
  {/* This is what shows through */}
  <View style={{ flex: 1, backgroundColor: 'blue' }} />
</MaskedView>

配合摄像头使用

有趣的地方来了。遮罩背后不再是纯色,而是实时摄像头画面:

<MaskedView
  maskElement={
    <Text style={{ color: 'black', fontSize: 40 }}>Hello</Text>
  }
>
  <CameraView style={{ flex: 1 }} />
</MaskedView>

摄像头画面只从文字中透出,其余部分都是透明的。

添加模糊效果

我想要毛玻璃质感,而不是原始的摄像头画面。于是我把 BlurViewexpo-blur 叠在摄像头之上:

<MaskedView
  style={{ flex: 1 }}
  maskElement={<MaskContent />}
>
  <BlurView style={StyleSheet.absoluteFillObject} intensity={100} />
  <CameraView style={StyleSheet.absoluteFillObject} />
</MaskedView>

模糊层位于摄像头之上(通过 zIndex 或渲染顺序实现),遮罩则同时穿透两者。所以你透过遮罩形状看到的是模糊后的摄像头画面。

模糊效果

这里是仓库地址。希望对你有所帮助。如果你真的基于这篇博客里学到的东西做出了什么,我很乐意看到你分享给我。

另外,如果你有其他想让我动手实现并分享的 UI 实验,欢迎私信告诉我。感谢阅读!

来源: Expo Blog← 返回首页