微信小程序基于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閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容