1#
因为前段时间抢到了华为荣耀3c,所以做项目的时候就用荣耀3c测试了一下项目,

  结果发现在华为的emotion ui上sencha touch的messagebox的弹窗,弹出后点击确认按钮时无法隐藏,

  有的圆角框还有会缺边,不过不仔细看倒是不看得出来,

  这是我的项目在手机上的截图,

  当我点击确定按钮的时候,messagebox的模态背景消失了,但是弹窗并不会消失,仔细看登陆框的圆角,有点缺边,我想华为应该是改过系统的浏览器内核了,至于做了哪些变动,这还真说不清


对于圆角缺边,我倒可以暂时无视,但是弹窗不能消失的状况就坑爹了,由于项目时间比较紧,加之其他手机暂无此bug,所以暂时没有处理

  在后来的测试中,发现更严重得bug,项目中所有组件的hide事件都不会触发,

  导致我在hide事件中手动控制的组件销毁全部失效,官方的例子运行起来也存在很多问题

  项目经理让我把这个手机上的问题解决了,这下头疼了,

  于是下了别人已经发布的sencha touch的apk进行了下测试,

  发现都是这个问题,无意中发现魔狼很久之前做的 迷尚豆捞 竟然没问题,经魔狼本人确认是2.0版本的sencha touch,

  所以接下来我下载了从2.0版本到2.3.1版本的sencha touch的sdk进行了测试,最终发现从2.2.1版本开始都存在这个问题,

  我想应该可以通过代码解决这个问题,于是花了两天时间开始调试查看源码,因为在pc上完全没有问题,只有在手机自带的浏览器上才会出现这个问题,自带的浏览器无法进行远程调试,

  所以一致通过在android上用logcat查看console输出来和pc端的调试结果进行比对,找出差别,我想做过sencha touch的一定知道这有多蛋疼了,

  功夫不负有心人,最终被我找到了问题,并且发现上面的所有bug都是这个问题产生的,

  原来,当组件执行隐藏的时候会触发Component.js里的hide方法

  里面有这么一段代码
  1. hide: function(animation) {
  2.         this.setCurrentAlignmentInfo(null);
  3.         if(this.activeAnimation) {//激活的动画对象,相当于正在运行中的动画
  4.             this.activeAnimation.on({
  5.                 animationend: function(){
  6.                     this.hide(animation);
  7.                 },
  8.                 scope: this,
  9.                 single: true
  10.             });
  11.             return this;
  12.         }

  13.      //判断组件是否被隐藏,如果没有被隐藏通过setHidden(true)进行隐藏操作
  14.         if (!this.getHidden()) {
  15.             if (animation === undefined || (animation && animation.isComponent)) {
  16.                 animation = this.getHideAnimation();
  17.             }
  18.             if (animation) {
  19.                 if (animation === true) {
  20.                     animation = 'fadeOut';
  21.                 }
  22.                 this.onBefore({
  23.                     hiddenchange: 'animateFn',
  24.                     scope: this,
  25.                     single: true,
  26.                     args: [animation]
  27.                 });
  28.             }
  29.             this.setHidden(true);//进行隐藏操作,正常情况下,操作执行完,激活的动画运行完会被重置为null
  30.         }
  31.         return this;
  32.     }
复制代码
当执行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.         if (!runningData) {
  6.             return;
  7.         }

  8.         var nameMap = runningData.nameMap,
  9.             nameList = runningData.nameList,
  10.             sessions = runningData.sessions,
  11.             ln, j, subLn, name,
  12.             i, session, map, list,
  13.             hasCompletedSession = false;

  14.         interrupt = Boolean(interrupt);
  15.         replace = Boolean(replace);

  16.         if (!sessions) {
  17.             return this;
  18.         }

  19.         ln = sessions.length;

  20.         if (ln === 0) {
  21.             return this;
  22.         }

  23.         if (replace) {
  24.             runningData.nameMap = {};
  25.             nameList.length = 0;

  26.             for (i = 0; i < ln; i++) {
  27.                 session = sessions[i];
  28.                 this.onAnimationEnd(element, session.data, session.animation, interrupt, replace);
  29.             }

  30.             sessions.length = 0;
  31.         }
  32.         else {
  33.             for (i = 0; i < ln; i++) {
  34.                 session = sessions[i];
  35.                 map = session.map;
  36.                 list = session.list;

  37.                 for (j = 0,subLn = propertyNames.length; j < subLn; j++) {
  38.                     name = propertyNames[j];

  39.                     if (map[name]) {//当执行transform的时候这里传过来的name是-webkit-transform,但是map里只有transform属性,问题就出在这里,匹配不一致导致动画不会被移除
  40.                         delete map[name];//动画存在移除匹配的动画属性
  41.                         Ext.Array.remove(list, name);
  42.                         session.length--;//因为map不匹配,导致少执行一次session.length--,session.length永远不为0
  43.                         if (--nameMap[name] == 0) {
  44.                             delete nameMap[name];
  45.                             Ext.Array.remove(nameList, name);
  46.                         }
  47.                     }
  48.                 }

  49.                 if (session.length == 0) {//当动画移除完毕时执行
  50.                     sessions.splice(i, 1);
  51.                     i--;
  52.                     ln--;

  53.                     hasCompletedSession = true;
  54.                     this.onAnimationEnd(element, session.data, session.animation, interrupt);//触发动画结束事件,最终组件被隐藏,hide事件被触发
  55.                 }
  56.             }
  57.         }

  58.         if (!replace && !interrupt && sessions.length == 0 && hasCompletedSession) {
  59.             this.onAllAnimationsEnd(element);
  60.         }
  61.     }
复制代码
问题就出在上面代码第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.         if (!this.listenersAttached) {
  16.             this.attachListeners();
  17.         }

  18.         animations = Ext.Array.from(animations);

  19.         for (i = 0,ln = animations.length; i < ln; i++) {
  20.             animation = animations[i];
  21.             animation = Ext.factory(animation, Ext.fx.Animation);
  22.             element = animation.getElement();

  23.             // Empty function to prevent idleTasks from running while we animate.
  24.             Ext.AnimationQueue.start(Ext.emptyFn, animation);

  25.             computedStyle = window.getComputedStyle(element.dom);

  26.             elementId = element.getId();

  27.             data = Ext.merge({}, animation.getData());

  28.             if (animation.onBeforeStart) {
  29.                 animation.onBeforeStart.call(animation.scope || this, element);
  30.             }
  31.             animation.fireEvent('animationstart', animation);
  32.             this.fireEvent('animationstart', this, animation);

  33.             data[elementId] = data;

  34.             before = data.before;
  35.             from = data.from;
  36.             to = data.to;

  37.             data.fromPropertyNames = fromPropertyNames = [];
  38.             data.toPropertyNames = toPropertyNames = [];

  39.             for (name in to) {
  40.                 if (to.hasOwnProperty(name)) {
  41.                     to[name] = toFormattedValue = this.formatValue(to[name], name);
  42.                     formattedName = this.formatName(name);//这里就是出问题的地方,传进去的name是transform,这个formatName就是判断你的浏览器属性然后对这个那么进行前缀添加
  43.                     isLengthProperty = isLengthPropertyMap.hasOwnProperty(name);

  44.                     if (!isLengthProperty) {
  45.                         toFormattedValue = this.getCssStyleValue(formattedName, toFormattedValue);
  46.                     }

  47.                     if (from.hasOwnProperty(name)) {
  48.                         from[name] = fromFormattedValue = this.formatValue(from[name], name);

  49.                         if (!isLengthProperty) {
  50.                             fromFormattedValue = this.getCssStyleValue(formattedName, fromFormattedValue);
  51.                         }

  52.                         if (toFormattedValue !== fromFormattedValue) {
  53.                             fromPropertyNames.push(formattedName);
  54.                             toPropertyNames.push(formattedName);
  55.                         }
  56.                     }
  57.                     else {
  58.                         computedValue = computedStyle.getPropertyValue(formattedName);

  59.                         if (toFormattedValue !== computedValue) {
  60.                             toPropertyNames.push(formattedName);
  61.                         }
  62.                     }
  63.                 }
  64.             }

  65.             propertiesLength = toPropertyNames.length;

  66.             if (propertiesLength === 0) {
  67.                 this.onAnimationEnd(element, data, animation);
  68.                 continue;
  69.             }

  70.             runningData = this.getRunningData(elementId);
  71.             runningSessions = runningData.sessions;

  72.             if (runningSessions.length > 0) {
  73.                 this.refreshRunningAnimationsData(
  74.                     element, Ext.Array.merge(fromPropertyNames, toPropertyNames), true, data.replacePrevious
  75.                 );
  76.             }

  77.             runningNameMap = runningData.nameMap;
  78.             runningNameList = runningData.nameList;

  79.             sessionNameMap = {};
  80.             for (j = 0; j < propertiesLength; j++) {
  81.                 name = toPropertyNames[j];
  82.                 sessionNameMap[name] = true;

  83.                 if (!runningNameMap.hasOwnProperty(name)) {
  84.                     runningNameMap[name] = 1;
  85.                     runningNameList.push(name);
  86.                 }
  87.                 else {
  88.                     runningNameMap[name]++;
  89.                 }
  90.             }

  91.             runningSession = {
  92.                 element: element,
  93.                 map: sessionNameMap,
  94.                 list: toPropertyNames.slice(),
  95.                 length: propertiesLength,
  96.                 data: data,
  97.                 animation: animation
  98.             };
  99.             runningSessions.push(runningSession);

  100.             animation.on('stop', 'onAnimationStop', this);

  101.             elementData = Ext.apply({}, before);
  102.             Ext.apply(elementData, from);

  103.             if (runningNameList.length > 0) {
  104.                 fromPropertyNames = Ext.Array.difference(runningNameList, fromPropertyNames);
  105.                 toPropertyNames = Ext.Array.merge(fromPropertyNames, toPropertyNames);
  106.                 elementData['transition-property'] = fromPropertyNames;
  107.             }

  108.             fromData[elementId] = elementData;
  109.             toData[elementId] = Ext.apply({}, to);

  110.             toData[elementId]['transition-property'] = toPropertyNames;
  111.             toData[elementId]['transition-duration'] = data.duration;
  112.             toData[elementId]['transition-timing-function'] = data.easing;
  113.             toData[elementId]['transition-delay'] = data.delay;

  114.             animation.startTime = Date.now();
  115.         }

  116.         message = this.$className;

  117.         this.applyStyles(fromData);

  118.         doApplyTo = function(e) {
  119.             if (e.data === message && e.source === window) {
  120.                 window.removeEventListener('message', doApplyTo, false);
  121.                 me.applyStyles(toData);
  122.             }
  123.         };

  124.         if(Ext.browser.is.IE) {
  125.             window.requestAnimationFrame(function() {
  126.                 window.addEventListener('message', doApplyTo, false);
  127.                 window.postMessage(message, '*');
  128.             });
  129.         }else{
  130.             window.addEventListener('message', doApplyTo, false);
  131.             window.postMessage(message, '*');
  132.         }
  133.     }
复制代码
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.         if (!formattedName) {
  5.             if ((Ext.os.is.Tizen || !Ext.feature.has.CssTransformNoPrefix) && this.prefixedProperties[name]) {//Ext.feature.has.CssTransformNoPrefix判断
  6.                 formattedName = this.vendorPrefix + name;                               //结果相反了,导致执行了else里的代码,将transform
  7.             }                                                         //在没有加前缀的情况下返回了回去
  8.             else {
  9.                 formattedName = name;
  10.             }

  11.             cache[name] = formattedName;
  12.         }

  13.         return formattedName;
  14.     }
复制代码
如上所示,在判断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为什么没这个问题,

因为老版本没有对

view sourceprint?
1.
(Ext.os.is.Tizen || !Ext.feature.has.CssTransformNoPrefix)
所以老版本没有出现这个问题,

在其他的android系统上,Ext.feature.has.CssTransformNoPrefix这个值都是false,即不支持没有前缀,

包括最新版本的chrome,但是在华为emotion ui上这个判断不对了,原因是什么,我也不清楚,

由于之前的解决方案造成htc sense的判断不正确,我重新修改了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]);
        }
    }
作者: 香辣牛肉面
原文: http://www.cnblogs.com/cjpx00008/p/3535557.html