react-native 项目搭建

需要实现的功能
1、视频播放;
2、音乐播放;
3、蓝牙链接;
4、沉浸式和安全区
5、轮播图
6、路由系统
7、iconfont
8、持久储存
9、可以使用的css

视频播放器的使用

使用的视频播放器组件为 react-native-video
文档地址

先说结论:

  • 一定要使用 ^6.0.0-alpha.1 版本,运行npm i react-native-video@6.0.0-alpha.1 下载;
  • 下载之后,要运行一次 npx react-native start --reset-cache,然后 再次重启下项目;
  • 关联版本 为 RN :0.69.5 ; react:18.0.0
  • 如果不行,按照以下增加配置
 // android/settings.gradle 
 include ':react-native-video'
 project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
   //  android/app/build.gradle 
   dependencies {
       ...
       implementation project(':react-native-video')
   }
   // 注意这里不是按照官网配置的
//  android/app/src/main/java/com/文件名/MainApplication.java

// 在头部 添加 import com.brentvatne.react.ReactVideoPackage;
// getPackages()中添加packages.add(new ReactVideoPackage());
+   import com.brentvatne.react.ReactVideoPackage;
   ...
   @Override
   protected List<ReactPackage> getPackages() {
     @SuppressWarnings("UnnecessaryLocalVariable")
     List<ReactPackage> packages = new PackageList(this).getPackages();
     // Packages that cannot be autolinked yet can be added manually here, for example:
     // packages.add(new MyReactNativePackage());
+     packages.add(new ReactVideoPackage());
     return packages;
   }

我安装时碰到的问题

开始是按照官网指南,直接使用 npm install --save react-native-video 命令下载组件。然后运行之后报错;
详细看了 Android installation 安装文档,在android项目中,添加了代码,

现在是 2022-9-8; 默认版本是 5.2

// android/app/build.gradle
    compile project(':react-native-video')
+   implementation "androidx.appcompat:appcompat:1.0.0"
-   implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"

然后就 报了 compile() 函数的问题

could not find method compile()for arguments

无奈删掉这些,删掉node_moduies,然后再npm install;
然后起来了,报错

ERROR TypeError: undefined is not an object (evalating '_reactNative.Image.propTypes.resizeMode') 

然后搜索和 查看issues,找到了issues/2714 ,然后还是报错, 就继续向下看,最终找到 npx react-native start --reset-cache; 之后再次重启,成功看到视频,
期间还有 react-native-video 的类型找不到,执行 npm i --save @types/react-native-video之后就好了。

注意, 视频 source 一定要填写,否者会app崩溃

使用默认版本(5.2),还有一个问题是:

Could not find com.yqritc:android-scalablevideoview:1.0.4.

这里其实是因为jCenter不允许更新包,所有其他包应该从mavenCentral获取。因此还需要配置
android/build.gradle文件,allprojects下添加如下配置:
测试也是有用的,但是升级 6.x之后就不用配置了;

allprojects {
    repositories {
        .... # rest of your code
        jcenter() {
            content {
                includeModule("com.yqritc", "android-scalablevideoview")
            }
        }
    }
}

蓝牙链接

使用 react-native-ble-manager 组件;

沉浸式和安全区

沉浸式的操作

使用<statusBar /> 组件来实现;

  1. statusBar 组件,会被后一次的设置覆盖;
  2. ios默认是沉浸式的,所以要给根组件设置一个默认的paddingTop
  3. android默认不是沉浸式的,需要设置以下代码变成沉浸式,然后根组件设置一个默认的paddingTop
 <StatusBar translucent={true} backgroundColor="transparent" />

设置的paddingTop值是statusBar的高度,获取方法如下:

import { Platform, NativeModules, StatusBar } from 'react-native';
// 系统信息
const OS = Platform.OS;
// 状态栏高度
const { StatusBarManager } = NativeModules;
let statusBarHeight = 0;
if (OS === 'ios') {
    StatusBarManager.getHeight((_statusBarHeight: number) => {
        statusBarHeight = _statusBarHeight;
    });
} else if (OS === 'android') {
    statusBarHeight = StatusBar.currentHeight || 0;
}

export { statusBarHeight, OS };

IOS安全区

安全区的是实现是通过 SafeAreaView 组件包裹;
SafeAreaView的目的是在一个“安全”的可视区域内渲染内容。具体来说就是因为目前有 iPhone X 这样的带有“刘海”的全面屏设备,所以需要避免内容渲染到不可见的“刘海”范围内。本组件目前仅支持 iOS 设备以及 iOS 11 或更高版本。
SafeAreaView会自动根据系统的各种导航栏、工具栏等预留出空间来渲染内部内容。更重要的是,它还会考虑到设备屏幕的局限,比如屏幕四周的圆角或是顶部中间不可显示的“刘海”区域。
只需简单地把你原有的视图用SafeAreaView包起来,同时设置一个flex: 1的样式。当然可能还需要一些和你的设计相匹配的背景色。

import React from 'react';
import { StyleSheet, Text, SafeAreaView } from 'react-native';

const App = () => {
  return (
    <SafeAreaView style={styles.container}>
      <Text>Page content</Text>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

export default App;

可以用的属性

文档地址
图片样式属性
布局属性
阴影样式属性
Text 样式属性
View 样式属性

轮播图组件 react-native-swiper

在首页,需要用轮播图做视频背景的切换,
使用 react-native-swiper 组件,文档地址;

  • style={styles.swiper} // 样式
  • height={200} // 组件高度
  • loop={true} // 如果设置为false,那么滑动到最后一张时,再次滑动将不会滑到第一张图片。
  • autoplay={true} // 自动轮播
  • autoplayTimeout={4} // 每隔4秒切换
  • horizontal={true} // 水平方向,为false可设置为竖直方向
  • paginationStyle={{bottom: 10}} // 小圆点的位置:距离底部10px
  • showsButtons={false} // 为false时不显示控制按钮
  • showsPagination={false} // 为false不显示下方圆点
  • dot={<View />} // 指示点的样式
  • activeDot={<View />} // 活跃的指示点的样式

import React from 'react'
import { Text, View } from 'react-native'
import Swiper from 'react-native-swiper'

var styles = {
  wrapper: {},
  slide1: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#9DD6EB'
  },
  slide2: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#97CAE5'
  },
  slide3: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#92BBD9'
  },
  text: {
    color: '#fff',
    fontSize: 30,
    fontWeight: 'bold'
  }
}

export default () => (
  <Swiper style={styles.wrapper} showsButtons loop={false}>
    <View testID="Hello" style={styles.slide1}>
      <Text style={styles.text}>Hello Swiper</Text>
    </View>
    <View testID="Beautiful" style={styles.slide2}>
      <Text style={styles.text}>Beautiful</Text>
    </View>
    <View testID="Simple" style={styles.slide3}>
      <Text style={styles.text}>And simple</Text>
    </View>
  </Swiper>
)

使用路由

文档地址

在页面上直接使用 navigate

  1. navigate
  2. push
  3. goBack
  4. popToTop
import * as React from 'react';
import {View, Text, Button} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';

function HomeScreen(props: any) {
  const {navigation} = props;
  return (
    <View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
      <Text>嘿嘿12</Text>
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details')}
      />
    </View>
  );
}

function DetailsScreen(props: any) {
  const {navigation} = props;
  return (
    <View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
      <Text>Details Screen</Text>
      <Button
        title="Go to Details... again"
        onPress={() => navigation.push('Details')}
      />
      <Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
      <Button title="Go back" onPress={() => navigation.goBack()} />
      <Button
        title="Go back to first screen in stack"
        onPress={() => navigation.popToTop()}
      />
    </View>
  );
}

const Stack = createNativeStackNavigator();

function RouterView() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{title: 'Overview'}}
        />
        <Stack.Screen name="Details" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default RouterView;

在组件中使用 useNavigate

import * as React from 'react';
import { Button } from 'react-native';
import { useNavigation } from '@react-navigation/native';

function MyBackButton() {
  const navigation = useNavigation();

  return (
    <Button
      title="Back"
      onPress={() => {
        navigation.goBack();
      }}
    />
  );
}

使用 react-native-storage做持久储存

保存、读取和删除

单独处理数据

保存

使用key来保存数据(key-only)。这些数据一般是全局独有的,需要谨慎单独处理的数据
批量数据请使用key和id来保存(key-id),具体请往后看
除非你手动移除,这些数据会被永久保存,而且默认不会过期。

storage.save({
  key: 'loginState', // 注意:请不要在key中使用_下划线符号!
  data: {
    from: 'some other site',
    userid: 'some userid',
    token: 'some token',
  },
  // 如果不指定过期时间,则会使用defaultExpires参数
  // 如果设为null,则永不过期
  expires: 1000 * 3600,
});
读取
// 读取
storage
  .load({
    key: 'loginState',

    // autoSync(默认为true)意味着在没有找到数据或数据过期时自动调用相应的sync方法
    autoSync: true, // 设置为false的话,则等待sync方法提供的最新数据(当然会需要更多时间)。

    // syncInBackground(默认为true)意味着如果数据过期,
    // 在调用sync方法的同时先返回已经过期的数据。
    syncInBackground: true,
    // 你还可以给sync方法传递额外的参数
    syncParams: {
      extraFetchOptions: {
        // 各种参数
      },
      someFlag: true,
    },
  })
  .then(ret => {
    // 如果找到数据,则在then方法中返回
    // 注意:这是异步返回的结果(不了解异步请自行搜索学习)
    // 你只能在then这个方法内继续处理ret数据
    // 而不能在then以外处理
    // 也没有办法“变成”同步返回
    // 你也可以使用“看似”同步的async/await语法

    console.log(ret.userid);
    this.setState({ user: ret });
  })
  .catch(err => {
    //如果没有找到数据且没有sync方法,
    //或者有其他异常,则在catch中返回
    console.warn(err.message);
    switch (err.name) {
      case 'NotFoundError':
        // TODO;
        break;
      case 'ExpiredError':
        // TODO
        break;
    }
  });

删除
  // 删除单个数据
  storage.remove({
    key: 'lastPage',
  });
  storage.remove({
    key: 'user',
    id: '1001',
  });

批量操作数据

保存

使用key和id来保存数据,一般是保存同类别(key)的大量数据。
所有这些"key-id"数据共有一个保存上限(无论是否相同key)
即在初始化storage时传入的size参数。
在默认上限参数下,第1001个数据会覆盖第1个数据。
覆盖之后,再读取第1个数据,会返回catch或是相应的sync方法。

  var userA = {
    name: 'A',
    age: 20,
    tags: ['geek', 'nerd', 'otaku'],
  };

  storage.save({
    key: 'user', // 注意:请不要在key中使用_下划线符号!
    id: '1001', // 注意:请不要在id中使用_下划线符号!
    data: userA,
    expires: 1000 * 60,
  });

读取
  //load 读取
  storage
    .load({
      key: 'user',
      id: '1001',
    })
    .then(ret => {
      // 如果找到数据,则在then方法中返回
      console.log(ret.userid);
    })
    .catch(err => {
      // 如果没有找到数据且没有sync方法,
      // 或者有其他异常,则在catch中返回
      console.warn(err.message);
      switch (err.name) {
        case 'NotFoundError':
          // TODO;
          break;
        case 'ExpiredError':
          // TODO
          break;
      }
    });

  // --------------------------------------------------

  // 获取某个key下的所有id(仅key-id数据)
  storage.getIdsForKey('user').then(ids => {
    console.log(ids);
  });

  // 获取某个key下的所有数据(仅key-id数据)
  storage.getAllDataForKey('user').then(users => {
    console.log(users);
  });

删除

  // !! 清除某个key下的所有数据(仅key-id数据)
  storage.clearMapForKey('user');
  // -------------------------------------------------
  // !! 清空map,移除所有"key-id"数据(但会保留只有key的数据)
  storage.clearMap();

同步远程数据(刷新)

storage.sync = {
  // sync方法的名字必须和所存数据的key完全相同
  // 参数从params中解构取出
  // 最后返回所需数据或一个promise
  async user(params) {
    const {
      id,
      syncParams: { extraFetchOptions, someFlag }
    } = params;
    const response = await fetch('user/?id=' + id, {
      ...extraFetchOptions
    });
    const responseText = await response.text();
    console.log(`user${id} sync resp: `, responseText);
    const json = JSON.parse(responseText);
    if (json && json.user) {
      storage.save({
        key: 'user',
        id,
        data: json.user
      });
      if (someFlag) {
        // 根据一些自定义标志变量操作
      }
      // 返回所需数据
      return json.user;
    } else {
      // 出错时抛出异常
      throw new Error(`error syncing user${id}`);
    }
  }
};

有了上面这个 sync 方法,以后再调用 storage.load 时,如果本地并没有存储相应的 user,那么会自动触发 storage.sync.user 去远程取回数据并无缝返回。

  storage.load({
    key: 'user',
    id: '1002'
  }).then(...)

读取批量数据

使用和load方法一样的参数读取批量数据,但是参数是以数组的方式提供。
会在需要时分别调用相应的sync方法,最后统一返回一个有序数组。

storage.getBatchData([
    { key: 'loginState' },
    { key: 'checkPoint', syncInBackground: false },
    { key: 'balance' },
    { key: 'user', id: '1009' }
])
.then(results => {
  results.forEach( result => {
    console.log(result);
  })
})

//根据key和一个id数组来读取批量数据
storage.getBatchDataWithIds({
  key: 'user',
  ids: ['1001', '1002', '1003']
})
.then( ... )

这两个方法除了参数形式不同,还有个值得注意的差异。getBatchData会在数据缺失时挨个调用不同的 sync 方法(因为 key 不同)。但是getBatchDataWithIds却会把缺失的数据统计起来,将它们的 id 收集到一个数组中,然后一次传递给对应的 sync 方法(避免挨个查询导致同时发起大量请求),所以你需要在服务端实现通过数组来查询返回,还要注意对应的 sync 方法的参数处理(因为 id 参数可能是一个字符串,也可能是一个数组的字符串)。

字体图标 react-native-vector-icons

使用的是 字体图标库react-native-vector-icons;
技术文档;
图标文档;

安装方式

先安装库

npm install --save react-native-vector-icons

安卓
Edit android/app/build.gradle ( NOT android/build.gradle ) and add the following:

apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

IOS
还未实践

使用

  1. 进入图标官网
  2. 选择自己要用的图标
  3. 看自己要用的图标在哪一个组件中(在页面最上边的红色部分);
  4. 在项目中引用组件,将name赋值;
    如下:

import Ionicons from 'react-native-vector-icons/Ionicons';
const icon = ()=>{
  const iconName = 'shirt-outline'
  const size = 24
  const color = 'red'
  return <Ionicons name={iconName} size={size} color={color} />;
}

icons.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容