问题1
问题描述:TextInput组件在页面底部时,弹出键盘时遮挡TextInput组件,用户无法正常输入内容
分析解决:弹出键盘浮在页面布局之上占用了一部分布局控件,可以监听键盘的弹出和隐藏事件,实现动态调整页面布局。
//页面装载时
componentWillMount() {
//监听键盘弹出事件
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShowHandler.bind(this));
//监听键盘隐藏事件
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHideHandler.bind(this));
}
//页面移除时
componentWillUnmount() {
//卸载键盘弹出事件监听
if (this.keyboardDidShowListener != null) {
this.keyboardDidShowListener.remove();
}
//卸载键盘隐藏事件监听
if (this.keyboardDidHideListener != null) {
this.keyboardDidHideListener.remove();
}
}
//自定义键盘事件处理
//键盘弹出事件响应
keyboardDidShowHandler(event) {
this.setState({keyboardShow: true});
console.log(event.endCoordinates.height);
}
//键盘隐藏事件响应
keyboardDidHideHandler(event) {
this.setState({keyboardShow: false});
}
//强制隐藏键盘
dissmissKeyboard() {
Keyboard.dismiss();
console.log("输入框当前焦点状态:" + this.refs.bottomInput.isFocused());
}
在render()方法中,通过keyboardShow实现动态布局
{this.state.keyboardShow ? null :
<Image style={styles.devImg}
source={require('../../../res/img/device/dev_solar_add.png')}/>
}
<Text style={this.state.keyboardShow ? [styles.textTipMain, {marginBottom:autoHeight(19)}] :styles.textTipMain}>{getString('solar_connect_input_model')}</Text>
问题2
问题描述:在某些情景下需要监听当前页面的物理返回键,例如双击back退出应用,或者某一页按下back键返回特定页面
分析解决:RN中的页面是通过组件的方式实现的,各页面之间的通过路由的方式连接起来,单纯在当前页面监听hardwareBackPress事件,不止会监听到当前页面的物理返回键事件,也会监听到该页面所有上层页面的物理返回键事件。
网上有很多说的是通过路由长度navigator.getCurrentRoutes().length 判断当前所在页面,尝试多次无果,之后找到了新版本react-navigation的当前页面物理返回键监听的正确方式:
class ScreenWithCustomBackBehavior extends Component {
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress',
this.onBackButtonPressAndroid);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress',
this.onBackButtonPressAndroid);
}
onBackButtonPressAndroid = () => {
if (this.props.navigation.isFocused()) {
if (this.lastBackPressed && this.lastBackPressed + 2000 >= Date.now()) {
//最近2秒内按过back键,可以退出应用。
return false;
}
this.lastBackPressed = Date.now();
ToastAndroid.show('再按一次退出应用', ToastAndroid.SHORT);
return true;
}
}
}
有些版本在componentWillUnmount里执行removeEventListener并没有用,还是会监听到,所以这个时候可以换一种写法:
componentDidMount() {
this.backHandler = BackHandler.addEventListener('hardwareBackPress',
this.onBackButtonPressAndroid);
}
componentWillUnmount() {
this.backHandler&&this.backHandler.remove();
}
问题3
问题描述:RN里边多语言适配一般用react-native-i18n第三方多语言库,具体使用参考https://github.com/AlexanderZaytsev/react-native-i18n,App多语言通常有一个需求就是切换语言,如果没有杀死应用进程,切换系统语言之后,App仍然后展示切换之前的语言。
分析解决:切换系统语言之后,App仍然显示为切换之前的语言,很可能是当前语言设置没有生效或者是没有进行语言的重新获取。通过原生方式监听切换系统语言的广播并发送消息给RN,然后再RN应用入口处接收系统语言切换的消息,并设置当前语言为切换后的系统语言:
//原生系统语言切换广播监听
public class LocalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_LOCALE_CHANGED)) {
reactContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("local_changed",null);
}
}
}
RN入口接收消息
componentWillMount() {
console.log('---componentWillMount---');
this.subLocalChangedListener = DeviceEventEmitter.addListener("local_changed", () => {
//每次进入应用前台检查语言
if (ObjectUtil.isEmpty(I18n.locale)) {
getLanguages()
.then(languages => {
console.log('当前语言' + languages);
if (languages.toString().match(I18nUtils.ZH)) {
I18n.locale = I18nUtils.ZH;
} else {
I18n.locale = I18nUtils.EN;
}
})
.catch((error) => {
console.log("获取语言失败" + error);
}
);
}
});
}
componentWillUnmount() {
console.log('---componentWillUnmount---');
this.susubLocalChangedListener .remove();
}
问题4
问题描述:手势滑动事件处理粒度太粗,导致部分手机上当前页面点击事件无法响应
分析解决:在处理首页预留空间滑动事件时,在onStartShouldSetPanResponder事件时就成为事件的响应者,导致该组件成为所有触摸事件(包括点击事件)的响应者,而该组件又不能很好的处理自己所需触摸事件之外的其他事件,默认是不做任何响应,这就导致了页面点击事件无响应。详细分析RN里边触摸事件的分发处理机制,逐步细化处理触摸事件的响应,不要在一开始就响应,当满足特定需求之后,例如滑动时滑动长度超过特定长度之后再处理,这样既不会影响其他组件的默认事件处理机制,也能够处理特定情况的手势事件。
componentWillMount() {
this._panResponder = PanResponder.create({
// 要求成为响应者:
onStartShouldSetPanResponder: (evt, gestureState) => false,
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => false,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
if (Math.abs(gestureState.dx) < this.thresholdMin && Math.abs(gestureState.dy) < this.thresholdMin) {
return false;
}else{
if ((this.show && gestureState.dy < 0) || (!this.show && gestureState.dy > 0)) {
return true;
} else {
return false;
}
}
},
onPanResponderGrant: (evt, gestureState) => {
// 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情!
console.log('onPanResponderGrant');
// gestureState.{x,y} 现在会被设置为0
},
onPanResponderMove: (evt, gestureState) => {
// 最近一次的移动距离为gestureState.move{X,Y}
console.log('onPanResponderMove');
// 从成为响应者开始时的累计手势移动距离为gestureState.d{x,y}
if ((this.show && gestureState.dy < 0) || (!this.show && gestureState.dy > 0)) {
}
}
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
// 用户放开了所有的触摸点,且此时视图已经成为了响应者。
console.log('onPanResponderRelease');
// 一般来说这意味着一个手势操作已经成功完
},
onPanResponderTerminate: (evt, gestureState) => {
// 另一个组件已经成为了新的响应者,所以当前手势将被取消。
console.log('onPanResponderTerminate');
},
onShouldBlockNativeResponder: (evt, gestureState) => {
// 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
// 默认返回true。目前暂时只支持android。
return true;
},
});
}
问题5
问题描述:FlatList列表加载圈莫名消失,场景:网络慢时导致首页获取不到设备列表,首页为空,不能下拉刷新。
分析解决:这个问题真的是莫名其妙,我第一感觉是怀疑逻辑处理不严谨,开始按照网络请求-正常-异常展示各种情况梳理,顺便整理了下逻辑,没有问题,就把它定义为莫名消失了。记得刚开始简单列表demo的时候就不存在这个问题,和demo对比,转换网络请求的方式,就差把demo代码全部替换过来了。正常逻辑怎么也想不到,把布局调整成最简单的了,终于在网络请求失败时加载圈也不会消失了,那么问题就是出在布局上,也不是布局有误,就是给FlatList的父布局设置了FlexBox布局的属性就出现这个问题了,如下:
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
}
去掉这三个属性,如下,其他代码逻辑复原,问题解决了。
container: {
flex: 1,
}
问题6
问题描述:高版本手机通知栏小图标处理,在部分5.0或7.0手机的消息通知栏不仅会显示消息图标,还会显示一个非常小的应用通知图标,和其他应用对比发现默认显示效果不明显
分析解决:在部分高版本手机上,如果没有设置应用通知图标,系统会默认用应用的icon缩略一个小的通知图标(图标有颜色的部分会填充为白色,导致显示不出来),除非自己指定通知小图标。
找UI出了一个透明背景的icon(因为有颜色的部分会填充为白色,影响图标原有显示效果),指定为通知小图标,这样通知小图标就能显示出正常效果。
问题7
问题描述:编译版本不一致,例如targetSdk,google-supprot,firebase等版本不统一导致编译不通过问题
报错1:Error:Execution failed for task ':app:transformClassesWithDexForDebug'.
com.android.build.api.transform.TransformException: com.android.ide.common.process.ProcessException:
报错2:Program type already present: android.support.v4.app.INotificationSideChannel
报错3:com.android.builder.internal.aapt.v2.Aapt2Exception: AAPT2 error: check logs for details
报错4:unable to find attribute android:fontVariationSettings and android:ttcIndex
报错5:java.util.zip.ZipException: duplicate entry
报错6:Android dependency 'com.google.android.gms:play-services-basement' has different version for the compile (16.0.1) and runtime (16.1.0) classpath. You should manually set the same version via DependencyResolution
分析解决:针对以上系列问题,花费了两天半时间,一直在排查这一系列问题,网上搜出的解决方案五花八门,但是都没有完全解决。之后回归问题初始是由于版本冲突/重复依赖第三方包导致的重复依赖或者依赖冲突。从第三方包依赖冲突入手,最容易出现依赖冲突的就是com.android.support组和com.google.firebase组下的包,参考Stack Overflow论坛里解决com.android.support包依赖冲突的解决方法,在project的build.gradle里配置com.android.support的统一版本,同时配置com.google.firebase和com.google.android.gms的同一版本,终于编译通过了。
allprojects {
repositories {
...
}
maven {
url 'https://maven.google.com/'
name 'Google'
}
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def requested = details.requested
if (requested.group == 'com.google.android.gms') {
details.useVersion '12.0.1'
}
if (requested.group == 'com.google.firebase') {
details.useVersion '12.0.1'
}
}
}
}
subprojects {
project.configurations.all {
resolutionStrategy.eachDependency { details ->
if (details.requested.group == 'com.android.support'
&& !details.requested.name.contains('multidex') ) {
details.useVersion "26.1.0"
}
}
}
}
}
问题8
问题描述:app启动后,先出现白色页面2.5S~3S再出现启动页
分析解决:白屏为js文件解析阶段,将此时的白屏用启动页替换,到真正启动页停留时间完毕,跳转到主页。冷启动时间比热启动时间长,所以白屏时间也相对较长。
参考:React Native Android启动屏,启动白屏,闪现白屏
也可以直接使用第三方库react-native-splash-screen,原理一样
问题9
问题描述:文字过长添加省略号,最大长度阈值需要区分中英文,但中文和英文可容纳字符个数不同,在中英文混合的情况下如何动态控制可显示文字的长度
分析解决:在指定长度的区域内显示文字,超过可容纳长度用省略号表示,中文和英文可容纳字符个数不同,一个中文占两个英文长度,以中文文字可显示标准为例,为了更好的显示效果,就需要动态转换可显示中文文字个数,例如“你hhh好kk啊”转换为中文标准长度就是5.5个,提供可显示中文长度的最大值,截取可显示文字即可。
static CutStr(str, len){
if (str.replace(/[^\x00-\xff]/g, "**").length <= 2 * len) {
return str;
}
let char_length = 0;
for (let i = 0; i < str.length; i++){
let son_str = str.charAt(i);
encodeURI(son_str).length > 2 ? char_length += 1 : char_length += 0.5;
if (char_length >= len){
let sub_len = char_length == len ? i+1 : i;
return str.substr(0, sub_len) + '...';
break;
}
}
}
问题10
问题描述:网络稳定的情况下,不能确保mqtt消息的正常接收,mqtt消息心跳检测,重连机制
分析解决:在物联网应用里,消息传输通常使用mqtt协议,MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。确保消息的正常接收就需要保证客户端和服务器的连接,通过重连机制和心跳检测来确保连接的可靠性。
重连机制:客户端和服务器mqtt建立连接失败/断开连接了则开启重连模式;三秒后进行一次重连,重连时首先检查网络是否正常,其次判断mqtt连接是否已经建立,如果没有建立连接才进行连接请求;当连接建立成功时,关闭重连循环,并开始消息订阅。
心跳检测:在RN的Android系统下,后台任务可以通过HeadlessJS 来实现,react-native-background-job就是基于HeadlessJS 实现的用来处理后台程序的第三方库,这里使用第三方库react-native-background-job做辅助,在应用切换到后台时,通过循环处理后台任务来确保连接不被断开,当应用再次回到前台时取消后台任务的执行。
//注册后台任务
BackgroundJob.register({
jobKey: GlobalConstant.REGULAR_JOB_KEY,
job: () => console.log("Running in background")
});
//前后台切换
handleAppStateChange(appState) {
console.log('当前状态为:' + appState);
if (appState === 'active') {
//回到前台
BackgroundJob.cancel({jobKey: GlobalConstant.REGULAR_JOB_KEY});
}else if(appState == 'background'){
//后台运行,触发执行
BackgroundJob.schedule({
jobKey: GlobalConstant.REGULAR_JOB_KEY,
period: 2000,
exact: true
});
}
}
问题11
问题描述:Android9.0网络请求都是失败
分析解决: Android9 开始,也会默认阻止 http 请求,督促开发者使用https请求。
方法一:APP网络请求更改为HTTPS(推荐);
方法二:targetSdkVersion 降到27及以下;
方法三:在 res 下新增一个 xml 目录,然后创建一个名为:network_security_config.xml 文件(名字自己取) ,内容如下,大概意思就是允许开启http请求
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
在项目的AndroidManifest.xml文件下的application标签增加以下属性,应用以上配置。
<application
...
android:networkSecurityConfig="@xml/network_security_config"
...>
</application>
问题12
问题描述:github下载代码报错
fatal: unable to access 'https://github.com/youyanping/react-native-project.git/': Failed to connect to 127.0.0.1 port 1080: Connection refused
分析解决:
git config --global http.proxy 查询到当前设置了代理,所以我取消这个设置,
git config --global --unset http.proxy 再查询,已经没有了代理,然后再clone,成功了!
问题13
问题描述:四指触控的报错
enabled a touch event which was not counted in 'tracedeTouchCount'
分析解决:网上人说是ReactNative 框架本身的bug,可以通过修改RN的源码解决,之后没改源码,打包成正式apk就不会有这个问题了。