React Native动态旋转秀:Animated打造三张图片炫彩效果

在移动端开发中,流畅的动画能够大大提升用户体验和界面活力。本文将带你用 React Native 的 Animated API,打造一个由三张图片组成的炫彩旋转秀。通过不同的旋转速度、延迟与交错,我们可以轻松实现视觉冲击力十足的动态效果,让你的页面瞬间“活”起来。

本文内容包含:

  1. 动画效果预览与思路概览
  2. 环境准备与依赖说明
  3. Animated 核心原理解析
  4. 完整示例代码(分步讲解)
  5. 图解布局与动画流程
  6. 常见疑问与扩展思路

一、动画效果预览与思路概览

1.1 效果预览

以下用文字和 ASCII 图示简单模拟效果(实际效果请运行示例代码查看):

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│   ① 图片A:缓慢顺时针旋转               ② 图片B:中速逆时针旋转      ③ 图片C:快速顺时针旋转    │
│                                                              │
│        ┌────────────┐    ┌────────────┐   ┌────────────┐       │
│        │   Image A  │    │   Image B  │   │   Image C  │       │
│        │   (慢速)   │    │   (中速)   │   │   (快速)   │       │
│        └────────────┘    └────────────┘   └────────────┘       │
│               ↻               ↺              ↻                 │
│                                                              │
└──────────────────────────────────────────────────────────────┘
  • Image A:以最慢速度顺时针旋转,每圈时长约 8 秒。
  • Image B:以中等速度逆时针旋转,每圈时长约 5 秒。
  • Image C:以最快速度顺时针旋转,每圈时长约 3 秒。

三张图片同步开始,但可以通过延迟或不同速度,制造有节奏的视觉效果。你也可以在代码中自行调整速度、延迟、方向、图片素材等,生成完全个性化的旋转秀。

TIP:下文我们使用三张示例图片,你可以替换成任意本地资产或网络图片,如头像、Logo、插画等。

1.2 实现思路

  1. 使用 Animated.Value 创建“旋转角度”动态值

    • 每个图片对应一个 Animated.Value(0),代表初始角度为 0。
    • 借助 Animated.timingAnimated.loop 不断更新该值,实现“无限旋转”。
  2. 通过 interpolate 将数值映射到角度(deg)

    • JS 层的 Animated.Value 是一个数字,我们要把它映射到字符串形式的 "0deg" → "360deg"
    • 通过 spin.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] }),让数值 0→1 对应角度 0°→360°。
  3. 在每个 Animated.Image 上,通过 style={{ transform: [{ rotate: spin }] }} 应用旋转

    • rotate 接受一个字符串(如 "45deg"),配合 interpolation,就会呈现旋转动画。
  4. 给不同图片设置不同的动画时长(duration)与方向(正向/反向),甚至延迟(delay)

    • 顺时针:toValue = 1 且插值为 "0deg""360deg"
    • 逆时针:可以把 outputRange 写成 ["360deg", "0deg"] 或者将 Animated.Value 从 0 → -1。
    • 无限循环:使用 Animated.loop 包裹 Animated.timing,并设置 useNativeDriver: true(更流畅)。
  5. 组合三个动画同时启动

    • 可以使用 Animated.parallel 或者在 useEffect 中分开 start()

下面就按照上述思路,逐步演示从环境准备到完整实现的全过程。


二、环境准备与依赖说明

2.1 React Native 项目环境

本文示例以 React Native 0.63+ 版本为基准(同样适用于 0.64、0.65、0.66 等)。请确保你的开发环境已经安装:

  • Node.js ≥ 12
  • Yarn 或 npm
  • React Native CLI(或使用 Expo,但示例中假设使用原生项目)
  • Xcode(macOS)或 Android Studio(若需 Android 兼容可略过)

若你尚未创建 RN 项目,可在终端执行:

npx react-native init AnimatedRotationDemo
cd AnimatedRotationDemo

接着,进入项目目录进行开发。

2.2 引用图片资源

在项目根目录下创建一个 assets 文件夹,将示例图片(imageA.pngimageB.pngimageC.png)放入其中。示例结构:

AnimatedRotationDemo/
├─ android/
├─ ios/
├─ node_modules/
├─ assets/
│   ├─ imageA.png
│   ├─ imageB.png
│   └─ imageC.png
├─ App.js
└─ package.json
提示:示例图片可以任意替换,只要确保路径正确即可。

三、Animated 核心原理解析

在正式编写代码之前,我们先梳理一下 Animated API 的一些核心概念。

3.1 Animated.Value 与插值 (interpolate)

  • new Animated.Value(initialValue)

    • 创建一个可动画化的数值对象。
    • initialValue 可以是数字,通常初始设为 0。
  • interpolate

    • 用于将 Animated.Value 在一定区间内映射到其他区间(数值、角度、颜色等)。
    • 例如:

      const spinValue = new Animated.Value(0);
      const spin = spinValue.interpolate({
        inputRange: [0, 1],
        outputRange: ["0deg", "360deg"]
      });
    • 这样当 spinValue 从 0 变化到 1 时,spin 会从 "0deg" 变化到 "360deg"

3.2 构建动画:Animated.timing

  • Animated.timing(animatedValue, config)

    • 基于时间(duration)驱动动画,将 animatedValueconfig.fromValue(或当前值)过渡到 config.toValue
    • config 常用参数:

      • toValue: 目标数值(如 1-1)。
      • duration: 动画持续时长(毫秒)。
      • easing: 缓动函数(可选,默认线性)。
      • delay: 延迟启动时长(毫秒,可选)。
      • useNativeDriver: 是否使用原生驱动(对于 transform、opacity 等属性必须设为 true)。

示例:

Animated.timing(spinValue, {
  toValue: 1,
  duration: 5000,
  useNativeDriver: true,
}).start();

3.3 无限循环:Animated.loop

  • Animated.loop(animation, config)

    • 将单次动画包装成一个“无限循环”动画。
    • config 可设 { iterations: number },若省略则无限执行。

例如:

const spinAnimation = Animated.loop(
  Animated.timing(spinValue, {
    toValue: 1,
    duration: 5000,
    easing: Easing.linear,
    useNativeDriver: true,
  })
);
spinAnimation.start();
  • 注意:如果要重新开始循环,需在每次循环结束前手动重置 spinValue,或者在 Animated.timing 中使用 useNativeDriver 并将 toValue: 1 后,在 loop 中自动回到初始值。

3.4 同步启动多个动画:Animated.parallel

  • Animated.parallel(arrayOfAnimations, config)

    • 同时启动一组动画。
    • config 可选 { stopTogether: boolean },默认为 true,表示其中一个动画停止时,其它动画也停止。

示例:

Animated.parallel([
  Animated.loop(Animated.timing(spinA, { toValue: 1, duration: 8000, useNativeDriver: true })),
  Animated.loop(Animated.timing(spinB, { toValue: 1, duration: 5000, useNativeDriver: true })),
  Animated.loop(Animated.timing(spinC, { toValue: 1, duration: 3000, useNativeDriver: true })),
]).start();

这样就能够同时让三张图片按照各自速度无限旋转。


四、完整示例代码(分步讲解)

下面给出一个完整的 App.js,示例会在中心水平布局三张图片,分别以不同速度和方向旋转。之后我们会分段解析每一步。

4.1 完整 App.js 代码

// App.js
import React, { useRef, useEffect } from "react";
import {
  View,
  StyleSheet,
  Animated,
  Easing,
  Dimensions,
} from "react-native";

const { width } = Dimensions.get("window");
const IMAGE_SIZE = 100; // 每张图片的宽高

export default function App() {
  // 1. 创建三个 Animated.Value,用于驱动旋转
  const spinValueA = useRef(new Animated.Value(0)).current;
  const spinValueB = useRef(new Animated.Value(0)).current;
  const spinValueC = useRef(new Animated.Value(0)).current;

  // 2. useEffect 中启动动画
  useEffect(() => {
    // A:顺时针,8 秒一圈
    Animated.loop(
      Animated.timing(spinValueA, {
        toValue: 1,
        duration: 8000,
        easing: Easing.linear,
        useNativeDriver: true,
      })
    ).start();

    // B:逆时针,5 秒一圈 -> 通过 outputRange 反转
    Animated.loop(
      Animated.timing(spinValueB, {
        toValue: 1,
        duration: 5000,
        easing: Easing.linear,
        useNativeDriver: true,
      })
    ).start();

    // C:顺时针,3 秒一圈,延迟 500ms 启动
    Animated.loop(
      Animated.timing(spinValueC, {
        toValue: 1,
        duration: 3000,
        easing: Easing.linear,
        delay: 500,
        useNativeDriver: true,
      })
    ).start();
  }, [spinValueA, spinValueB, spinValueC]);

  // 3. 使用 interpolate 将 0->1 映射到 '0deg'->'360deg'
  const spinA = spinValueA.interpolate({
    inputRange: [0, 1],
    outputRange: ["0deg", "360deg"], // 顺时针
  });
  const spinB = spinValueB.interpolate({
    inputRange: [0, 1],
    outputRange: ["360deg", "0deg"], // 逆时针
  });
  const spinC = spinValueC.interpolate({
    inputRange: [0, 1],
    outputRange: ["0deg", "360deg"], // 顺时针
  });

  return (
    <View style={styles.container}>
      {/* 图片 A */}
      <Animated.Image
        source={require("./assets/imageA.png")}
        style={[
          styles.image,
          {
            transform: [{ rotate: spinA }],
          },
        ]}
      />

      {/* 图片 B */}
      <Animated.Image
        source={require("./assets/imageB.png")}
        style={[
          styles.image,
          {
            transform: [{ rotate: spinB }],
            marginHorizontal: 20, // 三张图片间距
          },
        ]}
      />

      {/* 图片 C */}
      <Animated.Image
        source={require("./assets/imageC.png")}
        style={[
          styles.image,
          {
            transform: [{ rotate: spinC }],
          },
        ]}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    flexDirection: "row", // 水平排列三张图片
    backgroundColor: "#F5F5F5",
  },
  image: {
    width: IMAGE_SIZE,
    height: IMAGE_SIZE,
    borderRadius: 8, // 可选,为图片添加圆角
  },
});

4.2 代码解析

4.2.1 创建 Animated.Value

const spinValueA = useRef(new Animated.Value(0)).current;
const spinValueB = useRef(new Animated.Value(0)).current;
const spinValueC = useRef(new Animated.Value(0)).current;
  • 使用 useRef 创建三个不同的 Animated.Value,初始值都为 0
  • useRef(...).current 可以保证在组件多次渲染时,这三个值不会被重新创建,保持稳定。

4.2.2 启动动画循环

放在 useEffect 中,确保在组件挂载后只执行一次:

useEffect(() => {
  // A:顺时针,8 秒一圈
  Animated.loop(
    Animated.timing(spinValueA, {
      toValue: 1,
      duration: 8000,
      easing: Easing.linear,
      useNativeDriver: true,
    })
  ).start();

  // B:逆时针,5 秒一圈
  Animated.loop(
    Animated.timing(spinValueB, {
      toValue: 1,
      duration: 5000,
      easing: Easing.linear,
      useNativeDriver: true,
    })
  ).start();

  // C:顺时针,3 秒一圈,延迟 500ms
  Animated.loop(
    Animated.timing(spinValueC, {
      toValue: 1,
      duration: 3000,
      easing: Easing.linear,
      delay: 500,
      useNativeDriver: true,
    })
  ).start();
}, [spinValueA, spinValueB, spinValueC]);
  • Animated.loop(Animated.timing(...))

    • Animated.timing 包裹在 Animated.loop 中,使其无限循环。
    • 因为 toValue: 1,每次循环结束后,Animated.loop 会自动重置 spinValueX0 并重新开始。
  • 不同配置

    • spinValueAduration: 8000,即 8 秒完成 0→1。
    • spinValueBduration: 5000,5 秒一圈。
    • spinValueCduration: 3000,3 秒一圈,并且带 delay: 500,即挂载后先等待 500ms 再开始循环。

4.2.3 插值映射:Numeric → “deg”

const spinA = spinValueA.interpolate({
  inputRange: [0, 1],
  outputRange: ["0deg", "360deg"], // 顺时针
});
const spinB = spinValueB.interpolate({
  inputRange: [0, 1],
  outputRange: ["360deg", "0deg"], // 逆时针
});
const spinC = spinValueC.interpolate({
  inputRange: [0, 1],
  outputRange: ["0deg", "360deg"], // 顺时针
});
  • spinValueX 会从 0 → 1,不断循环。
  • interpolate 将其映射成旋转角度,生成一个新的 Animated 可动画化值(类型为字符串,如 "45deg")。
  • 顺时针["0deg", "360deg"]
  • 逆时针:把输出范围倒过来:["360deg", "0deg"]

4.2.4 在 Animated.Image 上应用 transform

<Animated.Image
  source={require("./assets/imageA.png")}
  style={[
    styles.image,
    {
      transform: [{ rotate: spinA }],
    },
  ]}
/>
  • Animated.Image 与普通 Image 组件唯一不同是它可以接受 Animated.Value 类型的样式属性。
  • style 中,以 transform: [{ rotate: spinA }] 应用插值后的旋转角度值,配合 useNativeDriver: true,实现高性能的原生动画。

4.2.5 布局:水平排列三张图片

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    flexDirection: "row", // 水平排列
    backgroundColor: "#F5F5F5",
  },
  image: {
    width: IMAGE_SIZE,
    height: IMAGE_SIZE,
    borderRadius: 8,
  },
});
  • 容器 flexDirection: "row",将三张图片水平排列。
  • justifyContent: "center" + alignItems: "center",确保内容在屏幕中央。
  • IMAGE_SIZE(100)可以按需调整。

4.2.6 运行效果

  • 项目启动:npx react-native run-ios
  • 在模拟器或真机中,你会看到三张图片并排在屏幕中央,它们分别以不同速度与方向不断旋转,形成炫彩效果。

五、图解布局与动画流程

5.1 布局示意图

┌────────────────────────────────────────────────────────────┐
│                                                            │
│   ←———  左间距  ————   三张图片   ————  右间距  ————›        │
│                                                            │
│  (center)   [ ImageA ]  ─── 20px ───  [ ImageB ]  ─── 20px ─ [ ImageC ]   │
│                                                            │
│           ↑                                             ↑         ↑      │
│           |                                             |         |      │
│        transform:                                  transform:   transform:│
│        rotate: spinA                               rotate: spinB  rotate: spinC│
│                                                            │
└────────────────────────────────────────────────────────────┘
  • ImageA 位于左侧,旋转动画由 spinA 驱动。
  • ImageB 位于中间,左右都有 20px 间距,旋转动画由 spinB 逆时针驱动。
  • ImageC 位于右侧,旋转动画由 spinC 驱动。

5.2 动画时序流程

Time Axis: 0ms ─────────────────────────────────────────> ∞

ImageA.spinValueA: 0 -> 1 (8 秒) → 重置为 0 → 继续循环
ImageB.spinValueB: 0 -> 1 (5 秒) → 重置为 0 → 继续循环
ImageC.spinValueC: (Delay 500ms) 0 -> 1 (3 秒) → 重置为 0 → 继续循环

             ┌──────────────────────────────────────────────┐
Time 0ms     │ 动画初始化: spinValueA/B/C 设为 0           │
             └──────────────────────────────────────────────┘
                 ↑              ↑             ↑
                 │              │             │
             A、B 同步开始     C 延迟 500ms  不旋转
             (spinA & spinB)

Time 500ms   ┌──────────────────────────────────────────────┐
             │ C 启动: spinValueC 从 0 开始旋转             │
             └──────────────────────────────────────────────┘

Time 3000ms  ┌──────────────────────────────────────────────┐
             │ C 完成一圈(0->1),循环重置为 0,继续下一圈  │
             └──────────────────────────────────────────────┘

Time 5000ms  ┌──────────────────────────────────────────────┐
             │ B 完成一圈(0->1),循环重置为 0,继续下一圈  │
             └──────────────────────────────────────────────┘

Time 8000ms  ┌──────────────────────────────────────────────┐
             │ A 完成一圈(0->1),循环重置为 0,继续下一圈  │
             └──────────────────────────────────────────────┘

    ... 无限循环 依此类推 ...
  • spinValueA:每 8000ms(8s)完成一次 0→1:对应旋转 0°→360°。
  • spinValueB:每 5000ms(5s)完成一次,逆时针
  • spinValueC:延迟 500ms 后,每 3000ms(3s)完成一次。

六、常见疑问与扩展思路

在实际开发中,你可能会对该示例进行各种扩展和优化。以下列举一些常见问题及可延展思路,帮助你更深入理解并灵活运用。

6.1 如何让三张图片“分时”启动动画?

如果想让三张图片依次启动,而不是同时启动,可以在 Animated.loop(Animated.timing) 中为后两者设置更大的 delay

Animated.loop(
  Animated.timing(spinValueA, {
    toValue: 1,
    duration: 8000,
    easing: Easing.linear,
    useNativeDriver: true,
    delay: 0,
  })
).start();

Animated.loop(
  Animated.timing(spinValueB, {
    toValue: 1,
    duration: 5000,
    easing: Easing.linear,
    useNativeDriver: true,
    delay: 1000, // 延迟 1 秒后启动
  })
).start();

Animated.loop(
  Animated.timing(spinValueC, {
    toValue: 1,
    duration: 3000,
    easing: Easing.linear,
    useNativeDriver: true,
    delay: 2000, // 延迟 2 秒后启动
  })
).start();
  • 这样会在 A 启动后 1 秒时启动 B,再过 1 秒时启动 C。
  • 三张图片的动画时间线更具层次感。

6.2 如何给旋转添加“缩放”或“位移”效果?

你可以在 transform 属性数组中添加更多动画值。例如,让图片在旋转时也做“呼吸”缩放。

// 在 hook 之外,创建缩放值
const scaleValueA = useRef(new Animated.Value(1)).current;

// 在 useEffect 中:
Animated.loop(
  Animated.sequence([
    Animated.timing(scaleValueA, {
      toValue: 1.2,
      duration: 2000,
      useNativeDriver: true,
      easing: Easing.ease,
    }),
    Animated.timing(scaleValueA, {
      toValue: 1.0,
      duration: 2000,
      useNativeDriver: true,
      easing: Easing.ease,
    }),
  ])
).start();

// 然后在 style 中:
transform: [
  { rotate: spinA },
  { scale: scaleValueA },
],
  • Animated.sequence 将多个动画按序执行。
  • scaleValueA 在 1.0 和 1.2 之间循环,配合 Animated.loop,实现“呼吸”效果。
  • 组合在 transform 数组中时,顺序就是“先旋转、再缩放”。

同理,你可以加入位移动画:

const translateYValueA = useRef(new Animated.Value(0)).current;

// 定义上下位移动画
Animated.loop(
  Animated.sequence([
    Animated.timing(translateYValueA, {
      toValue: -10,
      duration: 1000,
      useNativeDriver: true,
    }),
    Animated.timing(translateYValueA, {
      toValue: 10,
      duration: 1000,
      useNativeDriver: true,
    }),
    Animated.timing(translateYValueA, {
      toValue: 0,
      duration: 1000,
      useNativeDriver: true,
    }),
  ])
).start();

// Style:
transform: [
  { rotate: spinA },
  { translateY: translateYValueA },
],
  • 这样会让图片在旋转的同时,上下浮动 10 个像素,提升动态感。

6.3 为什么需要设置 useNativeDriver: true

  • useNativeDriver: true 表示将动画交由原生驱动(NativeDriver)执行,避免 JavaScript 线程的卡顿。
  • 转换(transform)、透明度(opacity)等样式均可使用原生驱动。
  • 如果不设置 useNativeDriver: true,且动画逻辑较多,会导致 JS 线程阻塞,界面渲染不流畅。

6.4 Animated vs. Reanimated

  • 本文示例使用 React Native 自带的 Animated。如果你对性能有更高要求,可考虑使用 Reanimated 库,它提供了更丰富的原生驱动功能和声明式动画 API。
  • Reanimated 在语法上与 Animated 略有不同,但思路相似,配置“旋转 + 缩放”等动画也非常方便。

七、总结

本文以“React Native动态旋转秀”为主题,详细展示了如何使用 Animated API 打造三张图片不同速度、方向的无限旋转动画,并对 Animated 的核心原理、常用方法(Animated.ValueinterpolateAnimated.timingAnimated.loopAnimated.parallel)进行了深入解析。我们提供了:

  1. 效果预览与思路:先通过 ASCII 图示了解动画效果,再制定实现思路。
  2. 环境准备:如何在 RN 项目中引用本地图片资源。
  3. 核心原理解析:Animated.Value 与插值、时序动画、无限循环、并行动画等。
  4. 完整示例代码:一个可复制粘贴的 App.js,三张图片并排渲染,分别以 8s/5s/3s 周期旋转。
  5. 代码分步解析:逐行解释如何创建动画驱动值、如何插值映射到角度、如何应用到组件样式。
  6. 图解布局与时序:用 ASCII 图示说明布局结构与动画时序。
  7. 扩展与疑问:包括分时启动、缩放/位移动画、NativeDriver 原理、以及使用 Reanimated 的可选方案。

通过本文,你应当能够:

  • 熟练使用 Animated API 实现旋转、缩放、位移动画
  • 使用 interpolate 将数值映射到度数、透明度、颜色等多种属性
  • 灵活组合 Animated.loopAnimated.sequence 制作复杂动画
  • 在组件挂载时启动并保持无限循环动画,以及实现分时延迟启动
  • 掌握动画性能优化要点,合理设置 useNativeDriver

React Native iOS上下文菜单库全解析:react-native-ios-context-menu

在原生 iOS 应用中,从 iOS 13 开始,系统提供了类似于 macOS “右键菜单”的 Context Menu(上下文菜单)功能,用户长按控件即可弹出菜单,支持预览(Preview)与弹出(Pop)交互,大大提升了交互体验。对于 React Native 开发者而言,react-native-ios-context-menu 库提供了一个方便的桥接层,让我们可以在 RN 中轻松调用 iOS 原生的 Context Menu。本篇教程将从以下几个方面进行全方位剖析,帮助你快速掌握该库的安装、用法与高级定制,配有详尽的代码示例与图解,便于快速上手。

  1. 背景与概述
  2. 库安装与原生配置
  3. 基本用法示例
  4. API 详解与常用属性
  5. 自定义菜单项与图标
  6. 预览(Preview)与弹出(Pop)交互
  7. 与 React Native 组件结合的最佳实践
  8. 常见问题排查与优化建议

一、背景与概述

1.1 iOS 原生 Context Menu 简介

  • Context Menu 是 iOS 13 推出的特性,长按某个视图(UIView)时,会弹出一个浮层菜单,包含菜单项以及预览内容。
  • 其核心原生 API 基于 UIContextMenuInteraction,可以实现:

    1. Preview(预览):用户轻触并按住时,下方会弹出一个“小预览窗口”(如照片、文档预览等)。
    2. Pop(弹出):当用户从 Preview 向上滑动或重按时,进入“Pop”状态,打开全屏或自定义视图。
  • 开发者需实现 UIContextMenuInteractionDelegate 的回调,创建菜单项 (UIActionUIMenu 等),并提供一个 PreviewProvider(返回一个 UIViewController)以显示预览。

1.2 React Native 下的需求

在 React Native 中,默认并没有对 iOS Context Menu 提供封装。传统的长按交互往往仅用于触发 onLongPress 事件,缺少原生的预览与弹出能力。react-native-ios-context-menu 库弥补了这一空缺,让我们能在 RN 层优雅地使用 Context Menu,主要特点:

  • 零侵入式:通过一个高阶组件(HOC)或 ContextMenuView 包裹任意 RN 组件,即可让其支持原生 Context Menu。
  • 自定义灵活:支持设置菜单标题、Icon、菜单项子标题、自定义颜色等,还可自定义预览组件。
  • 完美契合 iOS 设计语言:弹出的菜单风格与系统原生保持一致,用户体验更佳。
  • 可以在 macOS Catalyst 下使用,对支持 iOS 13+ 的平台通用。

二、库安装与原生配置

下面以 React Native 0.64+ 项目为例,介绍如何安装与配置 react-native-ios-context-menu

2.1 安装依赖

在项目根目录运行:

# 使用 npm
npm install react-native-ios-context-menu

# 或者使用 yarn
yarn add react-native-ios-context-menu

该库基于 CocoaPods 进行 iOS 原生依赖管理,安装后需要在 iOS 目录执行:

cd ios
pod install --repo-update
注意:如未安装 CocoaPods,请先参考 CocoaPods 官方安装文档 完成安装。

2.2 iOS 原生配置

  1. 打开 Xcode 项目
    执行 pod install 后,会在 ios 目录生成 .xcworkspace,请从此文件打开 Xcode:

    open ios/YourApp.xcworkspace
  2. 自动链接
    在 RN 0.60 及以上版本,react-native-ios-context-menu 已支持自动链接(Autolinking),无需手动修改 AppDelegate.m 或其他文件。
  3. 最低 iOS 版本要求
    该库基于 Context Menu API,仅需将 target iOS 版本设置为 iOS 13.0+。在 Xcode 左侧选中项目 → TARGETS → General → Deployment Info → 将 iOS Deployment Target 设置为 13.0 或以上。
  4. Swift 支持
    如果你的项目使用 Objective-C 编写,也无需任何额外配置。若想在 Swift 代码中使用,可在 Bridging-Header.h 中引入:

    #import <react_native_ios_context_menu/ContextMenuView-Swift.h>

    然后就可以在 Swift 文件中使用 ContextMenuView。通常对 RN 应用而言,这一步可忽略,使用 JavaScript 层即可。


三、基本用法示例

安装配置完成后,在 RN 层可以通过两种方式使用该库:

  1. 高阶组件(HOC):使用 withContextMenu 包裹任意组件。
  2. 专用组件:使用 ContextMenuView 作为容器,包裹内部子组件。

下面分别演示这两种方式的简单入门示例。

3.1 使用 ContextMenuView 包裹组件

ContextMenuView 是库提供的核心组件,用于包裹任意 RN 组件并赋予其 Context Menu 能力。

// App.js
import React from 'react';
import { View, Text, Image, StyleSheet } from 'react-native';
import { ContextMenuView } from 'react-native-ios-context-menu';

export default function App() {
  return (
    <View style={styles.container}>
      <ContextMenuView
        style={styles.menuButton}
        menuConfig={{
          menuTitle: '操作选项',
          menuItems: [
            {
              actionKey: 'key-like',
              actionTitle: '👍 点赞',
            },
            {
              actionKey: 'key-share',
              actionTitle: '🔗 分享',
            },
            {
              actionKey: 'key-delete',
              actionTitle: '🗑️ 删除',
              actionSubtitle: '永久删除该项目',
              menuAttributes: ['destructive'], // 红色高亮
            },
          ],
        }}
        onPressMenuItem={({ nativeEvent }) => {
          const { actionKey } = nativeEvent;
          console.log('Selected action:', actionKey);
          // 根据 actionKey 执行对应操作
        }}
      >
        {/* 被包裹的内容,将会响应长按弹出上下文菜单 */}
        <View style={styles.content}>
          <Image
            source={{ uri: 'https://via.placeholder.com/150' }}
            style={styles.image}
          />
          <Text>长按我弹出菜单</Text>
        </View>
      </ContextMenuView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  menuButton: {
    // 必须设置宽高,否则菜单不会正确定位
    width: 200,
    height: 200,
  },
  content: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  image: { width: 150, height: 150, marginBottom: 8 },
});

关键点说明

  1. menuConfig

    • menuTitle:菜单顶部标题,可选。
    • menuItems:一个数组,每个元素表示一个菜单项。

      • actionKey(必填):该项的唯一标识,点击后会随 nativeEvent 传回。
      • actionTitle(必填):展示给用户的文本,可内嵌 Emoji。
      • actionSubtitle(可选):菜单项下方的二级描述文本。
      • menuAttributes(可选):一个字符串数组,可传入系统支持的属性,如:

        • "destructive":红色高亮,用于删除等危险操作;
        • "disabled":禁用该菜单项,变灰且无法点击;
        • "hidden":隐藏该菜单项。
      • icon(可选):可传入一个系统或自定义 Icon(后面章节详解)。
  2. onPressMenuItem

    • 监听回调,当用户点击任意菜单项时触发,并返回 nativeEvent.actionKey。可在此回调中结合业务逻辑执行操作(如跳转、分享等)。
  3. 被包裹的组件

    • 任何 RN 组件都可以作为 ContextMenuView 的子组件。
    • 必须给 ContextMenuView 设置宽高,否则长按区域无法正确捕获触摸事件,菜单无法弹出。

3.2 使用 HOC withContextMenu

如果你不想在 JSX 中显式使用 ContextMenuView,可以使用高阶组件(HOC)方式,将菜单能力注入到一个已有组件中。

// MyButton.js
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
import { withContextMenu } from 'react-native-ios-context-menu';

// 普通按钮组件
const Button = ({ title, onPress }) => (
  <TouchableOpacity style={styles.btn} onPress={onPress}>
    <Text style={styles.btnText}>{title}</Text>
  </TouchableOpacity>
);

// 包裹 HOC,注入上下文菜单能力
const ButtonWithMenu = withContextMenu(Button);

// App.js
import React from 'react';
import { View } from 'react-native';
import ButtonWithMenu from './MyButton';

export default function App() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <ButtonWithMenu
        title="长按我有菜单"
        menuConfig={{
          menuTitle: '按钮菜单',
          menuItems: [
            { actionKey: 'edit', actionTitle: '✏️ 编辑' },
            { actionKey: 'close', actionTitle: '❌ 关闭' },
          ],
        }}
        onPressMenuItem={({ nativeEvent }) => {
          console.log('按钮菜单项被选中:', nativeEvent.actionKey);
        }}
        onPress={() => {
          console.log('按钮点击事件');
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  btn: {
    paddingHorizontal: 24,
    paddingVertical: 12,
    backgroundColor: '#007AFF',
    borderRadius: 8,
  },
  btnText: {
    color: '#fff',
    fontSize: 16,
  },
});

HOC 方式说明

  • withContextMenu(Component)

    • 返回一个新的组件,该组件会在内部用 ContextMenuView 包裹原组件。
    • 保留原组件的所有 props,并额外支持 menuConfigonPressMenuItem 等新属性。
  • 使用场景

    • 当你希望给已有组件(如按钮、列表项等)快速添加上下文菜单功能,无需代码侵入,只需 HOC 包裹。

四、API 详解与常用属性

下面对 ContextMenuViewwithContextMenu 提供的所有 Props 进行逐项讲解,帮助你更好地掌握该库的灵活配置能力。

4.1 ContextMenuView 所有 Props

属性名称类型说明默认值
menuConfigContextMenuConfig 对象必需。定义菜单的标题与菜单项数组。无默认,需要传入
onPressMenuItem(event) => void用户点击某个菜单项后的回调,event.nativeEvent.actionKey 即菜单项 key。undefined
onMenuDidShow() => void当菜单成功弹出时触发。可选,undefined
onMenuWillShow() => void当菜单即将弹出时触发。undefined
onMenuDidClose() => void当菜单关闭后触发,用于统计或做页面刷新等。undefined
onMenuWillClose() => void当菜单即将关闭时触发。undefined
disabledboolean如果为 true,则禁用上下文菜单(长按时不会弹出)。false
previewConfigPreviewConfig 对象(可选)定义“预览”视图,包括一个 React 组件,用于 Preview(Peek)阶段。undefined
styleViewStyle控制外层 View 样式,必须设置宽高,否则看不到菜单。无默认,需要传入
...其他 ViewPropsViewProps继承自 React Native 的 View,如 testIDaccessible 等。

4.1.1 ContextMenuConfig 详情

type ContextMenuConfig = {
  menuTitle?: string;            // 可选:菜单顶部大标题
  menuItems: ContextMenuItem[];  // 必需:菜单项数组,至少一项
  menuOptions?: {
    // iOS 14+ 特性:可定义菜单风格等,
    // 例如, {'preserveMenuPosition': true} 保持弹出位置。
    preserveMenuPosition?: boolean;
    tintColor?: string;           // 整体菜单的基调色
  };
};

4.1.2 ContextMenuItem 详情

type ContextMenuItem = {
  actionKey: string;            // 必需:唯一标识
  actionTitle: string;          // 必需:显示文本
  actionSubtitle?: string;      // 可选:副标题(小字)
  menuAttributes?: ('destructive' | 'disabled' | 'hidden')[];  
  // 可选:一个数组,可包含'menuAttributes'枚举值,实现红色高亮、禁用或隐藏
  icon?: ContextMenuIcon;       // 可选:菜单项图标(系统或自定义资源)
  discoverabilityTitle?: string; // 可选:VoiceOver 朗读提示
};
  • menuAttributes

    • 'destructive':菜单项文字变红,提示危险操作,如“删除”。
    • 'disabled':菜单项禁用,文字变灰,无法点击。
    • 'hidden':完全隐藏该菜单项,不在菜单中显示(可用于动态控制显示逻辑)。
  • discoverabilityTitle

    • 为无障碍(VoiceOver)提供额外提示文本,在无障碍模式下会朗读这个字段。

4.1.3 ContextMenuIcon 详情

type ContextMenuIcon =
  | { type: 'system'; systemName: string; } // 使用 SF Symbols 系统图标,例如 'trash', 'square.and.arrow.up'
  | { type: 'custom'; uri: string; width?: number; height?: number; }; 
  // 自定义图标,可使用本地或网络图片。width/height 可选,默认为 24*24。
  • 系统图标

    • iOS 原生的 SF Symbols 图标,使用 systemName 指定图标名称,例如:

      • 'trash' → 垃圾桶图标
      • 'square.and.arrow.up' → 分享图标
      • 'heart.fill' → 实心爱心
    • 系统图标会跟随 iOS 主题(浅色/深色)显示。
  • 自定义图标

    • 定义 type: 'custom'uri 可以是网络 URL 或 RN 中 require('./icon.png') 本地资源引用,支持远程资源。
    • 建议指定 widthheight,否则默认为 24×24

4.1.4 PreviewConfig 详情

type PreviewConfig = {
  previewType?: 'CUSTOM' | 'ICON' | 'TITLE';  
  // iOS 16+ 支持:'CUSTOM' 自定义组件;'ICON' 图标预览;'TITLE' 文本预览
  previewSize?: 'SMALL' | 'MEDIUM' | 'LARGE'; // 预览尺寸
  previewBackgroundColor?: string;            // 预览背景色
  renderPreview: () => React.ReactNode;       // **必需**:返回一个 React 组件,用于 “Preview” 阶段
};
  • renderPreview

    • 返回一个 React 元素,库会将其渲染为一个原生 UIView 并嵌入 Preview 中。
    • 这个组件内部可以是任意 RN 组件,例如 ImageText、自定义布局等。
  • previewSize

    • 'SMALL':预览尺寸较小;
    • 'MEDIUM':中等;
    • 'LARGE':大尺寸,适合图片或地图等。
  • previewType(iOS 16+ 新增)

    • 可选:对于仅需展示图标或纯文本预览,可用 'ICON''TITLE'
    • 若需要复杂布局,设置为 'CUSTOM' 并实现 renderPreview

完整水合 Props 示例:

<ContextMenuView
  style={{ width: 120, height: 120 }}
  menuConfig={{
    menuTitle: '操作',
    menuItems: [ /*…*/ ],
    menuOptions: {
      preserveMenuPosition: true,
      tintColor: '#007AFF',
    },
  }}
  previewConfig={{
    previewType: 'CUSTOM',
    previewSize: 'MEDIUM',
    previewBackgroundColor: '#FFF',
    renderPreview: () => (
      <View style={{
        width: 200,
        height: 150,
        backgroundColor: '#FFF',
        borderRadius: 12,
        overflow: 'hidden',
      }}>
        <Image
          source={{ uri: 'https://via.placeholder.com/200x150' }}
          style={{ width: '100%', height: '100%' }}
          resizeMode="cover"
        />
      </View>
    ),
  }}
  onPressMenuItem={({ nativeEvent }) => console.log(nativeEvent.actionKey)}
  onMenuWillShow={() => console.log('菜单即将弹出')}
  onMenuDidShow={() => console.log('菜单已弹出')}
  onMenuWillClose={() => console.log('菜单即将关闭')}
  onMenuDidClose={() => console.log('菜单已关闭')}
  disabled={false}
>
  <Image
    source={{ uri: 'https://via.placeholder.com/120' }}
    style={{ width: 120, height: 120, borderRadius: 8 }}
  />
</ContextMenuView>

4.2 withContextMenu 所有 Props

withContextMenu HOC 接受与 ContextMenuView 相同的 Props,只不过需要将它们以属性传递给包裹组件。示例:

const ButtonWithMenu = withContextMenu(Button);

// 使用时:
<ButtonWithMenu
  title="菜单按钮"
  menuConfig={…}
  onPressMenuItem={…}
  style={{ width: 100, height: 40 }}
/>

HOC 会自动将 menuConfig 等新增 Props 转给内部的 ContextMenuView


五、自定义菜单项与图标

菜单美观度与用户体验很大程度依赖于图标及文字细节。下面详细介绍如何配置并定制菜单项的图标、子标题、颜色等,使得 Context Menu 既原生又有辨识度。

5.1 系统 SF Symbols 图标

iOS 内置丰富的 SF Symbols 图标,可在菜单项中直接使用。示例:

const menuItems = [
  {
    actionKey: 'key-favourite',
    actionTitle: '❤️ 收藏',
    icon: { type: 'system', systemName: 'heart.fill' }, // 实心爱心
  },
  {
    actionKey: 'key-share',
    actionTitle: '🔗 分享',
    icon: { type: 'system', systemName: 'square.and.arrow.up' },
  },
  {
    actionKey: 'key-delete',
    actionTitle: '删除',
    icon: { type: 'system', systemName: 'trash' },
    menuAttributes: ['destructive'], // 红色高亮
  },
];
  • systemName

  • 自动适配深色模式

    • 系统图标会根据 iOS 主题自动变色,无需额外设置。

5.2 自定义图片图标

如果想使用自定义图标(如项目 logo、品牌 icon),可以使用 type: 'custom',并以 RN require 本地资源或网络 URL 作为 uri

const menuItems = [
  {
    actionKey: 'key-profile',
    actionTitle: '查看个人主页',
    icon: {
      type: 'custom',
      uri: require('./assets/profile-icon.png'),
      width: 28,
      height: 28,
    },
  },
  {
    actionKey: 'key-settings',
    actionTitle: '设置',
    icon: {
      type: 'custom',
      uri: { uri: 'https://example.com/icons/settings.png' },
      width: 24,
      height: 24,
    },
  },
];
  • width / height

    • 建议与原图像同宽高保持一致,或根据菜单项高度(约 32px)进行缩放,一般不超过 32px,否则会挤压文字布局。
  • 网络 URL

    • 可以将 uri 设为网络 URL,但注意网络请求与加载时间。如果图标未及时加载,可能出现空白或延迟显示。
    • 推荐在 App 启动时预先下载或使用本地资源以保证流畅体验。

5.3 子标题与属性组合

通过 actionSubtitle 可以在菜单项下方显示一行较小字体的描述。例如:

{
  actionKey: 'key-move',
  actionTitle: '移动到...',
  actionSubtitle: '选择一个收藏夹', // 二级描述
  icon: { type: 'system', systemName: 'folder' },
}
  • menuAttributes 示例:

    {
      actionKey: 'key-logout',
      actionTitle: '退出登陆',
      icon: { type: 'system', systemName: 'power' },
      menuAttributes: ['destructive'], // 红色高亮表示危险操作
    }

完整示例

<ContextMenuView
  style={{ width: 200, height: 50 }}
  menuConfig={{
    menuTitle: '示例菜单',
    menuItems: [
      {
        actionKey: 'key-favourite',
        actionTitle: '❤️ 收藏',
        actionSubtitle: '添加到收藏列表',
        icon: { type: 'system', systemName: 'heart' },
      },
      {
        actionKey: 'key-share',
        actionTitle: '🔗 分享',
        actionSubtitle: '分享到其他平台',
        icon: { type: 'system', systemName: 'square.and.arrow.up' },
      },
      {
        actionKey: 'key-settings',
        actionTitle: '⚙️ 设置',
        icon: {
          type: 'custom',
          uri: require('./assets/settings.png'),
          width: 24,
          height: 24,
        },
      },
      {
        actionKey: 'key-logout',
        actionTitle: '退出登陆',
        menuAttributes: ['destructive'],
        icon: { type: 'system', systemName: 'power' },
      },
    ],
    menuOptions: {
      tintColor: '#4B0082', // 紫色基调
    },
  }}
  onPressMenuItem={({ nativeEvent }) =>
    console.log('选择了菜单项:', nativeEvent.actionKey)
  }
>
  <View
    style={{
      flex: 1,
      justifyContent: 'center',
      alignItems: 'center',
      backgroundColor: '#EEE',
      borderRadius: 8,
    }}
  >
    <Text>长按区域打开菜单</Text>
  </View>
</ContextMenuView>

图解:菜单最终效果

┌──────────────────────────────────────────┐
│                  示例菜单               │  ← menuTitle
├──────────────────────────────────────────┤
│ ❤️ 收藏            添加到收藏列表          │  ← 带 SF Symbols 图标 + 子标题
│                                          │
│ 🔗 分享            分享到其他平台          │
│                                          │
│ ⚙️  设置                             │  ← 自定义本地资源图标
│                                          │
│ ❗  退出登陆       (红色高亮 destructive)    │
└──────────────────────────────────────────┘
  • 菜单顶部显示 “示例菜单” 作为大标题。
  • 每项左侧显示 Icon,右侧显示 actionTitle,下方为 actionSubtitle(如果有)。
  • “退出登陆” 项显示红色高亮,提示危险操作。

六、预览(Preview)与弹出(Pop)交互

Context Menu 最吸引人的功能在于 “Peek & Pop” 交互:即长按时先显示一个小预览视图(Peek),随后用户可以滑动或再按进入详情页面(Pop)。在 react-native-ios-context-menu 中,我们可以通过 previewConfig 属性轻松配置 Preview 视图。

6.1 配置 previewConfig

<ContextMenuView
  style={{ width: 200, height: 200 }}
  menuConfig={{ /*…菜单配置*/ }}
  previewConfig={{
    previewType: 'CUSTOM', // 'ICON' 或 'TITLE' 或 'CUSTOM'
    previewSize: 'MEDIUM',  // 'SMALL' | 'MEDIUM' | 'LARGE'
    previewBackgroundColor: '#FFFFFF',
    renderPreview: () => (
      <View
        style={{
          width: 180,
          height: 120,
          backgroundColor: '#333',
          borderRadius: 12,
          overflow: 'hidden',
        }}
      >
        <Image
          source={{ uri: 'https://via.placeholder.com/180x120.png' }}
          style={{ width: '100%', height: '100%' }}
        />
        <Text style={{ color: '#FFF', padding: 8 }}>预览标题</Text>
      </View>
    ),
  }}
  onPressMenuItem={/*…*/}
>
  {/* 被包裹内容 */}
</ContextMenuView>

6.1.1 Preview 交互流程

  1. 长按触发 Preview

    • 用户长按包裹区域时,会首先出现一个带有阴影的小浮层,这即是“Preview”界面。该浮层显示 renderPreview 返回的内容。
  2. 继续按压进入 Pop

    • 用户在 Preview 浮层内继续加力(Deep Press 或直接连续长按),则进入“Pop”阶段,此时可根据需要打开一个新页面导航到详情或者执行某些操作
  3. 松手或滑动取消

    • 如果用户在 Peek 阶段松手,则只关闭 Preview 不进入 Pop。
    • 若在 Peek 时直接向上滑动到某个菜单项,则会选中该菜单项并触发对应回调,菜单关闭。

6.1.2 previewType 含义

  • 'ICON'

    • 在 iOS 14+ 将只显示一个默认大小的 SF Symbol 图标,用户无需自定义 renderPreview
    • 例如:

      previewConfig={{
        previewType: 'ICON',
        previewSize: 'SMALL',
        icon: { type: 'system', systemName: 'photo' },
      }}
  • 'TITLE'

    • 仅显示一行文本(大标题),不展示图像。
    • 属性:

      previewConfig={{
        previewType: 'TITLE',
        previewSize: 'SMALL',
        previewTitle: '预览标题文本',
      }}
  • 'CUSTOM'

    • 完全自定义预览界面,必须提供 renderPreview
注意previewType: 'ICON' / 'TITLE' 目前仅在 iOS 14+ 可用,若需要兼容 iOS 13,请使用 previewType: 'CUSTOM' 自行渲染。

6.1.3 示例:图片 + 文本 Preview

<ContextMenuView
  style={{ width: 200, height: 200, borderRadius: 12, overflow: 'hidden' }}
  menuConfig={/*…*/}
  previewConfig={{
    previewType: 'CUSTOM',
    previewSize: 'LARGE',
    previewBackgroundColor: '#FFF',
    renderPreview: () => (
      <View style={{ flex: 1 }}>
        <Image
          source={{ uri: 'https://via.placeholder.com/200x120' }}
          style={{ width: '100%', height: '60%' }}
          resizeMode="cover"
        />
        <View style={{ padding: 8 }}>
          <Text style={{ fontSize: 16, fontWeight: 'bold' }}>美食预览</Text>
          <Text style={{ marginTop: 4, color: '#666' }}>
            这是一个超棒的餐厅预览描述信息
          </Text>
        </View>
      </View>
    ),
  }}
  onPressMenuItem={({ nativeEvent }) =>
    console.log('Selected:', nativeEvent.actionKey)
  }
>
  <Image
    source={{ uri: 'https://via.placeholder.com/200.png?text=Thumbnail' }}
    style={{ width: 200, height: 200 }}
  />
</ContextMenuView>
  • 用户在 Thumbnail 图像上长按时,会首先弹出一个大预览卡片,包含顶部照片和下方文字。
  • 继续按压会进入“Pop”状态(如果设置了 Pop 视图),否则 Preview 消失。

6.2 Pop 状态处理

react-native-ios-context-menu 中,Pop 状态并不会自动为你打开新的页面。你可以在 onPressMenuItem 回调中检查 nativeEvent.key,当用户选中某个菜单项时,根据需要执行导航。例如:

onPressMenuItem={({ nativeEvent }) => {
  const { actionKey } = nativeEvent;
  if (actionKey === 'key-open') {
    // 例如使用 React Navigation 跳转到详情页
    navigation.navigate('DetailScreen', { id: itemId });
  }
  if (actionKey === 'key-delete') {
    // 执行删除逻辑
    deleteItem(itemId);
  }
}}

如果你想直接在 Pop 阶段自动跳转(无需点击菜单项),可结合 onMenuWillShow / onMenuDidShow 回调,配合某个自定义操作标志。但一般推荐尊重用户意图,先弹出 Preview,让用户显式选择菜单项再 Pop。


七、与 React Native 组件结合的最佳实践

以下几点实践经验,可帮助你在项目中更好地使用 react-native-ios-context-menu

7.1 包裹列表项时的宽高控制

在 FlatList、SectionList 等列表中,常见需求是在长按某个列表项时弹出上下文菜单。示例:

// ItemRow.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ContextMenuView } from 'react-native-ios-context-menu';

export default function ItemRow({ item, onMenuAction }) {
  return (
    <ContextMenuView
      style={styles.rowContainer}
      menuConfig={{
        menuTitle: '操作',
        menuItems: [
          { actionKey: 'edit', actionTitle: '编辑' },
          { actionKey: 'remove', actionTitle: '删除', menuAttributes: ['destructive'] },
        ],
      }}
      onPressMenuItem={({ nativeEvent }) => {
        onMenuAction(item.id, nativeEvent.actionKey);
      }}
    >
      <View style={styles.rowContent}>
        <Text style={styles.rowText}>{item.title}</Text>
      </View>
    </ContextMenuView>
  );
}

const styles = StyleSheet.create({
  rowContainer: {
    // 宽度建议撑满屏幕或固定宽度,高度至少 44pt(系统交互舒适度)
    width: '100%',
    height: 60,
    backgroundColor: '#FFF',
  },
  rowContent: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 16,
  },
  rowText: {
    fontSize: 16,
  },
});
  • 宽度 width: '100%' 或指定 Dimensions.get('window').width

    • 确保 ContextMenuView 占据完整行宽,长按时菜单能正确弹出在该行中心。
  • 高度至少 44pt(推荐 50–60pt):

    • 符合 iOS 触控规范,长按区域更容易触发,也可避免菜单弹出区域过小导致交互困难。

7.2 与触摸事件的冲突

如果被 ContextMenuView 包裹的内部组件本身也绑定了 onLongPressonPress,可能会与 Context Menu 的长按手势冲突。推荐做法:

  • 仅在外部使用 ContextMenuView
    将所有长按交互交给 Context Menu 处理,内部不再绑定长按事件。
  • 使用条件渲染
    如果某些列表项不需要 Context Menu,可通过传入 disabled 属性动态判断禁用,例如:

    <ContextMenuView
      style={styles.rowContainer}
      menuConfig={someConfig}
      disabled={!item.canLongPress} // 当为 true 时,长按不会触发 Context Menu
      onPressMenuItem={...}
    >
      {/* 列表项内容 */}
    </ContextMenuView>

7.3 动态控制菜单项显示逻辑

大多数场景下,不同列表项可能需要不同的菜单项组合,可在渲染时根据数据动态构建 menuConfig。示例:

function getMenuItemsForItem(item) {
  const items = [];
  if (item.canEdit) {
    items.push({ actionKey: 'edit', actionTitle: '✏️ 编辑' });
  }
  if (item.canShare) {
    items.push({ actionKey: 'share', actionTitle: '🔗 分享' });
  }
  if (item.canDelete) {
    items.push({
      actionKey: 'delete',
      actionTitle: '🗑️ 删除',
      menuAttributes: ['destructive'],
    });
  }
  return items;
}

<ItemRow
  item={item}
  onMenuAction={(id, key) => handleAction(id, key)}
  menuConfig={{
    menuTitle: `项目:${item.title}`,
    menuItems: getMenuItemsForItem(item),
  }}
/>
  • 这样可以确保根据业务状态(如权限、是否已删除、是否已分享等)动态控制菜单项显示。
  • 如果需要临时隐藏某个菜单项,可在 menuItems 中为其添加 menuAttributes: ['hidden']

八、常见问题排查与优化建议

在集成 react-native-ios-context-menu 过程中,可能会遇到一些常见问题,下面列举并给出解决思路。

8.1 菜单不弹出

  • 没有正确设置宽高

    • ContextMenuView 必须有明确的宽度与高度,否则无法捕获触摸区域。
    • 确保 style={{ width: ..., height: ... }} 已生效,或者其父组件约束了尺寸。
  • iOS 版本过低

    • Context Menu 仅支持 iOS 13 及以上。请检查设备或模拟器 iOS 版本。
  • disabled={true}

    • 若误将 disabled 设置为 true,长按时会无响应。
  • 其他手势冲突

    • 如果在同一个视图层级绑定了原生 onLongPressPanResponder 等手势处理,可能导致冲突。可以尝试在 ContextMenuView 内部包裹一个无需任何手势的纯 View,然后在内部做进一步布局。

8.2 菜单样式异常

  • menuOptions.tintColor 无效

    • 只有在 iOS 14 及以上,且 ContextMenuConfig.menuOptionstintColor 才会生效。低版本 iOS 无效果。
  • 部分属性仅在高版本生效

    • menuOptions.preserveMenuPositionpreviewType: 'ICON''TITLE' 等在 iOS 14+ 才支持。请根据 iOS 版本条件性渲染或 fallback 到 CUSTOM

8.3 预览(Preview)无法显示或卡顿

  • 布局过于复杂

    • renderPreview 返回的组件若包含太多子组件,或图片过大,会导致预览渲染卡顿。建议预先将图片缓存至本地,保持 renderPreview 组件轻量化。
  • previewSize 未正确设置

    • previewSize 过小,但你渲染了很大的组件,可能出现裁剪或遮挡。请调整 previewSize 与自定义组件尺寸匹配。
  • 使用网络图片加载慢

    • 尽量使用本地资源或先行加载图片,避免网络图片影响 Preview 流畅度。

8.4 菜单项点击回调延迟

  • 大型操作阻塞 JS 线程

    • 如果在 onPressMenuItem 中执行耗时操作(如大规模数据处理),会阻塞 JS 线程,导致回调后界面卡顿。建议将耗时操作放到异步任务中(如 setTimeoutInteractionManager.runAfterInteractions 等)。
  • 导航跳转未使用异步

    • 如果使用 React Navigation,在菜单回调中直接调用 navigation.navigate,可能瞬间触发 UI 变更。可以在回调中先 console.log 验证,再做导航。

九、总结

本文围绕 react-native-ios-context-menu 库,对 iOS 原生 Context Menu 在 React Native 中的集成与使用进行了全面解析,包含:

  1. 背景与概述:介绍 iOS 原生 Context Menu 及 RN 下需求。
  2. 安装与原生配置:详细说明了库的安装、CocoaPods 配置、最低 iOS 版本要求等。
  3. 基本用法示例:分别演示了使用 ContextMenuView 与 HOC withContextMenu 的入门示例。
  4. API 详解与常用属性:逐项解读 ContextMenuView 的所有 Props、ContextMenuConfigContextMenuItemContextMenuIconPreviewConfig 等。
  5. 自定义菜单项与图标:演示如何使用 SF Symbols 系统图标与自定义图片图标,并使用子标题、属性组合等进行精细化定制。
  6. 预览(Preview)与弹出(Pop)交互:讲解两阶段交互流程及如何使用 previewConfig 自定义预览内容。
  7. 最佳实践:包括在列表项中正确设置宽高、避免手势冲突、动态控制菜单项显示逻辑等建议。
  8. 常见问题排查与优化:列举并分析了菜单不弹出、样式异常、预览卡顿、回调延迟等常见问题及解决思路。

通过以上内容,你可以快速在 RN 项目中集成 iOS 原生的上下文菜单功能,让你的 App 在长按交互时不仅能弹出原生风格的菜单,还能承载丰富的 Preview 预览互动,带来更佳的用户体验。如果后续需要支持 Android 或者扩展更多自定义动画效果,可以参考 Android 原生的 ContextMenu,并尝试社区提供的跨平台 Context Menu 库(如 react-native-context-menu-view 等),结合原生模块快速实现更多交互。祝你在 React Native 开发中游刃有余,创造出更生动的用户体验!

以下示例展示了如何在 React Native 中使用 react-native-svg 绘制一个“太阳”/“亮度”图标,并根据传入的亮度百分比(0–100%)精确地调整其视觉呈现。最终效果是在一个圆形太阳核心上,按照百分比动态改变填充半径或透明度,从而让图标“亮度”更直观。


一、思路概述

  1. 图标结构
    我们以最常见的太阳图标为例,基本由以下几部分组成:

    • 中央圆(Core):代表“光源”本体,可以用纯色圆或渐变圆。
    • 光线射线(Rays):环绕中央圆的若干条射线,用直线(Line)或矩形(Rect)表示。
  2. 亮度百分比映射
    常见做法有两种:

    • 改变中央圆的半径:当亮度为 0% 时,核心圆半径为最小(甚至 0);当亮度为 100% 时,核心圆半径为最大值。
    • 改变中央圆的颜色或透明度:例如,将 fillOpacity 设为 percent / 100,或者用 HSL/HSV 模型根据亮度值调整颜色明度。

    本示例主要演示中央圆半径随亮度百分比线性变化,同时保持射线(Rays)不变。这样既直观表现“亮度从小到大”,也能保证太阳形状清晰。

  3. 使用 react-native-svg
    react-native-svg 提供了类似 Web 上 SVG 的绘制能力:<Svg>、<Circle>、<Line>、<Defs>、<LinearGradient> 等组件。我们可以在 React Native 中直接引入并绘制矢量图形。

二、环境准备

  1. 安装 react-native-svg
    如果你还没有安装 react-native-svg,请在项目根目录执行:

    npm install react-native-svg
    # 或者使用 yarn
    # yarn add react-native-svg
  2. (仅限 Expo 用户)
    如果你使用的是 Expo Managed workflow,通常无需额外链接,Expo 已内置 react-native-svg;若报错,可以通过:

    expo install react-native-svg

    来确保安装与 Expo SDK 兼容的版本。


三、图标设计与绘制逻辑

下面先给出一个简化的 ASCII 图解,帮助你理解图标各部分的位置和坐标关系。假设我们将 SVG 视图框(viewBox)设为 100×100,那么:

            ┌─────────────────────────────────┐
            │                                 │
            │             ↓ Y                │
            │           50 ▲ (中心点)        │
            │             │                   │
        –––––––––––––––––––––––––––––––––––––––––
        ◄   50 —──────────────────────── 50   ►   X
        –––––––––––––––––––––––––––––––––––––––––
            │             │                   │
            │             ↓                   │
            │           (Rays)                │
            │                                 │
            └─────────────────────────────────┘
  • SVG 整体尺寸width=100, height=100viewBox="0 0 100 100"
  • 中心点(cx, cy) = (50, 50)
  • 中央圆最大半径:假设为 r_max = 20,最小半径 r_min = 4(可根据需求自由调整)。
  • 光线(Rays):围绕中心均匀分布 8 条直线(或更少/更多),长度从 r_max 延伸到边缘,比如长度 L = 28。每条光线用 <Line> 从中心向某一角度画出。

示意图:

      \   |   /         ← 8 条光线
       \  |  /
        \ | /
  ------- ● -------     ← 中心圆
        / | \
       /  |  \
      /   |   \

其中 ● 表示中央圆,八条 “/、\、–、|” 即为光线。


四、完整代码示例

下面给出一个可复用的组件 BrightnessIcon,接收如下 Props:

  • percent(必须):亮度百分比,0–100 之间的数字。
  • size(可选):SVG 画布宽高,一般以正方形为例,默认为 100。
  • color(可选):中央圆和光线的颜色,默认为黄色 #FFD700
  • minRadiusmaxRadius(可选):中央圆最小/最大半径。

该组件会根据 percent 动态计算中央圆半径 r = minRadius + (maxRadius - minRadius) * (percent / 100),并绘制中心半径为 r 的圆,以及外围 8 条等距光线。

// BrightnessIcon.js
import React from "react";
import { View } from "react-native";
import Svg, { Circle, Line } from "react-native-svg";

type BrightnessIconProps = {
  percent: number;    // 亮度百分比 (0 - 100)
  size?: number;      // SVG 画布大小 (正方形边长),默认 100
  color?: string;     // 图标颜色,默认金黄色
  minRadius?: number; // 中央圆最小半径,默认 4
  maxRadius?: number; // 中央圆最大半径,默认 20
};

const BrightnessIcon: React.FC<BrightnessIconProps> = ({
  percent,
  size = 100,
  color = "#FFD700",
  minRadius = 4,
  maxRadius = 20,
}) => {
  // 1. 限制 percent 范围在 [0, 100]
  const p = Math.max(0, Math.min(100, percent));

  // 2. 计算中央圆半径
  const radius = minRadius + (maxRadius - minRadius) * (p / 100);

  // 3. 中心坐标
  const cx = size / 2;
  const cy = size / 2;

  // 4. 光线长度(从中央圆中心延伸到的终点距离),略大于 maxRadius
  //    这里我们假设光线终点距离中心为 L = maxRadius + 8
  const rayLength = maxRadius + 8;

  // 5. 光线宽度 (strokeWidth)
  const rayStrokeWidth = 2;

  // 6. 生成 8 条光线的坐标:每隔 45° 画一条
  const rays = Array.from({ length: 8 }).map((_, i) => {
    const angle = (Math.PI / 4) * i; // 每隔 45° (π/4)
    // 起点在中央圆边缘:radius
    const x1 = cx + radius * Math.cos(angle);
    const y1 = cy + radius * Math.sin(angle);
    // 终点在半径为 rayLength 的圆环上
    const x2 = cx + rayLength * Math.cos(angle);
    const y2 = cy + rayLength * Math.sin(angle);
    return { x1, y1, x2, y2 };
  });

  return (
    <View>
      <Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
        {/* 1. 画中央圆 */}
        <Circle
          cx={cx}
          cy={cy}
          r={radius}
          fill={color}
          fillOpacity={1} // 可以配合 percent 调整透明度
        />

        {/* 2. 画 8 条光线 */}
        {rays.map((ray, idx) => (
          <Line
            key={idx}
            x1={ray.x1}
            y1={ray.y1}
            x2={ray.x2}
            y2={ray.y2}
            stroke={color}
            strokeWidth={rayStrokeWidth}
            strokeLinecap="round"
          />
        ))}
      </Svg>
    </View>
  );
};

export default BrightnessIcon;

4.1 关键点说明

  1. percent 范围约束

    const p = Math.max(0, Math.min(100, percent));

    确保传入亮度百分比在 [0, 100],避免因误传造成负半径或超大半径。

  2. 中央圆半径计算

    const radius = minRadius + (maxRadius - minRadius) * (p / 100);
    • p = 0 时,radius = minRadius
    • p = 100 时,radius = maxRadius
    • 之间线性插值,能够直观映射亮度。
  3. 光线坐标计算

    • 每条光线从 (x1, y1)(x2, y2),其中:

      • (x1, y1) 为中央圆边缘的一点,角度为 angle
      • (x2, y2) 为更远一点,半径为 rayLength,使光线长度 = rayLength - radius
    • 利用极坐标公式:

      x = cx + r * cos(angle)
      y = cy + r * sin(angle)
    • angle07*(π/4),即 0°、45°、90°、…315°,共 8 条。
  4. strokeLinecap="round"
    让光线尾部更圆润,看起来更像“太阳光线”而非锋利直线。
  5. 可选:调整透明度
    如果需要让“亮度=0”时完全看不见圆心,可以将圆心的 fillOpacity={p/100} 而非恒定 1

    <Circle
      cx={cx}
      cy={cy}
      r={radius}
      fill={color}
      fillOpacity={p / 100}
    />

    此时,当 percent = 0 时,圆心透明度为 0(完全透明),percent = 100 时,透明度为 1(完全不透明)。这种做法视觉上更突出“亮度从无到有”。


五、示例演示与用法

在任意页面或组件中引入 BrightnessIcon,并根据状态(State)动态传递 percent

// ExampleUsage.js
import React, { useState } from "react";
import { View, Text, Slider, StyleSheet } from "react-native";
import BrightnessIcon from "./BrightnessIcon";

const ExampleUsage: React.FC = () => {
  const [brightness, setBrightness] = useState(50); // 初始 50%

  return (
    <View style={styles.container}>
      <Text style={styles.title}>调整亮度:{brightness}%</Text>
      {/* 亮度图标 */}
      <BrightnessIcon percent={brightness} size={150} color="#FFA500" />
      
      {/* 下面是一个 Slider 控件,用于动态调节百分比 */}
      <View style={styles.sliderContainer}>
        <Slider
          style={styles.slider}
          minimumValue={0}
          maximumValue={100}
          step={1}
          value={brightness}
          onValueChange={(val) => setBrightness(val)}
          minimumTrackTintColor="#FFA500"
          maximumTrackTintColor="#ccc"
        />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#fff",
  },
  title: {
    fontSize: 18,
    marginBottom: 16,
  },
  sliderContainer: {
    width: 200,
    marginTop: 24,
  },
  slider: {
    width: "100%",
    height: 40,
  },
});

export default ExampleUsage;

5.1 效果说明

  1. 初始渲染时:

    • brightness = 50,中央圆半径约为 minRadius + (maxRadius - minRadius) * 0.5
    • 8 条光线固定,长度从圆边缘向外延伸。
  2. 拖动 Slider:

    • brightness 值从 0 变到 100,中央圆半径从最小 4 线性增大到最大 20。
    • 若使用 fillOpacity={p/100},圆心也会从完全透明逐步变为不透明。
  3. brightness = 0

    • 中央圆半径 = minRadius = 4,如果 fillOpacity = p/100 = 0,则中央圆肉眼不可见;只有 8 条光线留在画布上。
    • 如果 fillOpacity 恒为 1,那么即使亮度为 0,中央圆也会以半径 4 显示,表示“极低亮度”。
  4. brightness = 100

    • 中央圆半径 = maxRadius = 20,中央圆最大,光线从圆心边缘开始,显得“最明亮”。

六、扩展思路与进阶优化

  1. 渐变光晕

    • 可以利用 <Defs> + <RadialGradient> 为中央圆添加径向渐变,让“亮度越高中心越亮、边缘渐暗”更真实。例如:

      import Svg, { Defs, RadialGradient, Stop, Circle, Line } from "react-native-svg";
      ...
      <Svg ...>
        <Defs>
          <RadialGradient id="grad" cx="50%" cy="50%" r="50%">
            <Stop offset="0%" stopColor="#FFD700" stopOpacity={1} />
            <Stop offset="100%" stopColor="#FFD700" stopOpacity={0} />
          </RadialGradient>
        </Defs>
        <Circle cx={cx} cy={cy} r={radius} fill="url(#grad)" />
        {rays.map(...)}
      </Svg>
    • 上述示例让圆心为纯色、边缘透明,形成“光晕”效果。
    • 你也可以根据 percent 调整渐变半径或透明度,如:r={radius * 1.5}stopOpacity={p/100} 等。
  2. 高级光线动画

    • 利用 react-native-reanimatedAnimated API,让光线围绕中心缓慢旋转、闪烁或放射,产生动态“呼吸灯”效果。
    • 例如可以对每条 <Line>strokeOpacity 做循环动画,使光线呈现“闪烁”。
  3. 更多光线样式

    • 如果不想用直线,可以把光线画成三角形、矩形或路径(Path)来实现不同形状。
    • 例如,让光线在中心处更细,末端更粗,模拟“光芒发散”。
  4. 响应式布局

    • 如果需要在不同分辨率、设备像素密度下保持图标清晰,可将 sizePixelRatio 动态计算,或者使用 width: 100% + aspectRatio: 1 的方式让图标自动撑满父容器。
  5. 向量检索融合

    • 如果你的场景涉及“文案语义”与“地理信息”双重约束,不仅可以在图标层面做“亮度可视化”,也可以在搜索推荐逻辑中兼容“语义搜索 + 地理过滤”的思路,让用户既能看到“当前光标亮度”也能获得对应的地理语义推荐。

七、总结

本文详细介绍了如何在 React Native 中利用 react-native-svg 灵活绘制一个可根据亮度百分比动态变化的“太阳”图标,关键思路与要点如下:

  1. SVG 视图框与坐标系

    • viewBox="0 0 size size" 建立 0–size 的坐标系;
    • 中心点固定为 (size/2, size/2)
  2. 中央圆半径随百分比线性变化

    const radius = minRadius + (maxRadius - minRadius) * (percent / 100);
    • percent = 0100 时,分别对应 minRadiusmaxRadius
    • 可选地利用 fillOpacity 映射透明度。
  3. 光线(Rays)坐标计算

    • 以中心点为原点,使用极坐标 angle = i * (π/4),通过 Math.cos / Math.sin 计算起点与终点位置。
    • 可以通过调整 rayLengthstrokeWidthstrokeLinecap 等,快速定制光线样式。
  4. 动态渲染

    • 结合 React State、Slider 或其它交互控件,让用户实时拖动调节 %,看到图标“亮度”变化。
    • 若想更“有趣”,可使用 Animated 实现呼吸灯、旋转等动画效果。
  5. 扩展思路

    • 可以使用径向渐变 (RadialGradient) 实现更柔和的光晕效果;
    • 若业务需要展示“屏幕亮度”、“能量值”、“进度”等,完全可以复用此思路并做相应修改;
    • 用类似方式还能绘制“音量条”、“温度指示器”等其他动态图标。

通过本文示例,你即可在 React Native 中快速实现一个实时响应亮度百分比的“太阳”图标组件,既能直观提示亮度,也可以作为动态交互控件的可视化表现。希望对你在 RN 中绘制矢量图形、制作自定义图标有所帮助。祝你学习愉快!

2024-09-09

在第一部分的基础上,我们将继续构建Todo应用程序,并完成其余的功能。

  1. 创建一个新的Todo项

首先,我们需要在components/TodoList/NewTodo.tsx文件中添加一个表单来创建新的Todo项。




import React, { useState } from 'react';
import { useMutation } from '@apollo/client';
import { CREATE_TODO } from '../queries';
 
const NewTodo: React.FC = () => {
  const [text, setText] = useState('');
  const [createTodo] = useMutation(CREATE_TODO);
 
  const submitForm = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    createTodo({ variables: { text } });
    setText('');
  };
 
  return (
    <form onSubmit={submitForm}>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Enter new todo"
      />
      <button type="submit">Add</button>
    </form>
  );
};
 
export default NewTodo;

在上述代码中,我们使用了Apollo Client的useMutation钩子来处理创建新Todo的请求。我们还使用了React的useState钩子来管理输入字段的状态。

  1. 完成Todo项

components/TodoList/Todo.tsx文件中,我们需要添加一个按钮来标记Todo项为已完成。




import React from 'react';
import { useMutation } from '@apollo/client';
import { TOGGLE_TODO } from '../queries';
 
interface TodoProps {
  id: number;
  text: string;
  completed: boolean;
}
 
const Todo: React.FC<TodoProps> = ({ id, text, completed }) => {
  const [toggleTodo] = useMutation(TOGGLE_TODO, {
    variables: { id },
    refetchQueries: [{ query: ALL_TODOS }], // 假设ALL_TODOS是获取所有Todos的查询
  });
 
  return (
    <li onClick={toggleTodo}>
      {text}
    </li>
  );
};
 
export default Todo;

在上述代码中,我们使用了useMutation钩子来处理标记Todo为已完成的请求。

  1. 删除Todo项

components/TodoList/Todo.tsx文件中,我们需要添加一个按钮来删除Todo项。




import React from 'react';
import { useMutation } from '@apollo/client';
import { DELETE_TODO } from '../queries';
 
interface TodoProps {
  id: number;
  text: string;
  completed: boolean;
}
 
const Todo: React.FC<TodoProps> = ({ id, text, completed }) => {
  const [deleteTodo] = useMutation(DELETE_TODO, {
    variables: { id },
    refetchQueries: [{ query: ALL_TODOS }], // 假设ALL_TODOS是获取所有Todos的查询
  });
 
  return (
    <li>
      {text}
      <button onClick={deleteTodo}>X</button>
    </li>
  );
};
 
export default Todo;

在上述代码中,我们使用了useMutation钩子来处理删除Todo的请求。

  1. 清除已完成的Todo项

components/TodoList/Footer.tsx文件中,我们

2024-09-09



import SQLite from 'react-native-sqlite3';
 
const db = new SQLite.Database('myDatabase.db');
 
// 创建表
db.exec('CREATE TABLE IF NOT EXISTS people (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)', (err) => {
  if (err) {
    console.error(err);
  } else {
    console.log('表创建成功');
  }
});
 
// 插入数据
db.run('INSERT INTO people (name, age) VALUES (?, ?), (?, ?)', 'Alice', 30, 'Bob', 25, (err) => {
  if (err) {
    console.error(err);
  } else {
    console.log('数据插入成功');
  }
});
 
// 查询数据
db.all('SELECT name, age FROM people', (err, rows) => {
  if (err) {
    console.error(err);
  } else {
    console.log('查询结果:', rows);
  }
});
 
// 关闭数据库
db.close((err) => {
  if (err) {
    console.error(err);
  } else {
    console.log('数据库已关闭');
  }
});

这段代码展示了如何在React Native项目中使用react-native-sqlite3库来执行基本的SQLite数据库操作,包括创建表、插入数据、查询数据和关闭数据库。这对于开发者在React Native应用中集成SQLite数据库是一个很好的示例。

2024-09-09



# 假设我们已经有了Django项目和React前端项目,以下是Django后端的一个简化示例。
 
# 在Django的views.py中创建一个API视图
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
import json
 
# 假设我们有一个简单的用户模型和序列化器
from .models import User
from .serializers import UserSerializer
 
# 创建一个视图来获取所有用户
@csrf_exempt
def users_list(request):
    # 获取所有用户
    users = User.objects.all()
    serializer = UserSerializer(users, many=True)
    return JsonResponse(serializer.data, safe=False)
 
# 创建一个视图来获取单个用户
@csrf_exempt
def user_detail(request, pk):
    try:
        user = User.objects.get(pk=pk)
    except User.DoesNotExist:
        return JsonResponse({'error': 'User does not exist'}, status=404)
    
    if request.method == 'GET':
        serializer = UserSerializer(user)
        return JsonResponse(serializer.data)
 
    elif request.method == 'PUT':
        data = json.loads(request.body)
        serializer = UserSerializer(user, data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data)
        return JsonResponse(serializer.errors, status=400)
 
    elif request.method == 'DELETE':
        user.delete()
        return JsonResponse({'message': 'User was deleted successfully.'})
 
# 注册路由
# urls.py
from django.urls import path
from .views import users_list, user_detail
 
urlpatterns = [
    path('users/', users_list),
    path('users/<int:pk>/', user_detail),
]

这个示例展示了如何在Django中创建一个简单的RESTful API,包括获取所有用户和获取单个用户的接口。同时,它演示了如何使用JsonResponse返回JSON格式的响应,以及如何处理HTTP GET, PUT 和 DELETE 请求。注意,这个示例假设你已经有了User模型和UserSerializer,并且已经在Django项目中正确设置了路由。

2024-09-09

react-native-haptic-feedback 是一个 React Native 开源库,它允许开发者在支持触觉反馈的iOS和Android设备上触发轻触反馈。

以下是如何使用这个库的基本步骤:

  1. 首先,你需要使用npm或yarn安装这个库:



npm install react-native-haptic-feedback --save
# 或者
yarn add react-native-haptic-feedback
  1. 接下来,你需要链接原生模块到你的项目中。由于这个库使用了自动链接,你通常不需要手动链接它。但是,如果自动链接失败,你可以尝试以下命令:



react-native link react-native-haptic-feedback
  1. 最后,你可以在你的React Native代码中使用这个库来触发轻触反馈:



import HapticFeedback from 'react-native-haptic-feedback';
 
// 触发单一的轻触反馈
HapticFeedback.trigger('impactLight', {
  enableVibrateFallback: true, // 如果没有轻触硬件,是否允许震动
});
 
// 触发自定义的轻触反馈模式
HapticFeedback.trigger('selection', { enableVibrateFallback: true });

这个库支持多种轻触反馈模式,包括但不限于 impactLightimpactMediumimpactHeavyselectionnotification 等。你可以根据需要选择合适的模式来给用户反馈。

2024-09-06

使用Stable Diffusion的ReActor换脸插件通常涉及以下步骤:

  1. 安装ReActor插件。
  2. 准备一张你的脸部图片和一张目标人物的照片。
  3. 使用ReActor插件的界面来应用换脸。

由于ReActor是一个专用的商业插件,不提供开源代码,因此以下是一个概括性的流程示例,不包含具体的代码实现:




# 导入必要的库
import torch
from torch import nn
from torch.autograd import Variable
from PIL import Image
import numpy as np
import cv2
 
# 加载你的模型和ReActor的模型参数
your_face_model = ... # 加载你的脸部模型
reactor_model = ... # 加载ReActor模型
 
# 读取你的脸部图片和目标人物的图片
your_face_image = Image.open("your_face.png")
target_image = Image.open("target_image.png")
 
# 将图片转换为模型需要的输入格式
your_face_tensor = ... # 转换你的脸部图片为模型需要的输入格式
target_tensor = ... # 转换目标人物图片为模型需要的输入格式
 
# 使用ReActor模型进行换脸
combined_tensor = reactor_model(your_face_tensor, target_tensor)
 
# 将结果转换回图片格式
combined_image = ... # 将tensor转换回图片
 
# 保存并展示结果
combined_image.show()
combined_image.save("result.png")

请注意,上述代码是一个概括性的示例,实际使用时你需要替换模型加载和图片处理的具体代码,并确保所有的图片和模型都是按照ReActor插件的要求进行预处理的。由于ReActor是一个专有的商业工具,具体的实现细节和API调用会根据你的具体安装和设置有所不同。

2024-09-06

Reactor 线程模型是一种事件驱动的编程模式,常用于处理高并发的I/O事件。在Redis中,Reactor线程模型主要用于网络连接的管理和I/O事件的多路复用。

Redis的Reactor线程模型主要由文件事件处理器(file event handler)执行,它由四个主要部分组成:

  1. 多个socket
  2. 文件事件分派器(file event dispatcher):负责管理多个socket的I/O事件。
  3. 事件处理器(event handler):处理文件事件。
  4. 定时器事件处理器(time event handler):处理定时器事件。

以下是一个简化的Redis Reactor线程模型的伪代码示例:




while (server_is_not_shutting_down()) {
    // 多路复用,等待一个或多个事件
    aeApiPoll(server.el, timeout);
 
    // 处理文件事件
    for (file_event in ae_poll_events) {
        if (file_event.type == READABLE) {
            handle_readable_event(file_event.fd);
        } else if (file_event.type == WRITABLE) {
            handle_writable_event(file_event.fd);
        }
    }
 
    // 处理定时器事件
    for (time_event in server.time_event_queue) {
        if (time_event.when <= current_time) {
            execute_time_event(time_event);
        }
    }
}

在这个伪代码中,aeApiPoll 函数负责多路复用,它会阻塞等待文件描述符上的事件(如可读或可写事件)。当事件发生时,它们被传递给相应的处理函数,如handle_readable_eventhandle_writable_event。同时,它也会处理定时器事件,执行到期的回调函数。这个模型是Redis处理高并发网络请求的核心。

2024-09-05

这个错误信息表明Spring Cloud Gateway在启动时遇到了一个配置问题。具体来说,错误提示建议你应该设置spring.main.web-application-type属性为reactive,因为Spring Cloud Gateway是基于WebFlux构建的,它需要这个属性来确保正确的运行模式。

解决方法:

  1. 在你的Spring Cloud Gateway应用的application.propertiesapplication.yml配置文件中,添加以下配置:

    
    
    
    spring.main.web-application-type=reactive
  2. 如果你使用的是Spring Boot 2.0或更高版本,默认情况下,当你引入Spring WebFlux依赖时,这个配置会自动设置。确保你的项目中包含了Spring WebFlux依赖。

    Maven依赖示例:

    
    
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
  3. 如果你不打算使用Spring Cloud Gateway的反应式特性,你可以通过设置spring.main.web-application-type=none来避免这个错误,但这意味着你可能不会从Spring Cloud Gateway获得任何WebFlux特有的功能。

确保在做出更改后重新启动你的应用程序,以使配置生效。