缩小时不允许滚动,只有上滑动能唤醒动画,移至指定位置
完全展示出来后可以内部进行滑动,当滑动到顶部,再向上滑(手势是从上至下)时,缩小整个 list
list 采用 react-native 组件 ScrollView / FlatView / SectionList
动画: react-native 自带一套动画系统,性能尚可
手势: react-native 自带手势响应系统
前期知识准备不足的童靴,可以刷刷文档
手势响应系统
动画
// children
ScrollView 没有什么好说的, 设置好样式,定位在页面中下方就可以了。
需要注意的是高度设置成展开后的高度,要么无法滚动。
如果要使用动画的话,需要改变的是 ScrollView 的 top 值。
首先在 constructor 中定义 state, 为初始化的 top 值
this._top = 500
this.state = {
topValue: new Animated.Value(this._top)
}
之后来写两个动画开始的函数
Animated.timing(
this.state.fadeAnim,
{
toValue: 100,
duration: 500,
}
).start(() => {
this.setState({listScroll: false}) // 开启 or 关闭 ScrollView 滚动
}
)
向上 / 向下的函是一样的,值不一样而已,就不多赘述了。
组件 & 动画设计好了,就差什么时候触发动画了!
首先先给 ScrollView 一套自定义的手势响应
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture:this._handleMoveShouldSetPanResponderCapture,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd
})
这几个函数都没得说,在文档里已经写得很清楚了。下面我们来看看什么时候触发向上的动画。前面说了在初始画面里向上滚动,列表会变长,那么就判断是不是向上滚动。
_handlePanResponderEnd = (event, gestureState) => {
// 我们只需要在这里判断 gestureState.dy 的值是否为正负
if (gestureState.dy < 0) {
// 执行向上移动画
}
}
向上很简单,向下移动(缩小) 的话需要判断,用户已经在在 ScrollView 滚动到顶部,并且手势是向下滑动的,才可以收回 ScrollView。
// children
this._handleScrollEnd = (e) => {
this._switchScrollBottom = e.nativeEvent.contentOffset.y // 保存最后滚动位置
}
// 在申请成功时候保存上次滚动
_handlePanResponderGrant = (event, gestureState) => {
this._previousScrollValue = this._switchScrollBottom
}
_handlePanResponderEnd = (event, gestureState) => {
// 如果上次滚动和这次滚动位置的值一样,证明没有滚动, 已经到达顶部了。
// 这时候则触发移动至 bottom 的动画
if (gestureState.dy > 0) {
if (this._previousScrollValue == this._switchScrollBottom) {
this._scrollAnimToBotton()
}
}
rn 自带的动画系统性能还是可以的,而且使用起来也比较方便, 和手势系统一同使用可以解决很多需求。本文章只是大概提供了思路,由于时间紧迫,没来得及写 dome, 所以都是一些伪代码。如果遇到问题可以在下方留言,欢迎一同讨论!
————————————————
写在开始之前的话,说好的一个星期一篇的牛逼已然破了,破了就破了吧,我们该坚持的东西,还是需要坚持,不是吗?那么开始吧...
我们知道在react native
中,如果想要设置点击事件的话,并不像android
中那样,
可以直接设置View.setOnClickListener
,而是需要通过触摸控件来完成添加点击事
件,需要通过以下Touchable**
来设置:
TouchableHighlight,
TouchableNativeFeedback,
TouchableOpacity,
TouchableWithoutFeedback
他们的功能和使用方法基本类似,只是在Touch
的时候反馈的效果不同,他们有以下几个回调方法:
onPressIn:点击开始
onPressOut:点击结束或离开
onPress:单击事件回调
onLongPress:长按事件回调
console.log("onPressIn")}
onPressOut={() => console.log("onPressOut")}
onPress={() => console.log("onPress")}
onLongPress={() => console.log("onLongPress")}
>
用法比较简单,这里不再介绍。下面我们了解一下给不能点击的组件添加点击事件呢?
我们知道普通的组件并不能设置点击事件,那是因为普通的组件,它并不是触摸响应者,而想成为触摸响应者的话就需要你申请。
想要申请成为触摸着,需要通过下面两个方法:
View.props.onStartShouldSetResponder
这个属性接受一个回调函数,如果返回true
就是申请成为触摸事件的响应者。
View.props.onMoveShouldSetResponder
这个属性接受一个回调函数,如果返回true
就是申请成为滑动过程中的响应者。
在实际的代码中,我们通过设置PanResponder
来申请和监听的触摸事件。
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
其中
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
是表示,是否成为事件的劫持者,如果返回是,则不会把事件传递给它的子元素
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
表示是否成为滑动事件的劫持者,如果返回是,则不会把滑动事件传递给它的子元素。
onPanResponderGrant: (evt, gestureState) => {}
onPanResponderMove: (evt, gestureState) => {}
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {}
onPanResponderTerminate: (evt, gestureState) => {}
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
}
图片
在上面的代码中,我们看到,在触摸开始到滑动,在到触摸结束的过程中,会返回两个参数,evt
和gestureState
,下面我们介绍一下这两个参数:
触摸事件的参数--nativeEvent
nativeEvent
changedTouches - 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)
identifier - 触摸点的ID
locationX - 触摸点相对于父元素的横坐标
locationY - 触摸点相对于父元素的纵坐标
pageX - 触摸点相对于根元素的横坐标
pageY - 触摸点相对于根元素的纵坐标
target - 触摸点所在的元素ID
timestamp - 触摸事件的时间戳,可用于移动速度的计算
touches - 当前屏幕上的所有触摸点的集合
gestureState
stateID - 触摸状态的ID。在屏幕上有至少一个触摸点的情况下,这个ID会一直有效。
moveX - 最近一次移动时的屏幕横坐标
moveY - 最近一次移动时的屏幕纵坐标
x0 - 当响应器产生时的屏幕坐标
y0 - 当响应器产生时的屏幕坐标
dx - 从触摸操作开始时的累计横向路程
dy - 从触摸操作开始时的累计纵向路程
vx - 当前的横向移动速度
vy - 当前的纵向移动速度
numberActiveTouches - 当前在屏幕上的有效触摸点的数量
我们看一个具体的例子
效果如图:
image
代码如下:
import React, {PureComponent, Component} from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
PanResponder,
} from 'react-native';
export default class TouchStartAndRelease extends PureComponent {
constructor(props) {
super(props);
this.state = {
backgroundColor: 'red',
marginTop: 100,
marginLeft: 100,
};
this.lastX = this.state.marginLeft;
this.lastY = this.state.marginTop;
}
componentWillMount(){
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => {
return true;
},
onMoveShouldSetPanResponder: (evt, gestureState) => {
return true;
},
onPanResponderGrant: (evt, gestureState) => {
this._highlight();
},
onPanResponderMove: (evt, gestureState) => {
console.log(`gestureState.dx : ${gestureState.dx} gestureState.dy : ${gestureState.dy}`);
this.setState({
marginLeft: this.lastX + gestureState.dx,
marginTop: this.lastY + gestureState.dy,
});
},
onPanResponderRelease: (evt, gestureState) => {
this._unhighlight();
this.lastX = this.state.marginLeft;
this.lastY = this.state.marginTop;
},
onPanResponderTerminate: (evt, gestureState) => {
},
});
}
_unhighlight(){
this.setState({
backgroundColor: 'red',
});
}
_highlight(){
this.setState({
backgroundColor: 'blue',
});
}
render() {
return (
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
redView: {
width: 100,
height: 100,
},
});
为了做到图中的组件可以随我们的手指的滑动而更换位置,我们需要实时的设置它的绝对位置,left
和top
,这里面用到了两个属性gestureState.dx
和gestureState.dx
:
dx - 从触摸操作开始时的累计横向路程
dy - 从触摸操作开始时的累计纵向路程
我们通过记录它的初始位置marginLeft
和marginTop
,然后加上上面的dx
和dy
,也就是它的累积滑动距离,就知道了它的实时位置,然后把它设置给View
,就是上面我们想要的效果了。
在日历上添加横向滑动切换月份
这个是一开始我想要实现的效果,这里不弄gif
图了,太麻烦。
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => {
return true;
},
onMoveShouldSetPanResponder: (evt, gestureState) => {
return true;
},
onPanResponderGrant: (evt, gestureState) => {
},
onPanResponderMove: (evt, gestureState) => {
console.log(`gestureState.dx : ${gestureState.dx} gestureState.dy : ${gestureState.dy}`);
//上个月
if(gestureState.dx >100 && !this.state.isLoading){
this.setState({isLoading:true});
this.prevMonth();
//下个月
}else if(gestureState.dx < -100 && !this.state.isLoading){
this.setState({isLoading:true});
this.nextMonth();
}
},
onPanResponderRelease: (evt, gestureState) => {
this.setState({isLoading:false});
},
onPanResponderTerminate: (evt, gestureState) => {
},
});
}
当横向滑动的距离dx
大于100时,就是向右滑动,我们就切换到上个月,其中isLoading
用来判断是否正在刷新界面中。
距离dx
小于-100
时,就是向左滑,我们切换到下个月。
这里是View
的代码
{this.state.year +'年'+ (this.state.month<10?'0'+this.state.month:this.state.month) +'月'+ (this.state.day<10?'0'+this.state.day:this.state.day) +'日'}
我们通过{...this._panResponder.panHandlers}
这句把PanResponder
监听设置到想要设置的组件上,貌似不能设置在我们自定义的组件CalendarMain
上,我当时试了一下,没有起作用。
学习react native
的手势识别学习了下面的两篇文章,写的都非常好,其中的代码部分是出自下面的第一篇文章,向作者表示感谢。
panResponder详解及Demo
React Native 触摸事件处理详解
Over...