微信小程序实现如上效果。
分为两个组件:
- PinTuan: 整体组件
- PinTuanHead: 每个头像组件
代码如下:
PinTuan.js
Component({
attached:function() {
// 获取头像地址数据,返回一个头像地址数组
notification.addObserver('你自己的消息事件', (config) => {
this._getServeHeadData(config);
}, this);
},
detached: function() {
this._stopInterval();
this._stopTimeOut();
},
ready: function() {
// 初始化适配rpx
this._initRpx();
},
/**
* 组件的属性列表
*/
properties: {
},
/**
* 组件的初始数据
*/
data: {
personNum: 1,
successedImg: 'XXX.png',
joinText: "即将成团",
clockText: "00:00:00", // 倒计时文字初始值
headsArr: [], // 所有头像url地址库
headUrls: [], // 头像url数组
moveHead: '', // 头像飞动图片
headAniData: {}, // 头像动画animation
successAni: {}, // 成功动画animation
pinTuanAni: {}, // 拼团整体上滚刷新animation
headAniOrigin: true, // 头像飞动动画是否归位
successAniOrigin: true, // 成功动画是否归位
pinTuanAniOrigin: true, // 拼团组件动画是否归位
pinTuanSuccess: false, // 拼团成功图标显示
countDownSeedTimer: 10, // 倒计时最小随机数,也是拼团成功最大时间随机数
pinTuanMinSeedTimer: 3, // 拼团最小随机时间
maxHour: 8, // 倒计时随机最大小时
},
/**
* 组件的方法列表
*/
methods: {
_getServeHeadData: function(config) {
console.log('check config = ', config)
// 初始化服务器配置
if (!config.recent) return;
const recent = config.recent;
this.setData({
headsArr: recent,
});
this._updateHeadAni();
this._updateCountDown();
},
_initRpx: function() {
// 适配屏幕移动距离
wx.getSystemInfo({
success: res => {
let rpx = 1 * (res.windowWidth * res.pixelRatio) / (375 * res.pixelRatio);
this.setData({
rpx: rpx
});
},
})
},
_randomCountDownTime: function(time) {
if (time < 0) {
this._stopInterval();
return;
}
let hour = Math.floor(time / 3600) % 10;
let minute = Math.floor((time / 60) % 60);
let second = Math.floor(time % 60);
let timeStr = '0' + hour + ':' + (minute < 10 ? ('0' + minute) : minute) + ':' + (second < 10 ? ('0' + second) : second);
console.log('check _randomCountDownTime timeStr = ', timeStr);
this.setData({
clockText: timeStr,
});
},
_updateCountDown: function() {
this._stopInterval();
let min = this.data.countDownSeedTimer;
let maxH = this.data.maxHour;
let time = Math.ceil(min + Math.random() * (maxH * 60 * 60 - min));
this.timeInterval = setInterval(res => {
this._randomCountDownTime(time);
time--;
}, 1000);
},
_stopTimeOut: function() {
if (this.timeTimeout) clearTimeout(this.timeTimeout);
},
_stopInterval: function() {
if (this.timeInterval) clearInterval(this.timeInterval);
},
_updateHeadAni: function() {
let recent = this.data.headsArr;
let startIndex = Math.ceil(Math.random() * (recent.length - 3));
let urls = recent.slice(startIndex, startIndex + 3);
this.setData({
headsArr: recent,
headUrls: urls,
moveHead: urls[2].avatar
});
let min = this.data.pinTuanMinSeedTimer;
let num = Math.ceil(min + Math.random() * (this.data.countDownSeedTimer - min));
console.log('check updateHeadAni num = ', num);
this._headAnimation(num);
},
// 头像飞动
_headAnimation: function(timeOut) {
// 创建动画
let animation = wx.createAnimation({
delay: 0,
duration: 1300,
timingFunction: "ease"
});
// 设置循环
this.timeTimeout = setTimeout(() => {
animation.opacity(1).translateX(-123 * this.data.rpx).step();
this.setData({
headAniData: animation.export(),
headAniOrigin: false,
joinText: "拼团成功!"
});
}, timeOut * 1000);
},
// 动画结束回掉
headAniEnd: function(res) {
if (this.data.headAniOrigin) {
return;
}
// 头像动画归位
let aniOrigin = wx.createAnimation();
aniOrigin.opacity(0).translateX(0).step( {duration: 0} );
this.setData({
headAniData: aniOrigin.export(),
headAniOrigin: true,
pinTuanSuccess: true
});
this.selectComponent('.pin-tuan').setData({
person3Show: true
});
// 显示拼团成功动画
let aniSuccess = wx.createAnimation();
aniSuccess.scale(1.2).step( {duration: 300, delay: 0, timingFunction: 'ease-in-out'} );
aniSuccess.scale(1).translateY(0).step( {duration: 100, delay: 0, timingFunction: 'ease-in-out'} );
this.setData({
successAni: aniSuccess.export(),
successAniOrigin: false
});
},
// 拼团成功动画结束回调
successAniEnd: function() {
setTimeout(() => {
let aniEnd = wx.createAnimation();
if (!this.data.successAniOrigin) {
aniEnd.scale(0.3).translateY(0).step( {duration: 0, delay: 0} );
this.setData({
successAni: aniEnd.export(),
successAniOrigin: true,
pinTuanSuccess: false,
joinText: "即将成团"
});
this._pinTuanPlayAni();
}
}, 1500);
},
_pinTuanPlayAni: function() {
let ani = wx.createAnimation({
delay: 0,
duration: 500,
timingFunction: 'ease'
});
ani.opacity(0).translateY(-30).step();
this.setData({
pinTuanAniOrigin: false,
pinTuanAni: ani.export()
});
},
pinTuanAniEnd: function() {
if (this.data.pinTuanAniOrigin) {
return;
}
this.selectComponent('.pin-tuan').setData({
person3Show: false
});
let ani = wx.createAnimation();
ani.opacity(1).translateY(0).step({ duration: 0});
this.setData({
pinTuanAniOrigin: true,
pinTuanAni: ani.export()
});
// 刷新下一轮拼团倒计时
this._updateHeadAni();
this._updateCountDown();
},
}
})
PinTuan.json
{
"component": true,
"usingComponents": {
"PinTuanHead": "../PinTuanHead/PinTuanHead"
}
}
PintTuan.wxml
<view class="wrap">
<view class="text-wrap">
<text>还差</text>
<text class="persion-num">{{personNum}}人</text>
<text>成团,可直接参与</text>
</view>
<view class="pintuan-content">
<PinTuanHead class="pin-tuan" headUrls="{{headUrls}}" animation="{{pinTuanAni}}" bindtransitionend="pinTuanAniEnd"></PinTuanHead>
<view class="join">
<text class="join-text">{{joinText}}</text>
<text class="clock">还剩{{clockText}}</text>
</view>
<image animation="{{headAniData}}" bindtransitionend="headAniEnd" class="move-head" src="{{moveHead}}"></image>
<button class="goGroup">去参团</button>
</view>
<image wx:if="{{pinTuanSuccess}}" class="successed" src="{{successedImg}}" mode="widthFix"
animation="{{successAni}}" bindtransitionend="successAniEnd" style="transform: scale(0.3) opacity(0)"></image>
<view class="bottom-border"></view>
</view>
PinTuan.wxss
.wrap {
width: 100%;
height: 216rpx;
background: #fff;
font-family: PingFangSC-Semibold,PingFang SC;
display: block;
}
.successed {
position: absolute;
width: 180rpx;
height: 65rpx;
padding: 0;
top: 148rpx;
left: 60rpx;
z-index: 5;
}
.bottom-border {
position: absolute;
padding: 0;
width: 89.3%;
height: 0.5rpx;
background: #D8D8D8;
left: 5.35%;
top: 32%;
}
.text-wrap {
margin: 28rpx 0 0 40rpx;
padding: 0;
display: flex;
flex-direction: row;
align-items: stretch;
flex-grow: 1;
flex-shrink: 0;
font-size: 30rpx;
color: #000;
}
.persion-num {
margin: 0 10rpx;
color: #F44;
}
.pintuan-content {
display: flex;
flex-direction: row;
justify-content: space-between;
flex-grow: 1;
flex-shrink: 0;
}
.join {
margin-top: 44rpx;
margin-left: 30rpx;
display: flex;
flex-direction: column;
font-family: PingFangSC-Regular;
font-size: 24rpx;
}
.join-text {
color: #FF493B;
}
.clock {
color: #7F7F7F;
}
.goGroup {
margin-right: 40rpx;
margin-top: 38rpx;
padding: 0;
background: linear-gradient(#FF4444, #FF6E00);
border-radius: 2em;
width: 170rpx;
height: 70rpx;
font-size: 30rpx;
color: #fff;
line-height: 70rpx;
}
.move-head {
position: absolute;
width: 84rpx;
height: 84rpx;
border: 1rpx solid #E6E6E6;
border-radius: 50%;
right: 230rpx;
top: 100rpx;
opacity: 0;
z-index: 5;
}
PinTuanHead.js
Component({
attached: function () {},
detached: function () {},
/**
* 组件的属性列表
*/
properties: {
headUrls: {
type: Array,
value: [],
observer: "_headUpdate"
},
},
/**
* 组件的初始数据
*/
data: {
backImg: 'back.png',
person1Show: true,
person2Show: true,
person3Show: false,
},
/**
* 组件的方法列表
*/
methods: {
_headUpdate(newVal, oldVal) {
console.log(`check headUpdate newVal = ${JSON.stringify(newVal)}`);
this.setData({
headUrls: newVal
});
},
}
})
PinTuanHead.json
{
"component": true,
"usingComponents": {}
}
PinTuanHead.wxml
<view class="wrap">
<image wx:if="{{person1Show}}" class="icon" src="{{headUrls[0].avatar}}" mode="widthFix"></image>
<image wx:else class="back" src="{{backImg}}" mode="widthFix"></image>
<image wx:if="{{person2Show}}" class="icon" src="{{headUrls[1].avatar}}" mode="widthFix"></image>
<image wx:else class="back" src="{{backImg}}" mode="widthFix"></image>
<image wx:if="{{person3Show}}" class="icon" src="{{headUrls[2].avatar}}" mode="widthFix"></image>
<image wx:else class="back" src="{{backImg}}" mode="widthFix"></image>
</view>
PinTuanHead.wxss
.wrap {
margin: 34rpx 0 0 40rpx;
padding: 0;
display: flex;
flex-direction: row;
width: 100%;
height: 84rpx;
}
.icon, .back {
width: 84rpx !important;
height: 84rpx !important;
}
.icon {
border: 1rpx solid #E6E6E6;
border-radius: 50%;
}
.back:nth-child(1),
.icon:nth-child(1) {
margin-left: 0rpx;
z-index: 1;
}
.back:nth-child(2),
.icon:nth-child(2) {
margin-left: -12rpx;
z-index: 2;
}
.back:nth-child(3),
.icon:nth-child(3) {
margin-left: -12rpx;
z-index: 3;
}