微信小程序基于movable-area实现DIY T恤/logo定制

322 阅读5分钟

功能需求

可以通过上传两个图片,一个是可以定制的T恤/背包等背景图,一个是定制的logo图片。让用户可以可以拖动logo图片放置在背景图上粗略实现DIY的预览效果。具体要求:可手势放大/缩小,可面板操作切换图片,可面板操作放大缩小对应的图片,可本地选择图片。

实现效果

实现效果.gif

实现思路

原生容器组件的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.增加旋转功能