最近萌新制作的一个项目需要制作移动端应用, 为了减少开发成本首先想到了跨平台开发方式uni-app
uni-app一共有两种渲染方式:
一种是写
.vue
最后以web-view
渲染出页面, 这种模式因是基于浏览器所以很容易做到在ios
和Android
页面保持一致, 但是这种模式有一个致命的缺点: 性能问题第二种是写
.nvue
文件, 采用week
技术渲染成原生组件, 这种模式性能没得话说, 但是因为局限于week
本身的原因, 在有些方法很难做到ios
和Android
页面保持一致
因为性能是我优先考虑的东西, 所以这里只能使用 nvue
的方式去开发, 在开发页面时一般会使用list
包裹页面元素从而达到高性能的滚动, 本文所讨论的问题就出现在这里: 我在DCloud插件市场
查找到的侧滑菜单在 Android
平台都存在同样的问题, 因为内存回收机制只有页面打开时可视部分能正常使用, 下面不可见的地方侧滑菜单都失效了, 下面让我们探索原因以及解决方案.
list
app端nvue专用组件。在app-nvue下,如果是长列表,使用list
组件的性能高于使用view或scroll-view的滚动。原因在于list
在不可见部分的渲染资源回收有特殊的优化处理。
Android 平台,因
<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
值越高.
对于侧滑功能我们需要监听 聊天列表条目主体 (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
但是不能存在宽度或高度的计算.
加上亿点点细节
- 加入操作时背景颜色的提示
- 在打开过程滑动中阻止
list
滚动 - 打开一个侧滑菜单关闭其他条目的菜单
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"></text>
</div>
<!-- 删除 -->
<div class="operate-del">
<text class="chat-operate-icon operate-del-icon"></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');
// }
},
}
...