VisualUIEditor項目講解之撤消反撤消詳解

#VisualUIEditor項目講解之撤消反撤消詳解 撤消反撤消在UI編輯器裏面必備的功能,它能夠幫助咱們在編輯過程當中,無需懼怕誤操做,助您更好的使用編輯器node

#在項目中使用 當您在項目中錯誤的移動位置,或者設置了錯誤的屬性,這時候您能夠用CTRL+Z來撤消剛纔您進行的修改 當您在項目中撤消了剛纔的操做,又想快速的回到剛纔的狀態,這時候您可使用CTRL+R反撤消功能,來進行反撤消git

#源碼講解 ##源碼路徑 項目的實現源碼,可參考renderUndogithub

##源碼詳解 每一個Scene實例化的時候都會建立一個UndoObj,UndoObj裏面是一層UndoList的淺封裝,差異是在Undo和Redo的時候一般編輯器有節點發生變動編輯器

"ui:scene_change"(event, message) {
    let runScene = this.$.scene.getRunScene();
    if(!runScene._undo)
        runScene._undo =  new UndoObj();
}

UndoList維持着場景變動的記錄,UndoList的構造函數以下函數

class UndoList extends EventEmitter {
  constructor (type) {
    super()
    //是否變化時發送事件的變化
    this._silent = false
    //分爲local和global類型, local類型事件變化只會通知本地, global則會一般整個編輯器
    this._type = type

    //當前的命令列表
    this._curGroup = new CommandGroup()
    //操做過程的命令列表
    this._groups = []
    //記錄當前的位置信息
    this._position = -1
    //上一次保存時的位置信息
    this._savePosition = -1
  }
}

當有新的操做到達時, 好比節點移動位置發生變動, 這時候則會調用函數ui

add (cmd) {
    this._clearRedo()
    if (this._curGroup.isCanCombine(cmd)) {
        this._curGroup.combineCommand(cmd)
    } else {
        this.commit()
        this._curGroup.add(cmd)
    }
    this._changed('add-command')
}

由於整個撤消反撤消操做是單線的, 因此當有新的操做, 以前保存的redo操做變成無心義, 這時候會先清除redo列表, 即當前位置後命令列表 這時候咱們會判斷該命令是否可以合併, 若是能合併, 咱們會優先嚐試合併this

//CommandGroup的函數
  isCanCombine (other) {
    if (this._commands.length == 0) {
      return true
    }
    for ( let i = 0; i < this._commands.length; ++i) {
      if (this._commands[i].isCanCombine(other)) {
        return true
      }
    }
    if (this._time && Math.abs(this._time - other.info.time) < 1000) {
      return true
    }
    return false
  }

  //Command的函數
  isCanCombine (other) {
    if (!this.info || !other.info) {
      return false
    }

    if (this.info.op != other.info.op) {
      return false
    }

    if (this.info.uuid != other.info.uuid) {
      return false
    }

    if (this.info.op == 'prop' && (this.info.prop != other.info.prop)) {
      return false
    }

    if (Math.abs(this.info.time - other.info.time) >= 1000) {
      return false
    }
    return true
  }

在CommandGroup中, 咱們會遍歷全部的Command,看可否進行合併或者最後一條Command的時間距離如今的時間過短,咱們則認爲可以合併 在Command中,同一種操做類型,同一個節點,同一個屬性操做,時間在必定的時間內方可認爲能合併 若是判斷能合併,則進行合併並更新屬性值設計

###爲何如此設計?code

由於整個撤消系統中,都是假定對其它系統一無所知的,而其它系統除了有限的Add接口,並無暴露其它的接口信息 下面列舉兩種情景接口

  • 當在編輯器中,拖動某個節點移動的時候,每隔350ms會觸發一次mousemove事件,這時候節點屬性會頻繁更改,而咱們又認爲這是一次操做,則咱們應當在移動完成以後按一次撤消應該回到以前的狀態,而不該該是移動過程當中的任何狀態,這時候用到同屬性操做,在必定時間內合併操做
  • 當在編輯器中,一次拖動或者更改多個節點的屬性,這時候預期的撤消應該爲此次修改的全部屬性同時回到變動以前的狀態,而不該該是一個個節點的屬性變動,因此這時候會合並全部的操做,當成一個組別

當判斷不能合併的時候,則會新起一條CommandGroup來記錄新的Command,並更新位置信息

撤消Undo

undo () {
    // check if we have un-commit group
    if (this._curGroup.canCommit()) {
      this._curGroup.undo()
      this._changed('undo-cache')
      this._groups.push(this._curGroup)
      this._curGroup = new CommandGroup()
      return true
    }

    // check if can undo
    if (this._position < 0) {
      return false
    }

    let group = this._groups[this._position]
    group.undo()
    this._position--
    this._changed('undo')
    return true
  }

撤消時,存在如下三種狀況

  • 當前的Group可提交,即當前Group記錄着一些操做信息,而且咱們沒有提交操做,這時直接對當前的Group執行Undo,並更新列表信息
  • 無可撤消的內容,即位置信息小於0
  • 可撤消,獲取當前位置的Group進行撤消,並更新位置信息
//Command undo
  undo () {
    let node = cocosGetItemByUUID(this.info.scene, this.info.uuid)
    if (this.info.op == 'prop') {
      if (!node) {
        return false
      }
      if (this.info.doPropChange) {
        this.info.doPropChange(node, this.info.prop, this.info.oldValue)
      } else {
        NodePropChange(node, this.info.prop, this.info.oldValue)
      }
      return true
    }
    console.warn('Please implement undo function in your command')
  }

每一個命令列表,首先會先嚐試獲取節點,而後根據節點的屬性變動,進行新舊值的設置

反撤消Redo

redo () {
    // check if can redo
    if (this._position >= this._groups.length - 1) {
      return false
    }

    this._position++
    let group = this._groups[this._position]
    group.redo()

    this._changed('redo')
    return true
  }

存在如下兩種狀況

  • 當前位置處理列表的最後一位,即無可反撤消的內容,此時不執行任何操做
  • 獲取該反撤消的Group,對其執行redo操做
//Command redo
  redo () {
    let node = cocosGetItemByUUID(this.info.scene, this.info.uuid)
    if (this.info.op == 'prop') {
      if (!node) {
        return false
      }

      if (this.info.doPropChange) {
        this.info.doPropChange(node, this.info.prop, this.info.newValue)
      } else {
        NodePropChange(node, this.info.prop, this.info.newValue)
      }
      return true
    }
    console.warn('Please implement redo function in your command')
  }

每一個命令列表,首先會先嚐試獲取節點,而後根據節點的屬性變動,進行新舊值的設置

在編輯器中添加Command

function addNodeCommand (node, prop, oldValue, newValue, doPropChange) {
  let scene = getRootNode(node)
  if (!scene._undo) {
    return
  }

  tryAddCommand(scene._undo, newPropCommandChange(scene, node.uuid, prop, oldValue, newValue, doPropChange))
}

當屬性發生變動的時候,咱們會調用addNodeCommand值進行相應的設置,來添加撤消反撤消功能的支持,如下的參數說明

  • node即發生變化的節點
  • prop即發生變化的屬性值,值爲x,width這種
  • oldValue即當前node節點的值
  • newValue爲node屬性將要變動的值
  • doPropChange若是沒有傳該函數,則會調用NodePropChange進行修改,若是伴隨屬性變動,需調用相關函數,則能夠傳自定義函數,原型以下
function(node, prop, newValue) {}

其它未列出的信息,可參考源碼的實現細節

#其它信息 VisualUIEditor開發QQ羣歡迎您的加入: 453224679

相關文章
相關標籤/搜索