每天一篇系列:
强化知识体系,查漏补缺。
欢迎指正,共同学习!
1.调试环境
PC : Windows
OS : Android
2.环境准备
Android调试环境准备,有过Android开发经验的开发对Android调试环境不会陌生。JAVA环境、SDK配置、Android Studio环境准备,这些都是基本功,先把这个环境准备好.不是本章节的重点
注意:
1.Android SDK在23以上(app\build.gradle中的compileSdkVersion)
2.Android sdk build tools 在23.0.1以上(app\build.gradle中的buildToolsVersion)
3.Android Support Repository在17以上
4.Android NDK-r10e配置
参考配置:
在gradle构建的项目中,build.gradle配置的版本在Android Studio的SDK Manager中要相应的安装好
3.ReactNative
项目源码位置
通常如果安装和配置好git后,可以使用git clone这个项目,或者直接clone or download这个项目,推荐使用git,毕竟是搞开发的,这些工具还是要会用的,也能更好的管理项目。使用git clone好项目后,是没有Node_Modules模块的(react-native的核心库),为了配置Node_Modules需要先配置node.js下载地址:https://nodejs.org/en/版本最好在4.1以上,安装好后可以通过node -v测试是否安装成功
npm安装:
sudo npm install
安装完后,进入react-native根目录:
执行nmp install指令,nmp install会根据同级目录下的package.json下载依赖的node_module,这样就配置好运行需要的基本环境了。
4.Run App
我这里要说的Run App是跑起来自己实现的React-Native APP。
网站上有很多关于在目标终端启动React-Native Example或第一个Hello World的说明,我这里就不作说明了,可以先自己跑起来Hello World应用后,在来看我这里的说明,多少应该有些借鉴意义。
这篇文章是我关于React Native写的第一篇文章,也是我开发第一个React Native应用后写的文章。
首先我的技术栈是Android framework,在实际工作中也会基于原生API开发应用,但是我所开发的应用仅在Android 4.4这样的平台使用而已业务简单,至于如今火热的RxJava,Retrofit、Glide等框架用不上,也没有接触过React Native开发,我自认为是与Android生态脱轨了,现在想一步一步拾起来。
最后介绍下我的项目,这是一个通过分析实际网络交互后开发的一款视频应用,我分析了战旗直播的交互,最终目标是获取游戏视频的播放地址。
先来看一下效果:
image接下来说下代码:
1.首先创建好React Native工程
新版的RN已经没有了index.android.js和index.ios.js,在index.js中已经统一到APP.js中:
/** @format */
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => App);
这里我要说一下React Native的编程,使用了JSX语法,可以理解为JS+XML,我开始也很担心编程是个问题,但是我用了两天可以实现这个界面的编程及显示,我觉得熟练了就好。
2.为了方便页面跳转,我选择了react-navigation组件
我创建了一个StackNavigator,用来声明所有页面:
import React, {
Component
} from 'react';
import {
StackNavigator
} from 'react-navigation'
import MyCategaryPage from './MyCategaryPage'
import MyPosterPage from './MyPosterPage'
import MyPlayer from './MyPlayer'
const MyNavigator = StackNavigator({
MyCategaryPage:{
screen:MyCategaryPage,
navigationOptions: {
header:null //隐藏标题栏
}
},
MyPosterPage:{
screen:MyPosterPage,
navigationOptions: {
header:null //隐藏标题栏
}
},
MyPlayer:{
screen:MyPlayer,
navigationOptions: {
header:null //隐藏标题栏
}
}
});
export default MyNavigator;
使用react-navigation的好处就是可以方便页面的跳转和返回,比如跳转到MyPosterPage页面,并且可以携带参数跳转:
this.props.navigation.navigate('MyPosterPage',{id:item.id});
页面的返回处理我也统一到一个父类里面:
import React, { Component } from 'react';
import { BackHandler } from 'react-native';
const navigation_stack = [];
class BackInterface extends Component {
constructor(props) {
super(props);
BackHandler.addEventListener('hardwareBackPress', this.go_back.bind(this));
}
addNavigationStack(item){
//无法正常跳转
//navigation_stack.push(item);
}
go_back() {
if (this.props.navigation) {
if(navigation_stack.length > 0){
var item = navigation_stack.pop(); //通过Name也不能跳转
this.props.navigation.goBack(item);
}else{
//无法正常回退到首页.只能在传null时可以正常返回
//this.props.navigation.goBack();
this.props.navigation.goBack(null);
}
return true;
}
return false;
}
}
module.exports = BackInterface;
直接使用this.props.navigation.goBack();不能正常从第三级页面返回两次,本来打算通过一个Stack数据结构来实现跳转到指定页,但是也不行,最后只有通过传null参数实现随机跳转。当然子页面需要继承这个类。
3.分析战旗直播的交互
推荐用chrome来看这些交互文件,并且可以模拟手机端的交互。看到有效请求的地址和参数:
这样就知道如何拿到这些游戏分类的数据了。
游戏分类页的源码如下:
//获取全部游戏分类视频 400-1 表示1次获取400个数据
var REQUEST_URL = 'https://m.zhanqi.tv/api/static/game.lists';
var perNum = 400;
var pageNum = 1;
以上在分析战旗竞技交互时得出的数据。如果有直接可以对接的API这些就不需要自己来分析,我这里只是没有已经存在的API可以对接,所以就自己动手扒了一下网页的数据交互过程。
其次就是分类栏的界面显示:
let cols = 3;
let gap = 10;
let ScreenWidth = Dimensions.get('window').width;
let ImageWidth = (ScreenWidth - (cols + 1) * gap) / cols;
class MyCategaryPage extends BackInterface {
constructor(props) {
super(props);
super.addNavigationStack('MyCategaryPage');
this.state = {
refreshing: false,
dataArray: [],
};
this.loadData = this.loadData.bind(this);
this.select_item = this.select_item.bind(this);
}
componentDidMount() {
this.loadData();
}
loadData() {
fetch(REQUEST_URL + '/' + perNum + '-' + pageNum +'.json')
.then((response) => response.json())
.then((responseData) => {
let data = responseData.data.games;
this.state.dataArray = [];
this.setState({
dataArray:this.state.dataArray.concat(data),
});
}).catch((error) => {
}).done();
}
render() {
return (
<FlatList
data={this.state.dataArray}
renderItem={this.renderItem.bind(this)}
onRefresh={this.onRefresh.bind(this)}
refreshing={this.state.refreshing}
horizontal={false}
numColumns={cols}
/>
);
}
onRefresh(){
this.loadData();
}
renderItem(categary) {
var item = categary.item;
return (
<TouchableOpacity style={styles.container} onPress={() => {this.select_item(item)}}>
<View style={styles.innerViewStyle}>
<Image
source={{uri:item.spic}}
style={styles.iconStyle}>
</Image>
<Text numberOfLines={1} style={styles.textStyle}>{item.name}</Text>
</View>
</TouchableOpacity>
);
}
使用FlatList实现了一个GridView的效果,另外也对下拉刷新做了处理。
当用户点击某一个游戏分类图标时,响应onPress:
select_item(item) {
//通过navigation传递参数
this.props.navigation.navigate('MyPosterPage',{id:item.id});
}
这样就通过navigation跳转到MyPosterPage页面:
class MyPosterPage extends BackInterface {
constructor(props) {
super(props);
super.addNavigationStack('MyPosterPage');
this.state = {
refreshing: false,
dataArray: [],
showFoot: 0, //0:隐藏footer 1:已加载完成,没有更多数据
isRefreshing: false, //下拉控制
};
this.loadData = this.loadData.bind(this);
this.player_video = this.player_video.bind(this);
}
componentDidMount() {
this.loadData();
}
loadData() {
if(pageNo > totalPage){
return;
}
//通过navigation获取参数
const {params} = this.props.navigation.state;
const id = params.id;
fetch(REQUEST_URL + '/' + id + '/' + perNum + '-' + pageNo + '.json')
.then((response) => response.json())
.then((responseData) => {
//下拉刷新
if(pageNo == 1){
this.state.dataArray = [];
}
let cnt = responseData.data.cnt;
let data = responseData.data.rooms;
let dataBlob = [];
let i = itemNo;
let page = parseInt(cnt/perNum);
totalPage = cnt>perNum?(cnt%perNum?page + 1:page):1;
//console.warn('cnt:'+cnt+' totalPage:'+totalPage);
data.map(function (item) {
dataBlob.push({
key: i,
value: item,
})
i++;
});
//使用Map保证不重复
itemNo = i;
let foot = 0;
if(pageNo >= totalPage){
foot = 1;
}
this.setState({
dataArray:this.state.dataArray.concat(dataBlob),
showFoot:foot,
isRefreshing:false,
});
data = null;
dataBlob = null;
}).catch((error) => {
}).done();
}
...
}
页面跳转后又可以通过一个FlatList来显示游戏视频的列表,而且在这个页面对上拉加载、下拉刷新分别做了处理,navigation传递的参数通过:
const {params} = this.props.navigation.state;
const id = params.id;
获取。对每一项游戏视频信息的绘制如下:
renderItem(poster) {
var item = poster.item.value;
return (
<TouchableOpacity style={styles.container} onPress={() => {this.player_video(item)}}>
<View style={styles.container}>
<Image
source={{uri:item.spic}}
style={styles.small}>
</Image>
<View style={styles.rightContainer}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.introduce}>{"主播:"+item.nickname}</Text>
<Text style={styles.introduce}>{"观看人数:"+item.online}</Text>
</View>
</View>
</TouchableOpacity>
);
}
用户点击某个游戏视频信息栏时,可跳转到播放页面:
player_video(item) {
//通过navigation传递参数
this.props.navigation.navigate('MyPlayer',{play_url: PLAY_URL + '/' + item.videoId +'.m3u8'});
}
这里的播放地址是我分析战旗竞技交互得来的,item.videoId是在请求游戏视频信息时携带的。
播放器我使用了react-native-video,绑定的是ExoPlayer,也可以绑定Android的MediaPlayer,播放器代码如下:
'use strict';
import React, {
Component
} from 'react';
import {
AppRegistry,
StyleSheet,
Text,
TouchableOpacity,
View,
BackHandler
} from 'react-native';
import Video from 'react-native-video';
import BackInterface from './BackInterface';
class MyPlayer extends BackInterface {
constructor(props) {
super(props);
super.addNavigationStack('MyPlayer');
}
state = {
rate: 1,
volume: 1,
muted: false,
resizeMode: 'contain',
duration: 0.0,
currentTime: 0.0,
paused: true,
};
video: Video;
onLoad = (data) => {
this.setState({ duration: data.duration });
};
onProgress = (data) => {
this.setState({ currentTime: data.currentTime });
};
onEnd = () => {
this.setState({ paused: true })
this.video.seek(0)
};
onAudioBecomingNoisy = () => {
this.setState({ paused: true })
};
onAudioFocusChanged = (event: { hasAudioFocus: boolean }) => {
this.setState({ paused: !event.hasAudioFocus })
};
getCurrentTimePercentage() {
if (this.state.currentTime > 0) {
return parseFloat(this.state.currentTime) / parseFloat(this.state.duration);
}
return 0;
};
renderRateControl(rate) {
const isSelected = (this.state.rate === rate);
return (
<TouchableOpacity onPress={() => { this.setState({ rate }) }}>
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
{rate}x
</Text>
</TouchableOpacity>
);
}
renderResizeModeControl(resizeMode) {
const isSelected = (this.state.resizeMode === resizeMode);
return (
<TouchableOpacity onPress={() => { this.setState({ resizeMode }) }}>
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
{resizeMode}
</Text>
</TouchableOpacity>
)
}
renderVolumeControl(volume) {
const isSelected = (this.state.volume === volume);
return (
<TouchableOpacity onPress={() => { this.setState({ volume }) }}>
<Text style={[styles.controlOption, { fontWeight: isSelected ? 'bold' : 'normal' }]}>
{volume * 100}%
</Text>
</TouchableOpacity>
)
}
render() {
const flexCompleted = this.getCurrentTimePercentage() * 100;
const flexRemaining = (1 - this.getCurrentTimePercentage()) * 100;
//通过navigation获取参数
const {params} = this.props.navigation.state;
const url = params.play_url;
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.fullScreen}
onPress={() => this.setState({ paused: !this.state.paused })}
>
<Video
ref={(ref: Video) => { this.video = ref }}
/* For ExoPlayer */
source={{ uri:url}}
/*source={require('./broadchurch.mp4')}*/
style={styles.fullScreen}
rate={this.state.rate}
paused={this.state.paused}
volume={this.state.volume}
muted={this.state.muted}
resizeMode={this.state.resizeMode}
onLoad={this.onLoad}
onProgress={this.onProgress}
onEnd={this.onEnd}
onAudioBecomingNoisy={this.onAudioBecomingNoisy}
onAudioFocusChanged={this.onAudioFocusChanged}
repeat={false}
/>
</TouchableOpacity>
<View style={styles.controls}>
<View style={styles.generalControls}>
<View style={styles.rateControl}>
{this.renderRateControl(0.25)}
{this.renderRateControl(0.5)}
{this.renderRateControl(1.0)}
{this.renderRateControl(1.5)}
{this.renderRateControl(2.0)}
</View>
<View style={styles.volumeControl}>
{this.renderVolumeControl(0.5)}
{this.renderVolumeControl(1)}
{this.renderVolumeControl(1.5)}
</View>
<View style={styles.resizeModeControl}>
{this.renderResizeModeControl('cover')}
{this.renderResizeModeControl('contain')}
{this.renderResizeModeControl('stretch')}
</View>
</View>
<View style={styles.trackingControls}>
<View style={styles.progress}>
<View style={[styles.innerProgressCompleted, { flex: flexCompleted }]} />
<View style={[styles.innerProgressRemaining, { flex: flexRemaining }]} />
</View>
</View>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'black',
},
fullScreen: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
controls: {
backgroundColor: 'transparent',
borderRadius: 5,
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
},
progress: {
flex: 1,
flexDirection: 'row',
borderRadius: 3,
overflow: 'hidden',
},
innerProgressCompleted: {
height: 20,
backgroundColor: '#cccccc',
},
innerProgressRemaining: {
height: 20,
backgroundColor: '#2C2C2C',
},
generalControls: {
flex: 1,
flexDirection: 'row',
borderRadius: 4,
overflow: 'hidden',
paddingBottom: 10,
},
rateControl: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
volumeControl: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
resizeModeControl: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
controlOption: {
alignSelf: 'center',
fontSize: 11,
color: 'white',
paddingLeft: 2,
paddingRight: 2,
lineHeight: 12,
},
});
export default MyPlayer;
播放器的源码都是些基本代码,播控条UI,全屏等目前没有实现,当然这部分并不是我想要急切实现的内容,因为我之前扩展过原生API接口的ExoPlayer,我比较感兴趣的是react-native-video做了什么,是如何让ExoPlayer和MediaPlayer在React Native中可以这么方便的的使用?
源码Github地址:https://github.com/WoYang/GameVideo_RN
欢迎Fork,有什么好的建议或者指导,欢迎留言。
声明:本人只是对于React Native技术的喜好才使用了战旗直播的文字、图片以及视频资源,本人承诺禁止使用到任何商业用途。
网友评论