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

·  阅读 1220
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平台不可见部分的计算宽度都失效了,导致侧滑失败. 下面我来向大家汇报我的解决方法, 以及遇到的问题.

先来看看我的页面

聊天页面

截图于 iPhone xs max平台

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

...
<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. 打开一个侧滑菜单关闭其他条目的菜单

8aw2n-w51xm.gif

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模式时面对多平台有更好的方式, 欢迎来讨论.

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改