功能需求
可以通过上传两个图片,一个是可以定制的T恤/背包等背景图,一个是定制的logo图片。让用户可以可以拖动logo图片放置在背景图上粗略实现DIY的预览效果。具体要求:可手势放大/缩小,可面板操作切换图片,可面板操作放大缩小对应的图片,可本地选择图片。
实现效果
实现思路
原生容器组件的movable-area | 微信开放文档 (qq.com)已经内部实现了拖动和放大缩小,我们只需要理顺组件交互的思路以及注意事项,主要有以下: 1.movable-view必须为movable-area的子级元素。
2.两个movable-view不能同时设为可手势放大/缩小,存在冲突,因此需要在点击/拖动图片,还有点击下方tab切换背景图/logo时控制相应的movable-view是否可手势缩放。
3.点击或拖动logo/背景图片时候,与下方的操作面板的tab元素互动,因此需要监听touchstart事件。
4.点击/拖动logo时候,需要显示图片边框,在拖动结束的时候边框消失,显得更用户友好,因此需要在touchstart和touchend中做处理。
5.手势放大/缩小时,需要同步下方操作面板的放大倍数,因此需要绑定scale的值(movable-view提供)。
6.(重点)手势放大缩小事件是一种resize事件,如果每次resize都要更新一次面板计步器的话是十分浪费资源的,因此需要进行函数防抖(debounce),当触发时,如果规定时间间隔:500ms(个人设置的值)内再次触发resize事件,则把时间间隔更新,只有在最后一次resize事件执行后且500ms内没有再次触发resize事件,才进行计步器值的更新,具体防抖的原理和应用可以自行搜索。
代码实现
WXML
<view class="diy-container">
<van-nav-bar
title="定制预览"
left-text="返回"
left-arrow
class="head-nav-bar"
safe-area-inset-top="{{false}}"
bind:click-left="onClickLeft"
>
</van-nav-bar>
<view class="mv-container">
<movable-area class="mv-area" scale-area>
<movable-view model:scale="{{ chosenView === 'bg' }}" bindtouchstart="onBgTouchStart" bindscale="onBgScale" direction="all" model:scale-value="{{bgScaleRate}}" class="bg-view">
<view class="bg-view-label">
背景图
</view>
<image mode="widthFix" class="bg-image" src="{{bgImagePath}}"/>
</movable-view>
<movable-view model:scale="{{ chosenView == 'logo' }}" bindtouchstart="onLogoTouchStart" bindtouchend="onLogoTouchingEnd" bindscale="onLogoScale" direction="all" scale-value="{{logoScaleRate}}" class="logo-view">
<view class="logo-view-label {{ isLogoTouching ? '' : 'logo-view-label-touching' }}">
logo
</view>
<image mode="widthFix" class="logo-image {{ isLogoTouching ? 'logo-image-touching' : ''}}" src="{{logoImagePath}}"/>
</movable-view>
</movable-area>
</view>
<view class="operation-container">
<van-tabs active="{{chosenView}}" bind:change="onTabChange" class="tabs" color="#409EFF">
<van-tab name="bg" class="bg-tab" title="背景图">
<view wx:if="{{bgImagePath}}" class="bg-scale-rate-controller">
<view class="bg-scale-rate-label">
<view class="bg-scale-rate-text">
图片缩放倍数:
</view>
</view>
<view class="bg-scale-rate-stepper-container">
<van-stepper bind:change="onBgScaleRateChange" class="bg-scale-rate-stepper" model:value="{{ bgStepperValue }}" step="0.1" disable-input min="{{0.5}}" max="{{3}}" />
</view>
</view>
<view class="bg-selector-container">
<van-button bindtap="onBgPicChoose" size="small" type="primary" round>
本地选择图片
</van-button>
</view>
</van-tab>
<van-tab name="logo" title="logo">
<view wx:if="{{logoImagePath}}" class="logo-scale-rate-controller">
<view class="logo-scale-rate-label">
<view class="logo-scale-rate-text">
logo缩放倍数:
</view>
</view>
<view class="logo-scale-rate-stepper-container">
<van-stepper bind:change="onLogoStepperValueChange" class="logo-scale-rate-stepper" value="{{ logoStepperValue }}" step="0.1" disable-input min="{{0.5}}" max="{{3}}" />
</view>
</view>
<view class="logo-selector-container">
<van-button bindtap="onLogoPicChoose" size="small" type="primary" round>
本地选择图片
</van-button>
</view>
</van-tab>
</van-tabs>
</view>
</view>
WXSS
page {
padding: 0;
margin: 0;
}
.diy-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
.head-nav-bar {
padding: 0px;
margin: 0;
}
.mv-container {
flex-grow: 1;
}
.mv-area {
background: greenyellow;
left: 2.5%;
width: 95%;
height: 100%;
}
.bg-view {
width: 90%;
height: 80%;
top: 10%;
left: 5%;
position: relative;
}
.bg-view-label {
background: blue;
color: white;
display: inline-block;
padding: 5px;
font-size: 20rpx;
}
.bg-image {
width: 100%;
}
.logo-view {
width: 20%;
left: 40%;
top: 20%;
}
.logo-view-label {
color: white;
display: inline-block;
padding: 5px;
font-size: 20rpx;
background: red;
}
.logo-view-label-touching {
opacity: 0;
transition: .3s opacity ease-in-out;
}
.logo-image {
width: 100%;
border: 1px solid transparent;
transition: .3s border ease-in-out;
}
.logo-image-touching {
border: 1px dashed red;
transition: .3s border ease-in-out;
}
.operation-container {
height: 20vh;
min-height: 100px;
position: relative;
background: #fff;
}
.bg-scale-rate-controller {
display: flex;
align-items: center;
padding-left: 30rpx;
margin-top: 15rpx;
}
.bg-scale-rate-label {
flex-grow: 1;
text-align: left;
}
.bg-scale-rate-stepper-container {
flex-grow: 1;
}
.bg-selector-container {
margin-left: 30rpx;
margin-top: calc(20vh - 74px - 40px - 15rpx);
}
.logo-scale-rate-controller {
display: flex;
align-items: center;
padding-left: 30rpx;
margin-top: 15rpx;
}
.logo-scale-rate-label {
flex-grow: 1;
text-align: left;
}
.logo-scale-rate-stepper-container {
flex-grow: 1;
}
.logo-selector-container {
margin-left: 30rpx;
margin-top: calc(20vh - 74px - 40px - 15rpx);
}
js
import { debounce } from '../../utils/utils'
Page({
/**
* 页面的初始数据
*/
data: {
bgScaleRate: 1.0, //背景图放大倍数
bgStepperValue: 1.0, // 背景图放大倍数计步器数值
logoScaleRate: 1.0, // logo放大倍数
logoStepperValue:1.0, // logo计步器放大倍数
bgImagePath:'https://img.zcool.cn/community/01310c5afd1b97a801218cf453e8a4.jpg@1280w_1l_2o_100sh.jpg', // 背景图路径
logoImagePath:'https://www.logosc.cn/uploads/icon/2018/10/10/dfd25b38-ef01-4d83-abdb-57d1e0bfc25a.png', // logo图路径
chosenView:'bg', // 当前选择movable-view, 用于该元素是否可以手势放大
isLogoTouching: true // 是否正在点击/拖动logo,用于控制logo的边框线和label是否显示
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
},
/**
* 背景图片选择
*/
onBgPicChoose: function() {
const that = this;
wx.chooseMedia({
count:1,
mediaType:['image'],
sourceType:['album'],
success(res) {
if(res.tempFiles[0]?.tempFilePath) {
that.setData({
bgImagePath: res.tempFiles[0].tempFilePath,
bgScaleRate: 1,
bgStepperValue: 1
});
}
}
})
},
/**
* Logo选择
*/
onLogoPicChoose: function() {
const that = this;
wx.chooseMedia({
count:1,
mediaType:['image'],
sourceType:['album'],
success(res) {
that.setData({
logoImagePath: res.tempFiles[0].tempFilePath,
isLogoTouching: true,
logoStepperValue: 1,
logoScaleRate: 1
});
// console.log(res.tempFiles.size)
}
})
},
/**
* 背景图片步进器值发生变化事件
*/
onBgScaleRateChange: function(value) {
this.setData({
bgScaleRate:value.detail
})
},
/**
* 背景图片手势缩放事件监听
*/
onBgScale: debounce(function(event) {
if(event.detail.scale != this.data.bgScaleRate) {
this.setData({
bgStepperValue: event.detail.scale
});
}
}),
/**
* 背景图触摸开始事件
*/
onBgTouchStart: function() {
this.setData({
chosenView:'bg'
})
},
/**
* logo缩放计步器值改变事件
*/
onLogoStepperValueChange: function(event) {
this.setData({
logoScaleRate: event.detail
});
},
/**
* logo触摸开始事件
*/
onLogoTouchStart: function() {
this.setData({
isLogoTouching: true,
chosenView:'logo'
});
},
/**
* logo触摸结束事件
*/
onLogoTouchingEnd: function() {
this.setData({
isLogoTouching: false
});
},
/**
* logo图片手势缩放事件监听
*/
onLogoScale: debounce(function(event) {
if(this.data.logoScaleRate != event.detail.scale) {
this.setData({
logoStepperValue: event.detail.scale
});
}
}),
/**
* 选项卡点击事件
*/
onTabChange: function(event) {
this.setData({
chosenView: event.detail.name
})
},
/**
* 顶部返回点击事件
*/
onClickLeft: function() {
let pageObject = getCurrentPages();
if(pageObject.length == 1) {
wx.navigateTo({
url: '/pages/index/index',
})
}
}
})
utils(debounc防抖函数的实现)
/**
* 防抖函数
* @param {*} fun 需要进行防抖的函数
*/
export function debounce(fun, delay = 500, immediate= false) {
let timer = null; // 保存定时器
return function(args) {
let that = this;
let _args = args;
if(timer) clearTimeout(timer);
if(immediate) {
if(!timer) fun.apply(that,_args); // 定时器为空表示可以执行
timer = setTimeout(function() {
timer = null;// 到时间后设置定时器为空
},delay);
}
else {
// 如非立即执行,则重设定时器
timer = setTimeout(function() {
fun.call(that,_args);
},delay);
}
}
}
json (代码中用到的vant组件, 可以自行替换为原生组件)
{
"usingComponents": {
"van-tab": "@vant/weapp/tab/index",
"van-tabs": "@vant/weapp/tabs/index"
}
}
优化
1.增加保存功能,对完成的图片进行保存。 2.增加旋转功能