上一篇:使用react native 創(chuàng)建一個(gè)屬于你自己的云備忘錄app~(A。用node搭個(gè)服務(wù)嘹朗,基礎(chǔ)篇)
項(xiàng)目地址:cloud-memo
效果圖
目錄結(jié)構(gòu)
初始化那些東西就不贅述了,rn官方文檔很詳細(xì)晨逝。
在app目錄中新建一個(gè)js目錄捧存,作為主要部分代碼的目錄。
-js
+component 展示型組件
+container 包含型組件(主要是描述功能的)
-screen react-navigation的路由目錄
-navigations
-navOptions.js 設(shè)置導(dǎo)航樣式
index.js 配置路由
-utils 放了一些工具函數(shù)
-px2dp.js 根據(jù)設(shè)備分辨率轉(zhuǎn)換像素大小
-request.js
App.js 主組件袄友,用來(lái)把路由連接到這里
文本編輯器
構(gòu)造組件部分沒有什么難點(diǎn)殿托,主要是在編寫文本編輯器上,在github上找了不少插件大部分都是富文本且都是通過webview構(gòu)建的剧蚣,反而太過臃腫支竹,于是只能自己親自操刀了OVO。
效果展示
為了實(shí)現(xiàn)一邊輸入一邊增加行高鸠按,用TextInput
上的apionContentSizeChange
<TextInput
style={[{ height: inputBoxHeight }, style.inputBox]}
multiline // 代表可以輸入多行
defaultValue="可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行"
onContentSizeChange={this.onChange}
/>
綁定函數(shù)onChange
onChange(event) {
const contentSize = event.nativeEvent.contentSize.height;
this.setState({
inputBoxHeight: contentSize,
});
}
這樣寫出來(lái)的效果并不流暢礼搁,每次換行時(shí)會(huì)閃爍。這時(shí)加入動(dòng)畫起到一個(gè)過渡作用待诅。
onChange(event) {
const contentSize = event.nativeEvent.contentSize.height;
Animated.timing(
this.state.inputBoxHeight,
{
toValue: contentSize,
},
).start();
}
...
<Animated.View style={{ height: inputBoxHeight }}>
<TextInput
style={[{ flex: 1 }, style.inputBox]}
multiline // 代表可以輸入多行
defaultValue="可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行可以輸入多行"
onContentSizeChange={this.onChange}
underlineColorAndroid="transparent"
/>
</Animated.View>
導(dǎo)航
我用的是react-navigation來(lái)設(shè)置導(dǎo)航叹坦。
配置導(dǎo)航欄樣式
// navOptions.js
export default function navOptions(title = '', restParams = {}, isShowHeader = true) {
if (!isShowHeader) return { header: null };
return {
title,
headerTitle: (
<View style={styles.header}>
<Text style={{ fontSize: px2dp(25) }}>
{title}
</Text>
</View>),
headerLeft: (restParams && restParams.headerLeft) || <View />,
headerRight: (restParams && restParams.headerRight) || <View />,
};
}
主要是返回一個(gè)配置對(duì)象,包含導(dǎo)航名卑雁,導(dǎo)航欄左右按鈕等募书,詳見官方文檔。
配置路由
export default {
List: {
screen: List,
navigationOptions: () => navOptions(''),
},
Detail: {
screen: Detail,
navigationOptions: ({ navigation }) => navOptions(
'編輯',
{ headerLeft: <BackBtn navigation={navigation} /> },
),
},
};
通知組件
安裝組件
在android下测蹲,react native自身提供了ToastAndroid
莹捡;而在IOS中,需要安裝react-native-root-toast
扣甲。
統(tǒng)一API
這里需要自己簡(jiǎn)單封裝一下
// Toast.js
import { ToastAndroid, Platform } from 'react-native';
import RootToast from 'react-native-root-toast';
const Toast = Platform.select({
ios: RootToast,
android: ToastAndroid,
});
export { Toast };
調(diào)用方法
Toast.show('提示內(nèi)容',Toast.SHORT);
臨時(shí)示例
編輯后篮赢,提示用戶已保存到本地
處理網(wǎng)絡(luò)請(qǐng)求
我們使用基于promise的axios來(lái)處理請(qǐng)求,先安裝依賴琉挖,再按照上篇文章的數(shù)據(jù)返回格式封裝一下request函數(shù):
import axios from 'axios';
import createError from './createError';
export default function request(url, body) {
return axios
.post(`http://192.168.10.248:8888${url}`, JSON.stringify(body))
.then(response => response.data)
.then((response) => {
if (response.status === 1) throw createError(response.code);
return response.body || {};
})
.catch((error) => {
if (error instanceof SyntaxError) {
throw createError('SYNTAX_JSON');
}
// JSON解析錯(cuò)誤
/* eslint-disable no-console */
console.log(error);
throw createError('ERROR');
});
}
上傳按鈕的動(dòng)畫
寫這個(gè)組件費(fèi)了一點(diǎn)心思启泣,主要因?yàn)樗窃趎avigator上,并非是detail組件示辈。要通過上傳狀態(tài)更改這個(gè)組件的旋轉(zhuǎn)動(dòng)畫的狀態(tài)寥茫,通過props傳遞太過復(fù)雜,也不好實(shí)現(xiàn)矾麻。這里用DeviceEmitter
來(lái)解決這個(gè)問題纱耻。
效果圖
Upload按鈕組件
const TIMES = 30;
export default class UploadBtn extends React.Component {
static uploadEvent() {
DeviceEventEmitter.emit('uploading', true);
}
constructor(props) {
super(props);
this.state = {
rotateValue: new Animated.Value(0),
};
this.stopAnimate = this.stopAnimate.bind(this);
this.startAnimate = this.startAnimate.bind(this);
}
componentDidMount() {
this.deEmitter = DeviceEventEmitter.addListener('isUploaded', (a) => {
if (a) {
setTimeout(() => {
this.stopAnimate();
}, 1000);
}
});
}
componentWillUnmount() {
this.deEmitter.remove();
}
startAnimate() {
UploadBtn.uploadEvent();
Toast.show('上傳中...', 5000);
Animated.timing(this.state.rotateValue, {
toValue: 360 * TIMES,
duration: 800 * TIMES,
easing: Easing.linear,
}).start();// 開始spring動(dòng)畫
}
stopAnimate() {
this.state.rotateValue.stopAnimation(() => {
Toast.show('上傳完成', Toast.SHORT);
});
this.state.rotateValue.setValue(0);
}
render() {
return (
<Animated.View style={{
transform: [{ rotate: this.state.rotateValue.interpolate({ inputRange: [0, 360], outputRange: ['0deg', '360deg'] }) }],
}}
>
<NavBtn onPress={this.startAnimate} iconName="refresh" />
</Animated.View>
);
}
}
這里主要是設(shè)計(jì)思路比較麻煩芭梯,代碼上沒有什么特別大的坑,只有在接受isUploaded
信號(hào)弄喘,停止動(dòng)畫那里玖喘,有些異步的問題,旋轉(zhuǎn)動(dòng)畫無(wú)法正常停止蘑志,后來(lái)加了個(gè)定時(shí)器就解決了累奈。
本地存儲(chǔ)
react native使用AsyncStorage
來(lái)操作本地?cái)?shù)據(jù),返回的都是promise卖漫,所以是異步的费尽,不過也有同步函數(shù),不在這里詳細(xì)介紹了羊始。
按照官方api進(jìn)行簡(jiǎn)單封裝旱幼,如下:
async function getStorageData(key) {
let value;
try {
value = await AsyncStorage.getItem(key);
} catch (e) {
throw e;
}
return value;
}
async function getStorageAllData(keys) {
let data;
try {
data = await AsyncStorage.multiGet(keys);
} catch (e) {
throw e;
}
return data;
}
async function setStorage(arr) {
try {
await AsyncStorage.multiSet(arr);
} catch (e) {
throw e;
}
}
export { getStorageData, getStorageAllData, setStorage };
總結(jié)
最后加上首頁(yè)導(dǎo)航欄,組件的構(gòu)建基本上就完成了突委;下一篇內(nèi)容將會(huì)細(xì)化server端接口柏卤,聯(lián)調(diào)接口,部署服務(wù)器匀油,app端打包等缘缚,最終完成一個(gè)真正的應(yīng)用。