微信小程序基于movable-area實現(xiàn)DIY T恤/logo定制

功能需求

可以通過上傳兩個圖片乎完,一個是可以定制的T恤/背包等背景圖,一個是定制的logo圖片品洛。讓用戶可以可以拖動logo圖片放置在背景圖上粗略實現(xiàn)DIY的預覽效果树姨。具體要求:可手勢放大/縮小摩桶,可面板操作切換圖片,可面板操作放大縮小對應的圖片帽揪,可本地選擇圖片硝清。

實現(xiàn)效果

實現(xiàn)效果.png

實現(xiàn)思路

原生容器組件的movable-area | 微信開放文檔 (qq.com)已經(jīng)內(nèi)部實現(xiàn)了拖動和放大縮小,我們只需要理順組件交互的思路以及注意事項转晰,主要有以下:
1.movable-view必須為movable-area的子級元素耍缴。

2.兩個movable-view不能同時設為可手勢放大/縮小,存在沖突挽霉,因此需要在點擊/拖動圖片,還有點擊下方tab切換背景圖/logo時控制相應的movable-view是否可手勢縮放变汪。

3.點擊或拖動logo/背景圖片時候侠坎,與下方的操作面板的tab元素互動,因此需要監(jiān)聽touchstart事件裙盾。

4.點擊/拖動logo時候实胸,需要顯示圖片邊框,在拖動結(jié)束的時候邊框消失番官,顯得更用戶友好庐完,因此需要在touchstart和touchend中做處理。

5.手勢放大/縮小時徘熔,需要同步下方操作面板的放大倍數(shù)门躯,因此需要綁定scale的值(movable-view提供)。

6.(重點)手勢放大縮小事件是一種resize事件酷师,如果每次resize都要更新一次面板計步器的話是十分浪費資源的讶凉,因此需要進行函數(shù)防抖(debounce),當觸發(fā)時山孔,如果規(guī)定時間間隔:500ms(個人設置的值)內(nèi)再次觸發(fā)resize事件懂讯,則把時間間隔更新,只有在最后一次resize事件執(zhí)行后且500ms內(nèi)沒有再次觸發(fā)resize事件台颠,才進行計步器值的更新褐望,具體防抖的原理和應用可以自行搜索。

代碼實現(xiàn)

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">
              圖片縮放倍數(shù):
            </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縮放倍數(shù):
            </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({

  /**
   * 頁面的初始數(shù)據(jù)
   */
  data: {
    bgScaleRate: 1.0, //背景圖放大倍數(shù)
    bgStepperValue: 1.0, // 背景圖放大倍數(shù)計步器數(shù)值
    logoScaleRate: 1.0, // logo放大倍數(shù)
    logoStepperValue:1.0, // logo計步器放大倍數(shù)
    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是否顯示
  },

  /**
   * 生命周期函數(shù)--監(jiān)聽頁面加載
   */
  onLoad: function (options) {

  },

  /**
   * 生命周期函數(shù)--監(jiān)聽頁面初次渲染完成
   */
  onReady: function () {

  },

  /**
   * 生命周期函數(shù)--監(jiān)聽頁面顯示
   */
  onShow: function () {

  },

  /**
   * 生命周期函數(shù)--監(jiān)聽頁面隱藏
   */
  onHide: function () {

  },

  /**
   * 生命周期函數(shù)--監(jiān)聽頁面卸載
   */
  onUnload: function () {

  },

  /**
   * 頁面相關事件處理函數(shù)--監(jiān)聽用戶下拉動作
   */
  onPullDownRefresh: function () {

  },

  /**
   * 頁面上拉觸底事件的處理函數(shù)
   */
  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)
      }
    })
  },

  /**
   * 背景圖片步進器值發(fā)生變化事件
   */
  onBgScaleRateChange: function(value) {
    this.setData({
      bgScaleRate:value.detail
    })
  },
  /**
   * 背景圖片手勢縮放事件監(jiān)聽
   */
  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觸摸結(jié)束事件
   */
  onLogoTouchingEnd: function() {
    this.setData({
      isLogoTouching: false
    });
  },

  /**
   * logo圖片手勢縮放事件監(jiān)聽
   */
  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防抖函數(shù)的實現(xiàn))

/**
 * 防抖函數(shù)
 * @param {*} fun 需要進行防抖的函數(shù) 
 */
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); // 定時器為空表示可以執(zhí)行
      timer = setTimeout(function() {
        timer = null;// 到時間后設置定時器為空
      },delay);
    }
    else {
      // 如非立即執(zhí)行瘫里,則重設定時器
      timer = setTimeout(function() {
        fun.call(that,_args);
      },delay);
    }
  }
}

json (代碼中用到的vant組件, 可以自行替換為原生組件)

{
  "usingComponents": {
    "van-tab": "@vant/weapp/tab/index",
    "van-tabs": "@vant/weapp/tabs/index"
  }
}

優(yōu)化

1.增加保存功能,對完成的圖片進行保存荡碾。
2.增加旋轉(zhuǎn)功能

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末减宣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子玩荠,更是在濱河造成了極大的恐慌漆腌,老刑警劉巖贼邓,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異闷尿,居然都是意外死亡塑径,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門填具,熙熙樓的掌柜王于貴愁眉苦臉地迎上來统舀,“玉大人,你說我怎么就攤上這事劳景∮颍” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵盟广,是天一觀的道長闷串。 經(jīng)常有香客問我,道長筋量,這世上最難降的妖魔是什么烹吵? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮桨武,結(jié)果婚禮上肋拔,老公的妹妹穿的比我還像新娘。我一直安慰自己呀酸,他們只是感情好凉蜂,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著性誉,像睡著了一般跃惫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上艾栋,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天爆存,我揣著相機與錄音,去河邊找鬼蝗砾。 笑死先较,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的悼粮。 我是一名探鬼主播闲勺,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼扣猫!你這毒婦竟也來了菜循?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤申尤,失蹤者是張志新(化名)和其女友劉穎癌幕,沒想到半個月后衙耕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡勺远,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年橙喘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胶逢。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡厅瞎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出初坠,到底是詐尸還是另有隱情和簸,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布碟刺,位于F島的核電站锁保,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏南誊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一蜜托、第九天 我趴在偏房一處隱蔽的房頂上張望抄囚。 院中可真熱鬧,春花似錦橄务、人聲如沸幔托。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽重挑。三九已至,卻和暖如春棠涮,著一層夾襖步出監(jiān)牢的瞬間谬哀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工严肪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留史煎,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓驳糯,卻偏偏與公主長得像篇梭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子酝枢,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容