關於sencha touch在華為、紅米等部分手機下hide事件失效,msgbox無法關閉的解決方案(已更新最新解決方案)


 (急着解決問題的同學可以直接跳最底部查看最終的解決方案)

 問題描述

  因為前段時間搶到了華為榮耀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]);
        }
    }

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM