最近这段时间相对比较清闲,所以决定将最近学习积累的东西以及对于代码重构的经历做一下总结。
事情是这样的。。。14年1月份我参加了我人生的第一份正式工作,当时领导为了检验我的能力,让我着手开发一个小项目,项目第一个页面的基本需求是这样的:
也许这样的一个组件在各位看来很简单,但是对于当时的我来说,简直是一头雾水。当时的我还没正式离开学校,所谓的编程经验仅仅体现在自己在学校看过AS3.0这门编程语言,连一个基本的类都没有写过,各位一定很好奇我是怎么通过面试的,其实我也很好奇。所以当我拿到这个需求的时候,我直接就懵了,无限拖动?动画?还要平滑自然?绘制?用什么绘制?怎么绘制?缓存机制又是个什么东西?
好了,不扯那些没用的了,总之在领导不厌其烦的讲解、一次又一次的悉心指导下,组件总算坑坑洼洼的搞出来了。说了这么多,仅仅是想讲一下我这三次重构,重构的到底是个什么东东。
下面就来说说当初是怎么实现的。
1 /**根据randomTime与benchmarkTime相差多少个月,计算randomTime相对于benchmarkTime的位置*/ 2 public function locationX( randomTime:Date ):Number{ 3 if( monthOrYear == "year" ){ 4 var randomTimeXPosition:Number = locationXByYear( randomTime ); 5 }else{ 6 randomTimeXPosition = locationXByMonth( randomTime ); 7 } 8 return randomTimeXPosition; 9 } 10 11 private function locationXByYear( randomTime:Date ):Number{ 12 var monthWidth:Number = yearWidth/12; 13 var numMonthsApart:uint; 14 var randomYear:Number = randomTime.fullYear; 15 var randomMonth:Number = randomTime.month; 16 var benchmarkYear:Number = benchmarkTime.fullYear; 17 var benchmarkMonth:Number = benchmarkTime.month; 18 if( randomYear > benchmarkYear ){ 19 numMonthsApart = 11 - benchmarkMonth + ( randomYear - benchmarkYear - 1 ) * 12 + randomMonth + 1; 20 var randomTimeXPosition:Number = benchmarkTimePosition + numMonthsApart * monthWidth; 21 }else if( randomYear < benchmarkYear ){ 22 numMonthsApart = benchmarkMonth + ( benchmarkYear - randomYear - 1 ) * 12 + 12 - randomMonth; 23 randomTimeXPosition = benchmarkTimePosition - numMonthsApart * monthWidth; 24 }else{ 25 if( randomMonth >= benchmarkMonth ){ 26 numMonthsApart = randomMonth - benchmarkMonth; 27 randomTimeXPosition = benchmarkTimePosition + numMonthsApart * monthWidth; 28 }else{ 29 numMonthsApart = benchmarkMonth - randomMonth; 30 randomTimeXPosition = benchmarkTimePosition - numMonthsApart * monthWidth; 31 } 32 } 33 34 /**计算randomTime.month有多少天*/ 35 var forNumDaysMonthCom:MonthComponent = new MonthComponent(); 36 forNumDaysMonthCom.year = randomYear; 37 forNumDaysMonthCom.month = randomMonth; 38 var numDays:uint = forNumDaysMonthCom.numDays(); 39 40 randomTimeXPosition = randomTimeXPosition + randomTime.date * monthWidth/numDays; 41 42 trace( randomTimeXPosition, randomTime.fullYear, randomTime.month, randomTime.date ); 43 return randomTimeXPosition; 44 } 45 46 private function locationXByMonth( randomTime:Date ):Number{ 47 var randomMillisecond:Number = randomTime.time; 48 var benchmarkMillisecond:Number = benchmarkTime.time; 49 if( randomMillisecond >= benchmarkMillisecond ){ 50 var numDays:Number = ( randomMillisecond - benchmarkMillisecond )/86400000; 51 var randomTimeXPosition:Number = benchmarkTimePosition + numDays * 35; 52 }else{ 53 numDays = ( benchmarkMillisecond - randomMillisecond )/86400000; 54 randomTimeXPosition = benchmarkTimePosition - numDays * 35; 55 } 56 return randomTimeXPosition; 57 }
1 private function timeAxisMoveRemoveCom():void{ 2 //处理 “年”组件 3 for( var i:uint=0;i<yearsGroup.numElements;i++ ){ 4 var yearCom:YearComponent = yearsGroup.getElementAt( i ) as YearComponent; 5 //只需判断第一个和最后一个是否超出边界即可,其他的不用判断 6 if( i == 0 || i == yearsGroup.numElements - 1 ){ 7 if( yearCom.x + yearCom.width < 0 || yearCom.x > this.width ){ 8 yearsRemoveArray.push( yearCom ); 9 yearsBufferArray.push( yearCom ); 10 }else{ 11 yearCom.x += moveX; 12 } 13 }else{ 14 yearCom.x += moveX; 15 } 16 } 17 if( yearsRemoveArray && yearsRemoveArray.length > 0 ){ 18 for( i=0;i<yearsRemoveArray.length;i++ ){ 19 yearsGroup.removeElement( yearsRemoveArray[i] ); 20 } 21 yearsRemoveArray.splice( 0 ); 22 } 23 //处理 “月”组件 24 for( i=0;i<monthsGroup.numElements;i++ ){ 25 var monthCom:MonthComponent = monthsGroup.getElementAt( i ) as MonthComponent; 26 if( i == 0 || i == monthsGroup.numElements - 1 ){ 27 if( monthCom.x + monthCom.width < 0 || monthCom.x > this.width ){ 28 monthsRemoveArray.push( monthCom ); 29 monthsBufferArray.push( monthCom ); 30 }else{ 31 monthCom.x += moveX; 32 } 33 }else{ 34 monthCom.x += moveX; 35 } 36 } 37 if( monthsRemoveArray && monthsRemoveArray.length > 0 ){ 38 for( i=0;i<monthsRemoveArray.length;i++ ){ 39 monthsGroup.removeElement( monthsRemoveArray[i] ); 40 } 41 monthsRemoveArray.splice( 0 ); 42 } 43 //处理 “日”组件 44 for( i=0;i<daysGroup.numElements;i++ ){ 45 var dayCom:DayComponent = daysGroup.getElementAt( i ) as DayComponent; 46 if( i == 0 || i == daysGroup.numElements - 1 ){ 47 if( dayCom.x + dayCom.width < 0 || dayCom.x > this.width ){ 48 daysRemoveArray.push( dayCom ); 49 daysBufferArray.push( dayCom ); 50 }else{ 51 dayCom.x += moveX; 52 } 53 }else{ 54 dayCom.x += moveX; 55 } 56 } 57 if( daysRemoveArray && daysRemoveArray.length > 0 ){ 58 for( i=0;i<daysRemoveArray.length;i++ ){ 59 daysGroup.removeElement( daysRemoveArray[i] ); 60 } 61 daysRemoveArray.splice( 0 ); 62 } 63 } 64 65 private function timeAxisMoveAddCom():void{ 66 if( monthOrYear == "year" ){ 67 moveAddComByYear(); 68 }else{ 69 moveAddComByMonth(); 70 } 71 } 72 73 private function moveAddComByYear():void{ 74 //处理第一个 “年”,如果第一个 “年”的x坐标大于0,则需要添加一个新的 “年”,并且始终添加到索引为0的位置 75 var yearComFirst:YearComponent = yearsGroup.getElementAt( 0 ) as YearComponent; 76 while( yearComFirst.x > 0 ){ // 处理完了第一个,新增的将变成第一个,然后再去判断新增的这个第一个,然后循环... 77 if( yearsBufferArray && yearsBufferArray.length > 0 ){ 78 var yearAddCom:YearComponent = yearsBufferArray.pop() as YearComponent; 79 }else{ 80 yearAddCom = new YearComponent(); 81 } 82 yearAddCom.x = yearComFirst.x - yearComFirst.width; 83 yearAddCom.width = yearWidth; 84 yearAddCom.labelText = ( Number( yearComFirst.labelText ) - 1 ).toString(); 85 yearsGroup.addElementAt( yearAddCom, 0 ); 86 87 yearComFirst = yearAddCom; 88 } 89 //处理最后一个 “年”,如果最后一个 “年”的x坐标加宽度小于this.width,则需要添加一个新的 “年”,并且始终添加到最后位置 90 var yearComLast:YearComponent = yearsGroup.getElementAt( yearsGroup.numElements - 1 ) as YearComponent; 91 while( yearComLast.x + yearComLast.width < this.width ){ 92 if( yearsBufferArray && yearsBufferArray.length > 0 ){ 93 yearAddCom = yearsBufferArray.pop() as YearComponent; 94 }else{ 95 yearAddCom = new YearComponent(); 96 } 97 yearAddCom.x = yearComLast.x + yearComLast.width; 98 yearAddCom.width = yearWidth; 99 yearAddCom.labelText = ( Number( yearComLast.labelText ) + 1 ).toString(); 100 yearsGroup.addElement( yearAddCom ); 101 102 yearComLast = yearAddCom; 103 } 104 105 //处理第一个 “月” 106 var monthComFirst:MonthComponent = monthsGroup.getElementAt( 0 ) as MonthComponent; 107 while( monthComFirst.x > 0 ){ 108 if( monthsBufferArray && monthsBufferArray.length > 0 ){ 109 var monthAddCom:MonthComponent = monthsBufferArray.pop() as MonthComponent; 110 }else{ 111 monthAddCom = new MonthComponent(); 112 } 113 monthAddCom.x = monthComFirst.x - monthComFirst.width; 114 monthAddCom.width = monthWidth; 115 if( monthComFirst.month == 0 ) 116 monthAddCom.month = 11; 117 else 118 monthAddCom.month = monthComFirst.month - 1; 119 monthAddCom.monthOrYear = monthOrYear; 120 monthsGroup.addElementAt( monthAddCom, 0 ); 121 122 monthComFirst = monthAddCom; 123 } 124 //处理最后一个 “月” 125 var monthComLast:MonthComponent = monthsGroup.getElementAt( monthsGroup.numElements -1 ) as MonthComponent; 126 while( monthComLast.x + monthComLast.width < this.width ){ 127 if( monthsBufferArray && monthsBufferArray.length > 0 ){ 128 monthAddCom = monthsBufferArray.pop() as MonthComponent; 129 }else{ 130 monthAddCom = new MonthComponent(); 131 } 132 monthAddCom.x = monthComLast.x + monthComLast.width; 133 monthAddCom.width = monthWidth; 134 if( monthComLast.month == 11 ) 135 monthAddCom.month = 0; 136 else 137 monthAddCom.month = monthComLast.month + 1; 138 monthAddCom.monthOrYear = monthOrYear; 139 monthsGroup.addElement( monthAddCom ); 140 141 monthComLast = monthAddCom; 142 } 143 } 144 145 private function moveAddComByMonth():void{ 146 //处理第一个 “月” 147 var monthComFirst:MonthComponent = monthsGroup.getElementAt( 0 ) as MonthComponent; 148 while( monthComFirst.x > 0 ){ 149 if( monthsBufferArray && monthsBufferArray.length > 0 ){ 150 var monthAddCom:MonthComponent = monthsBufferArray.pop() as MonthComponent; 151 }else{ 152 monthAddCom = new MonthComponent(); 153 } 154 if( monthComFirst.month == 0 ){ 155 monthAddCom.year = monthComFirst.year - 1; 156 monthAddCom.month = 11; 157 }else{ 158 monthAddCom.year = monthComFirst.year; 159 monthAddCom.month = monthComFirst.month - 1; 160 } 161 monthAddCom.width = 35 * monthAddCom.numDays(); 162 monthAddCom.x = monthComFirst.x - monthAddCom.width; 163 monthAddCom.monthOrYear = monthOrYear; 164 monthsGroup.addElementAt( monthAddCom, 0 ); 165 166 monthComFirst = monthAddCom; 167 } 168 //处理最后一个 “月” 169 var monthComLast:MonthComponent = monthsGroup.getElementAt( monthsGroup.numElements -1 ) as MonthComponent; 170 while( monthComLast.x + monthComLast.width < this.width ){ 171 if( monthsBufferArray && monthsBufferArray.length > 0 ){ 172 monthAddCom = monthsBufferArray.pop() as MonthComponent; 173 }else{ 174 monthAddCom = new MonthComponent(); 175 } 176 if( monthComLast.month == 11 ){ 177 monthAddCom.year = monthComLast.year + 1; 178 monthAddCom.month = 0; 179 }else{ 180 monthAddCom.year = monthComLast.year; 181 monthAddCom.month = monthComLast.month + 1; 182 } 183 monthAddCom.width = 35 * monthAddCom.numDays(); 184 monthAddCom.x = monthComLast.x + monthComLast.width; 185 monthAddCom.monthOrYear = monthOrYear; 186 monthsGroup.addElement( monthAddCom ); 187 188 monthComLast = monthAddCom; 189 } 190 //处理第一个 “日” 191 var dayComFirst:DayComponent = daysGroup.getElementAt( 0 ) as DayComponent; 192 while( dayComFirst.x > 0 ){ 193 if( daysBufferArray && daysBufferArray.length > 0 ){ 194 var dayAddCom:DayComponent = daysBufferArray.pop() as DayComponent; 195 }else{ 196 dayAddCom = new DayComponent(); 197 } 198 var isPreviousMonth:Boolean = false; //是否到了前一个月,如果是,则dayAddCom.date = 前一个月的最后一天 199 for( i=0;i<monthsGroup.numElements;i++ ){ 200 var monthComForDayComFirst:MonthComponent = monthsGroup.getElementAt( i ) as MonthComponent; 201 if( dayComFirst.x == monthComForDayComFirst.x ){ 202 isPreviousMonth = true; 203 break; 204 } 205 } 206 if( isPreviousMonth ){ 207 var previousMonthNumDays:uint = numDays( monthComForDayComFirst.year, monthComForDayComFirst.month - 1 ); 208 dayAddCom.day = previousMonthNumDays; 209 }else{ 210 dayAddCom.day = dayComFirst.day - 1; 211 } 212 213 dayAddCom.x = dayComFirst.x - dayComFirst.width; 214 daysGroup.addElementAt( dayAddCom, 0 ); 215 216 dayComFirst = dayAddCom; 217 } 218 //处理最后一个 “日” 219 var dayComLast:DayComponent = daysGroup.getElementAt( daysGroup.numElements - 1 ) as DayComponent; 220 while( dayComLast.x + dayComLast.width < this.width ){ 221 if( daysBufferArray && daysBufferArray.length > 0 ){ 222 dayAddCom = daysBufferArray.pop() as DayComponent; 223 }else{ 224 dayAddCom = new DayComponent(); 225 } 226 var isNextMonth:Boolean = false; //是否到了后一个月,如果是,则dayAddCom.date = 1 227 for( var i:uint=0;i<monthsGroup.numElements;i++ ){ 228 var monthComForDayComLast:MonthComponent = monthsGroup.getElementAt( i ) as MonthComponent; 229 if( dayComLast.x + dayComLast.width == monthComForDayComLast.x + monthComForDayComLast.width ){ 230 isNextMonth = true; 231 } 232 } 233 if( isNextMonth ){ 234 dayAddCom.day = 1; 235 }else{ 236 dayAddCom.day = dayComLast.day + 1; 237 } 238 dayAddCom.x = dayComLast.x + dayComLast.width; 239 daysGroup.addElement( dayAddCom ); 240 241 dayComLast = dayAddCom; 242 } 243 }
1 private function moveRemoveCom():void{ 2 for( var i:uint=0;i<eventsGroup.numElements;i++ ){ 3 var eventCom:EventBarComponent = eventsGroup.getElementAt( i ) as EventBarComponent; 4 if( eventCom.x + eventCom.width < 0 || eventCom.x > this.width ){ 5 eventsRemoveArray.push( eventCom ); 6 eventsBufferArray.push( eventCom ); 7 if( eventCom.hasEventListener( MouseEvent.DOUBLE_CLICK ) ){ 8 eventCom.removeEventListener( MouseEvent.DOUBLE_CLICK, eventDoubleClickHandler ); 9 } 10 }else{ 11 eventCom.x += moveX; 12 } 13 } 14 if( eventsRemoveArray && eventsRemoveArray.length > 0 ){ 15 for( i=0;i<eventsRemoveArray.length;i++ ){ 16 eventsGroup.removeElement( eventsRemoveArray[i] ); 17 } 18 eventsRemoveArray.splice( 0 ); 19 } 20 } 21 22 private function moveAddCom():void{ 23 parent: 24 for each( var info:Object in this.dataProvider ){ 25 //首先判断新读取的事件是否在舞台上已经绘制出来,如果已经存在,则跳过;如果不存在,则需要绘制 26 for( var i:uint=0;i<eventsGroup.numElements;i++ ){ 27 var eventCom:EventBarComponent = eventsGroup.getElementAt( i ) as EventBarComponent; 28 if( info.id == eventCom.info.id ){ 29 continue parent; 30 } 31 } 32 var startPosition:Number = timeAxis.locationX( info.startDate ); 33 var endPosition:Number = timeAxis.locationX( info.endDate ); 34 if( ( startPosition < 0 && endPosition < 0 )||( startPosition > this.width && endPosition > this.width ) ){ 35 continue; 36 } 37 38 if( eventsBufferArray && eventsBufferArray.length > 0 ){ 39 eventCom = eventsBufferArray.pop() as EventBarComponent; 40 }else{ 41 eventCom = new EventBarComponent(); 42 } 43 eventCom.x = startPosition; 44 eventCom.y = 70; 45 eventCom.width = endPosition - startPosition; 46 eventCom.info = info; 47 eventCom.addEventListener( MouseEvent.DOUBLE_CLICK, eventDoubleClickHandler ); 48 eventsGroup.addElement( eventCom ); 49 if( eventCom.width < eventCom.height ){ 50 eventCom.shapeFlag = "circle"; 51 }else{ 52 eventCom.shapeFlag = "roundRect"; 53 } 54 } 55 }
1 private var sortArray:Array = new Array(); 2 private function sortByStartTime():void{ 3 if( sortArray ) 4 sortArray.splice( 0 ); 5 for( var i:uint=0;i<eventsGroup.numElements;i++ ){ 6 var eventCom:EventBarComponent = eventsGroup.getElementAt( i ) as EventBarComponent; 7 sortArray.push( eventCom ); 8 } 9 sortArray.sort( sortForEventsArray ); 10 reLayout( sortArray ); 11 } 12 13 private function sortForEventsArray( aEventCom:EventBarComponent, bEventCom:EventBarComponent ):Number{ 14 var aStartPosition:Number = aEventCom.x; 15 var bStartPosition:Number = bEventCom.x; 16 if( aStartPosition > bStartPosition ) 17 return 1; 18 else if( aStartPosition < bStartPosition ) 19 return -1; 20 else 21 return 0; 22 } 23 24 private var sortEventsArray:Array = new Array(); 25 private function reLayout( sortArray:Array ):void{ 26 if( sortEventsArray ) 27 sortEventsArray.splice( 0 ); 28 for( var i:uint=0;i<sortArray.length;i++ ){ 29 var isWrap:Boolean = false; 30 var eventCom:EventBarComponent = sortArray[i] as EventBarComponent; 31 if( sortEventsArray.length > 0 ){ 32 for( var j:uint=0;j<sortEventsArray.length;j++ ){ 33 var eventComSort:EventBarComponent = sortEventsArray[j] as EventBarComponent; 34 if( eventComSort.shapeFlag == "circle" ){ 35 if( eventCom.shapeFlag == "circle" ){ 36 if( eventComSort.x + eventComSort.width/2 + eventComSort.height/2 > 37 eventCom.x + eventCom.width/2 - eventCom.height/2 ){ 38 eventCom.y = eventComSort.y + 22; 39 }else{ 40 isWrap = true; 41 break; 42 } 43 }else if( eventCom.shapeFlag == "roundRect" ){ 44 if( eventComSort.x + eventComSort.width/2 + eventComSort.height/2 > eventCom.x ){ 45 eventCom.y = eventComSort.y + 22; 46 }else{ 47 isWrap = true; 48 break; 49 } 50 } 51 }else if( eventComSort.shapeFlag == "roundRect" ){ 52 if( eventCom.shapeFlag == "circle" ){ 53 if( eventComSort.x + eventComSort.width > 54 eventCom.x + eventCom.width/2 - eventCom.height/2 ){ 55 eventCom.y = eventComSort.y + 22; 56 }else{ 57 isWrap = true; 58 break; 59 } 60 }else if( eventCom.shapeFlag == "roundRect" ){ 61 if( eventComSort.x + eventComSort.width > eventCom.x ){ 62 eventCom.y = eventComSort.y + 22; 63 }else{ 64 isWrap = true; 65 break; 66 } 67 } 68 } 69 } 70 if( isWrap ){ 71 eventCom.y = eventComSort.y; 72 sortEventsArray.splice( j, 1, eventCom ); 73 }else{ 74 sortEventsArray.push( eventCom ); 75 } 76 }else{ 77 eventCom.y = 70; 78 sortEventsArray.push( eventCom ); 79 } 80 } 81 dynamicHeight = sortEventsArray.length * 22 + 10; 82 }
好了,截止到这,除了两种呈现场景的切换动画之后,基本功能已经完成了(当时这个切换动画并没有实现,只是很呆板的瞬间切换。一直到现在,我尝试过通过我能想到的各种方法控制这个动画,但是效果始终不理想,看来能力还是差的远哪~~~)。
//-------------------------------------------------------------------------------------
说完了当初的实现思路,那下面就来剖析一下它的问题,要不然干嘛还要跑来重构它呢?
首先,时间控件只不过是时间刻度的事,还要搞三个控件,在进行缓存利用时,还要判断是年、是月又或者是日,你不觉得这样很累么?每次进行控件的卸载与添加时,都要“处理第一个年”、“处理最后一个年”、“处理第一个月”...你不觉得这样很啰嗦么?
其次,每次进行缓存利用时,代码都是这么写的:
1 if( monthsBufferArray && monthsBufferArray.length > 0 ){ 2 var monthAddCom:MonthComponent = monthsBufferArray.pop() as MonthComponent; 3 }else{ 4 monthAddCom = new MonthComponent(); 5 }
你不觉得这样的代码看上去很不爽么?而且是每一个控件都是这么判断的,当真是一点代码的抽取工作都没做。现在假设即便是需要三个不同的控件要进行缓存利用,如果是按现在的思想来实现的话,我会定义一个接口IComponent,接口方法主要包括绘制刻度、显示日期等,这三个控件都实现这个接口,而且只需一个缓存数组即可,也即我的应用程序中有且仅有一个缓存池,那么上面重复的代码可以优化为以下一个方法:
1 private function getComponent( identifier:String ):IComponent 2 { 3 var component:IComponent = getReusableComponentWidthIdentifier( identifier ); 4 if( component == null ) 5 { 6 component = creationComponentWithIdentifier( identifier ); 7 } 8 return component; 9 }
这样就不用再去分别判断三种控件了,在使用时,只需给三种控件设置一个唯一标识就好了嘛,这样不管是从编程思想的角度看还是从代码简洁性的角度看,都要好很多嘛。
再者,当初实现的方案中,有很大的性能问题。时间轴的拖动功能是基于mouseMove实现的,也就意味着在拖动过程中,mouseMove方法会被频繁调用,但是在mouseMove中我却做了频繁、重复的计算逻辑。在可视区域中的时间控件的处理如移动、卸载、添加等,这些操作是必要的,暂时还没有想到有什么更好的优化方案。但是对于事件控件来说,每次拖动,我都会依据基准时间的最新位置计算所有事件的最新位置,然后再去判断哪些事件处于可视区域哪些事件被移除了可视区域,而且每次都对事件依据起止时间进行排序,进而判断是否该换行显示等,这些重复的逻辑被频繁的调用,确实影响到了性能。能不能有这样一种方案,一次性计算出已经加载的所有事件相对于基准时间的位置并且进行起止时间的排序操作,在拖动时,只需将所有事件的位置在原来的基础上加上基准时间的偏移量?答案当然是肯定的。这样的话,只是简单的移动事件的位置,而不是每次都依据事件的起止时间和基准时间重新计算事件的位置,性能上肯定会提升不少。其实这也就用到了MVC模型,一次性处理了所有事件对应的模型,在拖动时,只是移动模型的位置,模型变了,视图也就变了。不过这也是在第三次实现时才用到的方案,等会再说为啥第二次没有用到。
其实当初实现的方案中问题还有很多,对于对象的设计与抽象,完全没有考虑低耦合高内聚的原则,只是一味的需要什么,我就写什么,以至于现在回过头来看这部分代码,我自己都觉得头疼。我猜想如果当初有人和我一起做这个组件的话,那人一定会被我逼疯的。
//-------------------------------------------------------------------------------------
下面就开始讲讲第二次实现方案吧。当时进行代码重构时,想的远没有现在想的多,还是没有考虑过要使用MVC。当时只是在做了一个图片轮播器之后想到用同一种方案实现一下这个时间轴组件。因为图片轮播器在实现时用到了链表,所以我就在想,这时间控件和事件控件也都是一个接一个的,在进行mouseMove时只需一个节点接一个节点的处理就行了。
1 /** 2 * TimeComponent跟随。 3 * 4 */ 5 private function flowTimeComponent( timeComponent:TimeComponent ):void 6 { 7 var walkLeft:LinkNode, walkRight:LinkNode; 8 9 var linkNode:LinkNode = _linkList.searchNode( timeComponent ); 10 // 链表的头节点对应的timeComponent的x>0,此时需新增一个节点,作为新的头节点。 11 if( linkNode.preNode == null && timeComponent.getStartX() > 0 ) 12 { 13 timeComponent = creationTimeComponent( timeComponent, 'left' ); 14 walkLeft = _linkList.searchNode( timeComponent ).preNode; 15 } 16 else 17 { 18 walkLeft = linkNode.preNode; 19 } 20 while( walkLeft ) 21 { 22 timeComponent = walkLeft.data; 23 // 新的timeComponent的x等于其next(因为这是向左遍历)的开始位置减去本身的宽度。 24 timeComponent.x = walkLeft.nextNode.data.getStartX() - timeComponent.width; 25 // 不断更新横跨x = 0的对象。 26 if( timeComponent.getStartX() < 0 && timeComponent.getEndX() > 0 ) 27 { 28 _currentTimeComponent = timeComponent; 29 } 30 if( timeComponent.getEndX() < 0 ) 31 { 32 if( timeMcBase.contains( timeComponent ) ) 33 { 34 timeMcBase.removeElement( timeComponent ); 35 } 36 } 37 else 38 { 39 timeMcBase.addElement( timeComponent ); 40 } 41 42 // 链表的头节点对应的timeComponent的x>0,此时需新增一个节点,作为新的头节点。 43 if( walkLeft.preNode == null && timeComponent.getStartX() > 0 ) 44 { 45 timeComponent = creationTimeComponent( timeComponent, 'left' ); 46 walkLeft = _linkList.searchNode( timeComponent ).preNode; 47 } 48 else 49 { 50 walkLeft = walkLeft.preNode; 51 } 52 } 53 54 // 链表的尾节点对应的timeComponent的endX<width,此时需新增一个节点,作为新的尾节点。 55 timeComponent = linkNode.data; 56 if( linkNode.nextNode == null && timeComponent.getEndX() < width ) 57 { 58 timeComponent = creationTimeComponent( timeComponent ); 59 walkRight = _linkList.searchNode( timeComponent ).nextNode; 60 } 61 else 62 { 63 walkRight = linkNode.nextNode; 64 } 65 while( walkRight ) 66 { 67 timeComponent = walkRight.data; 68 // 新的timeComponent的x等于其previous(因为这是向右遍历)的结束位置。 69 timeComponent.x = walkRight.preNode.data.getEndX(); 70 if( timeComponent.getStartX() < 0 && timeComponent.getEndX() > 0 ) 71 { 72 _currentTimeComponent = timeComponent; 73 } 74 if( timeComponent.getStartX() > width ) 75 { 76 if( timeMcBase.contains( timeComponent ) ) 77 { 78 timeMcBase.removeElement( timeComponent ); 79 } 80 } 81 else 82 { 83 timeMcBase.addElement( timeComponent ); 84 } 85 86 // 链表的尾节点对应的timeComponent的endX<width,此时需新增一个节点,作为新的尾节点。 87 if( walkRight.nextNode == null && timeComponent.getEndX() < width ) 88 { 89 timeComponent = creationTimeComponent( timeComponent ); 90 walkRight = _linkList.searchNode( timeComponent ).nextNode; 91 } 92 else 93 { 94 walkRight = walkRight.nextNode; 95 } 96 } 97 98 trace( 'The number of timeComponent in stage is ', timeMcBase.numElements ); 99 }
但是事实看来,并没什么卵用,丝毫没有解决上面提及的那些问题,反而还把缓存给抛弃了。其实在时间控件、事件控件的问题上,用链表根本就不合适。假设可视区域最多可容纳4个时间控件,那么在整个应用中最多就只有4到5个时间控件就足够了。就像是这样:
不过有一点值得一提的就是,在这次实现的方案中,实现了时间轴的惯性滑动,这也是当初做这个组件时一直想实现而没有实现的功能。其实道理很简单,就是在mouseMove过程中不断记录时间轴上鼠标点的位置,在mouseUp那一刻,取得最后两个点的位置做差即可得到惯性滑动的初始速度,之后再利用enterFrame不断改变惯性滑动的速度与位移即可。
1 private var _mouseDownTarget:TimeComponent; 2 private var _mouseDownPoint:Point = new Point(); 3 4 private var pointX:Array = []; // 运动坐标。 5 private var speedX:Number = 0; // 滑动的速度。 6 private var mouseUpX:Number = 0; // mouseUp时的位置。 7 private var friction:Number = 0.95; // 滑动的摩擦力。 8 private var isMoving:Boolean = false; // 是否正在滑动。 9 10 private function prepareMouseMove():void 11 { 12 stage.addEventListener( MouseEvent.MOUSE_UP, stage_mouseUpHandler ); 13 stage.addEventListener( MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler ); 14 } 15 private function mouseDownHandler( event:MouseEvent ):void 16 { 17 event.stopImmediatePropagation(); 18 19 var timeComponent:TimeComponent = event.currentTarget as TimeComponent; 20 _mouseDownPoint.x = timeComponent.mouseX; 21 _mouseDownTarget = timeComponent; 22 23 prepareMouseMove(); 24 } 25 private function stage_mouseMoveHandler( event:MouseEvent ):void 26 { 27 _mouseDownTarget.x = timeMcBase.mouseX - _mouseDownPoint.x; 28 29 // mouseMove时,不断记录运动坐标。 30 pointX.push( timeMcBase.mouseX ); 31 32 flowTimeComponent( _mouseDownTarget ); 33 flowEventComponent(); 34 } 35 private function stage_mouseUpHandler( event:MouseEvent ):void 36 { 37 // 记录mouseUp时的位置。 38 mouseUpX = timeMcBase.mouseX; 39 40 stage.addEventListener( Event.ENTER_FRAME, enterFrameHandler ); 41 if( pointX.length > 2 ) 42 { 43 // 取最后两个位置作差,即:得到了最后时刻鼠标移动的速度。 44 speedX = ( pointX[ pointX.length - 1 ] - pointX[ pointX.length - 2 ] ) * 2; 45 isMoving = true; 46 } 47 48 _mouseDownPoint.x = 0; 49 stage.removeEventListener( MouseEvent.MOUSE_UP, stage_mouseUpHandler ); 50 stage.removeEventListener( MouseEvent.MOUSE_MOVE, stage_mouseMoveHandler ); 51 } 52 53 private function enterFrameHandler( event:Event ):void 54 { 55 // mouseUp之后的第一帧判断,mouseUp时的位置和第一帧之后的位置是否相同,若相同,则表示无需惯性滑动。 56 if( mouseUpX - timeMcBase.mouseX == 0 ) 57 { 58 stage.removeEventListener( Event.ENTER_FRAME, enterFrameHandler ); 59 pointX.splice( 0 ); 60 isMoving = false; 61 flowEventComponent(); 62 return; 63 } 64 65 // 当速度小于0.1时停止滑动。 66 if( Math.abs( speedX ) < 0.1 ) 67 { 68 stage.removeEventListener( Event.ENTER_FRAME, enterFrameHandler ); 69 pointX.splice( 0 ); 70 } 71 72 // 惯性滑动并不断更新TimeComponent的位置。 73 speedX *= friction; 74 _mouseDownTarget.x += speedX; 75 flowTimeComponent( _mouseDownTarget ); 76 77 // 如果滑动速度大于3,为了提高性能,不再计算EventComponent的位置。 78 if( Math.abs( speedX ) < 3 ) 79 { 80 isMoving = false; 81 flowEventComponent(); 82 } 83 }
或许还有更好的关于惯性滑动的实现方案,只是现在还没想到。
项目地址:https://git.oschina.net/You.wanwu/Memorabilia.git(原谅我家里的网连不上Github,欲哭无泪啊。。。)
//-------------------------------------------------------------------------------------
在第三次的实现方案中,以上提及的点基本上都做到了。比如缓存的分析以及实现,事件MVC模型的使用,类的封装与组织等,关于他们的原理和实现,上面已经讲完了,(额。。。难道这些不应该是在这里才讲的吗?)这里我就不在累述了,就直接上代码吧。
1 /** 2 * 移动事件。 3 * <p>这里所有的eventModel已经在初始化时计算了相对于基准时间的位置,所以在移动过程中,只需在原来的基础上加上移动的偏移量即可,而不是每移动一次就计算一次,相对提高了性能。 4 * 同时舞台上的eventView采用了缓存机制,舞台上eventView的数量不会很多,所以也是相对提高了性能。</p> 5 * @param moveX 6 * 7 */ 8 public function moveEvent( moveX:Number = 0 ):void 9 { 10 for each( var eventModel:EventModel in _cacheEventModel ) 11 { 12 eventModel.startX += moveX; 13 eventModel.endX += moveX; 14 15 // 此时eventModel需要一个eventView显示到舞台上 16 if( eventModel.endX >= 0 && eventModel.startX <= this.width ) 17 { 18 if( eventModel.hidden ) 19 { 20 eventModel.hidden = false; 21 var eventView:EventView = creationEventView( eventModel ); 22 moveX > 0 ? this.addElementAt( eventView, 0 ) : this.addElement( eventView ); 23 } 24 } 25 } 26 27 // 更新视图 28 var previousEventView:EventView; 29 for( var i:int = 0; i < this.numElements; i++ ) 30 { 31 eventView = this.getElementAt( i ) as EventView; 32 eventView.x = eventView.eventModel.startX; 33 eventView.y = 0; 34 35 if( eventView.maxX < 0 || eventView.x > this.width ) 36 { 37 eventView.eventModel.hidden = true; 38 eventView.eventModel = null; 39 _cacheEventView.push( this.removeElementAt( i ) ); 40 i--; 41 } 42 else 43 { 44 // 判断是否该换行 45 if( previousEventView && previousEventView.maxX > eventView.x ) 46 { 47 eventView.y = previousEventView.y + Const.EVENT_HEIGHT + 5; 48 } 49 previousEventView = eventView; 50 } 51 } 52 } 53 54 /** 55 * 获得一个可用的EventView。 56 * <p>这里只是简单实现只有EventView需要缓存的情况。 57 * 等一下,不是还有TimeView也需要缓存么,怎么这里没有? 58 * TimeView是在时间轴容器中进行处理的,EventView是在事件容器中处理的,这里并没有把他们放到一起。</p> 59 * 60 */ 61 private function creationEventView( eventModel:EventModel ):EventView 62 { 63 var eventView:EventView = _cacheEventView.pop(); 64 if( eventView == null ) 65 { 66 eventView = new EventView(); 67 68 trace( 'create a eventView' ); 69 } 70 eventView.eventModel = eventModel; 71 72 return eventView; 73 }
项目地址:https://git.oschina.net/You.wanwu/ZTest.git(再说一遍,算了,隔这么近,不说了。。。)
//-------------------------------------------------------------------------------------
好了,整理完这篇文章,对于缓存、MVC的理解似乎更透彻了一些。说来挺可笑的,一个两年开发经验的人,却还在这讨论这些东西,我想问问我自己,这两年我到底干了啥?其实最近我也一直在反思这些,两年了,关于编程思想、设计理念、数据结构等我学到了多少?关于编程语言,我又自学了几门?之前我还在为自己找借口,我在公司做的全是一些业务系统,和具体业务贴合的太紧密,框架也是公司自己封装的,好在并不是所有的项目都在用。可是事实却是,这么长时间以来,我是真真的第一次去考虑这些问题,我也是真真的第一次感觉到恐慌。。。。。
调整一下心情吧,上面就是我对这个小组件的三次实现方案,或许会有很多问题存在,如果有小伙伴愿意一起分享学习经历的话,欢迎评论留言。