在这里插入图片描述

引言:为什么需要文字放大镜?

在日常生活中,我们经常会遇到需要向他人展示文字的场景:

  • 🏥 医院排队:显示自己的号码给护士看
  • 🚗 打车接人:举着手机显示接机牌
  • 📢 会议演示:临时展示关键信息
  • 👴 老年人阅读:放大文字方便阅读

传统的做法是打开备忘录,把字号调到最大。但这种方式有几个问题:

  1. 字号调整不够灵活
  2. 背景颜色单一,可能在某些光线下看不清
  3. 没有专门的全屏展示模式

本文将实现一个专业的「文字放大镜」工具,支持:

  • ✅ 自定义文字内容
  • ✅ 6 档字体大小快速切换
  • ✅ 6 种主题颜色一键更换
  • ✅ 全屏沉浸式展示
  • ✅ 流畅的切换动画

本文所有代码均来自项目真实文件 src/pages/Magnifier.tsx


一、功能架构与技术选型

1.1 功能模块划分

整个放大镜工具可以分为两大区域:

区域 功能 占比
展示区 全屏显示放大后的文字 ~60%
控制区 输入文字、调整大小、切换颜色 ~40%

这种布局设计的考量:

  • 展示区占据主要空间,确保文字足够大、足够醒目
  • 控制区收纳在底部,不干扰主要展示,但随时可调整

1.2 技术要点预览

技术点 用途 React Native API
状态管理 文字、字号、颜色 useState
入场动画 页面淡入效果 Animated.timing
交互动画 字号切换弹性效果 Animated.sequence + spring
滚动容器 长文字滚动查看 ScrollView
触摸反馈 按钮点击响应 TouchableOpacity

二、工具注册与路由配置

在开始核心代码之前,先看看这个工具是如何被注册到工具箱系统中的。

2.1 工具列表注册

文件:src/tools/index.ts

{ id: 55, name: '放大镜', description: '文字放大显示', icon: '🔍', component: 'Magnifier' },

代码解析:

这行配置定义了放大镜工具的元信息:

  • id: 55:工具的唯一标识符,用于路由跳转和数据关联
  • name: '放大镜':在工具列表中显示的名称,简洁明了
  • description: '文字放大显示':工具的功能描述,帮助用户理解用途
  • icon: '🔍':使用放大镜 emoji 作为图标,直观表达功能
  • component: 'Magnifier':对应的组件名称,用于动态加载

💡 设计思考:为什么用 emoji 而不是图片图标?

  • 无需额外资源文件,减小包体积
  • 跨平台一致性好
  • 修改方便,不需要设计师介入

2.2 路由映射配置

文件:src/screens/ToolScreen.tsx

Magnifier: Pages.Magnifier,

代码解析:

这行代码建立了字符串 'Magnifier' 到实际组件 Pages.Magnifier 的映射关系。

工具箱的路由机制是这样工作的:

  1. 用户点击工具卡片,携带 component: 'Magnifier' 跳转
  2. ToolScreen 接收到参数,在 componentMap 中查找
  3. 找到 Pages.Magnifier 组件并渲染

这种「配置驱动」的架构有几个好处:

  • 解耦:工具配置与组件实现分离
  • 可扩展:新增工具只需添加配置,无需修改路由逻辑
  • 可维护:所有工具的入口一目了然

三、组件结构与状态设计

3.1 导入依赖

文件:src/pages/Magnifier.tsx

import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, StyleSheet, ScrollView, TouchableOpacity, Animated } from 'react-native';

依赖分析:

导入项 来源 用途
useState React 管理文字、字号、颜色等状态
useRef React 持有动画值的引用,避免重新创建
useEffect React 处理组件挂载时的入场动画
View RN 布局容器
Text RN 文字显示
TextInput RN 用户输入文字
StyleSheet RN 样式定义
ScrollView RN 长文字滚动
TouchableOpacity RN 可点击按钮,带透明度反馈
Animated RN 动画系统

🎯 最佳实践:只导入需要的组件,避免导入整个模块,有助于 Tree Shaking 优化包体积。

3.2 状态定义

export const Magnifier: React.FC = () => {
  const [text, setText] = useState('在此输入文字');
  const [fontSize, setFontSize] = useState(48);
  const [bgColor, setBgColor] = useState('#0f0f23');
  const [textColor, setTextColor] = useState('#fff');

状态设计详解:

📝 text - 显示的文字内容
const [text, setText] = useState('在此输入文字');
  • 初始值'在此输入文字' 作为占位提示
  • 为什么不用空字符串? 空字符串会让展示区看起来"空荡荡"的,用户可能不知道这里会显示什么
  • 交互设计:用户点击输入框时,可以选择全选删除或追加输入
🔤 fontSize - 字体大小
const [fontSize, setFontSize] = useState(48);
  • 初始值:48px,这是一个适中的大小
  • 为什么是 48? 太小(如 24)在展示区不够醒目;太大(如 100)可能导致默认文字换行过多
  • 可选范围:24、36、48、64、80、100,覆盖从"稍大"到"超大"的需求
🎨 bgColortextColor - 背景色和文字色
const [bgColor, setBgColor] = useState('#0f0f23');
const [textColor, setTextColor] = useState('#fff');
  • 为什么要成对管理? 背景色和文字色必须有足够对比度,否则看不清
  • 初始配色:深蓝背景 + 白色文字,这是一个高对比度、低刺激的组合
  • 设计考量:深色背景在暗光环境下不刺眼,白色文字清晰可见

四、动画系统实现

放大镜工具包含两套动画:入场动画和交互动画。

4.1 动画值初始化

const scaleAnim = useRef(new Animated.Value(1)).current;
const fadeAnim = useRef(new Animated.Value(0)).current;

代码解析:

动画值 初始值 用途
scaleAnim 1 控制文字的缩放比例,用于字号切换动画
fadeAnim 0 控制整个页面的透明度,用于入场动画

为什么用 useRef 而不是 useState

这是 React Native 动画的一个重要模式:

// ❌ 错误做法
const [fadeAnim] = useState(new Animated.Value(0));

// ✅ 正确做法
const fadeAnim = useRef(new Animated.Value(0)).current;

原因:

  1. Animated.Value 是一个可变对象,它的值会在动画过程中不断变化
  2. 如果用 useState,每次组件重渲染都可能创建新的 Animated.Value
  3. useRef 保证在组件整个生命周期内,引用的是同一个对象
  4. .current 直接取出引用值,后续使用更简洁

4.2 入场动画

useEffect(() => {
  Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start();
}, []);

动画配置详解:

参数 含义
toValue 1 目标透明度,1 表示完全不透明
duration 500 动画时长 500ms,约半秒
useNativeDriver true 使用原生驱动,性能更好

为什么入场动画很重要?

  • 心理感受:页面"渐显"比"突然出现"更舒适
  • 加载掩盖:如果有轻微的加载延迟,淡入动画可以掩盖这种"卡顿感"
  • 品质感:细节动画是区分"精品应用"和"普通应用"的重要因素

useNativeDriver: true 的意义:

React Native 的动画有两种执行方式:

方式 执行位置 性能 限制
JS 驱动 JavaScript 线程 较低
Native 驱动 原生 UI 线程 只支持 transform 和 opacity

由于我们只动画 opacity,完全可以使用 Native 驱动,获得 60fps 的流畅体验。

4.3 字号切换动画

const animateFontChange = (newSize: number) => {
  Animated.sequence([
    Animated.timing(scaleAnim, { toValue: 0.9, duration: 100, useNativeDriver: true }),
    Animated.spring(scaleAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
  ]).start();
  setFontSize(newSize);
};

动画序列解析:

这个函数实现了一个"按压回弹"的视觉效果,分为两个阶段:

阶段1: scale 1 → 0.9 (100ms, timing)
       ↓
阶段2: scale 0.9 → 1 (spring 弹性)

第一阶段:收缩

Animated.timing(scaleAnim, { toValue: 0.9, duration: 100, useNativeDriver: true })
  • 文字快速缩小到 90%
  • 使用 timing 线性动画,100ms 很短,给人"被按下"的感觉

第二阶段:回弹

Animated.spring(scaleAnim, { toValue: 1, friction: 4, useNativeDriver: true })
  • 文字弹回原始大小
  • 使用 spring 弹性动画,friction: 4 控制阻尼
  • 弹性动画会有轻微的"过冲"效果,更自然

friction 参数的影响:

friction 值 效果
1-3 弹性很大,会明显过冲和回弹
4-6 适中的弹性,自然舒适
7+ 几乎没有弹性,接近线性

这里选择 friction: 4 是一个平衡点:有弹性感,但不会过于夸张。

为什么 setFontSize 在动画外面?

Animated.sequence([...]).start();
setFontSize(newSize);  // 立即执行,不等动画完成

状态更新是立即的,动画是视觉上的"装饰"。这样设计的好处:

  • 用户点击后,字号立即生效
  • 动画只是增强体验,不阻塞功能

五、主题颜色配置

5.1 颜色预设数组

const colors = [
  { bg: '#0f0f23', text: '#fff', name: '深蓝' },
  { bg: '#fff', text: '#000', name: '白色' },
  { bg: '#e74c3c', text: '#fff', name: '红色' },
  { bg: '#2ecc71', text: '#fff', name: '绿色' },
  { bg: '#3498db', text: '#fff', name: '蓝色' },
  { bg: '#f39c12', text: '#000', name: '橙色' },
];

颜色选择的设计考量:

主题 背景色 文字色 适用场景
深蓝 #0f0f23 暗光环境,不刺眼
白色 #fff 明亮环境,最高对比度
红色 #e74c3c 紧急信息,醒目
绿色 #2ecc71 积极信息,舒适
蓝色 #3498db 通用场景,专业感
橙色 #f39c12 警示信息,温暖

为什么有的用白色文字,有的用黑色文字?

这涉及到颜色对比度的概念。根据 WCAG(Web 内容无障碍指南):

  • 文字与背景的对比度至少要达到 4.5:1
  • 大文字(18px 以上)可以放宽到 3:1

简单规则:

  • 深色背景 → 浅色文字
  • 浅色背景 → 深色文字

橙色 #f39c12 是一个中等亮度的颜色,配黑色文字比白色文字对比度更高。

为什么把颜色配置放在组件内部?

// 当前做法:组件内部定义
const colors = [...]

// 另一种做法:外部配置文件
import { MAGNIFIER_COLORS } from '../config/colors';

当前做法的优点:

  • 代码集中,一个文件看完所有逻辑
  • 颜色配置不太可能被其他组件复用
  • 修改方便,不需要跨文件查找

如果未来需要支持用户自定义颜色,可以考虑提取到配置文件。


六、UI 布局实现

6.1 整体容器结构

return (
  <Animated.View style={[styles.container, { opacity: fadeAnim }]}>
    <View style={[styles.display, { backgroundColor: bgColor }]}>
      {/* 展示区 */}
    </View>

    <View style={styles.controls}>
      {/* 控制区 */}
    </View>
  </Animated.View>
);

布局结构解析:

┌─────────────────────────────┐
│     Animated.View           │  ← 最外层,控制入场动画
│  ┌───────────────────────┐  │
│  │      display          │  │  ← 展示区,flex: 1 占满剩余空间
│  │   (放大的文字)         │  │
│  └───────────────────────┘  │
│  ┌───────────────────────┐  │
│  │      controls         │  │  ← 控制区,固定高度
│  │  (输入框、按钮等)      │  │
│  └───────────────────────┘  │
└─────────────────────────────┘

为什么用 Animated.View 包裹整个组件?

<Animated.View style={[styles.container, { opacity: fadeAnim }]}>
  • Animated.ViewView 的动画版本
  • 可以直接绑定 Animated.Value 到样式属性
  • 普通 View 无法响应动画值的变化

动态样式的写法:

style={[styles.container, { opacity: fadeAnim }]}

这是 React Native 的样式合并语法:

  • styles.container:静态样式
  • { opacity: fadeAnim }:动态样式
  • 数组中后面的样式会覆盖前面的同名属性

6.2 展示区实现

<View style={[styles.display, { backgroundColor: bgColor }]}>
  <ScrollView contentContainerStyle={styles.displayContent}>
    <Animated.Text style={[styles.displayText, { fontSize, color: textColor, transform: [{ scale: scaleAnim }] }]}>
      {text}
    </Animated.Text>
  </ScrollView>
</View>

层级结构详解:

层级 组件 职责
1 View 背景色容器,响应 bgColor 变化
2 ScrollView 当文字过长时提供滚动能力
3 Animated.Text 显示文字,响应字号和缩放动画

为什么需要 ScrollView?

考虑这个场景:

  • 用户输入了一段很长的文字
  • 字号设置为 100
  • 屏幕宽度有限

如果没有 ScrollView,超出屏幕的文字会被截断。有了 ScrollView,用户可以滚动查看完整内容。

contentContainerStyle vs style 的区别:

<ScrollView 
  style={...}                    // ScrollView 自身的样式
  contentContainerStyle={...}    // 内容容器的样式
>
  • style:控制 ScrollView 这个"窗口"的大小和位置
  • contentContainerStyle:控制内容的布局方式

这里我们用 contentContainerStyle 来居中文字:

displayContent: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },

Animated.Text 的样式绑定:

style={[
  styles.displayText,           // 基础样式
  { 
    fontSize,                   // 动态字号
    color: textColor,           // 动态颜色
    transform: [{ scale: scaleAnim }]  // 缩放动画
  }
]}

transform 是一个数组,可以组合多种变换:

transform: [
  { scale: 1.2 },      // 缩放
  { rotate: '45deg' }, // 旋转
  { translateX: 10 },  // X 轴平移
]

这里只用了 scale,配合 scaleAnim 实现字号切换时的弹性效果。

6.3 控制区 - 文字输入

<View style={styles.inputSection}>
  <Text style={styles.label}>📝 输入文字</Text>
  <TextInput 
    style={styles.input} 
    value={text} 
    onChangeText={setText} 
    placeholder="输入要放大的文字" 
    placeholderTextColor="#666" 
    multiline 
  />
</View>

TextInput 属性详解:

属性 作用
value text 受控组件,显示当前状态值
onChangeText setText 输入变化时更新状态
placeholder '输入要放大的文字' 空内容时的提示文字
placeholderTextColor '#666' 提示文字颜色,比正文浅
multiline true 允许多行输入

受控组件 vs 非受控组件:

// 受控组件(当前做法)
<TextInput value={text} onChangeText={setText} />

// 非受控组件
<TextInput defaultValue="初始值" ref={inputRef} />

受控组件的优点:

  • 状态集中管理,数据流清晰
  • 可以在 onChangeText 中做输入校验
  • 方便实现"清空"、"重置"等功能

6.4 控制区 - 字号选择

<View style={styles.sizeControl}>
  <Text style={styles.label}>🔤 字体大小: {fontSize}</Text>
  <View style={styles.sizeButtons}>
    {[24, 36, 48, 64, 80, 100].map(s => (
      <TouchableOpacity 
        key={s} 
        style={[styles.sizeBtn, fontSize === s && styles.sizeBtnActive]} 
        onPress={() => animateFontChange(s)}
      >
        <Text style={[styles.sizeBtnText, fontSize === s && styles.sizeBtnTextActive]}>{s}</Text>
      </TouchableOpacity>
    ))}
  </View>
</View>

字号按钮的渲染逻辑:

{[24, 36, 48, 64, 80, 100].map(s => (
  // 为每个字号生成一个按钮
))}

使用数组 map 而不是手写 6 个按钮的好处:

  • 代码简洁,避免重复
  • 修改字号选项只需改数组
  • 保持一致的结构和样式

条件样式的写法:

style={[styles.sizeBtn, fontSize === s && styles.sizeBtnActive]}

这是 React Native 中常用的条件样式模式:

  • styles.sizeBtn:基础样式,始终应用
  • fontSize === s && styles.sizeBtnActive:当条件为真时应用激活样式

&& 短路求值的特性:

  • 如果 fontSize === sfalse,整个表达式返回 false
  • 如果 fontSize === strue,返回 styles.sizeBtnActive
  • React Native 的样式数组会忽略 falsenullundefined

为什么选择这 6 个字号?

字号 适用场景
24 稍大,适合段落文字
36 中等,适合标题
48 较大,默认值,平衡可读性和展示效果
64 大号,适合短语
80 超大,适合单词
100 巨大,适合数字或单个字符

这个梯度设计让用户可以根据内容长度选择合适的字号。

6.5 控制区 - 颜色选择

<View style={styles.colorControl}>
  <Text style={styles.label}>🎨 主题颜色</Text>
  <View style={styles.colorRow}>
    {colors.map((c, i) => (
      <TouchableOpacity 
        key={i} 
        style={[styles.colorBtn, { backgroundColor: c.bg }, bgColor === c.bg && styles.colorBtnActive]} 
        onPress={() => { setBgColor(c.bg); setTextColor(c.text); }}
      >
        <Text style={[styles.colorName, { color: c.text }]}>{c.name}</Text>
      </TouchableOpacity>
    ))}
  </View>
</View>

颜色按钮的视觉设计:

每个颜色按钮本身就是一个"预览":

  • 按钮背景色 = 主题背景色
  • 按钮文字色 = 主题文字色
  • 用户可以直观看到选择后的效果

激活状态的视觉反馈:

bgColor === c.bg && styles.colorBtnActive
colorBtnActive: { borderColor: '#ffd700' },  // 金色边框

当某个颜色被选中时,显示金色边框。为什么用金色?

  • 金色在各种背景色上都比较醒目
  • 不会与任何主题颜色冲突
  • 给人"选中"、"高亮"的感觉

同时更新两个状态:

onPress={() => { setBgColor(c.bg); setTextColor(c.text); }}

这里在一个事件处理函数中调用了两个 setState。React 会将它们批量处理,只触发一次重渲染。


七、样式系统详解

7.1 容器样式

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0f0f23' },
  display: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  displayContent: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
  displayText: { textAlign: 'center', fontWeight: '600' },

Flex 布局解析:

container (flex: 1)
├── display (flex: 1)      ← 占据所有剩余空间
└── controls (无 flex)     ← 由内容撑开高度

flex: 1 的含义:

  • 在父容器中占据所有可用空间
  • 如果有多个 flex: 1 的兄弟元素,它们平分空间

这里只有 display 设置了 flex: 1,所以它会占据除 controls 之外的所有空间。

文字样式:

displayText: { textAlign: 'center', fontWeight: '600' },
  • textAlign: 'center':文字水平居中
  • fontWeight: '600':半粗体,比普通文字更醒目

7.2 控制区样式

controls: { 
  backgroundColor: '#1a1a3e', 
  padding: 16, 
  borderTopLeftRadius: 24, 
  borderTopRightRadius: 24, 
  borderWidth: 1, 
  borderColor: '#3a3a6a', 
  borderBottomWidth: 0 
},

设计细节解析:

属性 设计意图
backgroundColor #1a1a3e 比主背景稍浅,形成层次感
padding 16 内边距,让内容不贴边
borderTopLeftRadius 24 左上圆角
borderTopRightRadius 24 右上圆角
borderWidth 1 边框宽度
borderColor #3a3a6a 边框颜色,微妙的分隔线
borderBottomWidth 0 底部无边框,因为贴着屏幕底部

为什么只有顶部圆角?

控制区是一个"从底部弹出"的面板视觉效果:

┌────────────────────────┐
│                        │
│      展示区            │
│                        │
├────────────────────────┤  ← 这里有圆角
│      控制区            │
└────────────────────────┘  ← 这里贴着屏幕底部,无圆角

这种设计在 iOS 和 Material Design 中都很常见,给人"卡片浮起"的感觉。

7.3 输入框样式

input: { 
  backgroundColor: '#252550', 
  padding: 12, 
  borderRadius: 12, 
  color: '#fff', 
  borderWidth: 1, 
  borderColor: '#3a3a6a' 
},

输入框的视觉层次:

控制区背景: #1a1a3e (最深)
    ↓
输入框背景: #252550 (稍浅)
    ↓
输入框边框: #3a3a6a (最浅)

这种"深 → 浅"的层次让输入框在控制区中"凸显"出来,引导用户注意。

7.4 按钮样式

sizeBtn: { 
  padding: 10, 
  marginRight: 8, 
  marginBottom: 8, 
  backgroundColor: '#252550', 
  borderRadius: 8, 
  minWidth: 50, 
  alignItems: 'center', 
  borderWidth: 1, 
  borderColor: '#3a3a6a' 
},
sizeBtnActive: { backgroundColor: '#4A90D9', borderColor: '#4A90D9' },
sizeBtnText: { color: '#888' },
sizeBtnTextActive: { color: '#fff', fontWeight: '600' },

按钮状态对比:

状态 背景色 边框色 文字色 文字粗细
默认 #252550 #3a3a6a #888 普通
激活 #4A90D9 #4A90D9 #fff 600

激活状态使用蓝色高亮,与默认状态形成明显对比,用户一眼就能看出当前选择。

7.5 颜色按钮样式

colorBtn: { 
  width: 50, 
  height: 50, 
  borderRadius: 12, 
  marginRight: 8, 
  marginBottom: 8, 
  borderWidth: 2, 
  borderColor: 'transparent', 
  justifyContent: 'center', 
  alignItems: 'center' 
},
colorBtnActive: { borderColor: '#ffd700' },
colorName: { fontSize: 10, fontWeight: '600' },

颜色按钮的设计技巧:

  1. 固定尺寸width: 50, height: 50,保证所有颜色按钮大小一致
  2. 圆角borderRadius: 12,柔和的圆角,不是完全的圆形
  3. 透明边框:默认 borderColor: 'transparent',激活时变金色
  4. 小字号fontSize: 10,颜色名称不喧宾夺主

为什么默认边框是透明而不是无边框?

// 当前做法
borderWidth: 2,
borderColor: 'transparent',

// 另一种做法
borderWidth: 0,  // 默认无边框
// 激活时
borderWidth: 2,
borderColor: '#ffd700',

如果默认无边框,激活时突然出现边框会导致按钮"跳动"(因为边框占用空间)。

使用透明边框可以保持布局稳定,只改变颜色。


八、完整代码回顾

将所有部分组合起来,这是 src/pages/Magnifier.tsx 的完整代码:

import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, StyleSheet, ScrollView, TouchableOpacity, Animated } from 'react-native';

export const Magnifier: React.FC = () => {
  const [text, setText] = useState('在此输入文字');
  const [fontSize, setFontSize] = useState(48);
  const [bgColor, setBgColor] = useState('#0f0f23');
  const [textColor, setTextColor] = useState('#fff');
  
  const scaleAnim = useRef(new Animated.Value(1)).current;
  const fadeAnim = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start();
  }, []);

  const animateFontChange = (newSize: number) => {
    Animated.sequence([
      Animated.timing(scaleAnim, { toValue: 0.9, duration: 100, useNativeDriver: true }),
      Animated.spring(scaleAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
    ]).start();
    setFontSize(newSize);
  };

  const colors = [
    { bg: '#0f0f23', text: '#fff', name: '深蓝' },
    { bg: '#fff', text: '#000', name: '白色' },
    { bg: '#e74c3c', text: '#fff', name: '红色' },
    { bg: '#2ecc71', text: '#fff', name: '绿色' },
    { bg: '#3498db', text: '#fff', name: '蓝色' },
    { bg: '#f39c12', text: '#000', name: '橙色' },
  ];

  return (
    <Animated.View style={[styles.container, { opacity: fadeAnim }]}>
      <View style={[styles.display, { backgroundColor: bgColor }]}>
        <ScrollView contentContainerStyle={styles.displayContent}>
          <Animated.Text style={[styles.displayText, { fontSize, color: textColor, transform: [{ scale: scaleAnim }] }]}>
            {text}
          </Animated.Text>
        </ScrollView>
      </View>

      <View style={styles.controls}>
        <View style={styles.inputSection}>
          <Text style={styles.label}>📝 输入文字</Text>
          <TextInput style={styles.input} value={text} onChangeText={setText} placeholder="输入要放大的文字" placeholderTextColor="#666" multiline />
        </View>

        <View style={styles.sizeControl}>
          <Text style={styles.label}>🔤 字体大小: {fontSize}</Text>
          <View style={styles.sizeButtons}>
            {[24, 36, 48, 64, 80, 100].map(s => (
              <TouchableOpacity key={s} style={[styles.sizeBtn, fontSize === s && styles.sizeBtnActive]} onPress={() => animateFontChange(s)}>
                <Text style={[styles.sizeBtnText, fontSize === s && styles.sizeBtnTextActive]}>{s}</Text>
              </TouchableOpacity>
            ))}
          </View>
        </View>

        <View style={styles.colorControl}>
          <Text style={styles.label}>🎨 主题颜色</Text>
          <View style={styles.colorRow}>
            {colors.map((c, i) => (
              <TouchableOpacity key={i} style={[styles.colorBtn, { backgroundColor: c.bg }, bgColor === c.bg && styles.colorBtnActive]} onPress={() => { setBgColor(c.bg); setTextColor(c.text); }}>
                <Text style={[styles.colorName, { color: c.text }]}>{c.name}</Text>
              </TouchableOpacity>
            ))}
          </View>
        </View>
      </View>
    </Animated.View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0f0f23' },
  display: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  displayContent: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
  displayText: { textAlign: 'center', fontWeight: '600' },
  controls: { backgroundColor: '#1a1a3e', padding: 16, borderTopLeftRadius: 24, borderTopRightRadius: 24, borderWidth: 1, borderColor: '#3a3a6a', borderBottomWidth: 0 },
  inputSection: { marginBottom: 16 },
  label: { fontSize: 14, color: '#888', marginBottom: 8 },
  input: { backgroundColor: '#252550', padding: 12, borderRadius: 12, color: '#fff', borderWidth: 1, borderColor: '#3a3a6a' },
  sizeControl: { marginBottom: 16 },
  sizeButtons: { flexDirection: 'row', flexWrap: 'wrap' },
  sizeBtn: { padding: 10, marginRight: 8, marginBottom: 8, backgroundColor: '#252550', borderRadius: 8, minWidth: 50, alignItems: 'center', borderWidth: 1, borderColor: '#3a3a6a' },
  sizeBtnActive: { backgroundColor: '#4A90D9', borderColor: '#4A90D9' },
  sizeBtnText: { color: '#888' },
  sizeBtnTextActive: { color: '#fff', fontWeight: '600' },
  colorControl: { marginBottom: 8 },
  colorRow: { flexDirection: 'row', flexWrap: 'wrap' },
  colorBtn: { width: 50, height: 50, borderRadius: 12, marginRight: 8, marginBottom: 8, borderWidth: 2, borderColor: 'transparent', justifyContent: 'center', alignItems: 'center' },
  colorBtnActive: { borderColor: '#ffd700' },
  colorName: { fontSize: 10, fontWeight: '600' },
});

九、功能扩展思路

当前实现已经满足基本需求,如果想进一步完善,可以考虑:

9.1 功能增强

功能 实现思路
全屏模式 隐藏控制区,双击切换
自定义颜色 添加颜色选择器组件
字体选择 支持多种字体风格
文字阴影 增加 textShadow 样式
滚动字幕 使用 Animated 实现水平滚动

9.2 体验优化

优化点 实现思路
屏幕常亮 使用 react-native-keep-awake
手势缩放 使用 PanResponderreact-native-gesture-handler
历史记录 使用 AsyncStorage 保存常用文字
分享功能 截图并调用系统分享

9.3 无障碍支持

<TouchableOpacity
  accessible={true}
  accessibilityLabel={`字体大小 ${s}`}
  accessibilityRole="button"
  accessibilityState={{ selected: fontSize === s }}
>

添加无障碍属性,让视障用户也能使用屏幕阅读器操作。


十、总结

本文实现的「文字放大镜」工具,虽然功能简单,但涵盖了 React Native 开发的多个核心知识点:

知识点 本文应用
状态管理 4 个 useState 管理文字、字号、颜色
动画系统 timingspringsequence 组合使用
条件样式 && 短路求值实现激活状态
Flex 布局 展示区自适应 + 控制区固定
受控组件 TextInputvalue + onChangeText
列表渲染 map 生成按钮组

这个工具的代码量不大(约 80 行),但每一行都有其设计考量。希望通过本文的详细解析,能帮助你理解 React Native 组件开发的思路和技巧。


相关资源

  • 📦 项目源码:src/pages/Magnifier.tsx
  • 🔧 工具配置:src/tools/index.ts
  • 🗺️ 路由映射:src/screens/ToolScreen.tsx

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐