美文网首页
微信小程序基于movable-area实现DIY T恤/logo

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

作者: 跟着Damon写代码 | 来源:发表于2022-04-15 08:36 被阅读0次

    功能需求

    可以通过上传两个图片,一个是可以定制的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.增加旋转功能

    相关文章

      网友评论

          本文标题:微信小程序基于movable-area实现DIY T恤/logo

          本文链接:https://www.haomeiwen.com/subject/pasfertx.html