(急着解決問題的同學可以直接跳最底部查看最終的解決方案)
問題描述
因為前段時間搶到了華為榮耀3c,所以做項目的時候就用榮耀3c測試了一下項目,
結果發現在華為的emotion ui上sencha touch的messagebox的彈窗,彈出后點擊確認按鈕時無法隱藏,
有的圓角框還有會缺邊,不過不仔細看倒是不看得出來,
這是我的項目在手機上的截圖,
當我點擊確定按鈕的時候,messagebox的模態背景消失了,但是彈窗並不會消失,仔細看登陸框的圓角,有點缺邊,我想華為應該是改過系統的瀏覽器內核了,至於做了哪些變動,這還真說不清
對於圓角缺邊,只能暫時無視了,但是彈窗不能消失的情況嚴重影響用戶使用,
在后來的測試中,發現了更為嚴重的bug,項目中所有組件的hide事件都不會觸發,
導致我在hide事件中手動進行銷毀的全部失效了,
而官方的例子運行起來也存在很多問題
問題解析:
為了找出問題的所在,
首先,我下載了幾款別人已經發布的sencha touch的apk進行了下測試,
發現在emotion ui 2.0上都存在這些bug,無意中又發現魔狼在世很久之前做的 《迷尚豆撈》 竟然沒問題,經魔狼本人確認是使用2.0版本的sencha touch進行開發的項目,
於是我下載了從2.0版本到2.3.1版本的sencha touch的sdk進行了測試,
最終發現從2.2.1版本開始都存在這個問題,
我想應該可以通過代碼解決這個問題,於是花費了大量的時間開始調試查看源碼,對於這個在pc上完全沒有問題,在手機自帶的瀏覽器上才會出現的bug只能通過在android上用logcat查看console輸出來和pc端的調試結果進行比對來找出差別了,
通過大量的調試查找,最終被找到了問題所在,並且發現問題描述中的所有bug都是因為這個問題產生的,
原來,當組件執行隱藏的時候會觸發Component.js里的hide方法
代碼如下:
hide: function(animation) { this.setCurrentAlignmentInfo(null); if(this.activeAnimation) {//激活的動畫對象,相當於正在運行中的動畫 this.activeAnimation.on({ animationend: function(){ this.hide(animation); }, scope: this, single: true }); return this; }
//判斷組件是否被隱藏,如果沒有被隱藏通過setHidden(true)進行隱藏操作 if (!this.getHidden()) { if (animation === undefined || (animation && animation.isComponent)) { animation = this.getHideAnimation(); } if (animation) { if (animation === true) { animation = 'fadeOut'; } this.onBefore({ hiddenchange: 'animateFn', scope: this, single: true, args: [animation] }); } this.setHidden(true);//進行隱藏操作,正常情況下,操作執行完,激活的動畫運行完會被重置為null } return this; }
當執行setHidden時會觸發Evented.js里的設置方法並最終觸發Componet.js里的animateFn方法,此方法會將activateAnimation重置為null,
但是在華為的手機上並沒有被重置,
繼續查看animateFn函數
animateFn: function(animation, component, newState, oldState, options, controller) { var me = this; if (animation && (!newState || (newState && this.isPainted()))) { this.activeAnimation = new Ext.fx.Animation(animation);//給激活動畫對象設置一個動畫對象 this.activeAnimation.setElement(component.element); if (!Ext.isEmpty(newState)) { this.activeAnimation.setOnEnd(function() { me.activeAnimation = null;//當動畫結束的時候重置activateAnimation為null controller.resume(); }); controller.pause(); } Ext.Animator.run(me.activeAnimation);//運行動畫 } }
在這個方法中我們看到activeAnimation綁定了end事件,在setOnEnd里將activateAnimation進行了重置,但是在emotion ui上卻沒有觸發這段代碼,
於是繼續往下查找,通過
Ext.Animator.run(me.activeAnimation)
我們進入動畫執行階段
這里會調用到這下面的CssTransition.js里的run方法

run的執行過程沒有任何問題,關鍵問題就是這個js里有個onAnimationEnd方法,它在emotion ui上沒有被觸發,
而這個方法是通過refreshRunningAnimationsData這個方法觸發的
1 refreshRunningAnimationsData: function(element, propertyNames, interrupt, replace) { 2 var id = element.getId(), 3 runningAnimationsData = this.runningAnimationsData, 4 runningData = runningAnimationsData[id]; 5 6 if (!runningData) { 7 return; 8 } 9 10 var nameMap = runningData.nameMap, 11 nameList = runningData.nameList, 12 sessions = runningData.sessions, 13 ln, j, subLn, name, 14 i, session, map, list, 15 hasCompletedSession = false; 16 17 interrupt = Boolean(interrupt); 18 replace = Boolean(replace); 19 20 if (!sessions) { 21 return this; 22 } 23 24 ln = sessions.length; 25 26 if (ln === 0) { 27 return this; 28 } 29 30 if (replace) { 31 runningData.nameMap = {}; 32 nameList.length = 0; 33 34 for (i = 0; i < ln; i++) { 35 session = sessions[i]; 36 this.onAnimationEnd(element, session.data, session.animation, interrupt, replace); 37 } 38 39 sessions.length = 0; 40 } 41 else { 42 for (i = 0; i < ln; i++) { 43 session = sessions[i]; 44 map = session.map; 45 list = session.list; 46 47 for (j = 0,subLn = propertyNames.length; j < subLn; j++) { 48 name = propertyNames[j]; 49 50 if (map[name]) {//當執行transform的時候這里傳過來的name是-webkit-transform,但是map里只有transform屬性,問題就出在這里,匹配不一致導致動畫不會被移除 51 delete map[name];//動畫存在移除匹配的動畫屬性 52 Ext.Array.remove(list, name); 53 session.length--;//因為map不匹配,導致少執行一次session.length--,session.length永遠不為0 54 if (--nameMap[name] == 0) { 55 delete nameMap[name]; 56 Ext.Array.remove(nameList, name); 57 } 58 } 59 } 60 61 if (session.length == 0) {//當動畫移除完畢時執行 62 sessions.splice(i, 1); 63 i--; 64 ln--; 65 66 hasCompletedSession = true; 67 this.onAnimationEnd(element, session.data, session.animation, interrupt);//觸發動畫結束事件,最終組件被隱藏,hide事件被觸發 68 } 69 } 70 } 71 72 if (!replace && !interrupt && sessions.length == 0 && hasCompletedSession) { 73 this.onAllAnimationsEnd(element); 74 } 75 }
問題就出在上面代碼第50行的判斷那里,
propertyNames對應的是從onTransitionEnd方法里傳過來的e.browserEvent.propertyName參數
sencha touch里的這個browserEvent封裝的是瀏覽器的原生對象,當執行到css的transform時候,這個propertyName對應的是"-webkit-transform",
而map對象里保存的是run方法里傳的目標動畫的相關內容,map里卻是transform屬性,因為匹配不對,導致session.length--少執行一次,session.length永遠不為0,
所以后面的onAnimationEnd即動畫結束的方法永遠不被觸發,
然后Msgbox也就不會隱藏了,同時,所有的hide事件也沒有被觸發,
為什么會不匹配呢,
我們往上查找,
原來最終問題是在run方法里導致的
1 run: function(animations) { 2 var me = this, 3 isLengthPropertyMap = this.lengthProperties, 4 fromData = {}, 5 toData = {}, 6 data = {}, 7 element, elementId, from, to, before, 8 fromPropertyNames, toPropertyNames, 9 doApplyTo, message, 10 runningData, elementData, 11 i, j, ln, animation, propertiesLength, sessionNameMap, 12 computedStyle, formattedName, name, toFormattedValue, 13 computedValue, fromFormattedValue, isLengthProperty, 14 runningNameMap, runningNameList, runningSessions, runningSession; 15 16 if (!this.listenersAttached) { 17 this.attachListeners(); 18 } 19 20 animations = Ext.Array.from(animations); 21 22 for (i = 0,ln = animations.length; i < ln; i++) { 23 animation = animations[i]; 24 animation = Ext.factory(animation, Ext.fx.Animation); 25 element = animation.getElement(); 26 27 // Empty function to prevent idleTasks from running while we animate. 28 Ext.AnimationQueue.start(Ext.emptyFn, animation); 29 30 computedStyle = window.getComputedStyle(element.dom); 31 32 elementId = element.getId(); 33 34 data = Ext.merge({}, animation.getData()); 35 36 if (animation.onBeforeStart) { 37 animation.onBeforeStart.call(animation.scope || this, element); 38 } 39 animation.fireEvent('animationstart', animation); 40 this.fireEvent('animationstart', this, animation); 41 42 data[elementId] = data; 43 44 before = data.before; 45 from = data.from; 46 to = data.to; 47 48 data.fromPropertyNames = fromPropertyNames = []; 49 data.toPropertyNames = toPropertyNames = []; 50 51 for (name in to) { 52 if (to.hasOwnProperty(name)) { 53 to[name] = toFormattedValue = this.formatValue(to[name], name); 54 formattedName = this.formatName(name);//這里就是出問題的地方,傳進去的name是transform,這個formatName就是判斷你的瀏覽器屬性然后對這個那么進行前綴添加 55 isLengthProperty = isLengthPropertyMap.hasOwnProperty(name); 56 57 if (!isLengthProperty) { 58 toFormattedValue = this.getCssStyleValue(formattedName, toFormattedValue); 59 } 60 61 if (from.hasOwnProperty(name)) { 62 from[name] = fromFormattedValue = this.formatValue(from[name], name); 63 64 if (!isLengthProperty) { 65 fromFormattedValue = this.getCssStyleValue(formattedName, fromFormattedValue); 66 } 67 68 if (toFormattedValue !== fromFormattedValue) { 69 fromPropertyNames.push(formattedName); 70 toPropertyNames.push(formattedName); 71 } 72 } 73 else { 74 computedValue = computedStyle.getPropertyValue(formattedName); 75 76 if (toFormattedValue !== computedValue) { 77 toPropertyNames.push(formattedName); 78 } 79 } 80 } 81 } 82 83 propertiesLength = toPropertyNames.length; 84 85 if (propertiesLength === 0) { 86 this.onAnimationEnd(element, data, animation); 87 continue; 88 } 89 90 runningData = this.getRunningData(elementId); 91 runningSessions = runningData.sessions; 92 93 if (runningSessions.length > 0) { 94 this.refreshRunningAnimationsData( 95 element, Ext.Array.merge(fromPropertyNames, toPropertyNames), true, data.replacePrevious 96 ); 97 } 98 99 runningNameMap = runningData.nameMap; 100 runningNameList = runningData.nameList; 101 102 sessionNameMap = {}; 103 for (j = 0; j < propertiesLength; j++) { 104 name = toPropertyNames[j]; 105 sessionNameMap[name] = true; 106 107 if (!runningNameMap.hasOwnProperty(name)) { 108 runningNameMap[name] = 1; 109 runningNameList.push(name); 110 } 111 else { 112 runningNameMap[name]++; 113 } 114 } 115 116 runningSession = { 117 element: element, 118 map: sessionNameMap, 119 list: toPropertyNames.slice(), 120 length: propertiesLength, 121 data: data, 122 animation: animation 123 }; 124 runningSessions.push(runningSession); 125 126 animation.on('stop', 'onAnimationStop', this); 127 128 elementData = Ext.apply({}, before); 129 Ext.apply(elementData, from); 130 131 if (runningNameList.length > 0) { 132 fromPropertyNames = Ext.Array.difference(runningNameList, fromPropertyNames); 133 toPropertyNames = Ext.Array.merge(fromPropertyNames, toPropertyNames); 134 elementData['transition-property'] = fromPropertyNames; 135 } 136 137 fromData[elementId] = elementData; 138 toData[elementId] = Ext.apply({}, to); 139 140 toData[elementId]['transition-property'] = toPropertyNames; 141 toData[elementId]['transition-duration'] = data.duration; 142 toData[elementId]['transition-timing-function'] = data.easing; 143 toData[elementId]['transition-delay'] = data.delay; 144 145 animation.startTime = Date.now(); 146 } 147 148 message = this.$className; 149 150 this.applyStyles(fromData); 151 152 doApplyTo = function(e) { 153 if (e.data === message && e.source === window) { 154 window.removeEventListener('message', doApplyTo, false); 155 me.applyStyles(toData); 156 } 157 }; 158 159 if(Ext.browser.is.IE) { 160 window.requestAnimationFrame(function() { 161 window.addEventListener('message', doApplyTo, false); 162 window.postMessage(message, '*'); 163 }); 164 }else{ 165 window.addEventListener('message', doApplyTo, false); 166 window.postMessage(message, '*'); 167 } 168 }
54行的formatName這個方法是對瀏覽器進行css判斷然后給傳進去的name參數加上瀏覽器前綴,
最終回傳給formattedName,而這個formattedName最終會對應到map里的屬性,
但是這個formatName在emotion Ui上的判斷跟預期不一樣
我們看一下formatName的方法
1 formatName: function(name) { 2 var cache = this.formattedNameCache, 3 formattedName = cache[name]; 4 5 if (!formattedName) { 6 if ((Ext.os.is.Tizen || !Ext.feature.has.CssTransformNoPrefix) && this.prefixedProperties[name]) {//Ext.feature.has.CssTransformNoPrefix判斷 7 formattedName = this.vendorPrefix + name; //結果相反了,導致執行了else里的代碼,將transform 8 } //在沒有加前綴的情況下返回了回去 9 else { 10 formattedName = name; 11 } 12 13 cache[name] = formattedName; 14 } 15 16 return formattedName; 17 }
如上所示,在判斷Ext.feature.has.CssTransformNoPrefix的時候預期結果跟實際相反了,
emotion ui自帶瀏覽器判斷的結果是true,但實際上應該為false,
於是導致執行了下面else里的代碼,
name參數transform傳了進來,沒有加上前綴又以transform傳了回去,本應該傳-webkit-transform的
但是在后來的判斷中原生event對象里的propertyName又是加前綴的,
於是導致了refreshRunningAnimationsData里map["-webkit-transform"]匹配不一致,
代碼判斷不對,於是session.length--少執行一次,
session.length不會為0就不會觸發后面的onAnimationEnd方法了,最終,
組件沒有被隱藏,hide事件沒有被觸發,
那老版本的sencha touch為什么沒這個問題,
因為老版本沒有對
(Ext.os.is.Tizen || !Ext.feature.has.CssTransformNoPrefix)
所以老版本沒有出現這個問題,
在其他的android系統上,Ext.feature.has.CssTransformNoPrefix這個值都是false,即不支持沒有前綴,
包括最新版本的chrome,但是在華為emotion ui上這個判斷不對了,原因是什么,我也不清楚,
最終解決方案:
由於sencha touch對css前綴判斷有些問題,所以最終我修改了touch/src/fx/runner/CssTransition.js中的源碼,
因為前綴不匹配,所以我將瀏覽器自帶事件的propertyName做了處理,以保證前綴一致,
修改文件中onTransitionEnd方法如下:
onTransitionEnd: function (e) { var target = e.target, id = target.id, propertyName = e.browserEvent.propertyName, styleDashPrefix = Ext.browser.getStyleDashPrefix(); if (id && this.runningAnimationsData.hasOwnProperty(id)) { if (Ext.feature.has.CssTransformNoPrefix) { if (propertyName.indexOf(styleDashPrefix) >= 0) { propertyName = propertyName.substring(styleDashPrefix.length); } } this.refreshRunningAnimationsData(Ext.get(target), [propertyName]); } }