Browse Source

Merge pull request 'Merge 0.0.6a' (#2) from dev into master

Reviewed-on: https://git.kdxcxs.com:34472/kdxcxs/YooMoocRN/pulls/2
master
kdxcxs 4 years ago
parent
commit
79d4f5d179
  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 是一个第三方的优慕课客户端
![screenshot](./screenshot.jpg)
<img src="./screenshot.jpg" alt="screenshot" style="height:500px;" />
## 🎞 背景
@ -18,9 +18,9 @@
## 📄 TODO
- [ ] 弹出键盘不遮挡输入框
- [ ] 根据像素比例设置登录背景图片大小
- [ ] 添加应用图标
- [x] 弹出键盘不遮挡输入框
- [ ] ~~根据像素比例设置登录背景图片大小~~
- [x] 添加应用图标
- [ ] 适配 iOS 登录失败 toast
- [ ] 在启动时检测是否已登录
- [ ] 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 = {
hint: '准备中',
topics: [],
fetchedListPage: 0,
gettingTopicList: false,
};
this.initForum = this.initForum.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',
},
)
.then(() => {
this.getTopicList();
})
.then(this.getTopicList)
.catch(ShowErrorToast);
})
.catch(ShowErrorToast);
@ -101,8 +101,10 @@ export default class YooForum extends Component {
.catch(ShowErrorToast);
}
getTopicList(page = 1) {
this.setState({hint: '获取数据中'});
getTopicList() {
const page = this.state.fetchedListPage + 1;
page === 1 && this.setState({hint: '获取数据中'});
this.setState({gettingTopicList: true});
gbkFetch(
'GET',
`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(),
);
});
this.setState({topics: [...this.state.topics, ...newTopics], hint: ''});
this.setState({
topics: [...this.state.topics, ...newTopics],
hint: '',
fetchedListPage: page,
gettingTopicList: false,
});
});
}
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,
},
});
// 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(() => {
Animated.timing(imgPosition, {
toValue: {

15
src/ui/YooForumTopicUI.js

@ -32,6 +32,7 @@ export default class YooForumTopicUI extends Component {
this.state = {
detailShowing: false,
layoutY: 0,
layoutHeight: 0,
translate: new Animated.ValueXY({x: 0, y: 0}),
};
this.topicHeaderRef = createRef();
@ -45,14 +46,9 @@ export default class YooForumTopicUI extends Component {
onPress() {
if (!this.state.detailShowing) {
this.props.showDetail(
this.state.translate,
this.state.layoutY,
this.onAnimationFinished,
this.props.replies,
);
this.props.showDetail(this);
} 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}
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}>
<Text style={styles.owner}>{this.props.topic.owner}</Text>

102
src/ui/YooForumUI.js

@ -8,6 +8,9 @@ import {
Image,
Animated,
Dimensions,
Pressable,
BackHandler,
ToastAndroid,
} from 'react-native';
import YooForumTopic from '../component/YooForumTopic';
import YooReply from './YooReply';
@ -43,8 +46,37 @@ const styles = StyleSheet.create({
indicator: {
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 {
constructor(props) {
super(props);
@ -54,26 +86,55 @@ export default class YooForumUI extends Component {
translateX: new Animated.Value(0),
currentReplies: [],
replyTranslateY: new Animated.Value(0),
lastPressingBack: 0,
};
this.replyRef = createRef();
this.showDetail = this.showDetail.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.setState({
scrollEnabled: false,
currentReplies: replies,
currentReplies: this.currentForumTopicUI.props.replies,
});
this.state.replyTranslateY.setValue(
this.state.currentPosition + 100 + screenHeight,
this.state.currentPosition +
this.currentForumTopicUI.state.layoutHeight +
screenHeight,
);
Animated.parallel([
Animated.timing(translate, {
Animated.timing(this.currentForumTopicUI.state.translate, {
toValue: {
x: screenWidth,
y: this.state.currentPosition - layoutY + 8,
y:
this.state.currentPosition -
this.currentForumTopicUI.state.layoutY +
8,
},
duration: 250,
useNativeDriver: true,
@ -84,18 +145,21 @@ export default class YooForumUI extends Component {
useNativeDriver: true,
}),
Animated.timing(this.state.replyTranslateY, {
toValue: this.state.currentPosition + 100,
toValue:
this.state.currentPosition +
this.currentForumTopicUI.state.layoutHeight +
8,
duration: 250,
delay: 250,
useNativeDriver: true,
}),
]).start(onAnimationFinished);
]).start(this.currentForumTopicUI.onAnimationFinished);
}
hideDetail(translate, onAnimationFinished) {
hideDetail() {
this.setState({scrollEnabled: true});
Animated.parallel([
Animated.timing(translate, {
Animated.timing(this.currentForumTopicUI.state.translate, {
toValue: {
x: 0,
y: 0,
@ -113,7 +177,12 @@ export default class YooForumUI extends Component {
duration: 250,
useNativeDriver: true,
}),
]).start(onAnimationFinished);
]).start(
function () {
this.currentForumTopicUI.onAnimationFinished();
this.currentForumTopicUI = null;
}.bind(this),
);
}
render() {
@ -134,14 +203,17 @@ export default class YooForumUI extends Component {
this.setState({currentPosition: event.nativeEvent.contentOffset.y})
}>
{this.props.hint === '' ? (
this.props.topics.map((topic, key) => (
<YooForumTopic
key={key}
topic={topic}
<View>
<ForumTopic
topics={this.props.topics}
showDetail={this.showDetail}
hideDetail={this.hideDetail}
/>
))
<ShowMore
getTopicList={this.props.getTopicList}
gettingTopicList={this.props.gettingTopicList}
/>
</View>
) : (
<View style={styles.hintContainer}>
<Image

71
src/ui/YooLoginUI.js

@ -8,6 +8,8 @@ import {
Text,
ActivityIndicator,
ToastAndroid,
Animated,
Keyboard,
} from 'react-native';
const styles = StyleSheet.create({
@ -45,15 +47,37 @@ const styles = StyleSheet.create({
fontSize: 26,
},
});
const AnimatedInput = Animated.createAnimatedComponent(TextInput);
const AnimatedImage = Animated.createAnimatedComponent(Image);
const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity);
export default class YooLoginUI extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
translateY: new Animated.Value(0),
};
this.onButtonPress = this.onButtonPress.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() {
@ -68,31 +92,64 @@ export default class YooLoginUI extends Component {
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() {
return (
<View style={styles.container}>
<Image source={require('../../assets/icon.png')} style={styles.logo} />
<TextInput
style={styles.input}
<AnimatedImage
source={require('../../assets/icon.png')}
style={[
styles.logo,
{transform: [{translateY: this.state.translateY}]},
]}
/>
<AnimatedInput
style={[
styles.input,
{transform: [{translateY: this.state.translateY}]},
]}
placeholder={'账号'}
maxLength={10}
value={this.props.username}
onChangeText={(username) => this.props.setUsername(username)}
/>
<TextInput
style={styles.input}
<AnimatedInput
style={[
styles.input,
{transform: [{translateY: this.state.translateY}]},
]}
placeholder={'密码'}
secureTextEntry={true}
value={this.props.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 ? (
<ActivityIndicator size="large" color="#00ff00" />
) : (
<Text style={styles.buttonText}>登录</Text>
)}
</TouchableOpacity>
</AnimatedTouchableOpacity>
</View>
);
}

Loading…
Cancel
Save