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

功能需求

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

实现效果

实现效果.png

实现思路

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容