uni-app中nvue如何制作侧滑菜单, 安卓机解决方案, 低端机型解决方案

最近萌新制作的一个项目需要制作移动端应用, 为了减少开发成本首先想到了跨平台开发方式uni-app

uni-app一共有两种渲染方式:

  • 一种是写 .vue 最后以 web-view 渲染出页面, 这种模式因是基于浏览器所以很容易做到在iosAndroid页面保持一致, 但是这种模式有一个致命的缺点: 性能问题

  • 第二种是写 .nvue文件, 采用 week 技术渲染成原生组件, 这种模式性能没得话说, 但是因为局限于 week 本身的原因, 在有些方法很难做到iosAndroid页面保持一致

因为性能是我优先考虑的东西, 所以这里只能使用 nvue 的方式去开发, 在开发页面时一般会使用list 包裹页面元素从而达到高性能的滚动, 本文所讨论的问题就出现在这里: 我在DCloud插件市场查找到的侧滑菜单Android平台都存在同样的问题, 因为内存回收机制只有页面打开时可视部分能正常使用, 下面不可见的地方侧滑菜单都失效了, 下面让我们探索原因以及解决方案.

list

app端nvue专用组件。在app-nvue下,如果是长列表,使用list组件的性能高于使用viewscroll-view的滚动。原因在于list在不可见部分的渲染资源回收有特殊的优化处理。

Android 平台,因 <list> 高效内存回收机制,不在屏幕可见区域的组件不会被创建,导致一些内部需要计算宽高的组件无法正常工作

官网 list 组件文档

问题就出现在这里, Android平台不可见部分的计算宽度都失效了,导致侧滑失败. 下面我来向大家汇报我的解决方法, 以及遇到的问题.

先来看看我的页面

聊天页面

知道了失效原因, 那我们解决方式也很简单, 只要避免计算高度宽度就好了, 我将每一条聊天列表条目和侧滑菜单单独做成一个组件.

...
<cell v-for="(item,index) in chats">
    <chat-item :portrait="item.portrait"
           :userName="item.userName"
           :messages="item.messages"
           :key="'chat-item-'+index"
           :code="'chat-item-'+index"
           :unread="item.unread"
           :lastTime="item.lastTime"></chat-item>
</cell>
...

让我们看看自定义组件 <chat-item> 内长什么样子.

<!-- 聊天列表条目容器 -->
<div class="chat-container">
    <!-- 侧滑菜单 -->
    <div class="chat-operate">
        ...
    </div>
    <!-- 聊天列表条目主体 -->
    <div class="chat-item">
        ...
    </div>
</div>

因为局限于week没有提供z-index属性问题, 没法正确的设置此属性, 聊天列表条目主体 将来需要覆盖在 侧滑菜单 上面, 所以需要把 聊天列表条目主体 写在后面, 可以理解为越靠后 z-index 值越高.

week 通用样式

对于侧滑功能我们需要监听 聊天列表条目主体 (class="chat-item") 的手指事件, 这里需要监听的事件有3个:

  • @touchstart 手指按钮下触发
  • @touchmove 手指滑动触发
  • @touchend 手指离开屏幕触发
<template>
<!-- 聊天列表条目 -->
<div class="chat-container">
    <!-- 侧滑菜单 -->
    <div class="chat-operate">
        ...
    </div>
    <!-- 主体部分 -->
    <div class="chat-item" 
         @touchstart="touchStart" 
         @touchmove="touchMove" 
         @touchend="touchEnd"
         :style="chatItemStyle">
        ...
    </div>
</div>
</template>
<script>
export default {
    computed: {
        // 主体部分位移大小
        chatItemStyle(){
            return {
                transform: `translateX(-${this.moveX})`
        }
    }
    },
    data(){
    return {
            // 主体部分手指落下的位置
            startX: 0,
            // 主体部分位移距离
            moveX: 0
        }
    },
    methods: {
        touchStart(e){
            // 判断是否存在该事件
            if (e.changedTouches.length == 1) {
        // 设置触摸起始点水平方向位置
        this.startX = e.changedTouches[0].pageX
        this.startX += this.moveX;
            }
        },
        touchMove(e){
            if (e.changedTouches.length == 1) {
                // 手指移动时水平方向位置
        var moveX = e.changedTouches[0].pageX;
        // 手指起始点位置与移动期间的差值, 这里将值乘2是未了更方便打开侧滑动菜单
        var disX = (this.startX - moveX) * 2;
                // 赋值位移距离
                this.moveX = disX;
            }
        },
        touchEnd(e){
            if (e.changedTouches.length == 1) {
                // 手指移动结束后水平位置
        var endX = e.changedTouches[0].pageX;
        // 触摸开始与结束,手指移动的距离
        var disX = this.startX - endX;
                // 这里的为设置55为边界值, 如果当前手指松开时位移超过55会自动打开剩下的距离, 反之关闭侧滑菜单
                // 这里侧滑菜单的宽度为了避免计算必须设置固定值110
                if(disX > 55){
                    // 打开
                    this.moveX = 110
        }else{
                // 关闭
            this.moveX = 0
        }
            }
        }
    }
}
</script>
<style lang="scss" scoped>
    // 聊天列表条目容器
    .chat-container{    
    background-color: #FEFEFE;
    padding: 15rpx 20rpx;
    padding-bottom: 10rpx;
    position: relative;
    }
    // 聊天列表框
    .chat-item{
        flex-direction: row;
        align-items: stretch;
        border-radius: 20rpx;
        padding: 15rpx;
        background-color: #FEFEFE;
        transition-property: transform;
        transition-duration: .2s;
        transition-timing-function: ease;
    }
    // 菜单
    .chat-operate{
    position: absolute;
    width: 100px;
    height: 100rpx;
    top: 30rpx;
    right: 20rpx;
    flex-direction: row;
    justify-content: space-between;
    }
</style>

这里父容器为position: relative, 列表框一定不能position: absolute不然不能撑开条目的高度, 侧滑菜单可以为position: absolute但是不能存在宽度高度的计算.

加上亿点点细节

  1. 加入操作时背景颜色的提示
  2. 在打开过程滑动中阻止list滚动
  3. 打开一个侧滑菜单关闭其他条目的菜单
聊天页面

Android低端机型或老式机型侧滑Bug

在一些Android低端机型老式机型可能存在侧滑Bug(侧滑卡在一半, 外部list没有接收到是否正常开关, 认为还在滑动所以无法滚动页面), 这是因为这些机型可能并不能正确的触发 @touchend 手指离开事件, 导致侧滑菜单计算失败. 这里的解决方案可以使用 @touchcancel 手指中断事件代替, 可以在组件初始化时 uni.getSystemInfo 检查操作系统版本.

全部代码

chart-item.nvue

<template>
    <!-- 聊天列表 -->
    <div class="chat-container">
        <div class="chat-operate">
            <!-- 置顶 -->
            <div class="operate-top">
                <text class="chat-operate-icon operate-top-icon">&#xe61c;</text>
            </div>
            <!-- 删除 -->
            <div class="operate-del">
                <text class="chat-operate-icon operate-del-icon">&#xe6c7;</text>
            </div>
        </div><!-- @touchcancel="touchEnd2" -->
        <div :class="{'isMove':isMove}" class="chat-item" hover-class="none" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" @touchcancel="touchEnd2"  :style="chatItemStyle">
            <!--    头像 -->
            <div class="chat-portrait">
                <image class="chat-portrait-img" :src="portrait" mode="widthFix"></image>
            </div>
            <!-- 文字 -->
            <div class="chat-message-group">
                <text class="chat-userName">{{userName}}</text>
                <text class="chat-messages">{{messages}}</text>
            </div>
            <!-- 时间与小红点 -->
            <div class="chat-info">
                <!-- 小红点 -->
                <div class="info-tag" v-if="unread > 0">
                    <text class="info-tag-text">{{unread | numberForMat}}</text>
                </div>
                <div v-if="unread <= 0"></div>
                <!-- 时间 -->
                <text class="info-time">{{lastTime | timeConversion}}</text>
            </div>
        </div>
    </div>
</template>

<script>
    import { EventBus } from "../../unit/bus.js";
    export default {
        filters: {
            timeConversion(date){
                if(!new Date(date)) return '显示异常'
                var date = new Date(date);
                return (date.getHours() > 10 ? date.getHours() : '0' + date.getHours()) + ':' + (date.getMinutes() > 10 ? date.getMinutes() : "0" + date.getMinutes())
            },
            numberForMat(data){
                if(data > 999)
                    return '999+'
                else
                    return data
            }
        },
        computed: {
            chatItemStyle(){
                return {
                    transform: `translateX(-${this.moveX})`
                }
            }
        },
        props: {
            portrait: String,
            userName: String,
            messages: String,
            code: String,
            unread: Number,
            lastTime: [String,Number],
        },
        mounted() {
            this.init();
            EventBus.$on('chatItemOpen',(data)=>{
                if(data != this.code)
                    this.moveX = 0
            });
            EventBus.$on('chatListScroll',()=>{
                this.moveX = 0
                this.startX = 0
            });
        },
        watch: {
            moveX: function(val){
                if(val > 0)
                    this.isMove = true
                else if(val == 0)
                    this.isMove = false
            }
        },
        methods: {
            init(){
                uni.getSystemInfo({
                    success:(res) => {
                        this.platform = res.platform
                    }
                })
            },
            touchStart(e){
                if (e.changedTouches.length == 1) {
                    // 设置触摸起始点水平方向位置
                    this.startX = e.changedTouches[0].pageX
                    this.startX += this.moveX;
                }
            },
            touchMove(e){
                 if (e.changedTouches.length == 1) {
                    //手指移动时水平方向位置
                    var moveX = e.changedTouches[0].pageX;
                    // 手指起始点位置与移动期间的差值
                    var disX = (this.startX - moveX) * 2;
                    if(disX > 20){
                        // if(this.platform == 'ios'){
                            EventBus.$emit('chatItemMove',true);
                            if(disX > 80){
                                EventBus.$emit('chatItemMove',false);
                            }
                            if (disX == 0 || disX < 0) {
                                disX = 0
                            }else if(disX > 0){
                                EventBus.$emit('chatItemOpen',this.code);
                                if(disX >= 110){
                                    disX = 110 
                                }
                            }
                        // }else{
                        //  if (disX == 0 || disX < 0) {
                        //      disX = 0
                        //  }else if(disX > 20){
                        //      EventBus.$emit('chatItemOpen',this.code);
                        //      if(disX >= 110){
                        //          disX = 110 
                        //      }
                        //  }
                        // }
                        this.moveX = disX;
                    }
                 }
            },
            touchEnd(e){
                // if(this.platform !== 'ios') return;
                if (e.changedTouches.length == 1) {
                    EventBus.$emit('chatItemMove',false);
                    // 手指移动结束后水平位置
                    var endX = e.changedTouches[0].pageX;
                    // 触摸开始与结束,手指移动的距离
                    var disX = this.startX - endX
                    if(disX > 55){
                        // 打开
                        this.moveX = 110
                    }else{
                        // 关闭
                        this.moveX = 0
                    }
                }
            },
            touchEnd2(){
                EventBus.$emit('chatItemMove',false);
            }
            // touchEnd2(e){
            //  if(this.platform == 'ios') return;
            //  if (e.changedTouches.length == 1) {
            //      EventBus.$emit('chatItemMove',false);
            //      // 手指移动结束后水平位置
            //      var endX = e.changedTouches[0].pageX;
            //      // 触摸开始与结束,手指移动的距离
            //      var disX = this.startX - endX;
            //      if(disX > 55){
            //          // 打开
            //          this.moveX = 110
            //      }else{
            //          // 关闭
            //          this.moveX = 0
            //      }
            //  }
            // }
        },
        data(){
            return {
                startX: 0,
                moveX: 0,
                isMove: false,
                platform: ''
            }
        }
    }
</script>

<style lang="scss" scoped>
.chat-container{    
    background-color: #FEFEFE;
    padding: 15rpx 20rpx;
    padding-bottom: 10rpx;
    position: relative;
}
// 聊天列表框
.chat-item{
    flex-direction: row;
    align-items: stretch;
    border-radius: 20rpx;
    padding: 15rpx;
    background-color: #FEFEFE;
    transition-property: transform;
    transition-duration: .2s;
    transition-timing-function: ease;
}
// 正在移动
.chat-item.isMove{
    background-color: #EEEEEE;
}

// 头像
.chat-portrait{
    width: 100rpx;
    height: 100rpx;
    border-radius: 20rpx;
    overflow: hidden;
    align-items: center;
    justify-content: center;
}
.chat-portrait-img{
    width: 100rpx;
    height: 100rpx;
}

// 聊天
.chat-message-group{
    margin: 0 20rpx;
    justify-content: center;
    flex: 1;
}
.chat-userName{
    font-family: 'HarmonyOS_Sans_SC';
    color: $primaryText;
    font-size: 35rpx;
    font-weight: 600;
}
.chat-messages{
    font-family: 'HarmonyOS_Sans_SC';
    color: $regularText;
    font-size: 25rpx;
    font-weight: 400;
    flex: 1;
    text-overflow: ellipsis;
    overflow: hidden;
    lines: 1;
}
// 操作
.chat-operate{
    position: absolute;
    width: 100px;
    height: 100rpx;
    top: 30rpx;
    right: 20rpx;
    flex-direction: row;
    justify-content: space-between;
}
.operate-del{
    width: 45px;
    height: 100rpx;
    border-radius: 20rpx;
    align-items: center;
    justify-content: center;
    background-color: rgba($dangerColor,.3);
}
.chat-operate-icon{
    font-family: iconfont;
    font-size: 35rpx;
}
.operate-del-icon{
    color: $dangerColor;
}
.operate-top{
    width: 45px;
    height: 100rpx;
    border-radius: 20rpx;
    align-items: center;
    justify-content: center;
    background-color: rgba($warningColor,.3);
}
.operate-top-icon{
    color: $warningColor;
    font-size: 40rpx;
}
// 时间与小红点
.chat-info{
    justify-content: space-between;
    align-items: flex-end;
}
.info-tag{
    margin-top: 10rpx;
    background-color: #FE3B30;
    padding: 6rpx 12rpx;
    align-items: center;
    justify-content: center;
    border-radius: 20rpx;
}
.info-tag-text{
    color: #fff;
    line-height: 25rpx;
    font-size: 25rpx;
    font-weight: 500;
    font-family: 'HarmonyOS_Sans_SC';
}
.info-time{
    color: $regularText;
    font-size: 20rpx;
    font-family: 'HarmonyOS_Sans_SC';
}
</style>

message.vue

这是聊天列表页面, 这里的操作很简单, list 有一个属性可以控制本身是否开启滚动: scrollable, 监听子组件 chart-item 发出的 chatItemMove 滑动进行中事件控制 list 是否可以滚动.

...

mounted() {
    // if(this.platform == 'ios'){
    // 监听子组件是否滑动进行中
    EventBus.$on('chatItemMove',(data)=>{
        // 滑动已结束, list可以滚动
        if(!data)
            this.scrollable = true
        // 滑动进行中, list禁止滚动
        else
            this.scrollable = false
    });
    // }
}

...

监听 list@scroll 滚动触发事件, 当页面滚动时关闭所有侧滑菜单.

...

methods: {
    listScroll(e){
    // if(this.platform == 'ios'){
        // 当页面发生滚动时向子组件发送chatListScroll事件, 以关闭所有侧滑菜单
    EventBus.$emit('chatListScroll');
    // }
    },
}

...

如果你对于使用uni-appnvue模式时面对多平台有更好的方式, 欢迎来讨论.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容