记一次使用RN实现侧滑删除

Posted 人生如梦91

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记一次使用RN实现侧滑删除相关的知识,希望对你有一定的参考价值。

最近公司全面推行使用React-Native进行跨平台开发,虽然我对这方面不熟悉,而且对RN也没有啥兴趣,但是也没办法,也没有学啥js和RN的东西,直接刚。正好要实现一个侧滑删除功能,网上搜了一波,没发现啥好的说明,于是决定自己实现一波。但由于自己没有系统的学过RN,所以写的不怎么给力。

首先看下效果图,如下所示:

要写这个组件,有两个问题要先解决,

  1. 这个页面已经存在了,我如何能在做较小修改的情况下,实现这样一个侧滑删除的组件?
  2. 如果不修改原来的组件添加滑动手势?

经过考虑之后,决定自定义一个容器组件,只要包裹上一层并提供对应的信息,即可拥有侧滑删除的功能,也许这并不是最好的实现方式,但起码功能已经有了。在我项目里的使用方式如下所示:

return (
            <MessageSwiper actions=[
                <MessageSwiperAction title='删除' color='red' action=this.actionDemo.bind(this) confirm=this.confirmDemo.bind(this) animatable=true/>,
                <MessageSwiperAction title='移动' color='green' action=this.actionDemo.bind(this) confirm=this.confirmDemo.bind(this) animatable=true/>
            ]>
                <View style=style.container>
                    <View style=flexDirection: 'row'>
                        <Image style=style.msgImage source=this.props.item.type === 'businessCustomerService' ? uri: this.props.item.serviceLogo : messageImage[this.props.item.type]/>
                        comp
                    </View>
                    <View style=flexDirection: 'column', flex: 1>
                        <View style=flexDirection: 'row'>
                            <Text style=style.msgTitle>this.props.item.serviceName</Text>
                            <Text style=style.msgTime>this._getDate(this.props.item.nowMsgTime)</Text>
                        </View>
                        <Text style=style.msgSubTitle ellipsizeMode='tail'
                              numberOfLines=1>this.props.item.nowMsgContent</Text>
                        <View style=style.separator/>
                    </View>
                </View>
            </MessageSwiper>
        );

可以看到,只要在原来的组件外层使用一个并提供actions即可,在我看来,这样操作对原来的组件影响是最小的。想法有了,但是接下来就是需要怎么实现了。

首先从MessageSwiperAction开始,其实这个类只是一个空类,里面并没有使用实际代码,使用这个类的方式是用于在外侧编写一些需要的属性或者将来有什么其他的扩展需要做预留。

然后从render方法开始,做为一个容器组件,我们可以使用this.props.children拿到里面的子组件,render方法的代码如下所示:

render() 
        return (
            <Animated.View style=
                transform: [
                    translateX: this.state.translateAnim
                ]
            >
                <View style=style.swipeContainer>
                    this.getActions()
                </View>
                <View
                    ...this._panResponder.panHandlers
                    ref='maskView'
                >
                    this.props.children
                </View>
            </Animated.View>
        );
    

使用Animated.View来做删除动画,其实一个侧滑删除是由两个叠在一起的View组成的,所以可以看到,外层传入了actions之后,我使用了一个View包装,其中样式表如下所示:

const style = StyleSheet.create(
    swipeContainer: 
        flex: 1,
        flexDirection: 'row',
        backgroundColor: 'white',
        alignItems: 'flex-start',
        justifyContent: 'flex-end',
        position: 'absolute',
        top: 0.0,
        bottom: 0.0,
        left: 0.0,
        right: 0.0,
    ,
    actionTitle: 
        color: 'white',
        textAlign: 'center',
    ,
    actionContainer: 
        width: 80.0,
        height: '100%',
        alignItems: 'center',
        justifyContent: 'center'
    
)

要想让一个View叠在另外一个View的下面,我们需要将position修改为absolute,然后从右至左依次排列。之前已经提出了两个问题,现在先来解决第一个问题:

以上说过,我们可以使用this.props.children来获取到当前容器组件的子组件,所以我们只需要使用this.props.children即可。

第二个问题:
因为使用的是this.props.children,所以我没办法直接在这里添加手势,最后我决定在外层包裹一层View,用以添加手势。这样既可以实现侧滑又可以不影响原有组件。

添加手势的方法如下所示:

UNSAFE_componentWillMount(): void 
        this._panResponder = PanResponder.create(
            // 要求成为响应者:
            onStartShouldSetPanResponder: (evt, gestureState) => true,
            onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
            onMoveShouldSetPanResponder: (evt, gestureState) => true,
            onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,

            onPanResponderGrant: (evt, gestureState) => 
                // 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情!
                // gestureState.x,y 现在会被设置为0
                if(this._handle === undefined) 
                    this._handle = findNodeHandle(this.refs.maskView);
                

                UIManager.measure(this._handle, (x, y, width, height) => 
                    this._xStart = x;
                );
            ,
            onPanResponderMove: (evt, gestureState) => 
                // 最近一次的移动距离为gestureState.moveX,Y
                // 从成为响应者开始时的累计手势移动距离为gestureState.dx,y
                let maxOffset = this.props.actions.length * 80.0;

                if(gestureState.dx < 0 && gestureState.dx >= -maxOffset && this._xStart != -maxOffset) 
                    this.refs.maskView.setNativeProps(
                        style: 
                            transform: [
                                translateX: gestureState.dx
                            ],
                        
                    );
                else if(gestureState.dx > 0 && this._xStart < 0) 
                    this.refs.maskView.setNativeProps(
                        style: 
                            transform: [
                                translateX: Math.min(this._xStart + gestureState.dx, 0.0)
                            ],
                        
                    );
                
            ,
            onPanResponderTerminationRequest: (evt, gestureState) => true,
            onPanResponderRelease: (evt, gestureState) => 
                // 用户放开了所有的触摸点,且此时视图已经成为了响应者。
                // 一般来说这意味着一个手势操作已经成功完成。
                this._offsetJudgeOnRelease(gestureState.dx);
            ,
            onPanResponderTerminate: (evt, gestureState) => 
                // 另一个组件已经成为了新的响应者,所以当前手势将被取消。
                this._offsetJudgeOnRelease(gestureState.dx);
            ,
            onShouldBlockNativeResponder: (evt, gestureState) => 
                // 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
                // 默认返回true。目前暂时只支持android
                return true;
            ,
        );
    

onPanResponderGrant: 当手势开始的时候,我需要拿到组件的位置,于是使用UIManager.measure方法测量组件,得到当前状态下组件的x坐标。

onPanResponderMove:当滑动的时候,先拿到最左侧的坐标,然后分以下情况:

  1. 如果是向左滑,并且当前累计滑动距离没有达到最大值,并且当前组件的偏移也未达到最大值,这样,我们就可以向左滑动,使用setNativeProps调整transform的translateX。
  2. 如果是向右滑,并且当前组件的坐标没有归0(之前已经有向左滑动),那么我们就可以向右滑动,使用setNativeProps调整transform的translateX。

onPanResponderRelease/onPanResponderTerminate: 当滑动结束或者滑动被中断的时候,此时我们需要决定最终的偏移量,如下所示:

 _offsetJudgeOnRelease(xOffset) 
        let maxOffset = this.props.actions.length * 80.0;

        if(xOffset > 0 && this._xStart < 0) 
            this.refs.maskView.setNativeProps(
                style: 
                    transform: [
                        translateX: 0.0
                    ],
                
            );
            return ;
        

        UIManager.measure(this._handle, (x, y, width, height) => 
            let translateX = 0;
            if(Math.abs(x) >= maxOffset / 2) 
                translateX = -maxOffset;
            

            this.refs.maskView.setNativeProps(
                style: 
                    transform: [
                        translateX: translateX
                    ],
                
            );
        );
    

xOffset即为当前累计滑动偏移量,如果xOffset > 0,即代表用户向右滑动,如果xOffset<0即代码用户向左滑动,这样,如果当前是向右滑动,并且当前组件并没有归零,此时我们就需要将偏移量归零。否则测量当前的x坐标,如果超过总偏量的一半,则偏移到最大,否则归零。

点击事件处理,代码如下所示:

onPress(action) 
        this.refs.maskView.setNativeProps(
           style: 
               transform: [
                   translateX: 0.0
               ],
           
        );

        action.props.confirm((result) => 
            this._confirm(result, action);
        );
    

    _confirm(result, action) 
        if(result === true) 
            if(action.props.animatable === true) 
                Animated.timing(
                    this.state.translateAnim,
                    
                        toValue: -this.windowSize.width,
                        duration: 1000,
                    
                ).start(() => 
                    if(action.props.action !== undefined) 
                        action.props.action();
                    
                );
            else 
                if(action.props.action !== undefined) 
                    action.props.action();
                
            
        
    

点击之后,首先将偏移归零,然后调用外部组件提供的confirm方法,这个方法包含一个函数指针,外部组件最终需要调用该方法,传递一个布尔值,内部根据该值做进一步的操作。这么设计的起因是由于需求点击之后有一个alert,我原本以为alert是会阻塞的,结果发现自己错了, 同事专门学了RN,他使用promise,但我并不会这种方式,于是再三考虑之下,使用这种方式实现。。

最后确认和删除的使用方法示例如下所示:

onDeleteItem(item) 
        let message = this.state.messageList;
        let history = this.state.historyList;

        for(let idx in message) 
            if(item.messageId === message[idx].messageId) 
                message.splice(idx, 1);
                this.setState(
                    messageList: message,
                );
                return ;
            
        

        for(let idx in history) 
            if(item.messageId=== history[idx].messageId) 
                history.splice(idx, 1);
                this.setState(
                    historyList: history,
                );
                return ;
            
        
    
    
confirmDemo(confirm) 
        Alert.alert('删除?', '确认要删除该选项吗?', [
            text: '删除', onPress: () => confirm(true),
            text: '取消', onPress: () => confirm(false), style:'cancel',
        ]);
    

看来RN还是需要系统的学习一波,以后找工作跨平台开发应该会成为移动端开发的必备技能。缺点就是没有动画,待以后熟悉一波动画,再另行优化。

后记:

竟然忘记测试Android了,不知道是我的写法不对还是确实不行,UIManager.measure方法竟然在Android上拿不到x坐标,真操蛋,今天碰到了两个问题,一个是UIManager.measure方法在Android上无法正常使用,一个是多个item可以同时展开。

针对第一个问题,我使用了另外一个变量_offset来记录上一次的偏移量,不再使用测量,改用动态累加的方式计算当前的X坐标。

针对第二个问题,我使用了一个单例管理类,然后保存一个ref,如果当前有其他项展开,则将第一个的偏移量归零,如下所示:

class MessageSwiperManager 
   static _instance = undefined;

   constructor() 
       this._expandedRef = undefined;
   

   static sharedInstance() 
       if(this._instance === undefined) 
           this._instance = new MessageSwiperManager();
       
       return this._instance;
   

   replaceRef(ref) 
       if(this._expandedRef === ref) 
           return false;
       

       if(this._expandedRef !== undefined) 
           this._expandedRef.setNativeProps(
               style: 
                   transform: [
                       translateX: 0.0
                   ],
               
           );
       
       this._expandedRef = undefined;

       if(ref !== undefined) 
           this._expandedRef = ref;
           return true;
       

       return false;
   

最后贴上整体代码:

import React, PureComponent, Component from 'react';
import Text, View, StyleSheet, SafeAreaView, TouchableWithoutFeedback, PanResponder, Animated, Dimensions from 'react-native';

export class MessageSwiperAction

class MessageSwiperManager 
    static _instance = undefined;

    constructor() 
        this._expandedRef = undefined;
    

    static sharedInstance() 
        if(this._instance === undefined) 
            this._instance = new MessageSwiperManager();
        
        return this._instance;
    

    replaceRef(ref) 
        if(this._expandedRef === ref) 
            return false;
        

        if(this._expandedRef !== undefined) 
            this._expandedRef.setNativeProps(
                style: 
                    transform: [
                        translateX: 0.0
                    ],
                
            );
        
        this._expandedRef = undefined;

        if(ref !== undefined) 
            this._expandedRef = ref;
            return true;
        

        return false;
    


export default class MessageSwiper extends Component 

    constructor(props) 
        super(props);
        this.state = 
            translateAnim: new Animated.Value(0)以上是关于记一次使用RN实现侧滑删除的主要内容,如果未能解决你的问题,请参考以下文章

记一次僵尸进程的处理

[技术博客] 通过ItemTouchHelper实现侧滑删除功能

RecycleView实现侧滑删除item

Android基础控件——RecyclerView实现拖拽排序侧滑删除效果

Android实现侧滑recycleView+CardVeiw卡片阴影效果

Android实现侧滑recycleView+CardVeiw卡片阴影效果