實(shí)現(xiàn)目標(biāo)
1. 商品卡片點(diǎn)擊右上角的時(shí)候壳咕,彈出遮罩層以及對話框
2. 當(dāng)點(diǎn)擊遮罩層/滑動(dòng)窗口時(shí)的時(shí)候花吟,對話框隱藏
實(shí)現(xiàn)效果
具體實(shí)現(xiàn)思路和需要注意的點(diǎn)
1. 氣泡對話框的實(shí)現(xiàn)主要分為三角形元素和矩形元素檩帐,通過y軸偏移拼接在一起形成對話框
2. 由于氣泡對話框可能為上方彈出唾糯,或者下方彈出,因此三角形會(huì)存在上下兩個(gè)辆憔。
3.左右兩側(cè)的對話框掘而,由于右側(cè)靠近屏幕邊緣,因此矩形的X軸偏移量不一樣鹏漆。
// 三角形元素樣式
.triangle {
width: 0rpx;
height: 0rpx;
border: 25rpx solid transparent;
border-bottom: 25rpx solid #fff;
}
// 對話框矩形樣式
.dialgo-div {
height: 350rpx;
width: 200rpx;
background: #fff;
transform: translate(var(--translateX),-2rpx); // 左右兩列矩形的X軸偏移量不同巩梢,因此需要通過計(jì)算動(dòng)態(tài)傳入
border-radius: 10rpx;
overflow: hidden;
}
.bottom-triangle {
width: 0rpx;
height: 0rpx;
border: 25rpx solid transparent;
border-top: 25rpx solid #fff;
transform: translate(-15rpx, -5rpx);//偏移Y軸重合對話框,x偏移量艺玲,使三角形對準(zhǔn) "x" 圖標(biāo)
}
- 通過點(diǎn)擊事件獲取點(diǎn)擊右上角x后獲得event.detail內(nèi)的x,以及y變量是相對頁面元素整體的偏移量括蝠,并不是相對于屏幕的偏移量(頁面元素高度可能會(huì)大于屏幕高度,因此獲得的y可能會(huì)大于屏幕高度)饭聚。由于我事先彈出對話框時(shí)基于fixed布局忌警,top和left變量時(shí)基于屏幕坐標(biāo),因此需要通過計(jì)算得出基于屏幕的top偏移量(x軸不溢出屏幕秒梳,因此可以直接應(yīng)用x軸偏移量)法绵。
計(jì)算點(diǎn)擊時(shí)相對于屏幕的偏移量,可以通過監(jiān)聽頁面滾動(dòng)方法onPageScroll獲取當(dāng)前頁面滾動(dòng)頂部部的y軸量酪碘,使用點(diǎn)擊處的y軸偏移量 - 當(dāng)前頁面頂部的y軸偏移量就可以得出當(dāng)前點(diǎn)擊元素相對于屏幕的y軸偏移量朋譬,在Page下填入
// debounce為防抖函數(shù)
onPageScroll:function(event) {
const that = this;
debounce(function(){
that.setData({
scrollTop: Math.ceil(event.scrollTop)
});
}, 100)();
},
debounce為防抖函數(shù),由于onPageScroll會(huì)被頻繁觸發(fā)兴垦,為了避免拖動(dòng)時(shí)頻繁觸發(fā)setData函數(shù)更新造成頁面卡頓徙赢,而我們實(shí)際只需要拖動(dòng)結(jié)束后獲取這個(gè)值就行字柠,所以引入了防抖函數(shù),具體實(shí)現(xiàn)為
/**
* 防抖函數(shù)
* @param {*} fun 需要進(jìn)行防抖的函數(shù)
*/
export function debounce(fun, delay = 500, immediate= false) {
let timer = null; // 保存定時(shí)器
return function(args) {
let that = this;
let _args = args;
if(timer) clearTimeout(timer);
if(immediate) {
if(!timer) fun.apply(that,_args); // 定時(shí)器為空表示可以執(zhí)行
timer = setTimeout(function() {
timer = null;// 到時(shí)間后設(shè)置定時(shí)器為空
},delay);
}
else {
// 如非立即執(zhí)行狡赐,則重設(shè)定時(shí)器
timer = setTimeout(function() {
fun.call(that,_args);
},delay);
}
}
}
在Page頁面獲取到scrollTop后通過prop傳入到組件中在點(diǎn)擊事件中進(jìn)行計(jì)算窑业。
計(jì)算對話框從上彈出還是下彈出, 通過第四點(diǎn)枕屉,我們已經(jīng)計(jì)算出了當(dāng)前的對話框需要偏移的left和top值常柄。如果top值加上對話框的高度大于整個(gè)屏幕的高度時(shí),表示對話框溢出屏幕搀擂,此時(shí)就需要在上方彈出西潘。
獲取屏幕信息的接口是wx.getSystemInfoSync()底部的導(dǎo)航欄如果為系統(tǒng)原生,屬于是最頂層元素哥倔,手寫的遮罩層無法遮蓋住導(dǎo)航欄秸架,因此需要引入wx.hideTabBar() 接口,當(dāng)點(diǎn)擊時(shí)隱藏底部導(dǎo)航欄咆蒿,遮罩層消失時(shí)东抹,重新顯示,接口為wx.showTabBar沃测。(有遮罩層置頂?shù)奶幚矸椒g迎提出)
當(dāng)頁面進(jìn)行滾動(dòng)時(shí)缭黔,對話框及遮罩層也隱藏,因此在Page的滾動(dòng)事件中再引入蒂破,當(dāng)滾動(dòng)時(shí)設(shè)置isScrolling為true馏谨,傳入組件,在組件中監(jiān)聽obeserver附迷,當(dāng)變量改變?yōu)?strong>true的時(shí)候隱藏遮罩層以及對話框惧互。
遮罩層使用簡單的fixed布局,設(shè)置z-index來進(jìn)行遮蓋喇伯。點(diǎn)擊對話框選項(xiàng)后喊儡,顯示減少推薦,使用簡單absolute布局稻据。
組件完整代碼
頁面的onPageScroll函數(shù)
onPageScroll:function(event) {
// 滾動(dòng)時(shí)隱藏對話框和遮罩層
if(!this.data.isScrolling) {
this.setData({
isSrolling: true
});
}
const that = this;
debounce(function(){
that.setData({
scrollTop: Math.ceil(event.scrollTop),
isSrolling: false
});
}, 100)();
}
組件wxml
<!--pages/index/components/suggestCard/suggestCard.wxml-->
<view style="top: {{top}}; left: {{left}};height: {{realHeght}};" class="{{ index % 2 ? 'odd-card' : 'even-card' }}">
<van-image src="{{ itemData.imageUrl }}" fit="widthFix" width="325rpx">
</van-image>
<view class="info-panel">
<view class="info-title">
<van-tag class="new-tag" custom-class="new-tag" color="#95d475">上新</van-tag>
時(shí)尚百搭雙肩奶爸包 多功能兩用媽咪包防水休閑學(xué)生包 可定制
</view>
<view class="price-tag">
<view class="price-signal-container">
<text class="price-signal">¥</text>
<text>999.86</text>
</view>
</view>
</view>
<view class="close-icon-container">
<view bindtap="closeTap" class="close-icon-inside-container">
<van-icon name="cross" color="#C0C4CC" />
<!--van-transition name="fade" show="{{show}}" -->
<view catchtap="wrapperTap" class="{{ show ? 'popover-wrapper-active' : 'popover-wrapper'}}">
</view>
<view style="top: {{dialogTop}}; left: {{dialogleft}};--translateX: {{translateX}}" class="{{ show ? 'buble-dialog-open' :'buble-dialog' }}">
<view class="{{dialogDirection == 'bottom' ? 'triangle' : 'hidden-triangle'}}">
</view>
<view class="dialgo-div">
<view catchtap="menueTap" wx:for="{{menuItems}}" wx:key="unique" wx:for-item="item" class="dialog-menu-item">
<view>
{{ item.text }}
</view>
</view>
</view>
<view class="{{dialogDirection == 'up' ? 'bottom-triangle':'hidden-triangle'}}">
</view>
</view>
<!--/van-transition-->
</view>
</view>
<view class="{{ isCanceled ? 'cancel-cover ' : 'cancel-cover-deactive'}}">
<view class="cancel-text-container">
<text>您的反饋已收到</text>
<view>會(huì)減少此類內(nèi)容的推薦</view>
</view>
</view>
</view>
組件wxss
.odd-card {
width: 350rpx;
height: 0rpx;
background: #fff;
border: 1px solid #E4E7ED;
border-radius: 4px;
position: absolute;
left: 375rpx;
top: 0rpx;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: 2s all ease-in-out;
overflow: hidden;
}
.even-card {
width: 350rpx;
height: 0rpx;
background: #fff;
border: 1px solid #E4E7ED;
border-radius: 4px;
position: absolute;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: 2s all ease-in-out;
overflow: hidden;
}
.info-panel {
font-size: 28rpx;
display: flex;
flex-direction: column;
height: 100rpx;
}
.info-title {
padding-left: 2.5%;
padding-right: 2.5%;
width: 95%;
font-size: 25rpx;
overflow: hidden;
text-overflow:clip;
display: -webkit-box;
-webkit-line-clamp: 2; /*限制文本行數(shù)*/
-webkit-box-orient: vertical;
word-break: break-all;
}
.new-tag {
font-size: 20rpx !important;
}
.price-tag {
height: 0rpx;
flex-grow: 1;
font-size: 25rpx;
display: flex;
align-items: center;
}
.price-signal-container {
width: 50%;
color: red;
text-align: center;
}
.price-signal {
font-size: 20rpx;
}
.close-icon-container {
position: absolute;
top: 15rpx;
left: 310rpx;
font-size: 15rpx;
text-align: center;
}
.close-icon-inside-container {
height: 25rpx;
width: 25rpx;
}
.popover-wrapper {
position: fixed;
width: 750rpx;
height: 100vh;
top: 0px;
left: 0px;
background: rgba(0, 0, 0, 0.5);
z-index: -1;
display: none;
opacity: 0;
animation-name: hide;
animation-duration: .3s;
}
.popover-wrapper-active {
position: fixed;
width: 750rpx;
height: 100vh;
top: 0px;
left: 0px;
opacity: 1;
animation-name: show;
animation-duration: .3s;
animation-fill-mode: forwards;
z-index: 1999;
background: rgba(0, 0, 0, 0.5);
}
.cancel-cover {
width: 100%;
height: 100%;
position: absolute;
top: 0px;
left: 0px;
background-color: rgba(256, 256, 256, 0.8);
display: flex;
justify-content: center;
align-items: center;
font-size: 25rpx;
}
.cancel-cover-deactive {
display: none;
}
.cancel-text-container {
width: 80%;
}
@keyframes show {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes hide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.buble-dialog {
display: none;
animation: bubleShow .3s ease-in-out;
animation-fill-mode: forwards;
animation-direction: reverse;
}
.buble-dialog-open {
--translateX: '0rpx';
position: fixed;
z-index: 2000;
display: block;
animation: bubleShow .3s ease-in-out;
animation-fill-mode: forwards;
}
@keyframes bubleShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.dialgo-div {
height: 350rpx;
width: 200rpx;
background: #fff;
transform: translate(var(--translateX),-2rpx);
border-radius: 10rpx;
overflow: hidden;
}
.triangle {
width: 0rpx;
height: 0rpx;
border: 25rpx solid transparent;
border-bottom: 25rpx solid #fff;
transform: translateX(-15rpx);
}
.bottom-triangle {
width: 0rpx;
height: 0rpx;
border: 25rpx solid transparent;
border-top: 25rpx solid #fff;
transform: translate(-15rpx, -5rpx);
}
.hidden-triangle {
display: none;
}
.dialog-menu-item {
color: #323233;
height: 69rpx;
font-size: 20rpx;
width: 100%;
border-bottom: 1px solid #ebedf0;
display: flex;
justify-content: center;
align-items: center;
transition: .2s background ease-in-out;
}
.dialog-menu-item:hover {
background: #e1f3d8;
transition: .2s background ease-in-out;
}
.dialog-menu-item:last-child {
border-bottom: none;
}
組件js
Component({
/**
* 組件的屬性列表
*/
lifetimes: {
attached() {
let val = '-80rpx';
if(this.properties.index % 2) {
val = '-148rpx'
}
this.setData({
translateX: val
})
}
},
properties: {
index: {
type: Number,
value: 0,
/* observer: function(newVal, oldVal) {
let top = `${(newVal / 2 ) * 460}rpx`;
let left = `25rpx`;
if(newVal % 2) {
top = `${(Math.floor(newVal / 2))* 410}rpx`;
if(newVal == 1) {
// console.log(top);
}
left = `375rpx`;
}
this.setData({
top: top,
left: left
});
}*/
observer: function (newValue, oldValue) {
if ((newValue % 2)) {
this.setData({
left: "375rpx"
})
}
}
},
itemData: {
type: Object,
value: {
imageUrl: '',
top: '0rpx',
left: '0rpx',
realHeght: '450rpx'
},
observer: function (newValue, oldValue) {
this.setData({
top: newValue.top,
left: newValue.left,
realHeght: newValue.realHeight
})
}
},
scrollTop: {
type: Number,
value: 0,
observer: function (newValue) {
// console.log(newValue);
}
},
isSrolling: {
type: Boolean,
value: false,
observer: function (newValue) {
if (newValue && this.data.show) {
wx.showTabBar({
animation: true,
})
this.setData({
show: false
})
}
}
}
},
/**
* 組件的初始數(shù)據(jù)
*/
data: {
left: `25rpx`, // 組件left值
top: '0rpx', //組件top值
realHeght: `450rpx`, // 組件真實(shí)高度
show: false, // 用于控制點(diǎn)擊組件右上角x后艾猜,遮罩層和對話框是否顯示
dialogTop: '300rpx', // 對話框的top坐標(biāo)
dialogLeft: '350rpx', // 對話框的left坐標(biāo)
dialogDirection: 'bottom', // 對話框顯示的位置
translateX: '-80rpx', // 對話框的x偏移值,右邊列顯示對話框時(shí)需要向左偏移更多(-148rpx)
isCanceled: false,
menuItems: [
{
text: '不感興趣'
},
{
text: '品類不喜歡'
},
{
text: '已經(jīng)買了'
},
{
text: '圖片引起不適'
},
{
text: '涉及隱私'
}
]
},
/**
* 組件的方法列表
*/
methods: {
// 組件右上角 x 捻悯,關(guān)閉點(diǎn)擊事件
closeTap: function (event) {
// 重復(fù)點(diǎn)擊也關(guān)閉遮罩層
if(this.data.show) {
wx.showTabBar({
animation: true,
});
this.setData({
show: false,
});
return ;
}
let result = wx.getSystemInfoSync(); // 獲取系統(tǒng)信息
let windowHeight = Math.floor(result.windowHeight); // 獲取系統(tǒng)窗戶高度
let windowWidth = Math.floor(result.windowWidth); // 獲取系統(tǒng)窗戶寬度
let x = Math.floor(event.detail.x); // 獲取點(diǎn)擊的x坐標(biāo)
let y = Math.floor(event.detail.y); // 獲取點(diǎn)擊的y坐標(biāo)
// 計(jì)算當(dāng)前元素(y坐標(biāo) - scrollTop) 獲取當(dāng)前元素在屏幕處的Y坐標(biāo)匆赃,再加上 彈出對話框的高度
// 如果所得數(shù)值大于當(dāng)前屏幕的高度(或再減去81(底部導(dǎo)航欄高度)),證明數(shù)值溢出今缚,則顯示為頂部對話框算柳,否則則顯示為底部對話框。
let dialogDirection = (y - this.properties.scrollTop + windowWidth / 750 * 400) >= windowHeight - 81 ? 'up' : 'bottom';
//console.log({windowHeight})
//console.log({y:y - this.properties.scrollTop})
//console.log({event})
wx.hideTabBar(); // 隱藏底部
// 根據(jù)頂部對話框或底部對話框計(jì)算top的值
// 底部對話框的top值為當(dāng)前窗口的絕對y坐標(biāo)荚斯,計(jì)算方式為(點(diǎn)擊坐標(biāo)y值 - 當(dāng)前頁面的scrollTop)
// 頂部對話框top值為底部對話框top值的基礎(chǔ)上 - 對話框的高度埠居;
let tempTop = dialogDirection == 'up' ? `calc(${y - this.properties.scrollTop}px - 400rpx)` : `${y - this.properties.scrollTop}px`;
this.setData({
dialogTop: tempTop,
dialogLeft: `${x}px`,
show: true,
dialogDirection: dialogDirection
})
},
// 遮罩層點(diǎn)擊事件
wrapperTap: function (event) {
this.setData({
show: false
});
wx.showTabBar({
animation: true,
})
},
// 菜單點(diǎn)擊事件
menueTap: function() {
/*this.triggerEvent('deleteItem',this.properties.index);
this.setData({
show: false
})*/
this.setData({
show: false,
isCanceled: true
})
}
}
})
可優(yōu)化
遮罩層使用Vant組件自帶的遮罩層查牌,提供更優(yōu)化的動(dòng)畫事期。完善點(diǎn)擊取消后的動(dòng)畫滥壕。歡迎交流學(xué)習(xí)。創(chuàng)作不易兽泣,點(diǎn)個(gè)贊吧绎橘。