Merge 0.0.6a #2

Merged
kdxcxs merged 12 commits from dev into master 4 years ago
  1. 11
      README.md
  2. 28
      src/component/YooForum.js
  3. 11
      src/ui/YooBackground.js
  4. 15
      src/ui/YooForumTopicUI.js
  5. 102
      src/ui/YooForumUI.js
  6. 71
      src/ui/YooLoginUI.js

11
README.md

@ -2,7 +2,7 @@
哟慕课 APP 是一个第三方的优慕课客户端 哟慕课 APP 是一个第三方的优慕课客户端
![screenshot](./screenshot.jpg) <img src="./screenshot.jpg" alt="screenshot" style="height:500px;" />
## 🎞 背景 ## 🎞 背景
@ -18,9 +18,9 @@
## 📄 TODO ## 📄 TODO
- [ ] 弹出键盘不遮挡输入框 - [x] 弹出键盘不遮挡输入框
- [ ] 根据像素比例设置登录背景图片大小 - [ ] ~~根据像素比例设置登录背景图片大小~~
- [ ] 添加应用图标 - [x] 添加应用图标
- [ ] 适配 iOS 登录失败 toast - [ ] 适配 iOS 登录失败 toast
- [ ] 在启动时检测是否已登录 - [ ] 在启动时检测是否已登录
- [ ] Splash 检测网络出错后在 Splash UI 中显示错误并不跳转 - [ ] Splash 检测网络出错后在 Splash UI 中显示错误并不跳转
@ -29,5 +29,6 @@
- [ ] 解决当动画进行时点击话题标题动画会被打断的问题 - [ ] 解决当动画进行时点击话题标题动画会被打断的问题
- [ ] 支持显示回复图片 - [ ] 支持显示回复图片
- [ ] 在回复中显示头像 - [ ] 在回复中显示头像
- [ ] 当滑动到页面底部时显示"加载更多" - [x] 当滑动到页面底部时显示"加载更多"
- [x] 支持通过 Android BackHandler 从话题详情返回话题列表

28
src/component/YooForum.js

@ -14,6 +14,8 @@ export default class YooForum extends Component {
this.state = { this.state = {
hint: '准备中', hint: '准备中',
topics: [], topics: [],
fetchedListPage: 0,
gettingTopicList: false,
}; };
this.initForum = this.initForum.bind(this); this.initForum = this.initForum.bind(this);
this.getTopicList = this.getTopicList.bind(this); this.getTopicList = this.getTopicList.bind(this);
@ -87,9 +89,7 @@ export default class YooForum extends Component {
'http://eol.ctbu.edu.cn/meol/jpk/course/layout/newpage/index.jsp?courseId=46445', 'http://eol.ctbu.edu.cn/meol/jpk/course/layout/newpage/index.jsp?courseId=46445',
}, },
) )
.then(() => { .then(this.getTopicList)
this.getTopicList();
})
.catch(ShowErrorToast); .catch(ShowErrorToast);
}) })
.catch(ShowErrorToast); .catch(ShowErrorToast);
@ -101,8 +101,10 @@ export default class YooForum extends Component {
.catch(ShowErrorToast); .catch(ShowErrorToast);
} }
getTopicList(page = 1) { getTopicList() {
this.setState({hint: '获取数据中'}); const page = this.state.fetchedListPage + 1;
page === 1 && this.setState({hint: '获取数据中'});
this.setState({gettingTopicList: true});
gbkFetch( gbkFetch(
'GET', 'GET',
`http://eol.ctbu.edu.cn/meol/common/faq/forum.jsp?viewtype=thread&forumid=102211&cateId=0&s_gotopage=${page}`, `http://eol.ctbu.edu.cn/meol/common/faq/forum.jsp?viewtype=thread&forumid=102211&cateId=0&s_gotopage=${page}`,
@ -128,11 +130,23 @@ export default class YooForum extends Component {
current.next().text(), current.next().text(),
); );
}); });
this.setState({topics: [...this.state.topics, ...newTopics], hint: ''}); this.setState({
topics: [...this.state.topics, ...newTopics],
hint: '',
fetchedListPage: page,
gettingTopicList: false,
});
}); });
} }
render() { render() {
return <YooForumUI topics={this.state.topics} hint={this.state.hint} />; return (
<YooForumUI
topics={this.state.topics}
hint={this.state.hint}
getTopicList={this.getTopicList}
gettingTopicList={this.state.gettingTopicList}
/>
);
} }
} }

11
src/ui/YooBackground.js

@ -10,6 +10,17 @@ export default function YooBackground() {
zIndex: -1, zIndex: -1,
}, },
}); });
// The animation won't play for the first 10 seconds
// if we just use setInterval. So we start the animation
// at the component mounted just for the first 10 seconds.
Animated.timing(imgPosition, {
toValue: {
x: -Math.floor(Math.random() * 5000),
y: -Math.floor(Math.random() * 3000),
},
duration: 10000,
useNativeDriver: true,
}).start();
setInterval(() => { setInterval(() => {
Animated.timing(imgPosition, { Animated.timing(imgPosition, {
toValue: { toValue: {

15
src/ui/YooForumTopicUI.js

@ -32,6 +32,7 @@ export default class YooForumTopicUI extends Component {
this.state = { this.state = {
detailShowing: false, detailShowing: false,
layoutY: 0, layoutY: 0,
layoutHeight: 0,
translate: new Animated.ValueXY({x: 0, y: 0}), translate: new Animated.ValueXY({x: 0, y: 0}),
}; };
this.topicHeaderRef = createRef(); this.topicHeaderRef = createRef();
@ -45,14 +46,9 @@ export default class YooForumTopicUI extends Component {
onPress() { onPress() {
if (!this.state.detailShowing) { if (!this.state.detailShowing) {
this.props.showDetail( this.props.showDetail(this);
this.state.translate,
this.state.layoutY,
this.onAnimationFinished,
this.props.replies,
);
} else { } else {
this.props.hideDetail(this.state.translate, this.onAnimationFinished); this.props.hideDetail();
} }
} }
@ -65,7 +61,10 @@ export default class YooForumTopicUI extends Component {
]} ]}
onPress={this.onPress} onPress={this.onPress}
onLayout={(event) => { onLayout={(event) => {
this.setState({layoutY: event.nativeEvent.layout.y}); this.setState({
layoutY: event.nativeEvent.layout.y,
layoutHeight: event.nativeEvent.layout.height,
});
}}> }}>
<View ref={this.topicHeaderRef}> <View ref={this.topicHeaderRef}>
<Text style={styles.owner}>{this.props.topic.owner}</Text> <Text style={styles.owner}>{this.props.topic.owner}</Text>

102
src/ui/YooForumUI.js

@ -8,6 +8,9 @@ import {
Image, Image,
Animated, Animated,
Dimensions, Dimensions,
Pressable,
BackHandler,
ToastAndroid,
} from 'react-native'; } from 'react-native';
import YooForumTopic from '../component/YooForumTopic'; import YooForumTopic from '../component/YooForumTopic';
import YooReply from './YooReply'; import YooReply from './YooReply';
@ -43,8 +46,37 @@ const styles = StyleSheet.create({
indicator: { indicator: {
marginTop: 64, marginTop: 64,
}, },
showMoreText: {
fontSize: 18,
margin: 8,
},
}); });
function ForumTopic(props) {
return props.topics.map((topic, key) => (
<YooForumTopic
key={key}
topic={topic}
showDetail={props.showDetail}
hideDetail={props.hideDetail}
/>
));
}
function ShowMore(props) {
return (
<Pressable
style={{width: screenWidth, alignItems: 'center'}}
onPress={() => props.getTopicList()}>
{props.gettingTopicList ? (
<Text style={styles.showMoreText}>加载中</Text>
) : (
<Text style={styles.showMoreText}>加载更多</Text>
)}
</Pressable>
);
}
export default class YooForumUI extends Component { export default class YooForumUI extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -54,26 +86,55 @@ export default class YooForumUI extends Component {
translateX: new Animated.Value(0), translateX: new Animated.Value(0),
currentReplies: [], currentReplies: [],
replyTranslateY: new Animated.Value(0), replyTranslateY: new Animated.Value(0),
lastPressingBack: 0,
}; };
this.replyRef = createRef(); this.replyRef = createRef();
this.showDetail = this.showDetail.bind(this); this.showDetail = this.showDetail.bind(this);
this.hideDetail = this.hideDetail.bind(this); this.hideDetail = this.hideDetail.bind(this);
this.handleAndroidBack = this.handleAndroidBack.bind(this);
this.currentForumTopicUI = null;
} }
showDetail(translate, layoutY, onAnimationFinished, replies) { componentDidMount() {
this.backHandler = BackHandler.addEventListener(
'hardwareBackPress',
this.handleAndroidBack,
);
}
handleAndroidBack() {
if (this.currentForumTopicUI) {
this.hideDetail();
return true;
} else if (new Date().getTime() - this.state.lastPressingBack <= 1000) {
BackHandler.exitApp();
} else {
this.setState({lastPressingBack: new Date().getTime()});
ToastAndroid.show('再按一次以退出Yoomooc', ToastAndroid.SHORT);
return true;
}
}
showDetail(ForumTopicUI) {
this.currentForumTopicUI = ForumTopicUI;
this.replyRef.current.scrollTo({x: 0, y: 0, animated: false}); this.replyRef.current.scrollTo({x: 0, y: 0, animated: false});
this.setState({ this.setState({
scrollEnabled: false, scrollEnabled: false,
currentReplies: replies, currentReplies: this.currentForumTopicUI.props.replies,
}); });
this.state.replyTranslateY.setValue( this.state.replyTranslateY.setValue(
this.state.currentPosition + 100 + screenHeight, this.state.currentPosition +
this.currentForumTopicUI.state.layoutHeight +
screenHeight,
); );
Animated.parallel([ Animated.parallel([
Animated.timing(translate, { Animated.timing(this.currentForumTopicUI.state.translate, {
toValue: { toValue: {
x: screenWidth, x: screenWidth,
y: this.state.currentPosition - layoutY + 8, y:
this.state.currentPosition -
this.currentForumTopicUI.state.layoutY +
8,
}, },
duration: 250, duration: 250,
useNativeDriver: true, useNativeDriver: true,
@ -84,18 +145,21 @@ export default class YooForumUI extends Component {
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.timing(this.state.replyTranslateY, { Animated.timing(this.state.replyTranslateY, {
toValue: this.state.currentPosition + 100, toValue:
this.state.currentPosition +
this.currentForumTopicUI.state.layoutHeight +
8,
duration: 250, duration: 250,
delay: 250, delay: 250,
useNativeDriver: true, useNativeDriver: true,
}), }),
]).start(onAnimationFinished); ]).start(this.currentForumTopicUI.onAnimationFinished);
} }
hideDetail(translate, onAnimationFinished) { hideDetail() {
this.setState({scrollEnabled: true}); this.setState({scrollEnabled: true});
Animated.parallel([ Animated.parallel([
Animated.timing(translate, { Animated.timing(this.currentForumTopicUI.state.translate, {
toValue: { toValue: {
x: 0, x: 0,
y: 0, y: 0,
@ -113,7 +177,12 @@ export default class YooForumUI extends Component {
duration: 250, duration: 250,
useNativeDriver: true, useNativeDriver: true,
}), }),
]).start(onAnimationFinished); ]).start(
function () {
this.currentForumTopicUI.onAnimationFinished();
this.currentForumTopicUI = null;
}.bind(this),
);
} }
render() { render() {
@ -134,14 +203,17 @@ export default class YooForumUI extends Component {
this.setState({currentPosition: event.nativeEvent.contentOffset.y}) this.setState({currentPosition: event.nativeEvent.contentOffset.y})
}> }>
{this.props.hint === '' ? ( {this.props.hint === '' ? (
this.props.topics.map((topic, key) => ( <View>
<YooForumTopic <ForumTopic
key={key} topics={this.props.topics}
topic={topic}
showDetail={this.showDetail} showDetail={this.showDetail}
hideDetail={this.hideDetail} hideDetail={this.hideDetail}
/> />
)) <ShowMore
getTopicList={this.props.getTopicList}
gettingTopicList={this.props.gettingTopicList}
/>
</View>
) : ( ) : (
<View style={styles.hintContainer}> <View style={styles.hintContainer}>
<Image <Image

71
src/ui/YooLoginUI.js

@ -8,6 +8,8 @@ import {
Text, Text,
ActivityIndicator, ActivityIndicator,
ToastAndroid, ToastAndroid,
Animated,
Keyboard,
} from 'react-native'; } from 'react-native';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -45,15 +47,37 @@ const styles = StyleSheet.create({
fontSize: 26, fontSize: 26,
}, },
}); });
const AnimatedInput = Animated.createAnimatedComponent(TextInput);
const AnimatedImage = Animated.createAnimatedComponent(Image);
const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity);
export default class YooLoginUI extends Component { export default class YooLoginUI extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
loading: false, loading: false,
translateY: new Animated.Value(0),
}; };
this.onButtonPress = this.onButtonPress.bind(this); this.onButtonPress = this.onButtonPress.bind(this);
this.onLoginFailed = this.onLoginFailed.bind(this); this.onLoginFailed = this.onLoginFailed.bind(this);
this.onKeyboardDidShow = this.onKeyboardDidShow.bind(this);
this.onKeyboardDidHide = this.onKeyboardDidHide.bind(this);
}
componentDidMount() {
this.keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow',
this.onKeyboardDidShow,
);
this.keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide',
this.onKeyboardDidHide,
);
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
} }
onButtonPress() { onButtonPress() {
@ -68,31 +92,64 @@ export default class YooLoginUI extends Component {
ToastAndroid.show(hint, ToastAndroid.SHORT); ToastAndroid.show(hint, ToastAndroid.SHORT);
} }
onKeyboardDidShow() {
Animated.timing(this.state.translateY, {
toValue: -320,
duration: 250,
useNativeDriver: true,
}).start();
}
onKeyboardDidHide() {
Animated.timing(this.state.translateY, {
toValue: 0,
duration: 250,
useNativeDriver: true,
}).start();
}
render() { render() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Image source={require('../../assets/icon.png')} style={styles.logo} /> <AnimatedImage
<TextInput source={require('../../assets/icon.png')}
style={styles.input} style={[
styles.logo,
{transform: [{translateY: this.state.translateY}]},
]}
/>
<AnimatedInput
style={[
styles.input,
{transform: [{translateY: this.state.translateY}]},
]}
placeholder={'账号'} placeholder={'账号'}
maxLength={10} maxLength={10}
value={this.props.username} value={this.props.username}
onChangeText={(username) => this.props.setUsername(username)} onChangeText={(username) => this.props.setUsername(username)}
/> />
<TextInput <AnimatedInput
style={styles.input} style={[
styles.input,
{transform: [{translateY: this.state.translateY}]},
]}
placeholder={'密码'} placeholder={'密码'}
secureTextEntry={true} secureTextEntry={true}
value={this.props.password} value={this.props.password}
onChangeText={(password) => this.props.setPassword(password)} onChangeText={(password) => this.props.setPassword(password)}
/> />
<TouchableOpacity style={styles.button} onPress={this.onButtonPress}> <AnimatedTouchableOpacity
style={[
styles.button,
{transform: [{translateY: this.state.translateY}]},
]}
onPress={this.onButtonPress}>
{this.state.loading ? ( {this.state.loading ? (
<ActivityIndicator size="large" color="#00ff00" /> <ActivityIndicator size="large" color="#00ff00" />
) : ( ) : (
<Text style={styles.buttonText}>登录</Text> <Text style={styles.buttonText}>登录</Text>
)} )}
</TouchableOpacity> </AnimatedTouchableOpacity>
</View> </View>
); );
} }

Loading…
Cancel
Save