RN 常用 Api

import React, { useEffect } from 'react'
import {
  StyleSheet,
  View,
  Button,
  Alert,
  Text,
  Dimensions,
  useWindowDimensions,
  Platform,
  Linking,
  PixelRatio,
  BackHandler,
  PermissionsAndroid,
  Vibration,
  ToastAndroid,
  Keyboard,
  TextInput,
} from 'react-native'

import { useBackHandler } from '@react-native-community/hooks'

export default () => {
  // const { width, height, scale, fontScale } = useWindowDimensions();
  // console.log(`width=${width}, height=${height}`);
  // console.log(`scale=${scale}, fontScale=${fontScale}`);

  useBackHandler(() => {
    return true
  })

  useEffect(() => {
    const subcription = Dimensions.addEventListener(
      'change',
      (window, screen) => {
        console.log(window)
        console.log(screen)
      }
    )

    // BackHandler.addEventListener('hardwareBackPress', backForAndroid)

    const showSubscription = Keyboard.addListener(
      'keyboardDidShow',
      onKeyboardShow
    )
    const hideSubscription = Keyboard.addListener(
      'keyboardDidHide',
      onKeyboardHide
    )

    return () => {
      subcription.remove()
      showSubscription.remove()
      hideSubscription.remove()
      // BackHandler.removeEventListener('hardwareBackPress', backForAndroid);
    }
  }, [])

  const onKeyboardShow = () => {
    console.log('键盘出现')
  }

  const onKeyboardHide = () => {
    console.log('键盘隐藏')
  }

  // const backForAndroid = () => {
  //     return false;
  // }

  const onPress = () => {
    // alert和console你不知道的调试小技巧
    // alert('这是一条提示信息');
    // alert(123);
    // alert(false);

    // const buttons = [
    //     {text: '取消', onPress: () => console.log('取消')},
    //     {text: '确定', onPress: () => console.log('确定')},
    // ];
    // Alert.alert('这是标题', '这是一条提示信息', buttons);

    // console.log('这是普通的日志输出');
    // console.info('信息日志输出');
    // console.debug('调试日志输出');
    // console.warn('警告日志输出');
    // console.error('错误日志输出');

    // console.log('我是个人开发者%s,我学习RN%d年半了', '张三', 2);
    // const obj = {name: '张三', age: 12};
    // console.log('我是一个对象:%o', obj);

    // console.log('%c这行日志红色文字,字号大', 'color:red; font-size:x-large');
    // console.log('%c这行日志蓝色文字,字号中', 'color:blue; font-size:x-medium');
    // console.log('%c这行日志绿色文字,字号小', 'color:green; font-size:x-small');

    // const viewLayout = (
    //     <View style={{ flexDirection: 'column' }}>
    //         <Text style={{ fontSize: 20, color: 'red' }} >
    //             文字显示
    //         </Text>
    //     </View>
    // );
    // console.log(viewLayout);

    // const users = [
    //     {name: '张三', age: 12, hobby: '唱歌'},
    //     {name: '李四', age: 15, hobby: '跳舞'},
    //     {name: '王武', age: 18, hobby: '打篮球'},
    // ];
    // console.table(users);

    // console.group();
    // console.log('第1行日志');
    // console.log('第2行日志');
    // console.log('第3行日志');
    //     console.group();
    //     console.log('二级分组第1行日志');
    //     console.log('二级分组第2行日志');
    //     console.log('二级分组第3行日志');
    //     console.groupEnd();
    // console.groupEnd();

    // Dimension 和 useWindowDimension 获取屏幕信息
    // const { width, height, scale, fontScale } = Dimensions.get('window');
    // console.log(`width=${width}, height=${height}`);

    // Platform获取平台属性
    // console.log(Platform.OS);
    // console.log(Platform.Version);
    // console.log(Platform.constants);
    // console.log(Platform.isPad);
    // console.log(Platform.isTV);
    // const style = Platform.select({
    //     android: {
    //         marginTop: 20,
    //     },
    //     ios: {
    //         marginTop: 0,
    //     },
    //     default: {
    //         marginTop: 10,
    //     },
    // });
    // console.log(style);

    // StyleSheet构建灵活样式表

    // const s1 = {
    //     fontSize: 18,
    // };
    // const s2 = {
    //     fontSize: 20,
    //     color: 'red',
    // };
    // const composeStyle = StyleSheet.compose(s1, s2);
    // console.log(composeStyle);

    // const flattenStyle = StyleSheet.flatten([s1, s2]);
    // console.log(flattenStyle);

    // console.log(StyleSheet.absoluteFill);

    // console.log(StyleSheet.hairlineWidth);
    // console.log(1 / Dimensions.get('screen').scale);

    // Linking一个api节省50行代码
    // if (Linking.canOpenURL('https://www.baidu.com/')) {
    //     Linking.openURL('https://www.baidu.com/');
    // }
    // Linking.openURL('geo:37.2122, 12.222');
    // Linking.openURL('tel:10086');
    // Linking.openURL('smsto:10086');
    // Linking.openURL('mailto:10086@qq.com');
    // Linking.openURL('dagongjue://demo?name=李四');
    // Linking.openSettings();

    // if (Platform.OS === 'android') {
    //     Linking.sendIntent('com.dagongjue.demo.test', [{key: 'name', value: '王武'}]);
    // }
    // console.log(Linking.getInitialURL());

    // PixelRatio像素比例工具
    // console.log(PixelRatio.get());
    // console.log(PixelRatio.getFontScale());
    // console.log(
    //     PixelRatio.getPixelSizeForLayoutSize(200)
    // );

    // BackHandler安卓返回键适配
    // BackHandler.exitApp();

    // PermissionsAndroid轻松解决权限问题
    // const needPermission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;
    // PermissionsAndroid.check(
    //     needPermission
    // ).then(result => {
    //     if (!result) {
    //         PermissionsAndroid.request(needPermission).then(status => {
    //             console.log(status);
    //             if (status === 'granted') {
    //                 //获得
    //             } else if (status === 'denied') {
    //                 //拒绝
    //             }
    //         });
    //     }
    // });

    // PermissionsAndroid.requestMultiple([
    //     PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
    //     PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
    // ]);

    // Vibration简单好用的震动交互

    // Vibration.vibrate();
    // Vibration.vibrate(1000);//just android
    // Vibration.cancel();
    // android
    // Vibration.vibrate([100, 500, 200, 500]);
    // IOS
    // Vibration.vibrate([100, 200, 300, 400]);
    // Vibration.vibrate([100, 200, 300, 400], true);

    // ToastAndroid安卓平台的提示
    // ToastAndroid.show('这是一个提示', ToastAndroid.SHORT);
    // ToastAndroid.show('这是一个提示', ToastAndroid.LONG);
    // ToastAndroid.showWithGravity(
    //     '这是一个提示',
    //     ToastAndroid.LONG,
    //     ToastAndroid.TOP
    // );
    // ToastAndroid.showWithGravity(
    //     '这是一个提示',
    //     ToastAndroid.LONG,
    //     ToastAndroid.TOP,
    //     100, 200,
    // );

    // Transform矩阵变换的伪3D效果

    // Keyboard键盘操作有神器

    Keyboard.dismiss()
  }

  return (
    <View style={styles.root}>
      <Button title='按钮' onPress={onPress} />

      {/* <View style={styles.view}>
                <View style={styles.subView} />
                <View style={styles.subView} />
                <View style={styles.subView} />
                <View style={styles.subView} />
                <View style={styles.subView} />
                <View style={styles.subView} />
                <View style={styles.subView} />
                <View style={styles.subView} />
                <View style={styles.subView} />
                <View style={styles.subView} />
            </View> */}

      <View
        style={[
          {
            width: 100,
            height: 100,
            backgroundColor: '#3050ff',
            marginTop: 60,
            marginLeft: 60,
          },
          {
            transform: [
              // {translateX: 200}
              // {translateY: 150}
              { scale: 1.5 },
              // {scaleX: 1.5},
              // {scaleY: 1.5}
              { rotateX: '45deg' },
              // {rotateY: '45deg'},
              { rotateZ: '45deg' },
              // {rotate: '45deg'},
            ],
          },
        ]}
      />

      <TextInput
        style={{
          width: '100%',
          height: 56,
          backgroundColor: '#E0E0E0',
        }}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    ...Platform.select({
      android: {
        paddingTop: 20,
      },
      ios: {
        paddingTop: 0,
      },
      default: {
        paddingTop: 10,
      },
    }),
  },
  view: {
    width: '100%',
    backgroundColor: 'red',
  },
  subView: {
    width: '100%',
    backgroundColor: 'green',
    height: PixelRatio.roundToNearestPixel(32.1),
  },
})

RN 动画

Animated.View

import React, { useRef } from 'react'
import { StyleSheet, View, Button, Animated } from 'react-native'

export default () => {
  // Animated.Value
  const marginLeft = useRef(new Animated.Value(0)).current

  return (
    <View style={styles.root}>
      <Button
        title='按钮'
        onPress={() => {
          Animated.timing(marginLeft, {
            toValue: 300,
            duration: 500,
            useNativeDriver: false,
          }).start()
        }}
      />

      <Animated.View style={[styles.view, { marginLeft: marginLeft }]} />
    </View>
  )
}
//  支持动画组件
// Animated: View, Text, Image, ScrollView, FlatList, SectionList
const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
  },
  view: {
    width: 100,
    height: 100,
    backgroundColor: '#3050ff',
    marginTop: 20,
  },
})

动画四大类型

  • 平移,旋转,缩放,透明度变化(渐变)
import React, { useRef } from 'react'
import { StyleSheet, View, Button, Animated } from 'react-native'

export default () => {
  // const marginLeft = useRef(new Animated.Value(0)).current;
  // const rotate = useRef(new Animated.Value(0)).current;
  // const scale = useRef(new Animated.Value(1)).current;
  const opacity = useRef(new Animated.Value(0)).current

  // const rotateValue = rotate.interpolate({
  //     inputRange: [0, 45],
  //     outputRange: ['0deg', '45deg'],
  // })
  return (
    <View style={styles.root}>
      <Button
        title='按钮'
        onPress={() => {
          Animated.timing(opacity, {
            toValue: 1,
            duration: 1000,
            useNativeDriver: false,
          }).start()
        }}
      />

      <Animated.View
        style={[
          styles.view,
          // {marginLeft: marginLeft}
          {
            transform: [
              // { rotate: rotateValue },
              // { scale: scale }
            ],
          },
          { opacity: opacity },
        ]}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
  },
  view: {
    width: 100,
    height: 100,
    backgroundColor: '#3050ff',
    marginTop: 60,
    marginLeft: 60,
  },
})

平移多种属性支持

import React, { useRef } from 'react'
import { StyleSheet, View, Button, Animated } from 'react-native'

export default () => {
  const marginLeft = useRef(new Animated.Value(0)).current

  return (
    <View style={styles.root}>
      <Button
        title='按钮'
        onPress={() => {
          Animated.timing(marginLeft, {
            toValue: 300,
            duration: 500,
            useNativeDriver: false,
          }).start()
        }}
      />

      <Animated.View
        style={[
          styles.view,
          // {marginTop: marginLeft},
          // {transform: [{ translateX: marginLeft }]}
          {
            position: 'absolute',
            top: marginLeft,
            left: marginLeft,
          },
        ]}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
  },
  view: {
    width: 100,
    height: 100,
    backgroundColor: '#3050ff',
    marginTop: 20,
  },
})

三大动画函数

  • Animated.decay() : 以一个初始速度开始并且逐渐减慢停止。
import React, { useRef } from 'react'
import { StyleSheet, View, Button, Animated } from 'react-native'

export default () => {
  const marginLeft = useRef(new Animated.Value(0)).current

  return (
    <View style={styles.root}>
      <Button
        title='按钮'
        onPress={() => {
          // Animated.timing(marginLeft, {
          //     toValue: 300,
          //     duration: 500,
          //     useNativeDriver: false,
          // }).start();

          Animated.decay(marginLeft, {
            velocity: 1,
            deceleration: 0.99,
            useNativeDriver: false,
          }).start()
        }}
      />

      <Animated.View style={[styles.view, { marginLeft: marginLeft }]} />
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
  },
  view: {
    width: 100,
    height: 100,
    backgroundColor: '#3050ff',
    marginTop: 20,
  },
})
  • Animated.spring() : 产生一个基于 Rebound 和 Origami 实现的 Spring 动画。它会在 toValue 值更新的同时跟踪当前的速度状态,以确保动画连贯,默认的弹簧阻尼值为 4,可以通过设置 friction 属性来改变阻尼值,也可以通过设置 tension 属性来改变张力值。
import React, { useRef } from 'react'
import { StyleSheet, View, Button, Animated } from 'react-native'

export default () => {
  const marginLeft = useRef(new Animated.Value(0)).current

  return (
    <View style={styles.root}>
      <Button
        title='按钮'
        onPress={() => {
          Animated.spring(marginLeft, {
            toValue: 200,
            useNativeDriver: false,

            // 第一组配置
            bounciness: 25,
            speed: 10,

            // 第二组配置
            // tension: 40,
            // friction: 7,

            // 第三组配置
            // stiffness: 100,
            // damping: 10,
            // mass: 1,
          }).start()
        }}
      />

      <Animated.View style={[styles.view, { marginLeft: marginLeft }]} />
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
  },
  view: {
    width: 100,
    height: 100,
    backgroundColor: '#3050ff',
    marginTop: 20,
  },
})
  • Animated.timing() : 该动画会在给定的时间内逐步改变一个值。
import React, { useRef } from 'react'
import { StyleSheet, View, Button, Animated, Easing } from 'react-native'

export default () => {
  const marginLeft = useRef(new Animated.Value(0)).current

  return (
    <View style={styles.root}>
      <Button
        title='按钮'
        onPress={() => {
          Animated.timing(marginLeft, {
            toValue: 300,
            duration: 500,
            // easing: Easing.back(3),
            // easing: Easing.ease,
            // easing: Easing.bounce,
            // easing: Easing.elastic(3),

            // easing: Easing.linear,
            // easing: Easing.quad,
            // easing: Easing.cubic,

            // easing: Easing.bezier(0.7, 0.2, 0.42, 0.82),
            // easing: Easing.circle,
            // easing: Easing.sin,
            // easing: Easing.exp,

            // easing: Easing.in(Easing.bounce),
            // easing: Easing.out(Easing.exp),
            easing: Easing.inOut(Easing.elastic(3)),
            useNativeDriver: false,
          }).start()
        }}
      />

      <Animated.View style={[styles.view, { marginLeft: marginLeft }]} />
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
  },
  view: {
    width: 100,
    height: 100,
    backgroundColor: '#3050ff',
    marginTop: 20,
  },
})

矢量动画

  • Animated.ValueXY: 用于跟踪 2D 值的变化,除了可以像使用 Animated.Value 一样使用它,还可以直接通过设置 x 和 y 属性来进行操作。
import React, { useRef } from 'react'
import { StyleSheet, View, Button, Animated } from 'react-native'

export default () => {
  const vector = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current

  return (
    <View style={styles.root}>
      <Button
        title='按钮'
        onPress={() => {
          Animated.timing(vector, {
            toValue: { x: 300, y: 400 },
            duration: 500,
            useNativeDriver: false,
          }).start()
        }}
      />

      <Animated.View
        style={[styles.view, { marginLeft: vector.x, marginTop: vector.y }]}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
  },
  view: {
    width: 100,
    height: 100,
    backgroundColor: '#3050ff',
    marginTop: 20,
  },
})

组合动画

import React, { useRef } from 'react'
import { StyleSheet, View, Button, Animated } from 'react-native'

export default () => {
  const scale = useRef(new Animated.Value(1)).current
  const marginLeft = useRef(new Animated.Value(0)).current
  const marginTop = useRef(new Animated.Value(0)).current

  return (
    <View style={styles.root}>
      <Button
        title='按钮'
        onPress={() => {
          const moveX = Animated.timing(marginLeft, {
            toValue: 200,
            duration: 500,
            useNativeDriver: false,
          })
          const moveY = Animated.timing(marginTop, {
            toValue: 300,
            duration: 500,
            useNativeDriver: false,
          })
          const scaleAnim = Animated.timing(scale, {
            toValue: 1.5,
            duration: 500,
            useNativeDriver: false,
          })

          // Animated.parallel([moveX, moveY, scaleAnim]).start();
          // Animated.sequence([moveX, moveY, scaleAnim]).start();
          // Animated.stagger(1500, [moveX, moveY, scaleAnim]).start();
          Animated.sequence([
            moveX,
            Animated.delay(1000),
            moveY,
            Animated.delay(500),
            scaleAnim,
          ]).start()
        }}
      />

      <Animated.View
        style={[
          styles.view,
          {
            transform: [
              { scale: scale },
              { translateX: marginLeft },
              { translateY: marginTop },
            ],
          },
        ]}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
  },
  view: {
    width: 100,
    height: 100,
    backgroundColor: '#3050ff',
    marginTop: 20,
  },
})

跟随动画难题

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

const colors = ['red', 'green', 'blue', 'yellow', 'orange']

export default () => {
  // const [scrollY, setScrollY] = useState(0);
  const scrollY = useRef(new Animated.Value(0)).current

  const viewList = () => {
    const array = [
      1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
    ]
    return (
      <>
        {array.map((item, index) => (
          <View
            key={item}
            style={{
              width: 60,
              height: 100,
              backgroundColor: colors[index % 5],
            }}
          />
        ))}
      </>
    )
  }

  return (
    <View style={styles.root}>
      <View style={styles.leftLayout}>
        <Animated.View
          style={{
            width: 60,
            transform: [
              // {translateY: -scrollY}
              { translateY: Animated.multiply(-1, scrollY) },
            ],
          }}
        >
          {viewList()}
        </Animated.View>
      </View>

      <View style={styles.rightLayout}>
        <Animated.ScrollView
          showsVerticalScrollIndicator={false}
          // onScroll={(event) => {
          //     setScrollY(event.nativeEvent.contentOffset.y);
          // }}
          onScroll={Animated.event(
            [
              {
                nativeEvent: {
                  contentOffset: { y: scrollY },
                },
              },
            ],
            { useNativeDriver: true }
          )}
        >
          {viewList()}
        </Animated.ScrollView>
      </View>
    </View>
  )
}

// 注意: 要使用 const scrollY = useRef(new Animated.Value(0)).current;
// Animated.event 会桥接到原生的动画
const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    flexDirection: 'row',
    justifyContent: 'center',
  },
  leftLayout: {
    width: 60,
    backgroundColor: '#00FF0030',
    flexDirection: 'column',
  },
  rightLayout: {
    width: 60,
    height: '100%',
    backgroundColor: '#0000FF30',
    marginLeft: 100,
  },
})

优化 Modal 动画

import React, { useState, useRef } from 'react'
import {
  StyleSheet,
  View,
  Modal,
  Text,
  Button,
  SectionList,
  TouchableOpacity,
  Image,
  Animated,
  Dimensions,
} from 'react-native'
import icon_close_modal from '../assets/images/icon_close_modal.png'

import { SectionData } from '../constants/Data'

const { height: WINDOW_HEIGHT } = Dimensions.get('window')

export default () => {
  const [visible, setVisible] = useState(false)

  const marginTop = useRef(new Animated.Value(WINDOW_HEIGHT)).current

  const showModal = () => {
    setVisible(true)
    Animated.timing(marginTop, {
      toValue: 0,
      duration: 500,
      useNativeDriver: false,
    }).start()
  }

  const hideModal = () => {
    Animated.timing(marginTop, {
      toValue: WINDOW_HEIGHT,
      duration: 500,
      useNativeDriver: false,
    }).start(() => {
      // 回调函数
      setVisible(false)
    })
  }

  const renderItem = ({ item, index, section }) => {
    return <Text style={styles.txt}>{item}</Text>
  }

  const ListHeader = (
    <View style={styles.header}>
      <Text style={styles.extraTxt}>列表头部</Text>
      <TouchableOpacity style={styles.closeButton} onPress={() => hideModal()}>
        <Image style={styles.closeImg} source={icon_close_modal} />
      </TouchableOpacity>
    </View>
  )

  const ListFooter = (
    <View style={[styles.header, styles.footer]}>
      <Text style={styles.extraTxt}>列表尾部</Text>
    </View>
  )

  const renderSectionHeader = ({ section }) => {
    return <Text style={styles.sectionHeaderTxt}>{section.type}</Text>
  }

  return (
    <View style={styles.root}>
      <Button title='按钮' onPress={() => showModal()} />

      <Modal
        visible={visible}
        onRequestClose={() => hideModal()}
        transparent={true}
        statusBarTranslucent={true}
        animationType='fade'
      >
        <View style={styles.container}>
          <Animated.View
            style={[
              styles.contentView,
              {
                marginTop: marginTop,
              },
            ]}
          >
            <SectionList
              style={styles.sectionList}
              contentContainerStyle={styles.containerStyle}
              sections={SectionData}
              renderItem={renderItem}
              keyExtractor={(item, index) => `${item}-${index}`}
              showsVerticalScrollIndicator={false}
              ListHeaderComponent={ListHeader}
              ListFooterComponent={ListFooter}
              renderSectionHeader={renderSectionHeader}
              ItemSeparatorComponent={() => <View style={styles.separator} />}
              stickySectionHeadersEnabled={true}
            />
          </Animated.View>
        </View>
      </Modal>
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    paddingHorizontal: 16,
  },
  container: {
    width: '100%',
    height: '100%',
    backgroundColor: '#00000060',
  },
  contentView: {
    width: '100%',
    height: '100%',
    paddingTop: '30%',
  },
  sectionList: {
    width: '100%',
    height: '80%',
  },
  txt: {
    width: '100%',
    height: 56,
    fontSize: 20,
    color: '#333333',
    textAlignVertical: 'center',
    paddingLeft: 16,
  },
  containerStyle: {
    backgroundColor: '#F5F5F5',
  },
  header: {
    width: '100%',
    height: 48,
    backgroundColor: 'white',
    justifyContent: 'center',
    alignItems: 'center',
  },
  footer: {
    backgroundColor: '#ff000030',
  },
  extraTxt: {
    fontSize: 20,
    color: '#666666',
    textAlignVertical: 'center',
  },
  sectionHeaderTxt: {
    width: '100%',
    height: 36,
    backgroundColor: '#DDDDDD',
    textAlignVertical: 'center',
    paddingLeft: 16,
    fontSize: 20,
    color: '#333333',
    fontWeight: 'bold',
  },
  separator: {
    width: '100%',
    height: 2,
    backgroundColor: '#D0D0D0',
  },
  closeButton: {
    width: 24,
    height: 24,
    position: 'absolute',
    right: 16,
  },
  closeImg: {
    width: 24,
    height: 24,
  },
})

LayoutAnimation

// index.js 加入代码 安卓启动动画
if (Platform.OS === 'android') {
  if (UIManager.setLayoutAnimationEnabledExperimental) {
    console.log('enable ...')
    UIManager.setLayoutAnimationEnabledExperimental(true)
  }
}

// 动画文件
import React, { useState } from 'react'
import {
  StyleSheet,
  View,
  Button,
  LayoutAnimation,
  Image,
  Text,
} from 'react-native'
import icon_avatar from '../assets/images/default_avatar.png'

export default () => {
  const [showView, setShowView] = useState(false)

  const [showRight, setShowRight] = useState(false)

  return (
    <View style={styles.root}>
      <Button
        title='按钮'
        onPress={() => {
          // LayoutAnimation.configureNext(
          //     // LayoutAnimation.Presets.linear
          //     // LayoutAnimation.Presets.spring
          //     LayoutAnimation.Presets.easeInEaseOut,
          //     () => {
          //         console.log('动画结束');
          //     },
          //     () => {
          //         console.log('动画异常');
          //     }
          // );
          // setShowView(true);

          // LayoutAnimation.configureNext(
          //     LayoutAnimation.Presets.spring
          // );
          // setShowRight(true);

          // LayoutAnimation.linear();
          LayoutAnimation.spring()
          // LayoutAnimation.easeInEaseOut();
          setShowRight(true)
        }}
      />

      {/* {showView && <View style={styles.view} />} */}

      <View
        style={[
          styles.view,
          { flexDirection: showRight ? 'row-reverse' : 'row' },
        ]}
      >
        <Image style={styles.img} source={icon_avatar} />
        <Text style={styles.txt}>这是一行自我介绍的文本</Text>
      </View>
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
    justifyContent: 'center',
    alignItems: 'center',
  },
  view: {
    width: '100%',
    height: 100,
    backgroundColor: '#F0F0F0',
    marginTop: 20,
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
  },
  img: {
    width: 64,
    height: 64,
    borderRadius: 32,
  },
  txt: {
    fontSize: 20,
    color: '#303030',
    fontWeight: 'bold',
    marginHorizontal: 20,
  },
})

动画练习(动画菜单)

import React, { useRef, useState, useEffect } from 'react'
import {
  StyleSheet,
  View,
  Image,
  Text,
  Animated,
  Easing,
  TouchableOpacity,
} from 'react-native'

import icon_gift from '../assets/images/icon_gift.png'
import icon_mine from '../assets/images/icon_mine.png'
import icon_home from '../assets/images/icon_home.png'
import icon_show from '../assets/images/icon_show.png'

export default () => {
  const width1 = useRef(new Animated.Value(200)).current
  const width2 = useRef(new Animated.Value(64)).current
  const width3 = useRef(new Animated.Value(64)).current
  const width4 = useRef(new Animated.Value(64)).current

  const [index, setIndex] = useState(0)

  useEffect(() => {
    anim1(index === 0)
    anim2(index === 1)
    anim3(index === 2)
    anim4(index === 3)
  }, [index])

  const anim1 = (isOpen) => {
    Animated.timing(width1, {
      toValue: isOpen ? 200 : 64,
      duration: isOpen ? 500 : 300,
      easing: isOpen ? Easing.elastic(3) : Easing.ease,
      useNativeDriver: false,
    }).start()
  }

  const anim2 = (isOpen) => {
    Animated.timing(width2, {
      toValue: isOpen ? 200 : 64,
      duration: isOpen ? 500 : 300,
      easing: isOpen ? Easing.elastic(3) : Easing.ease,
      useNativeDriver: false,
    }).start()
  }

  const anim3 = (isOpen) => {
    Animated.timing(width3, {
      toValue: isOpen ? 200 : 64,
      duration: isOpen ? 500 : 300,
      easing: isOpen ? Easing.elastic(3) : Easing.ease,
      useNativeDriver: false,
    }).start()
  }

  const anim4 = (isOpen) => {
    Animated.timing(width4, {
      toValue: isOpen ? 200 : 64,
      duration: isOpen ? 500 : 300,
      easing: isOpen ? Easing.elastic(3) : Easing.ease,
      useNativeDriver: false,
    }).start()
  }

  return (
    <View style={styles.root}>
      <TouchableOpacity
        activeOpacity={0.8}
        onPress={() => {
          setIndex(0)
        }}
      >
        <Animated.View
          style={[
            styles.view,
            { width: width1, opacity: index === 0 ? 1 : 0.75 },
          ]}
        >
          <Image style={styles.img} source={icon_home} />
          <Text style={styles.txt} numberOfLines={1} ellipsizeMode='clip'>
            首页推荐
          </Text>
          <View style={styles.dot} />
        </Animated.View>
      </TouchableOpacity>

      <TouchableOpacity
        activeOpacity={0.8}
        onPress={() => {
          setIndex(1)
        }}
      >
        <Animated.View
          style={[
            styles.view,
            { width: width2, opacity: index === 1 ? 1 : 0.75 },
          ]}
        >
          <Image style={styles.img} source={icon_show} />
          <Text style={styles.txt} numberOfLines={1} ellipsizeMode='clip'>
            热门直播
          </Text>
          <View style={styles.dot} />
        </Animated.View>
      </TouchableOpacity>

      <TouchableOpacity
        activeOpacity={0.8}
        onPress={() => {
          setIndex(2)
        }}
      >
        <Animated.View
          style={[
            styles.view,
            { width: width3, opacity: index === 2 ? 1 : 0.75 },
          ]}
        >
          <Image style={styles.img} source={icon_gift} />
          <Text style={styles.txt} numberOfLines={1} ellipsizeMode='clip'>
            我的礼物
          </Text>
          <View style={styles.dot} />
        </Animated.View>
      </TouchableOpacity>

      <TouchableOpacity
        activeOpacity={0.8}
        onPress={() => {
          setIndex(3)
        }}
      >
        <Animated.View
          style={[
            styles.view,
            { width: width4, opacity: index === 3 ? 1 : 0.75 },
          ]}
        >
          <Image style={styles.img} source={icon_mine} />
          <Text style={styles.txt} numberOfLines={1} ellipsizeMode='clip'>
            个人信息
          </Text>
          <View style={styles.dot} />
        </Animated.View>
      </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
    flexDirection: 'column',
    justifyContent: 'center',
  },
  view: {
    height: 60,
    flexDirection: 'row',
    alignItems: 'center',
    marginVertical: 16,
    borderTopRightRadius: 28,
    borderBottomRightRadius: 28,
    backgroundColor: '#2030ff',
    paddingLeft: 16,
    overflow: 'hidden',
  },
  img: {
    width: 32,
    height: 32,
    tintColor: 'white',
  },
  txt: {
    fontSize: 18,
    color: '#ffffffD0',
    marginLeft: 16,
  },
  dot: {
    width: 10,
    height: 10,
    backgroundColor: '#20f020',
    marginLeft: 28,
    borderRadius: 5,
  },
})

Context 使用

  • React.createContext: 创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
import { createContext } from 'react'

export const ThemeContext = createContext<string>('dark')
import React, { useState } from 'react'
import { View, Button } from 'react-native'

import { ThemeContext } from './ThemeContext'

import PageView from './PageView'

export default () => {
  const [theme, setTheme] = useState('dark')

  return (
    <ThemeContext.Provider value={theme}>
      <Button
        title='切换主题'
        onPress={() => {
          setTheme((state) => {
            if (state === 'dark') {
              return 'light'
            } else {
              return 'dark'
            }
          })
        }}
      />
      <View style={{ width: '100%' }}>
        <PageView />
      </View>
    </ThemeContext.Provider>
  )
}
import React from 'react'
import { View } from 'react-native'

import Header from './Header'

export default () => {
  return (
    <View>
      <Header />
    </View>
  )
}
import React, { useContext } from 'react'
import { StyleSheet, View, Image, Text } from 'react-native'

import icon_avatar from '../assets/images/default_avatar.png'

import { ThemeContext } from './ThemeContext'

export default () => {
  // 获取主题
  const theme = useContext(ThemeContext)
  // 根据主题设置样式
  const styles = theme === 'dark' ? darkStyles : lightStyles
  return (
    <View style={styles.content}>
      <Image style={styles.img} source={icon_avatar} />
      <Text style={styles.txt}>个人信息介绍</Text>
      <View style={styles.infoLayout}>
        <Text style={styles.infoTxt}>
          各位产品经理大家好,我是个人开发者张三,我学习RN两年半了,我喜欢安卓、RN、Flutter。
        </Text>
      </View>
    </View>
  )
}

const darkStyles = StyleSheet.create({
  content: {
    width: '100%',
    height: '100%',
    backgroundColor: '#353535',
    flexDirection: 'column',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingTop: 64,
  },
  img: {
    width: 96,
    height: 96,
    borderRadius: 48,
    borderWidth: 4,
    borderColor: '#ffffffE0',
  },
  txt: {
    fontSize: 24,
    color: 'white',
    fontWeight: 'bold',
    marginTop: 32,
  },
  infoLayout: {
    width: '90%',
    padding: 16,
    backgroundColor: '#808080',
    borderRadius: 12,
    marginTop: 24,
  },
  infoTxt: {
    fontSize: 16,
    color: 'white',
  },
})

const lightStyles = StyleSheet.create({
  content: {
    width: '100%',
    height: '100%',
    backgroundColor: '#fafafa',
    flexDirection: 'column',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingTop: 64,
  },
  img: {
    width: 96,
    height: 96,
    borderRadius: 48,
    borderWidth: 4,
    borderColor: '#00000080',
  },
  txt: {
    fontSize: 24,
    color: '#333333',
    fontWeight: 'bold',
    marginTop: 32,
  },
  infoLayout: {
    width: '90%',
    padding: 16,
    backgroundColor: '#EAEAEA',
    borderRadius: 12,
    marginTop: 24,
  },
  infoTxt: {
    fontSize: 16,
    color: '#666666',
  },
})

Hoc 使用

  • Hoc: 高阶组件,是参数为组件,返回值为新组件的函数。
import React, { useEffect } from 'react'
import { TouchableOpacity, Image, StyleSheet } from 'react-native'

type IReactComponent =
  | React.ClassicComponentClass
  | React.ComponentClass
  | React.FunctionComponent
  | React.ForwardRefExoticComponent<any>

import icon_add from '../assets/images/icon_add.png'

export default <T extends IReactComponent>(OriginView: T, type: string): T => {
  const HOCView = (props: any) => {
    useEffect(() => {
      reportDeviceInfo()
    }, [])

    const reportDeviceInfo = () => {
      // 模拟上报
      const deviceInfo = {
        deviceId: 1,
        deviceName: '',
        modal: '',
        storage: 0,
        ip: '',
      }

      // reportDeviceInfo(deviceInfo);
    }

    return (
      <>
        <OriginView {...props} />
        <TouchableOpacity
          style={styles.addButton}
          onPress={() => {
            console.log(`onPress ...`)
          }}
        >
          <Image style={styles.addImg} source={icon_add} />
        </TouchableOpacity>
      </>
    )
  }

  return HOCView as T
}

const styles = StyleSheet.create({
  addButton: {
    position: 'absolute',
    bottom: 80,
    right: 28,
  },
  addImg: {
    width: 54,
    height: 54,
    resizeMode: 'contain',
  },
})
import React, { useEffect } from 'react'
import { StyleSheet, View, Image, Text } from 'react-native'

import icon_avatar from '../assets/images/default_avatar.png'
import withFloatButton from './withFloatButton'

export default withFloatButton(() => {
  const styles = darkStyles
  return (
    <View style={styles.content}>
      <Image style={styles.img} source={icon_avatar} />
      <Text style={styles.txt}>个人信息介绍</Text>
      <View style={styles.infoLayout}>
        <Text style={styles.infoTxt}>
          各位产品经理大家好,我是个人开发者张三,我学习RN两年半了,我喜欢安卓、RN、Flutter,Thank
          you!</Text>
      </View>
    </View>
  )
}, 'InfoView')

const darkStyles = StyleSheet.create({
  content: {
    width: '100%',
    height: '100%',
    backgroundColor: '#353535',
    flexDirection: 'column',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingTop: 64,
  },
  img: {
    width: 96,
    height: 96,
    borderRadius: 48,
    borderWidth: 4,
    borderColor: '#ffffffE0',
  },
  txt: {
    fontSize: 24,
    color: 'white',
    fontWeight: 'bold',
    marginTop: 32,
  },
  infoLayout: {
    width: '90%',
    padding: 16,
    backgroundColor: '#808080',
    borderRadius: 12,
    marginTop: 24,
  },
  infoTxt: {
    fontSize: 16,
    color: 'white',
  },
})

memo 与 性能优化

  • React.memo: 用于函数组件的性能优化,只有在组件的 props 发生改变时才会重新渲染组件,否则使用上一次的渲染结果。
  • useMemo: 用于函数组件的性能优化,依赖的值发生变化时才会重新计算值。
  • useCallback: 用于函数组件的性能优化,返回一个函数,依赖的值发生变化时才会重新计算值。
  • shouldComponentUpdate: 用于类组件的性能优化,返回 true 时才会重新渲染组件,否则使用上一次的渲染结果。
  • PureComponent: 用于类组件的性能优化,浅比较 propsstate,只有发生改变时才会重新渲染组件,否则使用上一次的渲染结果。
  • React.memoPureComponent 的区别:React.memo 只能用于函数组件,PureComponent 只能用于类组件,React.memoPureComponent 都是浅比较,React.memo 可以自定义比较函数。
  • React.memouseMemo 的区别:React.memo 用于组件,useMemo 用于值,React.memo 依赖的值发生变化时才会重新渲染组件,useMemo 依赖的值发生变化时才会重新计算值。
  • React.memouseCallback 的区别:React.memo 用于组件,useCallback 用于函数,React.memo 依赖的值发生变化时才会重新渲染组件,useCallback 依赖的值发生变化时才会重新计算值。
  • React.memoshouldComponentUpdate 的区别:React.memo 用于组件,shouldComponentUpdate 用于类组件,React.memo 依赖的值发生变化时才会重新渲染组件,shouldComponentUpdate 返回 true 时才会重新渲染组件。
import React, { useState } from 'react'
import { View, Button } from 'react-native'
import InfoView from './InfoView'
import InfoView2 from './InfoView2'
import ConsumeList from './ConsumeList'

import { avatarUri } from '../constants/Uri'

export default () => {
  const [info, setInfo] = useState<UserInfo>({
    avatar: '',
    name: '',
    desc: '',
  })

  return (
    <View style={{ width: '100%' }}>
      {/* <Button
                title='按钮'
                onPress={() => {
                    setInfo({
                        avatar: avatarUri,
                        name: '尼古拉斯·段坤',
                        desc: '各位产品经理大家好,我是个人开发者段坤,我学习RN两年半了,我喜欢安卓、RN、Flutter,Thank you!。'
                    });
                }}
            /> */}
      {/* <InfoView info={info} /> */}
      {/* <InfoView2 info={info} /> */}

      <ConsumeList />
    </View>
  )
}
import React from 'react'
import { StyleSheet, View, Image, Text } from 'react-native'

type Props = {
  info: UserInfo
}

// React.memo 用于函数组件的性能优化,只有在组件的 props 发生改变时才会重新渲染组件,否则使用上一次的渲染结果。
export default React.memo(
  (props: Props) => {
    const { info } = props

    console.log('render ...')
    return (
      <View style={darkStyles.content}>
        <Image style={darkStyles.img} source={{ uri: info.avatar }} />
        <Text style={darkStyles.txt}>{info.name}</Text>
        <View style={darkStyles.infoLayout}>
          <Text style={darkStyles.infoTxt}>{info.desc}</Text>
        </View>
      </View>
    )
  },
  (preProps: Props, nextProps: Props) =>
    JSON.stringify(preProps.info) === JSON.stringify(nextProps.info)
)

const darkStyles = StyleSheet.create({
  content: {
    width: '100%',
    height: '100%',
    backgroundColor: '#353535',
    flexDirection: 'column',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingTop: 64,
  },
  img: {
    width: 96,
    height: 96,
    borderRadius: 48,
    borderWidth: 4,
    borderColor: '#ffffffE0',
  },
  txt: {
    fontSize: 24,
    color: 'white',
    fontWeight: 'bold',
    marginTop: 32,
  },
  infoLayout: {
    width: '90%',
    padding: 16,
    backgroundColor: '#808080',
    borderRadius: 12,
    marginTop: 24,
  },
  infoTxt: {
    fontSize: 16,
    color: 'white',
  },
})
import React from 'react'
import { StyleSheet, View, Image, Text } from 'react-native'

type Props = {
  info: UserInfo
}

export default class InfoView2 extends React.Component<Props, any> {
  constructor(props: Props) {
    super(props)
  }

  // shouldComponentUpdate: 用于类组件的性能优化,返回 true 时才会重新渲染组件,否则使用上一次的渲染结果。
  shouldComponentUpdate(nextProps: Readonly<Props>): boolean {
    return JSON.stringify(nextProps.info) !== JSON.stringify(this.props.info)
  }

  render(): React.ReactNode {
    console.log('render ...')
    const { info } = this.props
    return (
      <View style={darkStyles.content}>
        <Image style={darkStyles.img} source={{ uri: info.avatar }} />
        <Text style={darkStyles.txt}>{info.name}</Text>
        <View style={darkStyles.infoLayout}>
          <Text style={darkStyles.infoTxt}>{info.desc}</Text>
        </View>
      </View>
    )
  }
}

const darkStyles = StyleSheet.create({
  content: {
    width: '100%',
    height: '100%',
    backgroundColor: '#353535',
    flexDirection: 'column',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingTop: 64,
  },
  img: {
    width: 96,
    height: 96,
    borderRadius: 48,
    borderWidth: 4,
    borderColor: '#ffffffE0',
  },
  txt: {
    fontSize: 24,
    color: 'white',
    fontWeight: 'bold',
    marginTop: 32,
  },
  infoLayout: {
    width: '90%',
    padding: 16,
    backgroundColor: '#808080',
    borderRadius: 12,
    marginTop: 24,
  },
  infoTxt: {
    fontSize: 16,
    color: 'white',
  },
})
import React, { useState, useMemo, useCallback } from 'react'
import {
  View,
  Button,
  StyleSheet,
  FlatList,
  Switch,
  Text,
  TouchableOpacity,
} from 'react-native'

import { ListData, ListData2 } from '../constants/Data'
import { TypeColors } from '../constants/Data'

export default () => {
  const [data, setData] = useState<any>(ListData)
  const [showType, setShowType] = useState<boolean>(true)

  // const calculateTotal = useMemo(() => {
  //     console.log('重新计算合计');
  //     return data.map((item: any) => item.amount)
  //         .reduce((pre: number, cur: number) => pre + cur);
  // }, [data])

  const totalAmountView = useMemo(() => {
    const total = data
      .map((item: any) => item.amount)
      .reduce((pre: number, cur: number) => pre + cur)
    console.log('重新渲染合计')
    return (
      <View style={styles.totalLayout}>
        <Text style={styles.totalTxt}>{total}</Text>
        <Text style={styles.totalTxt}>合计:</Text>
      </View>
    )
  }, [data])

  const onItemPress = useCallback(
    (item: any, index: number) => () => {
      console.log(`点击第${item.index}`)
    },
    []
  )

  const renderItem = ({ item, index }: any) => {
    const styles = StyleSheet.create({
      itemLayout: {
        width: '100%',
        padding: 16,
        flexDirection: 'column',
        borderBottomWidth: 1,
        borderBottomColor: '#E0E0E0',
      },
      labelRow: {
        width: '100%',
        flexDirection: 'row',
        alignItems: 'center',
      },
      valueRow: {
        marginTop: 10,
      },
      labelTxt: {
        flex: 1,
        fontSize: 14,
        color: '#666',
      },
      first: {
        flex: 0.4,
      },
      second: {
        flex: 0.3,
      },
      last: {
        flex: 0.6,
      },
      valueTxt: {
        flex: 1,
        fontSize: 18,
        color: '#333',
        fontWeight: 'bold',
      },
      typeLayout: {
        flex: 0.3,
      },
      typeTxt: {
        width: 20,
        height: 20,
        textAlign: 'center',
        textAlignVertical: 'center',
        color: 'white',
        borderRadius: 4,
        fontWeight: 'bold',
      },
    })
    return (
      <TouchableOpacity
        style={styles.itemLayout}
        onPress={onItemPress(item, index)}
      >
        <View style={styles.labelRow}>
          <Text style={[styles.labelTxt, styles.first]}>序号</Text>
          {showType && (
            <Text style={[styles.labelTxt, styles.second]}>类型</Text>
          )}
          <Text style={styles.labelTxt}>消费名称</Text>
          <Text style={[styles.labelTxt, styles.last]}>消费金额</Text>
        </View>
        <View style={[styles.labelRow, styles.valueRow]}>
          <Text style={[styles.valueTxt, styles.first]}>{item.index}</Text>
          {showType && (
            <View style={styles.typeLayout}>
              <Text
                style={[
                  styles.typeTxt,
                  { backgroundColor: TypeColors[item.type] },
                ]}
              >
                {item.type}
              </Text>
            </View>
          )}
          <Text style={styles.valueTxt}>{item.name}</Text>
          <Text style={[styles.valueTxt, styles.last]}>{item.amount}</Text>
        </View>
      </TouchableOpacity>
    )
  }

  return (
    <View style={styles.root}>
      <View style={styles.titleLayout}>
        <Text style={styles.titleTxt}>消费记账单</Text>
        <Switch
          style={styles.switch}
          value={showType}
          onValueChange={(value) => setShowType(value)}
        />
        <Button
          title='切换数据'
          onPress={() => {
            setData(ListData2)
          }}
        />
      </View>
      <FlatList
        data={data}
        keyExtractor={(item, index) => `${item.index}-${item.name}`}
        renderItem={renderItem}
      />
      {/* <View style={styles.totalLayout}>
                <Text style={styles.totalTxt}>{calculateTotal}</Text>
                <Text style={styles.totalTxt}>合计:</Text>
            </View> */}
      {totalAmountView}
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
  },
  titleLayout: {
    width: '100%',
    height: 56,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
  },
  titleTxt: {
    fontSize: 18,
    color: '#333',
    fontWeight: 'bold',
  },
  totalLayout: {
    width: '100%',
    height: 60,
    flexDirection: 'row-reverse',
    borderTopWidth: 1,
    borderTopColor: '#c0c0c0',
    alignItems: 'center',
    paddingHorizontal: 16,
  },
  totalTxt: {
    fontSize: 20,
    color: '#333',
    fontWeight: 'bold',
  },
  switch: {
    position: 'absolute',
    right: 16,
  },
})
type UserInfo = {
  avatar: string
  name: string
  desc: string
}

Ref 转发

  • Ref: 用于函数组件中获取组件实例,使用 ForwardRef 可以将 Ref 传递给子组件
  • Ref: 用户类组件中获取组件实例
import React, { useRef } from 'react'
import { StyleSheet, View, Button, TextInput } from 'react-native'

// import CustomInput, { CustomInputRef } from './CustomInput';
import CustomInput2 from './CustomInput2'

export default () => {
  const inputRef = useRef<CustomInput2>(null)

  return (
    <View style={styles.root}>
      <Button
        title='聚焦'
        onPress={() => {
          inputRef.current?.customFocus()
        }}
      />
      <Button
        title='失焦'
        onPress={() => {
          inputRef.current?.customBlur()
          inputRef.current?.customXXX()
        }}
      />
      {/* <CustomInput ref={inputRef} /> */}

      <CustomInput2 ref={inputRef} />
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
    paddingHorizontal: 20,
    paddingTop: 64,
  },
})
import React, { useState, useRef, forwardRef, useImperativeHandle } from 'react'
import {
  StyleSheet,
  View,
  Image,
  Text,
  TextInput,
  LayoutAnimation,
  TouchableOpacity,
} from 'react-native'

import icon_error from '../assets/images/icon_error.png'
import icon_right from '../assets/images/icon_right.png'
import icon_question from '../assets/images/icon_question.webp'
import icon_delete from '../assets/images/icon_delete.png'

export interface CustomInputRef {
  customFocus: () => void
  customBlur: () => void
}

export default forwardRef((props, ref) => {
  const inputRef = useRef<TextInput>(null)

  const [value, setValue] = useState<string>('')

  const customFocus = () => {
    inputRef.current?.focus()
  }

  const customBlur = () => {
    inputRef.current?.blur()
  }
  // 暴露给父组件的方法
  useImperativeHandle(ref, () => {
    return {
      customFocus,
      customBlur,
    }
  })

  return (
    <View style={styles.root}>
      <View
        style={[
          styles.inputWrap,
          {
            borderColor: !value
              ? '#888'
              : value?.length === 11
              ? '#00CD00'
              : '#ff3050',
          },
        ]}
      >
        <TextInput
          ref={inputRef}
          style={styles.input}
          value={value}
          keyboardType='number-pad'
          onChangeText={(value) => {
            LayoutAnimation.spring()
            setValue(value)
          }}
          maxLength={11}
        />

        {!!value && (
          <TouchableOpacity
            style={styles.deleteButton}
            onPress={() => {
              LayoutAnimation.spring()
              setValue('')
            }}
          >
            <Image style={styles.deleteImg} source={icon_delete} />
          </TouchableOpacity>
        )}
      </View>
      <View style={styles.tipsLayout}>
        {!value ? (
          <>
            <Image style={styles.tipImg} source={icon_question} />
            <Text style={styles.tipsTxt}>请输入您的手机号</Text>
          </>
        ) : value.length === 11 ? (
          <>
            <Image style={styles.tipImgRight} source={icon_right} />
            <Text style={styles.tipsTxtRight}>输入正确,可进行提交</Text>
          </>
        ) : (
          <>
            <Image style={styles.tipImgError} source={icon_error} />
            <Text style={styles.tipsTxtError}>格式错误,请输入正确手机号</Text>
          </>
        )}
      </View>
    </View>
  )
})

const styles = StyleSheet.create({
  root: {
    width: '100%',
    flexDirection: 'column',
  },
  input: {
    width: '100%',
    height: 56,
    backgroundColor: 'transparent',
    paddingHorizontal: 16,
    fontSize: 22,
    color: '#333',
  },
  inputWrap: {
    width: '100%',
    borderWidth: 2,
    borderRadius: 12,
    flexDirection: 'row',
    alignItems: 'center',
  },
  tipsLayout: {
    flexDirection: 'row',
    alignItems: 'center',
    marginTop: 6,
    paddingHorizontal: 6,
  },
  tipImg: {
    width: 22,
    height: 22,
    resizeMode: 'contain',
    tintColor: '#888',
  },
  tipsTxt: {
    fontSize: 15,
    color: '#666',
    marginLeft: 6,
    fontWeight: 'bold',
  },
  tipImgRight: {
    width: 18,
    height: 18,
    resizeMode: 'contain',
    tintColor: '#00CD00',
  },
  tipsTxtRight: {
    fontSize: 15,
    color: '#00CD00',
    marginLeft: 6,
    fontWeight: 'bold',
  },
  tipImgError: {
    width: 18,
    height: 18,
    resizeMode: 'contain',
    tintColor: '#ff3050',
  },
  tipsTxtError: {
    fontSize: 15,
    color: '#ff3050',
    marginLeft: 6,
    fontWeight: 'bold',
  },
  deleteButton: {
    position: 'absolute',
    right: 16,
  },
  deleteImg: {
    width: 24,
    height: 24,
    resizeMode: 'contain',
    borderRadius: 12,
  },
})
import React from 'react'
import {
  StyleSheet,
  View,
  Image,
  Text,
  TextInput,
  LayoutAnimation,
  TouchableOpacity,
} from 'react-native'

import icon_error from '../assets/images/icon_error.png'
import icon_right from '../assets/images/icon_right.png'
import icon_question from '../assets/images/icon_question.webp'
import icon_delete from '../assets/images/icon_delete.png'

export default class CustomInput extends React.Component {
  inputRef = React.createRef<TextInput>()

  state = {
    value: '',
  }

  constructor(props: any) {
    super(props)
    this.state = {
      value: '',
    }
  }

  customFocus = () => {
    this.inputRef.current?.focus()
  }

  customBlur = () => {
    this.inputRef.current?.blur()
  }

  customXXX = () => {
    console.log('customXXX ...')
  }

  render() {
    const { value } = this.state
    return (
      <View style={styles.root}>
        <View
          style={[
            styles.inputWrap,
            {
              borderColor: !value
                ? '#888'
                : value?.length === 11
                ? '#00CD00'
                : '#ff3050',
            },
          ]}
        >
          <TextInput
            ref={this.inputRef}
            style={styles.input}
            value={value}
            keyboardType='number-pad'
            onChangeText={(value) => {
              LayoutAnimation.spring()
              this.setState({
                value,
              })
            }}
            maxLength={11}
          />

          {!!value && (
            <TouchableOpacity
              style={styles.deleteButton}
              onPress={() => {
                LayoutAnimation.spring()
                this.setState({
                  value: '',
                })
              }}
            >
              <Image style={styles.deleteImg} source={icon_delete} />
            </TouchableOpacity>
          )}
        </View>
        <View style={styles.tipsLayout}>
          {!value ? (
            <>
              <Image style={styles.tipImg} source={icon_question} />
              <Text style={styles.tipsTxt}>请输入您的手机号</Text>
            </>
          ) : value.length === 11 ? (
            <>
              <Image style={styles.tipImgRight} source={icon_right} />
              <Text style={styles.tipsTxtRight}>输入正确,可进行提交</Text>
            </>
          ) : (
            <>
              <Image style={styles.tipImgError} source={icon_error} />
              <Text style={styles.tipsTxtError}>
                格式错误,请输入正确手机号
              </Text>
            </>
          )}
        </View>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    flexDirection: 'column',
  },
  input: {
    width: '100%',
    height: 56,
    backgroundColor: 'transparent',
    paddingHorizontal: 16,
    fontSize: 22,
    color: '#333',
  },
  inputWrap: {
    width: '100%',
    borderWidth: 2,
    borderRadius: 12,
    flexDirection: 'row',
    alignItems: 'center',
  },
  tipsLayout: {
    flexDirection: 'row',
    alignItems: 'center',
    marginTop: 6,
    paddingHorizontal: 6,
  },
  tipImg: {
    width: 22,
    height: 22,
    resizeMode: 'contain',
    tintColor: '#888',
  },
  tipsTxt: {
    fontSize: 15,
    color: '#666',
    marginLeft: 6,
    fontWeight: 'bold',
  },
  tipImgRight: {
    width: 18,
    height: 18,
    resizeMode: 'contain',
    tintColor: '#00CD00',
  },
  tipsTxtRight: {
    fontSize: 15,
    color: '#00CD00',
    marginLeft: 6,
    fontWeight: 'bold',
  },
  tipImgError: {
    width: 18,
    height: 18,
    resizeMode: 'contain',
    tintColor: '#ff3050',
  },
  tipsTxtError: {
    fontSize: 15,
    color: '#ff3050',
    marginLeft: 6,
    fontWeight: 'bold',
  },
  deleteButton: {
    position: 'absolute',
    right: 16,
  },
  deleteImg: {
    width: 24,
    height: 24,
    resizeMode: 'contain',
    borderRadius: 12,
  },
})

桥接原生

实现 JS 调用原生方法

  • NativeModules 获取 原生模块
import React from 'react'
import {
  StyleSheet,
  View,
  Button,
  NativeModules,
  Image,
  Text,
} from 'react-native'

import NativeInfoView from './NativeInfoView'
import NativeInfoViewGroup from './NativeInfoViewGroup'

import { avatarUri } from '../constants/Uri'

export default () => {
  return (
    <View style={styles.root}>
      <Button
        title='调用原生方法'
        onPress={() => {
          const { App } = NativeModules
          // 打开原生相册
          // App?.openGallery();

          // 获取原生版本号(异步)
          // App?.getVersionName().then((data: string) => {
          //     console.log(`versionName=${data}`);
          // })

          // 桥接原生实现JS层获取原生常量 获取原生版本号
          const { versionName, versionCode } = App
          console.log(`versionName=${versionName}, versionCode=${versionCode}`)
        }}
      />

      {/* <NativeInfoView /> */}
      <NativeInfoViewGroup>
        <View style={styles.content}>
          <Image style={styles.avatarImg} source={{ uri: avatarUri }} />
          <View style={styles.nameLayout}>
            <Text style={styles.nameTxt}>尼古拉斯·段坤</Text>
            <Text style={styles.descTxt}>
              各位产品经理大家好,我是个人开发者张三,我学习RN两年半了,我喜欢安卓、RN、Flutter,Thank
              you!。
            </Text>
          </View>
        </View>
      </NativeInfoViewGroup>
    </View>
  )
}

const styles = StyleSheet.create({
  root: {
    width: '100%',
    height: '100%',
    backgroundColor: 'white',
  },
  content: {
    width: '100%',
    height: 120,
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingTop: 10,
    backgroundColor: 'white',
  },
  avatarImg: {
    width: 100,
    height: 100,
    resizeMode: 'contain',
    borderRadius: 50,
  },
  nameLayout: {
    flex: 1,
    flexDirection: 'column',
    marginLeft: 16,
  },
  nameTxt: {
    fontSize: 20,
    color: '#333',
    fontWeight: 'bold',
    marginTop: 4,
  },
  descTxt: {
    fontSize: 16,
    color: '#666',
    marginTop: 4,
  },
})

原子组件实现原生组件

  • requireNativeComponent
  • 与 原生通信传值 和 调用原生方法
  • 继承是SimpleViewManager
import React, { useRef, useEffect } from 'react'
import {
  StyleSheet,
  View,
  requireNativeComponent,
  ViewProps,
  findNodeHandle,
  UIManager,
} from 'react-native'

import { avatarUri } from '../constants/Uri'

type NativeInfoViewType =
  | ViewProps
  | {
      // 这部分是自定义的属性
      avatar: string
      name: string
      desc: string
      onShapeChange: (e: any) => void
    }

const NativeInfoView =
  requireNativeComponent<NativeInfoViewType>('NativeInfoView')

export default () => {
  const ref = useRef(null)

  useEffect(() => {
    setTimeout(() => {
      // 原生模块 方法
      sendCommand('setShape', ['round'])
    }, 3000)
  }, [])

  const sendCommand = (command: string, params: any[]) => {
    const viewId = findNodeHandle(ref.current)
    // @ts-ignore
    const commands = UIManager.NativeInfoView.Commands[command].toString()
    UIManager.dispatchViewManagerCommand(viewId, commands, params)
  }

  return (
    <NativeInfoView
      ref={ref}
      style={styles.infoView}
      avatar={avatarUri}
      name='尼古拉斯·段坤'
      desc='各位产品经理大家好,我是个人开发者张三,我学习RN两年半了,我喜欢安卓、RN、Flutter,Thank you!。'
      onShapeChange={(e: any) => {
        console.log(e.nativeEvent.shape)
      }}
    />
  )
}

const styles = StyleSheet.create({
  infoView: {
    width: '100%',
    height: '100%',
  },
})

桥接原生容器组件

  • 原生继承是ViewGroupManager
import React from 'react'
import { StyleSheet, requireNativeComponent, ViewProps } from 'react-native'

type NativeInfoViewGroupType =
  | ViewProps
  | {
      // 这部分是自定义的属性
    }

const NativeInfoViewGroup = requireNativeComponent<NativeInfoViewGroupType>(
  'NativeInfoViewGroup'
)

export default (props: any) => {
  const { children } = props

  return (
    <NativeInfoViewGroup style={styles.infoView}>
      {children}
    </NativeInfoViewGroup>
  )
}

const styles = StyleSheet.create({
  infoView: {
    width: '100%',
    flexDirection: 'row',
  },
})

React-Navigation 使用

  • 安装
# 安装依赖
npm install @react-navigation/native
npm install @react-navigation/stack
npm install @react-navigation/bottom-tabs

npm install react-native-gesture-handler
npm install react-native-screens
npm install react-native-safe-area-context

import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'

export default () => {
  const Stack = createStackNavigator()
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name='Home' component={HomeScreen} />
        <Stack.Screen name='Details' component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  )
}
  • @react-navigation/bottom-tabs
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'

const Tab = createBottomTabNavigator()

export default () => {
  return (
    <Tab.Navigator>
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
    </Tab.Navigator>
  )
}
// MainTab.tsx
import { StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'
import React from 'react'

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
// 手机相册
import { launchImageLibrary } from 'react-native-image-picker'

import Home from '../home/Home'
import Shop from '../shop/Shop'
import Mine from '../mine/Mine'
import Message from '../message/Message'

// import icon_tab_home_normal from '../../assets/icon_tab_home_normal.png';
// import icon_tab_home_selected from '../../assets/icon_tab_home_selected.png';

// import icon_tab_shop_normal from '../../assets/icon_tab_shop_normal.png';
// import icon_tab_shop_selected from '../../assets/icon_tab_shop_selected.png';

// import icon_tab_message_normal from '../../assets/icon_tab_message_normal.png';
// import icon_tab_message_selected from '../../assets/icon_tab_message_selected.png';

// import icon_tab_mine_normal from '../../assets/icon_tab_mine_normal.png';
// import icon_tab_mine_selected from '../../assets/icon_tab_mine_selected.png';

import icon_tab_publish from '../../assets/icon_tab_publish.png'

const BottomTab = createBottomTabNavigator()
export default () => {
  // 自定义 TabBar
  // eslint-disable-next-line react/no-unstable-nested-components
  const RedBookTabBar = ({ state, descriptors, navigation }: any) => {
    const { routes, index } = state
    return (
      <View style={styles.tabBarContainer}>
        {routes.map((route: any, i: number) => {
          const { options } = descriptors[route.key]
          // 标题
          const label = options.title
          // 是否选中
          const isFoucsed = index === i

          // 发布按钮
          if (i === 2) {
            return (
              <TouchableOpacity
                key={label}
                style={styles.tabItem}
                onPress={() => {
                  // TODO: 选择手机相册
                  launchImageLibrary(
                    {
                      mediaType: 'photo',
                      includeBase64: false,
                      maxHeight: 300,
                      maxWidth: 300,
                    },
                    (response) => {
                      // console.log('response', response);
                      const { assets } = response
                      if (!assets || assets.length === 0) {
                        return
                      }
                      const { uri, width, height, fileName, fileSize, type } =
                        assets[0]

                      console.log('uri', uri)
                      console.log('width', width)
                      console.log('height', height)
                      console.log('fileName', fileName)
                      console.log('fileSize', fileSize)
                      console.log('type', type)
                    }
                  )
                }}
              >
                <Image
                  style={styles.icon_tab_publish}
                  source={icon_tab_publish}
                />
              </TouchableOpacity>
            )
          }
          return (
            <TouchableOpacity
              key={label}
              style={styles.tabItem}
              onPress={() => {
                const event = navigation.emit({
                  type: 'tabPress',
                  target: route.key,
                })
                if (!isFoucsed && !event.defaultPrevented) {
                  navigation.navigate(route.name)
                }
              }}
            >
              <Text
                style={
                  isFoucsed
                    ? styles.tabItemTextSelected
                    : styles.tabItemTextNormal
                }
              >
                {label}
              </Text>
            </TouchableOpacity>
          )
        })}
      </View>
    )
  }

  return (
    <View style={styles.root}>
      <BottomTab.Navigator
        // screenOptions={({ route }) => ({
        //   // eslint-disable-next-line react/no-unstable-nested-components
        //   tabBarIcon: ({ focused, color, size }) => {
        //     let img = null;
        //     if (route.name === 'Home') {
        //       img = focused ? icon_tab_home_selected : icon_tab_home_normal;
        //     } else if (route.name === 'Shop') {
        //       img = focused ? icon_tab_shop_selected : icon_tab_shop_normal;
        //     } else if (route.name === 'Message') {
        //       img = focused
        //         ? icon_tab_message_selected
        //         : icon_tab_message_normal;
        //     } else if (route.name === 'Mine') {
        //       img = focused ? icon_tab_mine_selected : icon_tab_mine_normal;
        //     }

        //     return (
        //       <Image
        //         source={img}
        //         style={{ width: size, height: size, tintColor: color }}
        //       />
        //     );
        //   },
        //   tabBarActiveTintColor: '#ff2442',
        //   tabBarInactiveTintColor: '#999999',
        // })}

        // eslint-disable-next-line react/no-unstable-nested-components
        tabBar={(props) => <RedBookTabBar {...props} />}
      >
        <BottomTab.Screen
          name='Home'
          component={Home}
          options={{
            title: '首页',
            headerShown: false,
          }}
        />
        <BottomTab.Screen
          name='Shop'
          component={Shop}
          options={{
            title: '购物',
            headerShown: false,
          }}
        />
        <BottomTab.Screen
          name='Publish'
          component={Shop}
          options={{
            title: '发布',
            headerShown: false,
          }}
        />
        <BottomTab.Screen
          name='Message'
          component={Message}
          options={{
            title: '消息',
            headerShown: false,
          }}
        />
        <BottomTab.Screen
          name='Mine'
          component={Mine}
          options={{
            title: '我',
            headerShown: false,
          }}
        />
      </BottomTab.Navigator>
    </View>
  )
}
const styles = StyleSheet.create({
  root: {
    height: '100%',
    width: '100%',
    background: '#f5f5f5',
  },
  tabBarContainer: {
    width: '100%',
    height: 52,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
  },
  tabItem: {
    flex: 1,
    height: '100%',
    justifyContent: 'center',
    alignItems: 'center',
  },
  tabItemTextNormal: {
    fontSize: 16,
    color: '#999999',
  },
  tabItemTextSelected: {
    fontSize: 18,
    color: '#333',
    fontWeight: 'bold',
  },
  icon_tab_publish: {
    width: 52,
    height: 52,
    resizeMode: 'contain',
  },
})

参考资料