上一节我们已经简单的实现了侧滑删除,
我们最后还留了一个小小的功能,那就是当手指抬起的时候,我们需要选择打开或者关闭侧滑功能,我们分几种情况考虑:
大的条件分两种:
一、侧滑已经打开
1、抬起手指的时候,如果偏移的距离>=删除按钮距离(打开)
1、抬起手指的时候,如果偏移的距离<删除按钮距离(关闭)
二、侧滑没有打开
1、抬起手指的时候,如果偏移的距离>=删除按钮距离*偏移量(打开)
1、抬起手指的时候,如果偏移的距离<删除按钮距离*偏移量(关闭)
有了逻辑之后我们的代码就很简单了:
/**
* 结束事件的时候回调
* @param event
* @param gestureState
* @private
*/
_handlePanResponderEnd(event: Object, gestureState: Object): void {
if(this._isOpen){
if (Math.abs(this.state.currentLeft._value) >= 100) {
this._animateToOpenPosition();
} else {
this._animateToClosedPosition();
}
}else{
if (Math.abs(this.state.currentLeft._value) >= 100 / 3) {
this._animateToOpenPosition();
} else {
this._animateToClosedPosition();
}
}
this._previousLeft = null;
}
最后是SwipeRow.js的全部代码:
/**
* @author YASIN
* @version [React-Native Ocj V01, 2018/3/13]
* @date 17/2/23
* @description SwipeRow
*/
import React, {
Component,
} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
PanResponder,
Platform,
StyleSheet,
TouchableOpacity,
ViewPropTypes,
View,
Text
} from 'react-native';
export default class SwipeRow extends Component {
// 构造
constructor(props) {
super(props);
// 初始状态
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: this._handleMoveShouldSetPanResponderCapture.bind(this),
onPanResponderGrant: this._handlePanResponderGrant.bind(this),
onPanResponderMove: this._handlePanResponderMove.bind(this),
onPanResponderRelease: this._handlePanResponderEnd.bind(this),
onPanResponderTerminate: this._handlePanResponderEnd.bind(this),
onShouldBlockNativeResponder: (event, gestureState) => false,//表示是否用 Native 平台的事件处理,默认是禁用的,全部使用 JS 中的事件处理,注意此函数目前只能在 Android 平台上使用
});
//上一次滑动最后的left偏移量
this._previousLeft = 0;
//left偏移动画
this.state = {
currentLeft: new Animated.Value(this._previousLeft),
};
this._isOpen = false;
}
render() {
return (
<View style={[styles.swipeContainer, this.props.style]}>
<View style={styles.swipeActions}>
{this.props.children[0]}
</View>
{this.renderRowContent()}
</View>
);
}
renderRowContent() {
return (
<Animated.View
{...this._panResponder.panHandlers}
style={{
transform: [
{translateX: this.state.currentLeft}
]
}}
>
{this.props.children[1]}
</Animated.View>
);
}
/**
* 是否需要成为move事件响应者,返回true直接走onPanResponderMove
* @param event
* @param gestureState
* @returns {boolean}
* @private
*/
_handleMoveShouldSetPanResponderCapture(event: Object, gestureState: Object,): boolean {
//当垂直滑动的距离<10 水平滑动的距离>10的时候才让捕获事件
console.log('_handleMoveShouldSetPanResponderCapture');
return Math.abs(gestureState.dy) < 10 && Math.abs(gestureState.dx) > 10;
}
/**
* 表示申请成功,组件成为了事件处理响应者
* @param event
* @param gestureState
* @private
*/
_handlePanResponderGrant(event: Object, gestureState: Object): void {
console.log('_handlePanResponderGrant');
}
/**
* 处理滑动事件
* @param event
* @param gestureState
* @private
*/
_handlePanResponderMove(event: Object, gestureState: Object): void {
if (this._previousLeft === null) {
this._previousLeft = this.state.currentLeft._value
}
let nowLeft = this._previousLeft + gestureState.dx * 1;
//右滑最大距离为0(边界值)
nowLeft = Math.min(nowLeft, 0);
this.state.currentLeft.setValue(
nowLeft,
);
}
/**
* 结束事件的时候回调
* @param event
* @param gestureState
* @private
*/
_handlePanResponderEnd(event: Object, gestureState: Object): void {
if(this._isOpen){
if (Math.abs(this.state.currentLeft._value) >= 100) {
this._animateToOpenPosition();
} else {
this._animateToClosedPosition();
}
}else{
if (Math.abs(this.state.currentLeft._value) >= 100 / 3) {
this._animateToOpenPosition();
} else {
this._animateToClosedPosition();
}
}
this._previousLeft = null;
}
_shouldAnimateRemainder(gestureState: Object): boolean {
/**
* If user has swiped past a certain distance, animate the rest of the way
* if they let go
*/
return (
Math.abs(gestureState.dx) > 100 / 3 ||
gestureState.vx > 0.3
);
}
_animateToOpenPosition(): void {
this._isOpen = true;
this._animateTo(-100);
}
_animateToClosedPosition(duration: number = 300): void {
this._isOpen = false;
this._animateTo(0, duration);
}
_animateTo(toValue, duration = 300, callback): void {
Animated.spring(
this.state.currentLeft,
{
toValue,
}
).start((value) => {
});
}
}
const styles = StyleSheet.create({
swipeContainer: {
width: '100%',
},
swipeActions: {
backgroundColor: 'grey',
width: '100%',
overflow: 'hidden',
...StyleSheet.absoluteFillObject,
flexDirection: 'row',
justifyContent: 'flex-end'
},
});
我们运行项目:
好啦!!我们算是简单实现了我们的侧滑删除控件了,小伙伴自己动手敲一敲哦~
其实在RN的官方sdk中也是有侧滑删除控件的,只是fb没有在文档上暴露出来而已,下面我们就一起看看fb大牛是怎么封装控件的吧~
我们找到这么一个文件:
可以看到,fb直接把listview跟flatlist都封装了起来,我就不带着去研究了哈,我们重点看一下SwipeableRow.js
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule SwipeableRow
* @flow
*/
'use strict';
const Animated = require('Animated');
const I18nManager = require('I18nManager');
const PanResponder = require('PanResponder');
const React = require('React');
const PropTypes = require('prop-types');
const StyleSheet = require('StyleSheet');
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
* found when Flow v0.54 was deployed. To see the error delete this comment and
* run Flow. */
const TimerMixin = require('react-timer-mixin');
const View = require('View');
const createReactClass = require('create-react-class');
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
* found when Flow v0.54 was deployed. To see the error delete this comment and
* run Flow. */
const emptyFunction = require('fbjs/lib/emptyFunction');
const IS_RTL = I18nManager.isRTL;
// NOTE: Eventually convert these consts to an input object of configurations
// Position of the left of the swipable item when closed
const CLOSED_LEFT_POSITION = 0;
// Minimum swipe distance before we recognize it as such
const HORIZONTAL_SWIPE_DISTANCE_THRESHOLD = 10;
// Minimum swipe speed before we fully animate the user's action (open/close)
const HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD = 0.3;
// Factor to divide by to get slow speed; i.e. 4 means 1/4 of full speed
const SLOW_SPEED_SWIPE_FACTOR = 4;
// Time, in milliseconds, of how long the animated swipe should be
const SWIPE_DURATION = 300;
/**
* On SwipeableListView mount, the 1st item will bounce to show users it's
* possible to swipe
*/
const ON_MOUNT_BOUNCE_DELAY = 700;
const ON_MOUNT_BOUNCE_DURATION = 400;
// Distance left of closed position to bounce back when right-swiping from closed
const RIGHT_SWIPE_BOUNCE_BACK_DISTANCE = 30;
const RIGHT_SWIPE_BOUNCE_BACK_DURATION = 300;
/**
* Max distance of right swipe to allow (right swipes do functionally nothing).
* Must be multiplied by SLOW_SPEED_SWIPE_FACTOR because gestureState.dx tracks
* how far the finger swipes, and not the actual animation distance.
*/
const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR;
/**
* Creates a swipable row that allows taps on the main item and a custom View
* on the item hidden behind the row. Typically this should be used in
* conjunction with SwipeableListView for additional functionality, but can be
* used in a normal ListView. See the renderRow for SwipeableListView to see how
* to use this component separately.
*/
const SwipeableRow = createReactClass({
displayName: 'SwipeableRow',
_panResponder: {},
_previousLeft: CLOSED_LEFT_POSITION,
mixins: [TimerMixin],
propTypes: {
children: PropTypes.any,
isOpen: PropTypes.bool,
preventSwipeRight: PropTypes.bool,
maxSwipeDistance: PropTypes.number.isRequired,
onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onSwipeEnd: PropTypes.func.isRequired,
onSwipeStart: PropTypes.func.isRequired,
// Should bounce the row on mount
shouldBounceOnMount: PropTypes.bool,
/**
* A ReactElement that is unveiled when the user swipes
*/
slideoutView: PropTypes.node.isRequired,
/**
* The minimum swipe distance required before fully animating the swipe. If
* the user swipes less than this distance, the item will return to its
* previous (open/close) position.
*/
swipeThreshold: PropTypes.number.isRequired,
},
getInitialState(): Object {
return {
currentLeft: new Animated.Value(this._previousLeft),
/**
* In order to render component A beneath component B, A must be rendered
* before B. However, this will cause "flickering", aka we see A briefly
* then B. To counter this, _isSwipeableViewRendered flag is used to set
* component A to be transparent until component B is loaded.
*/
isSwipeableViewRendered: false,
rowHeight: (null: ?number),
};
},
getDefaultProps(): Object {
return {
isOpen: false,
preventSwipeRight: false,
maxSwipeDistance: 0,
onOpen: emptyFunction,
onClose: emptyFunction,
onSwipeEnd: emptyFunction,
onSwipeStart: emptyFunction,
swipeThreshold: 30,
};
},
UNSAFE_componentWillMount(): void {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: this._handleMoveShouldSetPanResponderCapture,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminationRequest: this._onPanResponderTerminationRequest,
onPanResponderTerminate: this._handlePanResponderEnd,
onShouldBlockNativeResponder: (event, gestureState) => false,
});
},
componentDidMount(): void {
if (this.props.shouldBounceOnMount) {
/**
* Do the on mount bounce after a delay because if we animate when other
* components are loading, the animation will be laggy
*/
this.setTimeout(() => {
this._animateBounceBack(ON_MOUNT_BOUNCE_DURATION);
}, ON_MOUNT_BOUNCE_DELAY);
}
},
UNSAFE_componentWillReceiveProps(nextProps: Object): void {
/**
* We do not need an "animateOpen(noCallback)" because this animation is
* handled internally by this component.
*/
if (this.props.isOpen && !nextProps.isOpen) {
this._animateToClosedPosition();
}
},
render(): React.Element<any> {
// The view hidden behind the main view
let slideOutView;
if (this.state.isSwipeableViewRendered && this.state.rowHeight) {
slideOutView = (
<View style={[
styles.slideOutContainer,
{height: this.state.rowHeight},
]}>
{this.props.slideoutView}
</View>
);
}
// The swipeable item
const swipeableView = (
<Animated.View
onLayout={this._onSwipeableViewLayout}
style={{transform: [{translateX: this.state.currentLeft}]}}>
{this.props.children}
</Animated.View>
);
return (
<View
{...this._panResponder.panHandlers}>
{slideOutView}
{swipeableView}
</View>
);
},
close(): void {
this.props.onClose();
this._animateToClosedPosition();
},
_onSwipeableViewLayout(event: Object): void {
this.setState({
isSwipeableViewRendered: true,
rowHeight: event.nativeEvent.layout.height,
});
},
_handleMoveShouldSetPanResponderCapture(
event: Object,
gestureState: Object,
): boolean {
// Decides whether a swipe is responded to by this component or its child
return gestureState.dy < 10 && this._isValidSwipe(gestureState);
},
_handlePanResponderGrant(event: Object, gestureState: Object): void {
},
_handlePanResponderMove(event: Object, gestureState: Object): void {
if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) {
return;
}
this.props.onSwipeStart();
if (this._isSwipingRightFromClosed(gestureState)) {
this._swipeSlowSpeed(gestureState);
} else {
this._swipeFullSpeed(gestureState);
}
},
_isSwipingRightFromClosed(gestureState: Object): boolean {
const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx;
return this._previousLeft === CLOSED_LEFT_POSITION && gestureStateDx > 0;
},
_swipeFullSpeed(gestureState: Object): void {
this.state.currentLeft.setValue(this._previousLeft + gestureState.dx);
},
_swipeSlowSpeed(gestureState: Object): void {
this.state.currentLeft.setValue(
this._previousLeft + gestureState.dx / SLOW_SPEED_SWIPE_FACTOR,
);
},
_isSwipingExcessivelyRightFromClosedPosition(gestureState: Object): boolean {
/**
* We want to allow a BIT of right swipe, to allow users to know that
* swiping is available, but swiping right does not do anything
* functionally.
*/
const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx;
return (
this._isSwipingRightFromClosed(gestureState) &&
gestureStateDx > RIGHT_SWIPE_THRESHOLD
);
},
_onPanResponderTerminationRequest(
event: Object,
gestureState: Object,
): boolean {
return false;
},
_animateTo(
toValue: number,
duration: number = SWIPE_DURATION,
callback: Function = emptyFunction,
): void {
Animated.timing(
this.state.currentLeft,
{
duration,
toValue,
useNativeDriver: true,
},
).start(() => {
this._previousLeft = toValue;
callback();
});
},
_animateToOpenPosition(): void {
const maxSwipeDistance = IS_RTL ? -this.props.maxSwipeDistance : this.props.maxSwipeDistance;
this._animateTo(-maxSwipeDistance);
},
_animateToOpenPositionWith(
speed: number,
distMoved: number,
): void {
/**
* Ensure the speed is at least the set speed threshold to prevent a slow
* swiping animation
*/
speed = (
speed > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD ?
speed :
HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD
);
/**
* Calculate the duration the row should take to swipe the remaining distance
* at the same speed the user swiped (or the speed threshold)
*/
const duration = Math.abs((this.props.maxSwipeDistance - Math.abs(distMoved)) / speed);
const maxSwipeDistance = IS_RTL ? -this.props.maxSwipeDistance : this.props.maxSwipeDistance;
this._animateTo(-maxSwipeDistance, duration);
},
_animateToClosedPosition(duration: number = SWIPE_DURATION): void {
this._animateTo(CLOSED_LEFT_POSITION, duration);
},
_animateToClosedPositionDuringBounce(): void {
this._animateToClosedPosition(RIGHT_SWIPE_BOUNCE_BACK_DURATION);
},
_animateBounceBack(duration: number): void {
/**
* When swiping right, we want to bounce back past closed position on release
* so users know they should swipe right to get content.
*/
const swipeBounceBackDistance = IS_RTL ?
-RIGHT_SWIPE_BOUNCE_BACK_DISTANCE :
RIGHT_SWIPE_BOUNCE_BACK_DISTANCE;
this._animateTo(
-swipeBounceBackDistance,
duration,
this._animateToClosedPositionDuringBounce,
);
},
// Ignore swipes due to user's finger moving slightly when tapping
_isValidSwipe(gestureState: Object): boolean {
if (this.props.preventSwipeRight && this._previousLeft === CLOSED_LEFT_POSITION && gestureState.dx > 0) {
return false;
}
return Math.abs(gestureState.dx) > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD;
},
_shouldAnimateRemainder(gestureState: Object): boolean {
/**
* If user has swiped past a certain distance, animate the rest of the way
* if they let go
*/
return (
Math.abs(gestureState.dx) > this.props.swipeThreshold ||
gestureState.vx > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD
);
},
_handlePanResponderEnd(event: Object, gestureState: Object): void {
const horizontalDistance = IS_RTL ? -gestureState.dx : gestureState.dx;
if (this._isSwipingRightFromClosed(gestureState)) {
this.props.onOpen();
this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION);
} else if (this._shouldAnimateRemainder(gestureState)) {
if (horizontalDistance < 0) {
// Swiped left
this.props.onOpen();
this._animateToOpenPositionWith(gestureState.vx, horizontalDistance);
} else {
// Swiped right
this.props.onClose();
this._animateToClosedPosition();
}
} else {
if (this._previousLeft === CLOSED_LEFT_POSITION) {
this._animateToClosedPosition();
} else {
this._animateToOpenPosition();
}
}
this.props.onSwipeEnd();
},
});
const styles = StyleSheet.create({
slideOutContainer: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
});
module.exports = SwipeableRow;
小伙伴看了是不是觉得很熟悉呢?哈哈~对的!思路大概跟我们的差不多,只不过把一些渲染跟变量抽离出来了,想必小伙伴都能很轻松的看懂者代码了,因为项目中会出现一个侧滑的删除事件跟flatlist跟listview这类控件的手势冲突,那我们就看看fb那帮老头是咋解决冲突的,
我们可以看到在手势开始也就是_handlePanResponderMove方法中:
_handlePanResponderMove(event: Object, gestureState: Object): void {
if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) {
return;
}
this.props.onSwipeStart();
if (this._isSwipingRightFromClosed(gestureState)) {
this._swipeSlowSpeed(gestureState);
} else {
this._swipeFullSpeed(gestureState);
}
},
用了一个回调:
this.props.onSwipeStart();
那我就去SwipeableFlatList.js看看回调后看了啥:
return (
<SwipeableRow
slideoutView={slideoutView}
isOpen={key === this.state.openRowKey}
maxSwipeDistance={this._getMaxSwipeDistance(info)}
onOpen={() => this._onOpen(key)}
onClose={() => this._onClose(key)}
shouldBounceOnMount={shouldBounceOnMount}
onSwipeEnd={this._setListViewScrollable}
onSwipeStart={this._setListViewNotScrollable}>
{this.props.renderItem(info)}
</SwipeableRow>
);
可以看到调用了_setListViewNotScrollable方法:
_setListViewNotScrollable = () => {
this._setListViewScrollableTo(false);
};
继续走最后调用了:
_setListViewScrollableTo(value: boolean) {
if (this._flatListRef) {
this._flatListRef.setNativeProps({
scrollEnabled: value,
});
}
}
好吧,看到这里大家是不是有点感触了,这里的_flatListRef引用就是我们的flatlist,然后通过setNativeProps方法把flatlist的scrollEnabled设为了false,(也就是当滑动的时候通知flatlist你不要滑动了)然后当手指抬起的时候告诉flatlist,你可以滑动了,也就是下面方法。
onSwipeEnd={this._setListViewScrollable}
_setListViewScrollable = () => {
this._setListViewScrollableTo(true);
};
好啦~说到这里我们不禁对rn的事件传递机制提出了疑问,明明子控件都已经消耗掉了滑动事件,为啥父控件还会收到事件,盗用下网上帖子的一张图:
我们的C组件onMoveShouldSetPanResponderCapture都直接返回true了,为啥还会走到A组件????我也有点疑惑哈~还没去研究native源码的,小伙伴知道的可以跟我说说哈~拜谢!!!
好啦! 看完事件冲突,我们顺便研究下setNativeProps方法,
大家多多少少用过或者了解过setNativeProps方法,我们简单的带着大家撸一撸源码,我们打开View的源码,
大家可以看到,view里面根本就没有这个方法,于是我们看看mixins里面有没有?,果然是看到了~~
mixins: [NativeMethodsMixin],
我们点开NativeMethodsMixin文件:
'use strict';
const {
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
} = require('ReactNative');
import type {NativeMethodsMixinType} from 'ReactNativeTypes';
const {NativeMethodsMixin} = __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
module.exports = ((NativeMethodsMixin: any): $Exact<NativeMethodsMixinType>);
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED这尼玛是什么鬼?不懂!继续走:
'use strict';
import type {ReactNativeType} from 'ReactNativeTypes';
let ReactNative;
if (__DEV__) {
ReactNative = require('ReactNativeRenderer-dev');
} else {
ReactNative = require('ReactNativeRenderer-prod');
}
module.exports = (ReactNative: ReactNativeType);
我们最终在ReactNativeRenderer-dev.js中找到了NativeMethodsMixin对象,然后看到了setNativeProps方法:
setNativeProps: function(nativeProps) {
// Class components don't have viewConfig -> validateAttributes.
// Nor does it make sense to set native props on a non-native component.
// Instead, find the nearest host component and set props on it.
// Use findNodeHandle() rather than findNumericNodeHandle() because
// We want the instance/wrapper (not the native tag).
var maybeInstance = void 0;
// Fiber errors if findNodeHandle is called for an umounted component.
// Tests using ReactTestRenderer will trigger this case indirectly.
// Mimicking stack behavior, we should silently ignore this case.
// TODO Fix ReactTestRenderer so we can remove this try/catch.
try {
maybeInstance = findNodeHandle(this);
} catch (error) {}
// If there is no host component beneath this we should fail silently.
// This is not an error; it could mean a class component rendered null.
if (maybeInstance == null) {
return;
}
var viewConfig = maybeInstance.viewConfig;
{
warnForStyleProps(nativeProps, viewConfig.validAttributes);
}
var updatePayload = create(nativeProps, viewConfig.validAttributes);
// Avoid the overhead of bridge calls if there's no update.
// This is an expensive no-op for Android, and causes an unnecessary
// view invalidation for certain components (eg RCTTextInput) on iOS.
if (updatePayload != null) {
UIManager.updateView(
maybeInstance._nativeTag,
viewConfig.uiViewClassName,
updatePayload
);
}
},
其实最主要就是执行了:
UIManager.updateView(
maybeInstance._nativeTag,
viewConfig.uiViewClassName,
updatePayload
);
UIManager是native的模块,所以我们看看native干了啥:
@ReactMethod
public void updateView(int tag, String className, ReadableMap props) {
this.mUIImplementation.updateView(tag, className, props);
}
好吧~ android里面逻辑有点复杂了,我简单说一下思路 native根据传tag findviewbyid找到特定的view—>然后根据传过来的uiViewClassName找到manager—>把updatePayload传给manager–>最后直接view.setxxx设置属性
我很天真的认为所有的自定义组件也可以直接调用:
UIManager.updateView(
maybeInstance._nativeTag,
viewConfig.uiViewClassName,
updatePayload
);
如果可以直接调用,我就可以直接修改我直接view的某些属性而不需要render了,但是事实是native根据传tag findviewbyid找到特定的view—>然后根据传过来的uiViewClassName找到manager—>把updatePayload传给manager–>最后直接view.setxxx设置属性;
然后根据传过来的uiViewClassName找到manage走到这一步的时候拿到的manager是viewmanager,尴尬!!!! 好吧~ 有知道的童鞋告知一下,拜谢!!!