在这里插入图片描述

BMI(Body Mass Index,身体质量指数)是衡量人体胖瘦程度的常用指标。今天我们用 React Native 实现一个 BMI 计算器,不仅能计算 BMI 值,还能通过可视化的刻度尺直观展示结果所在的区间。

什么是 BMI

BMI 的计算公式很简单:体重(公斤)除以身高(米)的平方。比如一个身高 1.7 米、体重 65 公斤的人,BMI = 65 / (1.7 × 1.7) ≈ 22.5。

根据中国的标准,BMI 的分类如下:

  • 小于 18.5:偏瘦
  • 18.5 到 24:正常
  • 24 到 28:偏胖
  • 大于 28:肥胖

这个工具不仅要计算 BMI,还要给出直观的视觉反馈,让用户一眼就能看出自己的健康状况。

状态和动画定义

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

export const BMICalculator: React.FC = () => {
  const [height, setHeight] = useState('');
  const [weight, setWeight] = useState('');
  const [result, setResult] = useState<any>(null);
  const scaleAnim = useRef(new Animated.Value(0)).current;
  const slideAnim = useRef(new Animated.Value(0)).current;

这里我们定义了两个输入状态 heightweight,分别存储用户输入的身高和体重。result 用来存储计算结果,包括 BMI 值、健康状态、对应的颜色等信息。

动画方面,scaleAnim 用于结果卡片的弹出效果,从 0 放大到 1,给用户一个"结果出来了"的视觉反馈。slideAnim 则用于刻度尺上指示器的滑动动画,让指示器从左边滑动到对应的位置,这个动画能让用户更直观地感受到自己的 BMI 在整个范围中的位置。

鸿蒙 ArkTS 对比:状态定义

@Entry
@Component
struct BMICalculator {
  @State height: string = ''
  @State weight: string = ''
  @State result: BMIResult | null = null
  @State scaleValue: number = 0
  @State slidePosition: number = 0

interface BMIResult {
  bmi: string
  status: string
  color: string
  idealWeight: string
  emoji: string
}

在 ArkTS 中,我们同样使用 @State 装饰器来声明响应式状态。不同的是,ArkTS 推荐使用接口来定义复杂数据的类型,这样代码的可读性和可维护性更好。当 result 的类型明确后,在使用时 IDE 能提供更好的代码提示。

React Native 中我们用了 any 类型,虽然灵活但牺牲了类型安全。在实际项目中,建议也定义明确的接口类型。

核心计算逻辑

  const calculate = () => {
    const h = parseFloat(height) / 100;
    const w = parseFloat(weight);
    if (!h || !w) return;
    
    const bmi = w / (h * h);
    let status = '', color = '#4A90D9', emoji = '😊';
    
    if (bmi < 18.5) { status = '偏瘦'; color = '#74b9ff'; emoji = '🥗'; }
    else if (bmi < 24) { status = '正常'; color = '#00b894'; emoji = '💪'; }
    else if (bmi < 28) { status = '偏胖'; color = '#fdcb6e'; emoji = '🏃'; }
    else { status = '肥胖'; color = '#e17055'; emoji = '⚠️'; }
    
    const idealWeight = 22 * h * h;

计算函数首先把身高从厘米转换成米(除以 100),然后用标准公式计算 BMI。这里有个小细节:if (!h || !w) return; 这行代码确保了用户必须输入有效的数值才能计算,避免出现 NaN 或 Infinity 的情况。

根据 BMI 值,我们设置不同的状态文字、颜色和 emoji。颜色的选择很有讲究:蓝色表示偏瘦(需要多吃点),绿色表示正常(健康状态),黄色表示偏胖(需要注意),红色表示肥胖(需要重视)。这种颜色编码符合用户的直觉认知。

理想体重的计算用的是 BMI = 22 时的体重,这是正常范围的中间值,被认为是最健康的 BMI 值。

鸿蒙 ArkTS 对比:BMI 计算

calculate() {
  let h = parseFloat(this.height) / 100
  let w = parseFloat(this.weight)
  
  if (!h || !w) return
  
  let bmi = w / (h * h)
  let status = ''
  let color = '#4A90D9'
  let emoji = '😊'
  
  if (bmi < 18.5) {
    status = '偏瘦'
    color = '#74b9ff'
    emoji = '🥗'
  } else if (bmi < 24) {
    status = '正常'
    color = '#00b894'
    emoji = '💪'
  } else if (bmi < 28) {
    status = '偏胖'
    color = '#fdcb6e'
    emoji = '🏃'
  } else {
    status = '肥胖'
    color = '#e17055'
    emoji = '⚠️'
  }
  
  let idealWeight = 22 * h * h
  
  this.result = {
    bmi: bmi.toFixed(1),
    status: status,
    color: color,
    idealWeight: idealWeight.toFixed(1),
    emoji: emoji
  }
}

计算逻辑在 ArkTS 中完全一样,这就是跨平台开发的优势——业务逻辑代码可以直接复用。唯一的区别是 ArkTS 中访问状态变量需要用 this.,而 React 的函数组件中直接使用变量名。

动画触发

    // 动画
    scaleAnim.setValue(0);
    slideAnim.setValue(0);
    Animated.parallel([
      Animated.spring(scaleAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
      Animated.timing(slideAnim, { toValue: (bmi - 15) / 20, duration: 800, useNativeDriver: false }),
    ]).start();

    setResult({ bmi: bmi.toFixed(1), status, color, idealWeight: idealWeight.toFixed(1), emoji });
  };

动画部分值得详细解释。首先我们把两个动画值都重置为 0,这样每次计算都能看到完整的动画效果。

Animated.parallel 让两个动画同时执行:

  1. scaleAnim 使用弹簧动画从 0 到 1,friction: 4 设置较小的摩擦力,让动画有一点弹性效果
  2. slideAnim 使用时间动画,800 毫秒内从 0 滑动到目标位置

指示器位置的计算 (bmi - 15) / 20 需要解释一下:我们的刻度尺范围是 15 到 35,总共 20 个单位。把 BMI 减去 15 再除以 20,就得到了 0 到 1 之间的比例值,正好可以映射到刻度尺的百分比位置。

注意 slideAnimuseNativeDriver 设为 false,因为我们要动态改变 left 属性的百分比值,这个属性不支持原生驱动。

界面渲染:头部

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerEmoji}>⚖️</Text>
        <Text style={styles.headerTitle}>BMI 计算器</Text>
        <Text style={styles.headerSubtitle}>身体质量指数</Text>
      </View>

头部设计简洁明了,用天平 emoji 作为图标,直观表达"称重"的概念。标题和副标题让用户一眼就知道这个工具的用途。

输入区域

      <View style={styles.inputCard}>
        <View style={styles.inputGroup}>
          <View style={styles.inputIcon}>
            <Text style={styles.inputIconText}>📏</Text>
          </View>
          <View style={styles.inputWrapper}>
            <Text style={styles.inputLabel}>身高</Text>
            <View style={styles.inputRow}>
              <TextInput
                style={styles.input}
                value={height}
                onChangeText={setHeight}
                keyboardType="numeric"
                placeholder="170"
                placeholderTextColor="#666"
              />
              <Text style={styles.inputUnit}>cm</Text>
            </View>
          </View>
        </View>

        <View style={styles.inputGroup}>
          <View style={styles.inputIcon}>
            <Text style={styles.inputIconText}>🏋️</Text>
          </View>
          <View style={styles.inputWrapper}>
            <Text style={styles.inputLabel}>体重</Text>
            <View style={styles.inputRow}>
              <TextInput
                style={styles.input}
                value={weight}
                onChangeText={setWeight}
                keyboardType="numeric"
                placeholder="65"
                placeholderTextColor="#666"
              />
              <Text style={styles.inputUnit}>kg</Text>
            </View>
          </View>
        </View>
      </View>

输入区域的设计考虑了用户体验的多个方面:

  1. 图标提示:每个输入框前面都有一个圆形图标(📏 表示身高,🏋️ 表示体重),让用户不用看文字就能理解输入内容
  2. 标签说明:小字的标签"身高"和"体重"进一步明确输入内容
  3. 单位显示:输入框后面显示单位(cm 和 kg),避免用户输入错误的单位
  4. 占位符:placeholder 显示示例值(170 和 65),给用户一个参考
  5. 数字键盘keyboardType="numeric" 确保弹出数字键盘,减少输入错误

这种设计模式在表单输入中很常见,值得在其他项目中复用。

鸿蒙 ArkTS 对比:输入组件

Row() {
  Column() {
    Text('📏')
      .fontSize(24)
  }
  .width(50)
  .height(50)
  .borderRadius(25)
  .backgroundColor('#252550')
  .justifyContent(FlexAlign.Center)
  
  Column() {
    Text('身高')
      .fontSize(12)
      .fontColor('#888888')
    
    Row() {
      TextInput({ placeholder: '170' })
        .layoutWeight(1)
        .fontSize(28)
        .fontColor(Color.White)
        .type(InputType.Number)
        .onChange((value: string) => {
          this.height = value
        })
      
      Text('cm')
        .fontSize(18)
        .fontColor('#4A90D9')
    }
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(16)

ArkTS 中实现相同的布局需要嵌套 RowColumn 组件。layoutWeight(1) 相当于 React Native 中的 flex: 1,让组件占据剩余空间。InputType.Number 对应 keyboardType="numeric"

两种写法的结构类似,只是语法风格不同。React Native 用 JSX 和 style 对象,ArkTS 用声明式 UI 和链式调用。

结果显示:BMI 值和状态

      {result && (
        <Animated.View style={[styles.resultCard, { transform: [{ scale: scaleAnim }] }]}>
          <View style={styles.resultHeader}>
            <Text style={styles.resultEmoji}>{result.emoji}</Text>
            <View style={[styles.bmiCircle, { borderColor: result.color }]}>
              <Text style={[styles.bmiValue, { color: result.color }]}>{result.bmi}</Text>
            </View>
            <Text style={[styles.statusText, { color: result.color }]}>{result.status}</Text>
          </View>

结果区域的设计突出了最重要的信息:BMI 值。用一个大圆圈包裹数字,边框颜色和文字颜色都根据健康状态动态变化。上面的 emoji 和下面的状态文字形成完整的信息层次。

Animated.View 包裹整个结果卡片,应用缩放动画。style 数组的写法 [styles.resultCard, { transform: [...] }] 是 React Native 中组合静态样式和动态样式的常用方式。

刻度尺可视化

          <View style={styles.scaleContainer}>
            <View style={styles.scale}>
              <View style={[styles.scaleSection, { flex: 3.5, backgroundColor: '#74b9ff' }]} />
              <View style={[styles.scaleSection, { flex: 5.5, backgroundColor: '#00b894' }]} />
              <View style={[styles.scaleSection, { flex: 4, backgroundColor: '#fdcb6e' }]} />
              <View style={[styles.scaleSection, { flex: 7, backgroundColor: '#e17055' }]} />
            </View>
            <Animated.View
              style={[
                styles.scaleIndicator,
                { left: slideAnim.interpolate({ inputRange: [0, 1], outputRange: ['0%', '100%'] }) },
              ]}
            >
              <View style={styles.indicatorArrow} />
            </Animated.View>
            <View style={styles.scaleLabels}>
              <Text style={styles.scaleLabel}>15</Text>
              <Text style={styles.scaleLabel}>18.5</Text>
              <Text style={styles.scaleLabel}>24</Text>
              <Text style={styles.scaleLabel}>28</Text>
              <Text style={styles.scaleLabel}>35</Text>
            </View>
          </View>

刻度尺是这个组件最有特色的部分。它由三层组成:

  1. 彩色条:用四个 View 组成,每个代表一个 BMI 区间。flex 值按照区间宽度比例设置(3.5:5.5:4:7 对应 15-18.5:18.5-24:24-28:28-35)
  2. 指示器:一个白色的倒三角形,通过 left 属性定位。interpolate 把 0-1 的动画值映射成 0%-100% 的位置
  3. 刻度标签:显示关键的 BMI 值,帮助用户理解刻度

这种可视化设计比单纯显示数字更直观,用户一眼就能看出自己的 BMI 在整个范围中的位置。

附加信息

          <View style={styles.infoRow}>
            <View style={styles.infoItem}>
              <Text style={styles.infoLabel}>理想体重</Text>
              <Text style={styles.infoValue}>{result.idealWeight} kg</Text>
            </View>
            <View style={styles.infoItem}>
              <Text style={styles.infoLabel}>BMI范围</Text>
              <Text style={styles.infoValue}>18.5 - 24</Text>
            </View>
          </View>
        </Animated.View>
      )}

除了 BMI 值,我们还显示了理想体重和正常 BMI 范围。理想体重给用户一个具体的目标,正常范围让用户知道健康的标准是什么。

图例说明

      <View style={styles.legend}>
        <Text style={styles.legendTitle}>BMI 分类标准</Text>
        {[
          { range: '< 18.5', status: '偏瘦', color: '#74b9ff' },
          { range: '18.5 - 24', status: '正常', color: '#00b894' },
          { range: '24 - 28', status: '偏胖', color: '#fdcb6e' },
          { range: '> 28', status: '肥胖', color: '#e17055' },
        ].map((item, i) => (
          <View key={i} style={styles.legendItem}>
            <View style={[styles.legendDot, { backgroundColor: item.color }]} />
            <Text style={styles.legendRange}>{item.range}</Text>
            <Text style={[styles.legendStatus, { color: item.color }]}>{item.status}</Text>
          </View>
        ))}
      </View>
    </ScrollView>
  );
};

底部的图例解释了颜色和 BMI 范围的对应关系。用数组和 map 渲染,代码简洁且易于维护。如果将来需要修改分类标准,只需要改数组数据就行。

每个图例项包含三个元素:颜色圆点、BMI 范围、状态文字。颜色圆点和刻度尺的颜色一致,形成视觉上的关联。

样式定义:输入区域

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0f0f23', padding: 20 },
  header: { alignItems: 'center', marginBottom: 30 },
  headerEmoji: { fontSize: 50, marginBottom: 8 },
  headerTitle: { fontSize: 28, fontWeight: '700', color: '#fff' },
  headerSubtitle: { fontSize: 14, color: '#888', marginTop: 4 },
  inputCard: {
    backgroundColor: '#1a1a3e',
    borderRadius: 20,
    padding: 20,
    marginBottom: 20,
    borderWidth: 1,
    borderColor: '#3a3a6a',
  },
  inputGroup: { flexDirection: 'row', alignItems: 'center', marginBottom: 16 },
  inputIcon: {
    width: 50,
    height: 50,
    borderRadius: 25,
    backgroundColor: '#252550',
    justifyContent: 'center',
    alignItems: 'center',
    marginRight: 16,
  },
  inputIconText: { fontSize: 24 },
  inputWrapper: { flex: 1 },
  inputLabel: { color: '#888', fontSize: 12, marginBottom: 4 },
  inputRow: { flexDirection: 'row', alignItems: 'center' },
  input: { flex: 1, fontSize: 28, color: '#fff', fontWeight: '300' },
  inputUnit: { color: '#4A90D9', fontSize: 18, marginLeft: 8 },

输入区域的样式设计遵循了几个原则:

  1. 层次分明:卡片背景 #1a1a3e,图标背景 #252550,形成视觉层次
  2. 圆角统一:卡片圆角 20,图标圆角 25(正圆),保持风格一致
  3. 字号对比:输入框字号 28(大),标签字号 12(小),突出重要信息
  4. 颜色语义:单位用蓝色 #4A90D9,和主题色一致

样式定义:结果区域

  btn: {
    backgroundColor: '#4A90D9',
    borderRadius: 16,
    marginBottom: 24,
    shadowColor: '#4A90D9',
    shadowOffset: { width: 0, height: 8 },
    shadowOpacity: 0.4,
    shadowRadius: 15,
    elevation: 10,
  },
  btnInner: { paddingVertical: 18, alignItems: 'center' },
  btnText: { color: '#fff', fontSize: 18, fontWeight: '700' },
  resultCard: {
    backgroundColor: '#1a1a3e',
    borderRadius: 20,
    padding: 24,
    marginBottom: 20,
    borderWidth: 1,
    borderColor: '#3a3a6a',
  },
  resultHeader: { alignItems: 'center', marginBottom: 24 },
  resultEmoji: { fontSize: 40, marginBottom: 12 },
  bmiCircle: {
    width: 120,
    height: 120,
    borderRadius: 60,
    borderWidth: 6,
    justifyContent: 'center',
    alignItems: 'center',
    marginBottom: 12,
  },
  bmiValue: { fontSize: 40, fontWeight: '700' },
  statusText: { fontSize: 24, fontWeight: '600' },

BMI 圆圈用 6 像素的粗边框,让它成为视觉焦点。边框颜色动态变化,和健康状态对应。

样式定义:刻度尺

  scaleContainer: { marginBottom: 24 },
  scale: { flexDirection: 'row', height: 12, borderRadius: 6, overflow: 'hidden' },
  scaleSection: {},
  scaleIndicator: { position: 'absolute', top: -8, marginLeft: -8 },
  indicatorArrow: {
    width: 0,
    height: 0,
    borderLeftWidth: 8,
    borderRightWidth: 8,
    borderTopWidth: 12,
    borderLeftColor: 'transparent',
    borderRightColor: 'transparent',
    borderTopColor: '#fff',
  },
  scaleLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 },
  scaleLabel: { color: '#888', fontSize: 10 },

指示器的三角形用 CSS 边框技巧实现:设置左右边框为透明,上边框为白色,宽度和高度都为 0,就形成了一个向下的三角形。这是一个经典的 CSS 技巧,在 React Native 中同样适用。

overflow: 'hidden' 确保彩色条的圆角不会被子元素覆盖。

样式定义:图例

  infoRow: { flexDirection: 'row' },
  infoItem: { flex: 1, alignItems: 'center' },
  infoLabel: { color: '#888', fontSize: 12, marginBottom: 4 },
  infoValue: { color: '#fff', fontSize: 18, fontWeight: '600' },
  legend: {
    backgroundColor: '#1a1a3e',
    borderRadius: 16,
    padding: 16,
    borderWidth: 1,
    borderColor: '#3a3a6a',
  },
  legendTitle: { color: '#fff', fontSize: 16, fontWeight: '600', marginBottom: 12 },
  legendItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8 },
  legendDot: { width: 12, height: 12, borderRadius: 6, marginRight: 12 },
  legendRange: { flex: 1, color: '#888', fontSize: 14 },
  legendStatus: { fontSize: 14, fontWeight: '600' },
});

图例的布局用 flexDirection: 'row' 实现水平排列,flex: 1 让范围文字占据中间的空间,状态文字靠右对齐。

小结

这个 BMI 计算器展示了如何用 React Native 实现一个既实用又美观的健康工具。刻度尺的可视化设计是亮点,让用户直观地理解自己的健康状况。动画效果增强了交互体验,颜色编码传达了健康信息。

在 OpenHarmony 平台上,这些功能都能正常工作。BMI 计算是纯数学运算,动画使用 React Native 的 Animated API,都是跨平台兼容的。


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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐