├── README.md ├── animate.js ├── cache_canvas_pool.js ├── canvas_list.js ├── canvas_util.js ├── event_object.js ├── image.js ├── index_catetab.js ├── layout.js ├── list_item.js ├── main.js ├── render_layer.js ├── scroller.js └── text.js /README.md: -------------------------------------------------------------------------------- 1 | CanvasList 2 | =============== 3 | 4 | CanvasList.js is a lib to draw list in canvas element,which require zepto. 5 | 6 | -------------------------------------------------------------------------------- /animate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Scroller 3 | * http://github.com/zynga/scroller 4 | * 5 | * Copyright 2011, Zynga Inc. 6 | * Licensed under the MIT License. 7 | * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt 8 | * 9 | * Based on the work of: Unify Project (unify-project.org) 10 | * http://unify-project.org 11 | * Copyright 2011, Deutsche Telekom AG 12 | * License: MIT + Apache (V2) 13 | */ 14 | 15 | /** 16 | * Generic animation class with support for dropped frames both optional easing and duration. 17 | * 18 | * Optional duration is useful when the lifetime is defined by another condition than time 19 | * e.g. speed of an animating object, etc. 20 | * 21 | * Dropped frame logic allows to keep using the same updater logic independent from the actual 22 | * rendering. This eases a lot of cases where it might be pretty complex to break down a state 23 | * based on the pure time difference. 24 | */ 25 | (function(global) { 26 | var time = Date.now || function() { 27 | return +new Date(); 28 | }; 29 | var desiredFrames = 60; 30 | var millisecondsPerSecond = 1000; 31 | var running = {}; 32 | var counter = 1; 33 | 34 | // Create namespaces 35 | if (!global.core) { 36 | global.core = { effect : {} }; 37 | 38 | } else if (!core.effect) { 39 | core.effect = {}; 40 | } 41 | 42 | core.effect.Animate = { 43 | 44 | /** 45 | * A requestAnimationFrame wrapper / polyfill. 46 | * 47 | * @param callback {Function} The callback to be invoked before the next repaint. 48 | * @param root {HTMLElement} The root element for the repaint 49 | */ 50 | requestAnimationFrame: (function() { 51 | 52 | // Check for request animation Frame support 53 | var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame; 54 | var isNative = !!requestFrame; 55 | 56 | if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) { 57 | isNative = false; 58 | } 59 | 60 | if (isNative) { 61 | return function(callback, root) { 62 | requestFrame(callback, root) 63 | }; 64 | } 65 | 66 | var TARGET_FPS = 60; 67 | var requests = {}; 68 | var requestCount = 0; 69 | var rafHandle = 1; 70 | var intervalHandle = null; 71 | var lastActive = +new Date(); 72 | 73 | return function(callback, root) { 74 | var callbackHandle = rafHandle++; 75 | 76 | // Store callback 77 | requests[callbackHandle] = callback; 78 | requestCount++; 79 | 80 | // Create timeout at first request 81 | if (intervalHandle === null) { 82 | 83 | intervalHandle = setInterval(function() { 84 | 85 | var time = +new Date(); 86 | var currentRequests = requests; 87 | 88 | // Reset data structure before executing callbacks 89 | requests = {}; 90 | requestCount = 0; 91 | 92 | for(var key in currentRequests) { 93 | if (currentRequests.hasOwnProperty(key)) { 94 | currentRequests[key](time); 95 | lastActive = time; 96 | } 97 | } 98 | 99 | // Disable the timeout when nothing happens for a certain 100 | // period of time 101 | if (time - lastActive > 2500) { 102 | clearInterval(intervalHandle); 103 | intervalHandle = null; 104 | } 105 | 106 | }, 1000 / TARGET_FPS); 107 | } 108 | 109 | return callbackHandle; 110 | }; 111 | 112 | })(), 113 | 114 | 115 | /** 116 | * Stops the given animation. 117 | * 118 | * @param id {Integer} Unique animation ID 119 | * @return {Boolean} Whether the animation was stopped (aka, was running before) 120 | */ 121 | stop: function(id) { 122 | var cleared = running[id] != null; 123 | if (cleared) { 124 | running[id] = null; 125 | } 126 | 127 | return cleared; 128 | }, 129 | 130 | 131 | /** 132 | * Whether the given animation is still running. 133 | * 134 | * @param id {Integer} Unique animation ID 135 | * @return {Boolean} Whether the animation is still running 136 | */ 137 | isRunning: function(id) { 138 | return running[id] != null; 139 | }, 140 | 141 | 142 | /** 143 | * Start the animation. 144 | * 145 | * @param stepCallback {Function} Pointer to function which is executed on every step. 146 | * Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }` 147 | * @param verifyCallback {Function} Executed before every animation step. 148 | * Signature of the method should be `function() { return continueWithAnimation; }` 149 | * @param completedCallback {Function} 150 | * Signature of the method should be `function(droppedFrames, finishedAnimation) {}` 151 | * @param duration {Integer} Milliseconds to run the animation 152 | * @param easingMethod {Function} Pointer to easing function 153 | * Signature of the method should be `function(percent) { return modifiedValue; }` 154 | * @param root {Element ? document.body} Render root, when available. Used for internal 155 | * usage of requestAnimationFrame. 156 | * @return {Integer} Identifier of animation. Can be used to stop it any time. 157 | */ 158 | start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) { 159 | 160 | var start = time(); 161 | var lastFrame = start; 162 | var percent = 0; 163 | var dropCounter = 0; 164 | var id = counter++; 165 | 166 | if (!root) { 167 | root = document.body; 168 | } 169 | 170 | // Compacting running db automatically every few new animations 171 | if (id % 20 === 0) { 172 | var newRunning = {}; 173 | for (var usedId in running) { 174 | newRunning[usedId] = true; 175 | } 176 | running = newRunning; 177 | } 178 | 179 | // This is the internal step method which is called every few milliseconds 180 | var step = function(virtual) { 181 | 182 | // Normalize virtual value 183 | var render = virtual !== true; 184 | 185 | // Get current time 186 | var now = time(); 187 | 188 | // Verification is executed before next animation step 189 | if (!running[id] || (verifyCallback && !verifyCallback(id))) { 190 | 191 | running[id] = null; 192 | completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false); 193 | return; 194 | 195 | } 196 | 197 | // For the current rendering to apply let's update omitted steps in memory. 198 | // This is important to bring internal state variables up-to-date with progress in time. 199 | if (render) { 200 | 201 | var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1; 202 | for (var j = 0; j < Math.min(droppedFrames, 4); j++) { 203 | step(true); 204 | dropCounter++; 205 | } 206 | 207 | } 208 | 209 | // Compute percent value 210 | if (duration) { 211 | percent = (now - start) / duration; 212 | if (percent > 1) { 213 | percent = 1; 214 | } 215 | } 216 | 217 | // Execute step callback, then... 218 | var value = easingMethod ? easingMethod(percent) : percent; 219 | if ((stepCallback(value, now, render) === false || percent === 1) && render) { 220 | running[id] = null; 221 | completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null); 222 | } else if (render) { 223 | lastFrame = now; 224 | core.effect.Animate.requestAnimationFrame(step, root); 225 | } 226 | }; 227 | 228 | // Mark as running 229 | running[id] = true; 230 | 231 | // Init first step 232 | core.effect.Animate.requestAnimationFrame(step, root); 233 | 234 | // Return unique animation ID 235 | return id; 236 | } 237 | }; 238 | })(this); 239 | 240 | -------------------------------------------------------------------------------- /cache_canvas_pool.js: -------------------------------------------------------------------------------- 1 | (function(CL){ 2 | var devicPixelRatio = window.devicPixelRatio; 3 | //缓存的canvas池 4 | var CacheCanvasPool = { 5 | init:function(opt){ 6 | opt = opt || {}; 7 | this.poolSize = opt.poolSize || 30; 8 | this.cacheCanvasList = []; 9 | }, 10 | get:function(layer){ 11 | var targetCache; 12 | var id = layer.id; 13 | 14 | $.each(this.cacheCanvasList,function(i,cache){ 15 | if(cache.id == id){ 16 | targetCache = cache; 17 | return false; 18 | } 19 | }); 20 | 21 | 22 | return targetCache; 23 | 24 | }, 25 | add:function(layer,isScrollingDown){ 26 | 27 | var cacheCanvasList = this.cacheCanvasList; 28 | var newCanvas; 29 | var newCache; 30 | 31 | //如果超出了池子大小,拿最旧的来复用 32 | if(cacheCanvasList.length && (cacheCanvasList.length + 1 > this.poolSize)){ 33 | console.log('reuse!'); 34 | if(window.reused == null){ 35 | window.reused = 0; 36 | } 37 | else{ 38 | window.reused++; 39 | $('.reused-count').html(window.reused); 40 | } 41 | 42 | var oldCache; 43 | //根据滚动方向,使用不同的复用缓存方案,避免导致缓存canvas循环使用不当导致的卡顿 44 | if(isScrollingDown){ 45 | oldCache = cacheCanvasList.shift(); 46 | } 47 | else{ 48 | oldCache = cacheCanvasList.pop(); 49 | } 50 | console.log(isScrollingDown); 51 | 52 | newCanvas = oldCache.canvas; 53 | newCtx = oldCache.context; 54 | //清空原来内容后再用 55 | newCtx.clearRect(0,0,newCanvas[0].width,newCanvas[0].height); 56 | } 57 | else{ 58 | newCanvas = $(''); 59 | newCtx = newCanvas[0].getContext('2d'); 60 | } 61 | //缓存canvas和层的大小一致 62 | newCanvas.prop('width',layer.drawWidth * 2); 63 | newCanvas.prop('height',layer.drawHeight * 2); 64 | 65 | newCache = { 66 | id:layer.id, 67 | canvas:newCanvas, 68 | context:newCtx 69 | }; 70 | 71 | if(isScrollingDown){ 72 | //增加到列表并返回 73 | cacheCanvasList.push(newCache); 74 | } 75 | else{ 76 | //增加到列表并返回 77 | cacheCanvasList.unshift(newCache); 78 | } 79 | 80 | 81 | 82 | //test 83 | //$('body').append(newCanvas); 84 | 85 | return newCache; 86 | 87 | }, 88 | remove:function(id){ 89 | 90 | this.cacheCanvasList = this.cacheCanvasList.filter(function(cache){ 91 | //test 92 | // if(id == cache.id){ 93 | // cache.canvas.remove(); 94 | // } 95 | 96 | return cache.id != id; 97 | }); 98 | 99 | 100 | 101 | } 102 | }; 103 | 104 | CL.CacheCanvasPool = CacheCanvasPool; 105 | 106 | 107 | })(window.CanvasList = window.CanvasList || {}); -------------------------------------------------------------------------------- /canvas_list.js: -------------------------------------------------------------------------------- 1 | (function(CL){ 2 | 3 | var RenderLayer = CL.RenderLayer; 4 | var EventObject = CL.EventObject; 5 | var CacheCanvasPool = CL.CacheCanvasPool; 6 | var CanvasUtil = CL.CanvasUtil; 7 | var ListItem = CL.ListItem; 8 | 9 | //默认列表项样式类 10 | var DEFAULT_ITEM_CLASS = 'canvas-list-item'; 11 | //当前为激活态的列表项 12 | var currentActiveListItem; 13 | //当前鼠标/手指位置 14 | var pageX; 15 | var pageY; 16 | var currentTouchList; 17 | var touchActiveId; 18 | var devicePixelRatio = window.devicePixelRatio || 1; 19 | 20 | var raf = (function(){ 21 | return window.requestAnimationFrame || 22 | window.webkitRequestAnimationFrame || 23 | window.mozRequestAnimationFrame || 24 | function( callback ){ 25 | window.setTimeout(callback, 1000 / 60); 26 | }; 27 | })(); 28 | 29 | 30 | //找出layout变更节点的根节点们 31 | function getLayoutChangedRoots(layer,rootsArr){ 32 | 33 | rootsArr = rootsArr || []; 34 | //layout发生变化 35 | if(layer.layoutChanged){//console.log('changed'); 36 | rootsArr.push(layer); 37 | layer.layoutChanged = false; 38 | } 39 | else{ 40 | $.each(layer.children,function(i,child){ 41 | getLayoutChangedRoots(child,rootsArr); 42 | }); 43 | } 44 | 45 | return rootsArr; 46 | }; 47 | 48 | //滚动处理 49 | var scrollHandlers = { 50 | touchstart:function(scroller,e){ 51 | scroller.doTouchStart(e.touches, e.timeStamp); 52 | }, 53 | touchmove:function(scroller,e){ 54 | scroller.doTouchMove(e.touches, e.timeStamp, e.scale); 55 | }, 56 | touchend:function(scroller,e){ 57 | scroller.doTouchEnd(e.timeStamp); 58 | currentTouchList = null; 59 | }, 60 | touchcancel:function(scroller,e){ 61 | scroller.doTouchEnd(e.timeStamp); 62 | currentTouchList = null; 63 | } 64 | 65 | }; 66 | 67 | //touch事件处理程序 68 | function touchHandler(list,e){ 69 | 70 | var rect = list.canvasPosition; 71 | //滚动处理 72 | scrollHandlers[e.type] && scrollHandlers[e.type](list.scroller,e); 73 | //事件处理 74 | pageX = e.touches ? (e.touches[0] ? e.touches[0].pageX : pageX) : e.pageX; 75 | pageY = e.touches ? (e.touches[0] ? e.touches[0].pageY : pageY) : e.pageY; 76 | 77 | //点击点对象(高清屏两倍的原因,所以这里计算要乘以2) 78 | var point = { 79 | left:(pageX - rect.left), 80 | top:(pageY - rect.top) 81 | }; 82 | //需要触发事件监听程序的节点 83 | //handleNodeEvent(point,list,e); 84 | }; 85 | 86 | 87 | //获取某个事件的节点树 88 | function handleNodeEvent(point,root,originEventObject){ 89 | var node = getEventNode(point,root,type); 90 | 91 | //console.log(node); 92 | var type = originEventObject.type; 93 | var parent = node; 94 | var handlersArr; 95 | 96 | while(parent){ 97 | if(parent.handlers &&(handlersArr = parent.handlers[type])){ 98 | $.each(handlersArr,function(i,handler){ 99 | var eve = new EventObject({ 100 | target:node, 101 | type:type, 102 | layer:parent, 103 | originEventObject:originEventObject 104 | }); 105 | //调用事件处理程序 106 | handler.call(parent,eve); 107 | }); 108 | } 109 | 110 | //停止冒泡 111 | if(parent.getStopEventPropagation(type)) return; 112 | //继续检查上层元素的事件监听 113 | parent = parent.parent; 114 | } 115 | }; 116 | //获取某个事件的最深层节点 117 | function getEventNode(point,root,type){ 118 | var node; 119 | var childNode; 120 | 121 | //节点存在事件监听并且击中该点 122 | if(root.hitPoint(point)){ 123 | node = root; 124 | } 125 | //存在子元素 126 | if(root.children.length){ 127 | $.each(root.children,function(i,child){ 128 | childNode = getEventNode(point,child,type); 129 | if(childNode){ 130 | node = childNode; 131 | return false; 132 | } 133 | 134 | }); 135 | 136 | } 137 | 138 | return node; 139 | }; 140 | 141 | //canvas列表对象 142 | var List = function(opt){ 143 | if(!this instanceof List){ 144 | return new List(opt); 145 | } 146 | this.init(opt); 147 | }; 148 | 149 | List.prototype = Object.create(RenderLayer.prototype); 150 | List.prototype.constructor = RenderLayer; 151 | 152 | 153 | $.extend(List.prototype,{ 154 | init:function(opt){ 155 | 156 | this.classMap = opt.classMap || {}; 157 | 158 | RenderLayer.prototype.init.apply(this,arguments); 159 | 160 | this.canvasElement = opt.canvasElement || $(''); 161 | this.avgFPSContainer = opt.avgFPSContainer; 162 | this.ctx = this.getContext(); 163 | 164 | //初始化canvas缓存池 165 | CacheCanvasPool.init(); 166 | 167 | //初始化canvas滚动组件 168 | this.scroller = new Scroller(this.updateScrollPosition.bind(this),{ 169 | scrollingX: false, 170 | scrollingY: true, 171 | bouncing: false 172 | }); 173 | 174 | this.firstRun = true; 175 | 176 | //列表项的激活态样式类 177 | if(opt.activeClassName){ 178 | this.setActiveClass(opt.activeClassName); 179 | } 180 | 181 | this.bind(); 182 | 183 | this.root = this; 184 | }, 185 | //事件绑定 186 | bind:function(){ 187 | var self = this; 188 | var scroller = this.scroller; 189 | 190 | $(this.canvasElement).on('touchstart',function(e){ 191 | currentTouchList = self; 192 | touchHandler(currentTouchList,e); 193 | }); 194 | 195 | $(this.canvasElement).on('click',function(e){ 196 | touchHandler(self,e); 197 | }); 198 | 199 | $(this.canvasElement).on('touchmove',function(e){ 200 | e.preventDefault(); 201 | }); 202 | 203 | 204 | if(this.activeClassName){ 205 | //按下态监听 206 | this.addEventListener('touchstart',function(e){ 207 | var listItem = e.target.closestClass(DEFAULT_ITEM_CLASS); 208 | currentActiveListItem && currentActiveListItem.removeClass(currentTouchList.activeClassName); 209 | if(listItem){ 210 | //列表项 211 | clearTimeout(touchActiveId); 212 | //按下态 213 | touchActiveId = setTimeout(function(){ 214 | currentActiveListItem = listItem; 215 | currentActiveListItem.addClass(self.activeClassName); 216 | },300); 217 | } 218 | }); 219 | //点击态监听 220 | this.addEventListener('click',function(e){ 221 | var listItem = e.target.closestClass(DEFAULT_ITEM_CLASS); 222 | 223 | if(listItem){ 224 | //列表项 225 | clearTimeout(touchActiveId); 226 | 227 | currentActiveListItem = listItem; 228 | currentActiveListItem.addClass(self.activeClassName); 229 | //点击态 230 | touchActiveId = setTimeout(function(){ 231 | currentActiveListItem.removeClass(self.activeClassName); 232 | currentActiveListItem = null; 233 | },300); 234 | } 235 | }); 236 | } 237 | }, 238 | updateCanvasPosition:function(){ 239 | this.canvasPosition = this.canvasElement.offset(); 240 | }, 241 | //列表项的按下态和点击态 242 | setActiveClass:function(activeClassName){ 243 | this.activeClassName = activeClassName; 244 | }, 245 | setSize:function(width,height){ 246 | this.style.width = width; 247 | this.style.height = height; 248 | 249 | this.canvasElement.prop('width',width * devicePixelRatio); 250 | this.canvasElement.prop('height',height * devicePixelRatio); 251 | 252 | this.canvasElement.css({ 253 | width:width, 254 | height:height 255 | }); 256 | 257 | this.updateCanvasPosition(); 258 | 259 | }, 260 | //获取列表项 261 | getListItems:function(){ 262 | return this.children; 263 | }, 264 | //获取滚动区域高度 265 | getScrollHeight:function(){ 266 | var scrollHeight = 0; 267 | var listItems = this.getListItems(); 268 | $.each(listItems,function(i,item){ 269 | scrollHeight += item.drawHeight; 270 | }); 271 | return scrollHeight; 272 | }, 273 | setScrollerSize:function(){ 274 | var finalStyle = this.finalStyle; 275 | var scrollHeight = this.getScrollHeight(); 276 | this.scroller.setDimensions(finalStyle.width, finalStyle.height, finalStyle.width, scrollHeight); 277 | }, 278 | //更新滚动位置 279 | updateScrollPosition:function(left,top){//console.log('scroll'); 280 | this.scrollTop = top; 281 | this.run(true); 282 | }, 283 | 284 | calculateScrollHalf:function(top){ 285 | 286 | 287 | if(!this.preScrollTop){ 288 | this.preScrollTop = this.scrollTop; 289 | } 290 | 291 | //判断是否往下滚动 292 | CL.isScrollingDown = this.scrollTop >= this.preScrollTop; 293 | 294 | 295 | //滚动到一半开始加载下一页内容 296 | if(CL.isScrollingDown && this.scrollTop + this.drawHeight + 0 >= this.getScrollHeight()){ 297 | this.onScrollToHalf && this.onScrollToHalf(this.scrollTop); 298 | } 299 | 300 | this.preScrollTop = this.scrollTop; 301 | }, 302 | addChildren:function(listItem){ 303 | if(listItem instanceof ListItem){ 304 | //list_item的默认class 305 | listItem.className += ' ' + DEFAULT_ITEM_CLASS; 306 | } 307 | 308 | RenderLayer.prototype.addChildren.apply(this,arguments); 309 | }, 310 | getContext:function(){ 311 | return this.canvasElement[0].getContext('2d'); 312 | }, 313 | clear:function(){ 314 | this.ctx.clearRect(0,0,this.style.width,this.style.height); 315 | }, 316 | update:function(){ 317 | var self = this; 318 | 319 | //todo:仅仅更新脏层的layout树 320 | //从变化节点中找出根节点,更新layout树 321 | //var layoutRoots = getLayoutChangedRoots(this); 322 | 323 | //子元素layoutchanged会同步给list元素 324 | if(this.layoutChanged){//console.log('layout changed'); 325 | 326 | this.setSize(this.finalStyle.width,this.finalStyle.height); 327 | CanvasUtil.updateSubRenderTree(this); 328 | this.setScrollerSize(); 329 | 330 | this.layoutChanged = false; 331 | } 332 | 333 | }, 334 | //计算平均FPS 335 | calculateAvgFps:function(){ 336 | var now = Date.now(); 337 | 338 | if(this.sumDuration == null){ 339 | this.sumDuration = 16; 340 | this.count = 0; 341 | this.lastUpdateTime = now; 342 | } 343 | 344 | var duration = now - this.lastUpdateTime; 345 | 346 | if(this.sumDuration > 1000){ 347 | this.avgFPS = this.count; 348 | this.count = 0; 349 | this.sumDuration = 0; 350 | if(this.avgFPSContainer){ 351 | //外显平均fps 352 | this.avgFPSContainer.text('fps:' + this.avgFPS); 353 | } 354 | } 355 | 356 | this.sumDuration += duration; 357 | this.count ++; 358 | 359 | this.lastUpdateTime = now; 360 | }, 361 | //下一个循环的行为 362 | nextTick:function(callback){ 363 | raf(function(){ 364 | callback && callback(); 365 | }); 366 | }, 367 | draw:function(){ 368 | 369 | var self = this; 370 | var ctx = this.ctx; 371 | 372 | this.clear(); 373 | 374 | ctx.save(); 375 | ctx.scale(devicePixelRatio,devicePixelRatio); 376 | 377 | RenderLayer.prototype.draw.apply(this,arguments); 378 | 379 | ctx.restore(); 380 | 381 | 382 | }, 383 | onBeforeDrawChildren:function(){ 384 | this.ctx.save(); 385 | this.ctx.translate(0,-this.scrollTop); 386 | }, 387 | onAfterDrawChildren:function(){ 388 | this.ctx.restore(); 389 | }, 390 | test:function(){ 391 | var self = this; 392 | self.calculateAvgFps(); 393 | this.nextTick(function(){ 394 | self.test(); 395 | }); 396 | 397 | }, 398 | 399 | run:function(scrolling){ 400 | 401 | var self = this; 402 | if(this.needRun) return; 403 | 404 | this.needRun = true; 405 | 406 | //第一次run 407 | if(this.firstRun){ 408 | //计算自己和子元素的初始绘制样式 409 | this.initializeStyle(); 410 | //改变第一次run的标志位 411 | this.firstRun = false; 412 | } 413 | 414 | //如果在滚动过程中,计算滚动位置 415 | if(scrolling){ 416 | this.calculateScrollHalf(); 417 | } 418 | 419 | //下一帧统一draw 420 | this.nextTick(function(){//console.log('run'); 421 | 422 | if(scrolling){ 423 | //计算平均FPS 424 | self.calculateAvgFps(); 425 | } 426 | //scrolling过程中不监测layoutchanged 427 | else{ 428 | self.update(); 429 | } 430 | 431 | 432 | self.draw(); 433 | self.needRun = false; 434 | }); 435 | 436 | 437 | } 438 | }); 439 | 440 | 441 | 442 | 443 | 444 | 445 | //统一处理的touch事件 446 | $(document.body).on('touchmove touchend touchcancel',function(e){ 447 | // if(e.type == 'touchend' || e.type == 'touchcancel'){ 448 | // alert(0); 449 | // } 450 | clearTimeout(touchActiveId); 451 | if(currentTouchList){ 452 | currentActiveListItem && currentActiveListItem.removeClass(currentTouchList.activeClassName); 453 | touchHandler(currentTouchList,e); 454 | } 455 | }); 456 | 457 | 458 | CL.List = List; 459 | 460 | 461 | 462 | })(window.CanvasList = window.CanvasList || {}); -------------------------------------------------------------------------------- /canvas_util.js: -------------------------------------------------------------------------------- 1 | (function(CL){ 2 | var CanvasUtil = { 3 | //设置圆角的绘制上下文 4 | setBorderRadiusContext:function(ctx,x,y,width,height,borderRadius){ 5 | ctx.beginPath(); 6 | ctx.moveTo(x + borderRadius, y); 7 | ctx.arcTo(x + width, y, x + width,y + height, borderRadius); 8 | ctx.arcTo(x + width, y + height, x, y + height, borderRadius); 9 | ctx.arcTo(x, y + height, x, y, borderRadius); 10 | ctx.arcTo(x, y, x + width, y, borderRadius); 11 | ctx.closePath(); 12 | }, 13 | //设置单边边框的绘制上下文 14 | setSingleBorderContext:function(ctx,x,y,width,height,halfLineWidth,direction){ 15 | var lineWidth = ctx.lineWidth; 16 | 17 | //底部边框 18 | if(direction == 'bottom'){ 19 | ctx.beginPath(); 20 | ctx.moveTo(x, height - halfLineWidth); 21 | ctx.lineTo(width, height - halfLineWidth); 22 | ctx.closePath(); 23 | } 24 | //左边边框 25 | if(direction == 'left'){ 26 | ctx.beginPath(); 27 | ctx.moveTo(x + halfLineWidth, y); 28 | ctx.lineTo(x + halfLineWidth, height); 29 | ctx.closePath(); 30 | } 31 | //右边边框 32 | if(direction == 'right'){ 33 | ctx.beginPath(); 34 | ctx.moveTo(width - halfLineWidth, y); 35 | ctx.lineTo(width - halfLineWidth, height); 36 | ctx.closePath(); 37 | } 38 | //顶部边框 39 | if(direction == 'top'){ 40 | ctx.beginPath(); 41 | ctx.moveTo(x, y + halfLineWidth); 42 | ctx.lineTo(x + width,y + halfLineWidth); 43 | ctx.closePath(); 44 | } 45 | }, 46 | //获取随机id 47 | getRandomId:function(){ 48 | return ~~(Math.random() * 1e8); 49 | }, 50 | //当前层更新时,同时更新子层渲染 51 | updateSubRenderTree:function(layer){ 52 | //计算样式 53 | var layoutObj = CanvasUtil.wrapTreeStyle(layer); 54 | computeLayout.computeLayout(layoutObj); 55 | //应用样式 56 | CanvasUtil.applyTreeStyle(layoutObj); 57 | }, 58 | //遍历包装计算整个结构树的样式 59 | wrapTreeStyle:function(layer){ 60 | var style = layer.style; 61 | var layoutObj = { 62 | layer:layer, 63 | layout:{ 64 | width:undefined, 65 | height:undefined, 66 | left:0, 67 | top:0 68 | }, 69 | style:layer.finalStyle, 70 | onBeforeComputed:function(){ 71 | layer.beforeLayerLayoutComputed && layer.beforeLayerLayoutComputed(this); 72 | 73 | } 74 | }; 75 | 76 | layoutObj.children = layer.children.map(function(child){ 77 | var obj = CanvasUtil.wrapTreeStyle(child); 78 | obj.parentLayoutObj = layoutObj.layout; 79 | return obj; 80 | }); 81 | 82 | //准备计算布局树之前的回调 83 | layer.beforeComputeLayoutTree && layer.beforeComputeLayoutTree(); 84 | 85 | return layoutObj; 86 | }, 87 | //把计算的样式值应用到某每个layer 88 | applyTreeStyle:function(layerObj){ 89 | var layer = layerObj.layer; 90 | var layout = layerObj.layout; 91 | //计算样式应用在layer上,left和top为原始属性,所以要增加drawLeft和drawTop用于绘制 92 | layer.drawLeft = layout.left; 93 | layer.drawTop = layout.top; 94 | layer.drawWidth = layout.width; 95 | layer.drawHeight = layout.height; 96 | //layer自己实现的额外自定义的layout计算 97 | layer.customLayoutCalculation && layer.customLayoutCalculation(); 98 | 99 | layerObj.children.map(function(child){ 100 | CanvasUtil.applyTreeStyle(child); 101 | }); 102 | } 103 | }; 104 | 105 | 106 | CL.CanvasUtil = CanvasUtil; 107 | 108 | })(window.CanvasList = window.CanvasList || {}); -------------------------------------------------------------------------------- /event_object.js: -------------------------------------------------------------------------------- 1 | (function(CL){ 2 | 3 | function EventObject(opt){ 4 | this.target = opt.target; 5 | this.type = opt.type; 6 | this.layer = opt.layer; 7 | this.originEventObject = opt.originEventObject; 8 | }; 9 | 10 | EventObject.prototype = { 11 | constructor:EventObject, 12 | stopPropagation:function(){ 13 | this.layer.setStopEventPropagation(this.type); 14 | } 15 | } 16 | 17 | 18 | CL.EventObject = EventObject; 19 | 20 | })(window.CanvasList = window.CanvasList || {}); -------------------------------------------------------------------------------- /image.js: -------------------------------------------------------------------------------- 1 | (function(CL){ 2 | 3 | var RenderLayer = CL.RenderLayer; 4 | var CanvasUtil = CL.CanvasUtil; 5 | 6 | //图片对象 7 | function CanvasImage(opt){ 8 | if(!(this instanceof CanvasImage)){ 9 | return new CanvasImage(opt); 10 | } 11 | this.init(opt); 12 | }; 13 | 14 | CanvasImage.prototype = Object.create(RenderLayer.prototype); 15 | CanvasImage.prototype.constructor = CanvasImage; 16 | 17 | $.extend(CanvasImage.prototype,{ 18 | init:function(opt){ 19 | 20 | RenderLayer.prototype.init.apply(this,arguments); 21 | 22 | this.url = opt.url; 23 | //初始化图片对象 24 | this.imageElement = $(''); 25 | 26 | this.bind(); 27 | 28 | this.setUrl(this.url); 29 | }, 30 | bind:function(){ 31 | var self = this; 32 | this.imageElement.on('load',function(e){ 33 | var element = e.target; 34 | var finalStyle = self.finalStyle; 35 | //图片是否已加载 36 | self.isLoaded = true; 37 | 38 | self.naturalWidth = element.naturalWidth; 39 | self.naturalHeight = element.naturalHeight; 40 | 41 | if(typeof self.width == 'undefined'){ 42 | finalStyle.width = self.naturalWidth; 43 | } 44 | if(typeof self.height == 'undefined'){ 45 | finalStyle.height = self.naturalHeight; 46 | } 47 | //删除根元素的缓存canvas,以便更新缓存 48 | self.updateParentCacheableLayer(); 49 | //初始化样式 50 | self.initializeStyle(); 51 | 52 | //触发缓存canvas更新 53 | var root = self.getRoot(); 54 | //让根元素重新绘制 55 | if(root){ 56 | root.run(); 57 | } 58 | 59 | 60 | }); 61 | 62 | }, 63 | setUrl:function(url){ 64 | this.url = url; 65 | this.isLoaded = false; 66 | this.imageElement.prop('src',url); 67 | }, 68 | drawSelf:function(ctx){ 69 | 70 | //已加载图片才允许绘制 71 | if(this.isLoaded){ 72 | 73 | var width = this.drawWidth; 74 | var height = this.drawHeight; 75 | var borderRadius = this.finalStyle.borderRadius; 76 | 77 | //圆角图片裁剪 78 | if(borderRadius){ 79 | CanvasUtil.setBorderRadiusContext(ctx,0,0,width,height,borderRadius); 80 | ctx.clip(); 81 | } 82 | ctx.drawImage(this.imageElement[0],0,0,this.naturalWidth,this.naturalHeight,0,0,Math.round(width),Math.round(height)); 83 | 84 | } 85 | 86 | }, 87 | onClick:function(){ 88 | 89 | } 90 | }); 91 | 92 | CL.CanvasImage = CanvasImage; 93 | 94 | })(window.CanvasList = window.CanvasList || {}); -------------------------------------------------------------------------------- /index_catetab.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) {if (typeof define === "function" && define.amd) {define(factory);} else {root["TmplInline_index_catetab"] = factory();}}(this, function (){ 2 | var TmplInline_index_catetab = {}; 3 | var cate_tab = "
  • \r\n
    \r\n
    \r\n
    \r\n
    \r\n 图集\r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    {{post.ding | numHelper}}
    \r\n
    {{post.reply | numHelper}}
    \r\n
    \r\n
    {{post.from}}部落
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n\r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    {{post.ding | numHelper}}
    \r\n
    {{post.reply | numHelper}}
    \r\n
    \r\n
    {{post.from}}部落
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n\r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    {{post.ding | numHelper}}
    \r\n
    {{post.reply | numHelper}}
    \r\n
    \r\n
    {{post.from}}部落
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n\r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n 活动\r\n
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    {{post.ding | numHelper}}
    \r\n
    {{post.reply | numHelper}}
    \r\n
    \r\n
    {{post.from}}部落
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n\r\n
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    {{post.ding | numHelper}}
    \r\n
    {{post.reply | numHelper}}
    \r\n
    \r\n
    {{post.from}}部落
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n\r\n
    \r\n
    \r\n \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    {{post.ding | numHelper}}
    \r\n
    {{post.reply | numHelper}}
    \r\n
    \r\n
    {{post.from}}部落
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n\r\n
    \r\n
    \r\n \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    {{post.ding | numHelper}}
    \r\n
    {{post.reply | numHelper}}
    \r\n
    \r\n
    {{post.from}}部落
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n\r\n
    \r\n
    \r\n \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    \r\n
    {{post.ding | numHelper}}
    \r\n
    {{post.reply | numHelper}}
    \r\n
    \r\n
    {{post.from}}部落
    \r\n
    \r\n
    \r\n
    \r\n \r\n
    \r\n
    \r\n
  • \r\n"; 4 | TmplInline_index_catetab.cate_tab = "TmplInline_index_catetab.cate_tab"; 5 | Tmpl.addTmpl(TmplInline_index_catetab.cate_tab, cate_tab); 6 | 7 | var cate_wrapper = "
    \r\n
    \r\n \t\r\n
    \r\n
    "; 8 | TmplInline_index_catetab.cate_wrapper = "TmplInline_index_catetab.cate_wrapper"; 9 | Tmpl.addTmpl(TmplInline_index_catetab.cate_wrapper, cate_wrapper); 10 | return TmplInline_index_catetab;})); -------------------------------------------------------------------------------- /layout.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Copyright (c) 2014, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. An additional grant 8 | * of patent rights can be found in the PATENTS file in the same directory. 9 | */ 10 | 11 | var computeLayout = (function() { 12 | 13 | var CSS_UNDEFINED; 14 | 15 | var CSS_FLEX_DIRECTION_ROW = 'row'; 16 | var CSS_FLEX_DIRECTION_COLUMN = 'column'; 17 | 18 | // var CSS_JUSTIFY_FLEX_START = 'flex-start'; 19 | var CSS_JUSTIFY_CENTER = 'center'; 20 | var CSS_JUSTIFY_FLEX_END = 'flex-end'; 21 | var CSS_JUSTIFY_SPACE_BETWEEN = 'space-between'; 22 | var CSS_JUSTIFY_SPACE_AROUND = 'space-around'; 23 | 24 | var CSS_ALIGN_FLEX_START = 'flex-start'; 25 | var CSS_ALIGN_CENTER = 'center'; 26 | // var CSS_ALIGN_FLEX_END = 'flex-end'; 27 | var CSS_ALIGN_STRETCH = 'stretch'; 28 | 29 | var CSS_POSITION_RELATIVE = 'relative'; 30 | var CSS_POSITION_ABSOLUTE = 'absolute'; 31 | 32 | var leading = { 33 | row: 'left', 34 | column: 'top' 35 | }; 36 | var trailing = { 37 | row: 'right', 38 | column: 'bottom' 39 | }; 40 | var pos = { 41 | row: 'left', 42 | column: 'top' 43 | }; 44 | var dim = { 45 | row: 'width', 46 | column: 'height' 47 | }; 48 | 49 | function capitalizeFirst(str) { 50 | return str.charAt(0).toUpperCase() + str.slice(1); 51 | } 52 | 53 | function getSpacing(node, type, suffix, location) { 54 | var key = type + capitalizeFirst(location) + suffix; 55 | if (key in node.style) { 56 | return node.style[key]; 57 | } 58 | 59 | key = type + suffix; 60 | if (key in node.style) { 61 | return node.style[key]; 62 | } 63 | 64 | return 0; 65 | } 66 | function fillNodes(node) { 67 | node.layout = { 68 | width: undefined, 69 | height: undefined, 70 | top: 0, 71 | left: 0 72 | }; 73 | if (!node.style) { 74 | node.style = {}; 75 | } 76 | 77 | if (!node.children || node.style.measure) { 78 | node.children = []; 79 | } 80 | node.children.forEach(fillNodes); 81 | return node; 82 | } 83 | 84 | function extractNodes(node) { 85 | var layout = node.layout; 86 | delete node.layout; 87 | if (node.children && node.children.length > 0) { 88 | layout.children = node.children.map(extractNodes); 89 | } else { 90 | delete node.children; 91 | } 92 | return layout; 93 | } 94 | 95 | function getPositiveSpacing(node, type, suffix, location) { 96 | var key = type + capitalizeFirst(location) + suffix; 97 | if (key in node.style && node.style[key] >= 0) { 98 | return node.style[key]; 99 | } 100 | 101 | key = type + suffix; 102 | if (key in node.style && node.style[key] >= 0) { 103 | return node.style[key]; 104 | } 105 | 106 | return 0; 107 | } 108 | 109 | function isUndefined(value) { 110 | return value === undefined; 111 | } 112 | 113 | function getMargin(node, location) { 114 | return getSpacing(node, 'margin', '', location); 115 | } 116 | 117 | function getPadding(node, location) { 118 | return getPositiveSpacing(node, 'padding', '', location); 119 | } 120 | 121 | function getBorder(node, location) { 122 | return getPositiveSpacing(node, 'border', 'Width', location); 123 | } 124 | 125 | function getPaddingAndBorder(node, location) { 126 | return getPadding(node, location) + getBorder(node, location); 127 | } 128 | 129 | function getMarginAxis(node, axis) { 130 | return getMargin(node, leading[axis]) + getMargin(node, trailing[axis]); 131 | } 132 | 133 | function getPaddingAndBorderAxis(node, axis) { 134 | return getPaddingAndBorder(node, leading[axis]) + getPaddingAndBorder(node, trailing[axis]); 135 | } 136 | 137 | function getJustifyContent(node) { 138 | if ('justifyContent' in node.style) { 139 | return node.style.justifyContent; 140 | } 141 | return 'flex-start'; 142 | } 143 | 144 | function getAlignItem(node, child) { 145 | if ('alignSelf' in child.style) { 146 | return child.style.alignSelf; 147 | } 148 | if ('alignItems' in node.style) { 149 | return node.style.alignItems; 150 | } 151 | return 'stretch'; 152 | } 153 | 154 | function getFlexDirection(node) { 155 | if ('flexDirection' in node.style) { 156 | return node.style.flexDirection; 157 | } 158 | return 'column'; 159 | } 160 | 161 | function getPositionType(node) { 162 | if ('position' in node.style) { 163 | return node.style.position; 164 | } 165 | return 'relative'; 166 | } 167 | 168 | function getFlex(node) { 169 | return node.style.flex; 170 | } 171 | 172 | function isFlex(node) { 173 | return ( 174 | getPositionType(node) === CSS_POSITION_RELATIVE && 175 | getFlex(node) > 0 176 | ); 177 | } 178 | 179 | function isFlexWrap(node) { 180 | return node.style.flexWrap === 'wrap'; 181 | } 182 | 183 | function getDimWithMargin(node, axis) { 184 | return node.layout[dim[axis]] + getMarginAxis(node, axis); 185 | } 186 | 187 | function isDimDefined(node, axis) { 188 | return !isUndefined(node.style[dim[axis]]) && node.style[dim[axis]] >= 0; 189 | } 190 | 191 | function isPosDefined(node, pos) { 192 | return !isUndefined(node.style[pos]); 193 | } 194 | 195 | function isMeasureDefined(node) { 196 | return 'measure' in node.style; 197 | } 198 | 199 | function getPosition(node, pos) { 200 | if (pos in node.style) { 201 | return node.style[pos]; 202 | } 203 | return 0; 204 | } 205 | 206 | function boundAxis(node, axis, value) { 207 | var min = { 208 | row: node.style.minWidth, 209 | column: node.style.minHeight 210 | }[axis]; 211 | 212 | var max = { 213 | row: node.style.maxWidth, 214 | column: node.style.maxHeight 215 | }[axis]; 216 | 217 | var boundValue = value; 218 | if (!isUndefined(max) && max >= 0 && boundValue > max) { 219 | boundValue = max; 220 | } 221 | if (!isUndefined(min) && min >= 0 && boundValue < min) { 222 | boundValue = min; 223 | } 224 | return boundValue; 225 | } 226 | 227 | function fmaxf(a, b) { 228 | if (a > b) { 229 | return a; 230 | } 231 | return b; 232 | } 233 | 234 | // When the user specifically sets a value for width or height 235 | function setDimensionFromStyle(node, axis) { 236 | // The parent already computed us a width or height. We just skip it 237 | if (!isUndefined(node.layout[dim[axis]])) { 238 | return; 239 | } 240 | // We only run if there's a width or height defined 241 | if (!isDimDefined(node, axis)) { 242 | return; 243 | } 244 | 245 | // The dimensions can never be smaller than the padding and border 246 | node.layout[dim[axis]] = fmaxf( 247 | boundAxis(node, axis, node.style[dim[axis]]), 248 | getPaddingAndBorderAxis(node, axis) 249 | ); 250 | } 251 | 252 | // If both left and right are defined, then use left. Otherwise return 253 | // +left or -right depending on which is defined. 254 | function getRelativePosition(node, axis) { 255 | if (leading[axis] in node.style) { 256 | return getPosition(node, leading[axis]); 257 | } 258 | return -getPosition(node, trailing[axis]); 259 | } 260 | 261 | function layoutNode(node, parentMaxWidth) { 262 | 263 | //ADD:节点计算layout前的回调 264 | node.onBeforeComputed && node.onBeforeComputed(); 265 | 266 | var/*css_flex_direction_t*/ mainAxis = getFlexDirection(node); 267 | var/*css_flex_direction_t*/ crossAxis = mainAxis === CSS_FLEX_DIRECTION_ROW ? 268 | CSS_FLEX_DIRECTION_COLUMN : 269 | CSS_FLEX_DIRECTION_ROW; 270 | 271 | // Handle width and height style attributes 272 | setDimensionFromStyle(node, mainAxis); 273 | setDimensionFromStyle(node, crossAxis); 274 | 275 | // The position is set by the parent, but we need to complete it with a 276 | // delta composed of the margin and left/top/right/bottom 277 | node.layout[leading[mainAxis]] += getMargin(node, leading[mainAxis]) + 278 | getRelativePosition(node, mainAxis); 279 | node.layout[leading[crossAxis]] += getMargin(node, leading[crossAxis]) + 280 | getRelativePosition(node, crossAxis); 281 | 282 | if (isMeasureDefined(node)) { 283 | var/*float*/ width = CSS_UNDEFINED; 284 | if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { 285 | width = node.style.width; 286 | } else if (!isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_ROW]])) { 287 | width = node.layout[dim[CSS_FLEX_DIRECTION_ROW]]; 288 | } else { 289 | width = parentMaxWidth - 290 | getMarginAxis(node, CSS_FLEX_DIRECTION_ROW); 291 | } 292 | width -= getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); 293 | 294 | // We only need to give a dimension for the text if we haven't got any 295 | // for it computed yet. It can either be from the style attribute or because 296 | // the element is flexible. 297 | var/*bool*/ isRowUndefined = !isDimDefined(node, CSS_FLEX_DIRECTION_ROW) && 298 | isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_ROW]]); 299 | var/*bool*/ isColumnUndefined = !isDimDefined(node, CSS_FLEX_DIRECTION_COLUMN) && 300 | isUndefined(node.layout[dim[CSS_FLEX_DIRECTION_COLUMN]]); 301 | 302 | // Let's not measure the text if we already know both dimensions 303 | if (isRowUndefined || isColumnUndefined) { 304 | var/*css_dim_t*/ measureDim = node.style.measure( 305 | /*(c)!node->context,*/ 306 | /*(java)!layoutContext.measureOutput,*/ 307 | width 308 | ); 309 | if (isRowUndefined) { 310 | node.layout.width = measureDim.width + 311 | getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); 312 | } 313 | if (isColumnUndefined) { 314 | node.layout.height = measureDim.height + 315 | getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_COLUMN); 316 | } 317 | } 318 | return; 319 | } 320 | 321 | var/*int*/ i; 322 | var/*int*/ ii; 323 | var/*css_node_t**/ child; 324 | var/*css_flex_direction_t*/ axis; 325 | 326 | // Pre-fill some dimensions straight from the parent 327 | for (i = 0; i < node.children.length; ++i) { 328 | child = node.children[i]; 329 | // Pre-fill cross axis dimensions when the child is using stretch before 330 | // we call the recursive layout pass 331 | if (getAlignItem(node, child) === CSS_ALIGN_STRETCH && 332 | getPositionType(child) === CSS_POSITION_RELATIVE && 333 | !isUndefined(node.layout[dim[crossAxis]]) && 334 | !isDimDefined(child, crossAxis)) { 335 | child.layout[dim[crossAxis]] = fmaxf( 336 | boundAxis(child, crossAxis, node.layout[dim[crossAxis]] - 337 | getPaddingAndBorderAxis(node, crossAxis) - 338 | getMarginAxis(child, crossAxis)), 339 | // You never want to go smaller than padding 340 | getPaddingAndBorderAxis(child, crossAxis) 341 | ); 342 | } else if (getPositionType(child) === CSS_POSITION_ABSOLUTE) { 343 | // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both 344 | // left and right or top and bottom). 345 | for (ii = 0; ii < 2; ii++) { 346 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 347 | if (!isUndefined(node.layout[dim[axis]]) && 348 | !isDimDefined(child, axis) && 349 | isPosDefined(child, leading[axis]) && 350 | isPosDefined(child, trailing[axis])) { 351 | child.layout[dim[axis]] = fmaxf( 352 | boundAxis(child, axis, node.layout[dim[axis]] - 353 | getPaddingAndBorderAxis(node, axis) - 354 | getMarginAxis(child, axis) - 355 | getPosition(child, leading[axis]) - 356 | getPosition(child, trailing[axis])), 357 | // You never want to go smaller than padding 358 | getPaddingAndBorderAxis(child, axis) 359 | ); 360 | } 361 | } 362 | } 363 | } 364 | 365 | var/*float*/ definedMainDim = CSS_UNDEFINED; 366 | if (!isUndefined(node.layout[dim[mainAxis]])) { 367 | definedMainDim = node.layout[dim[mainAxis]] - 368 | getPaddingAndBorderAxis(node, mainAxis); 369 | } 370 | 371 | // We want to execute the next two loops one per line with flex-wrap 372 | var/*int*/ startLine = 0; 373 | var/*int*/ endLine = 0; 374 | // var/*int*/ nextOffset = 0; 375 | var/*int*/ alreadyComputedNextLayout = 0; 376 | // We aggregate the total dimensions of the container in those two variables 377 | var/*float*/ linesCrossDim = 0; 378 | var/*float*/ linesMainDim = 0; 379 | while (endLine < node.children.length) { 380 | // Layout non flexible children and count children by type 381 | 382 | // mainContentDim is accumulation of the dimensions and margin of all the 383 | // non flexible children. This will be used in order to either set the 384 | // dimensions of the node if none already exist, or to compute the 385 | // remaining space left for the flexible children. 386 | var/*float*/ mainContentDim = 0; 387 | 388 | // There are three kind of children, non flexible, flexible and absolute. 389 | // We need to know how many there are in order to distribute the space. 390 | var/*int*/ flexibleChildrenCount = 0; 391 | var/*float*/ totalFlexible = 0; 392 | var/*int*/ nonFlexibleChildrenCount = 0; 393 | 394 | var/*float*/ maxWidth; 395 | for (i = startLine; i < node.children.length; ++i) { 396 | child = node.children[i]; 397 | var/*float*/ nextContentDim = 0; 398 | 399 | // If it's a flexible child, accumulate the size that the child potentially 400 | // contributes to the row 401 | if (isFlex(child)) { 402 | flexibleChildrenCount++; 403 | totalFlexible += getFlex(child); 404 | 405 | // Even if we don't know its exact size yet, we already know the padding, 406 | // border and margin. We'll use this partial information, which represents 407 | // the smallest possible size for the child, to compute the remaining 408 | // available space. 409 | nextContentDim = getPaddingAndBorderAxis(child, mainAxis) + 410 | getMarginAxis(child, mainAxis); 411 | 412 | } else { 413 | maxWidth = CSS_UNDEFINED; 414 | if (mainAxis !== CSS_FLEX_DIRECTION_ROW) { 415 | maxWidth = parentMaxWidth - 416 | getMarginAxis(node, CSS_FLEX_DIRECTION_ROW) - 417 | getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); 418 | 419 | if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { 420 | maxWidth = node.layout[dim[CSS_FLEX_DIRECTION_ROW]] - 421 | getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); 422 | } 423 | } 424 | 425 | // This is the main recursive call. We layout non flexible children. 426 | if (alreadyComputedNextLayout === 0) { 427 | layoutNode(/*(java)!layoutContext, */child, maxWidth); 428 | } 429 | 430 | // Absolute positioned elements do not take part of the layout, so we 431 | // don't use them to compute mainContentDim 432 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 433 | nonFlexibleChildrenCount++; 434 | // At this point we know the final size and margin of the element. 435 | nextContentDim = getDimWithMargin(child, mainAxis); 436 | } 437 | } 438 | 439 | // The element we are about to add would make us go to the next line 440 | if (isFlexWrap(node) && 441 | !isUndefined(node.layout[dim[mainAxis]]) && 442 | mainContentDim + nextContentDim > definedMainDim && 443 | // If there's only one element, then it's bigger than the content 444 | // and needs its own line 445 | i !== startLine) { 446 | alreadyComputedNextLayout = 1; 447 | break; 448 | } 449 | alreadyComputedNextLayout = 0; 450 | mainContentDim += nextContentDim; 451 | endLine = i + 1; 452 | } 453 | 454 | // Layout flexible children and allocate empty space 455 | 456 | // In order to position the elements in the main axis, we have two 457 | // controls. The space between the beginning and the first element 458 | // and the space between each two elements. 459 | var/*float*/ leadingMainDim = 0; 460 | var/*float*/ betweenMainDim = 0; 461 | 462 | // The remaining available space that needs to be allocated 463 | var/*float*/ remainingMainDim = 0; 464 | if (!isUndefined(node.layout[dim[mainAxis]])) { 465 | remainingMainDim = definedMainDim - mainContentDim; 466 | } else { 467 | remainingMainDim = boundAxis(node, mainAxis, fmaxf(mainContentDim, 0)) - mainContentDim; 468 | } 469 | 470 | // If there are flexible children in the mix, they are going to fill the 471 | // remaining space 472 | if (flexibleChildrenCount !== 0) { 473 | var/*float*/ flexibleMainDim = remainingMainDim / totalFlexible; 474 | var/*float*/ baseMainDim; 475 | var/*float*/ boundMainDim; 476 | 477 | // Iterate over every child in the axis. If the flex share of remaining 478 | // space doesn't meet min/max bounds, remove this child from flex 479 | // calculations. 480 | for (i = startLine; i < endLine; ++i) { 481 | child = node.children[i]; 482 | if (isFlex(child)) { 483 | baseMainDim = flexibleMainDim * getFlex(child) + 484 | getPaddingAndBorderAxis(child, mainAxis); 485 | boundMainDim = boundAxis(child, mainAxis, baseMainDim); 486 | 487 | if (baseMainDim !== boundMainDim) { 488 | remainingMainDim -= boundMainDim; 489 | totalFlexible -= getFlex(child); 490 | } 491 | } 492 | } 493 | flexibleMainDim = remainingMainDim / totalFlexible; 494 | 495 | // The non flexible children can overflow the container, in this case 496 | // we should just assume that there is no space available. 497 | if (flexibleMainDim < 0) { 498 | flexibleMainDim = 0; 499 | } 500 | // We iterate over the full array and only apply the action on flexible 501 | // children. This is faster than actually allocating a new array that 502 | // contains only flexible children. 503 | for (i = startLine; i < endLine; ++i) { 504 | child = node.children[i]; 505 | if (isFlex(child)) { 506 | // At this point we know the final size of the element in the main 507 | // dimension 508 | child.layout[dim[mainAxis]] = boundAxis(child, mainAxis, 509 | flexibleMainDim * getFlex(child) + getPaddingAndBorderAxis(child, mainAxis) 510 | ); 511 | 512 | maxWidth = CSS_UNDEFINED; 513 | if (isDimDefined(node, CSS_FLEX_DIRECTION_ROW)) { 514 | maxWidth = node.layout[dim[CSS_FLEX_DIRECTION_ROW]] - 515 | getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); 516 | } else if (mainAxis !== CSS_FLEX_DIRECTION_ROW) { 517 | maxWidth = parentMaxWidth - 518 | getMarginAxis(node, CSS_FLEX_DIRECTION_ROW) - 519 | getPaddingAndBorderAxis(node, CSS_FLEX_DIRECTION_ROW); 520 | } 521 | 522 | // And we recursively call the layout algorithm for this child 523 | layoutNode(/*(java)!layoutContext, */child, maxWidth); 524 | } 525 | } 526 | 527 | // We use justifyContent to figure out how to allocate the remaining 528 | // space available 529 | } else { 530 | var/*css_justify_t*/ justifyContent = getJustifyContent(node); 531 | if (justifyContent === CSS_JUSTIFY_CENTER) { 532 | leadingMainDim = remainingMainDim / 2; 533 | } else if (justifyContent === CSS_JUSTIFY_FLEX_END) { 534 | leadingMainDim = remainingMainDim; 535 | } else if (justifyContent === CSS_JUSTIFY_SPACE_BETWEEN) { 536 | remainingMainDim = fmaxf(remainingMainDim, 0); 537 | if (flexibleChildrenCount + nonFlexibleChildrenCount - 1 !== 0) { 538 | betweenMainDim = remainingMainDim / 539 | (flexibleChildrenCount + nonFlexibleChildrenCount - 1); 540 | } else { 541 | betweenMainDim = 0; 542 | } 543 | } else if (justifyContent === CSS_JUSTIFY_SPACE_AROUND) { 544 | // Space on the edges is half of the space between elements 545 | betweenMainDim = remainingMainDim / 546 | (flexibleChildrenCount + nonFlexibleChildrenCount); 547 | leadingMainDim = betweenMainDim / 2; 548 | } 549 | } 550 | 551 | // Position elements in the main axis and compute dimensions 552 | 553 | // At this point, all the children have their dimensions set. We need to 554 | // find their position. In order to do that, we accumulate data in 555 | // variables that are also useful to compute the total dimensions of the 556 | // container! 557 | var/*float*/ crossDim = 0; 558 | var/*float*/ mainDim = leadingMainDim + 559 | getPaddingAndBorder(node, leading[mainAxis]); 560 | 561 | for (i = startLine; i < endLine; ++i) { 562 | child = node.children[i]; 563 | 564 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE && 565 | isPosDefined(child, leading[mainAxis])) { 566 | // In case the child is position absolute and has left/top being 567 | // defined, we override the position to whatever the user said 568 | // (and margin/border). 569 | child.layout[pos[mainAxis]] = getPosition(child, leading[mainAxis]) + 570 | getBorder(node, leading[mainAxis]) + 571 | getMargin(child, leading[mainAxis]); 572 | } else { 573 | // If the child is position absolute (without top/left) or relative, 574 | // we put it at the current accumulated offset. 575 | child.layout[pos[mainAxis]] += mainDim; 576 | } 577 | 578 | // Now that we placed the element, we need to update the variables 579 | // We only need to do that for relative elements. Absolute elements 580 | // do not take part in that phase. 581 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 582 | // The main dimension is the sum of all the elements dimension plus 583 | // the spacing. 584 | mainDim += betweenMainDim + getDimWithMargin(child, mainAxis); 585 | // The cross dimension is the max of the elements dimension since there 586 | // can only be one element in that cross dimension. 587 | crossDim = fmaxf(crossDim, boundAxis(child, crossAxis, getDimWithMargin(child, crossAxis))); 588 | } 589 | } 590 | 591 | var/*float*/ containerMainAxis = node.layout[dim[mainAxis]]; 592 | // If the user didn't specify a width or height, and it has not been set 593 | // by the container, then we set it via the children. 594 | if (isUndefined(containerMainAxis)) { 595 | containerMainAxis = fmaxf( 596 | // We're missing the last padding at this point to get the final 597 | // dimension 598 | boundAxis(node, mainAxis, mainDim + getPaddingAndBorder(node, trailing[mainAxis])), 599 | // We can never assign a width smaller than the padding and borders 600 | getPaddingAndBorderAxis(node, mainAxis) 601 | ); 602 | } 603 | 604 | var/*float*/ containerCrossAxis = node.layout[dim[crossAxis]]; 605 | if (isUndefined(node.layout[dim[crossAxis]])) { 606 | containerCrossAxis = fmaxf( 607 | // For the cross dim, we add both sides at the end because the value 608 | // is aggregate via a max function. Intermediate negative values 609 | // can mess this computation otherwise 610 | boundAxis(node, crossAxis, crossDim + getPaddingAndBorderAxis(node, crossAxis)), 611 | getPaddingAndBorderAxis(node, crossAxis) 612 | ); 613 | } 614 | 615 | // Position elements in the cross axis 616 | 617 | for (i = startLine; i < endLine; ++i) { 618 | child = node.children[i]; 619 | 620 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE && 621 | isPosDefined(child, leading[crossAxis])) { 622 | // In case the child is absolutely positionned and has a 623 | // top/left/bottom/right being set, we override all the previously 624 | // computed positions to set it correctly. 625 | child.layout[pos[crossAxis]] = getPosition(child, leading[crossAxis]) + 626 | getBorder(node, leading[crossAxis]) + 627 | getMargin(child, leading[crossAxis]); 628 | 629 | } else { 630 | var/*float*/ leadingCrossDim = getPaddingAndBorder(node, leading[crossAxis]); 631 | 632 | // For a relative children, we're either using alignItems (parent) or 633 | // alignSelf (child) in order to determine the position in the cross axis 634 | if (getPositionType(child) === CSS_POSITION_RELATIVE) { 635 | var/*css_align_t*/ alignItem = getAlignItem(node, child); 636 | if (alignItem === CSS_ALIGN_STRETCH) { 637 | // You can only stretch if the dimension has not already been set 638 | // previously. 639 | if (!isDimDefined(child, crossAxis)) { 640 | child.layout[dim[crossAxis]] = fmaxf( 641 | boundAxis(child, crossAxis, containerCrossAxis - 642 | getPaddingAndBorderAxis(node, crossAxis) - 643 | getMarginAxis(child, crossAxis)), 644 | // You never want to go smaller than padding 645 | getPaddingAndBorderAxis(child, crossAxis) 646 | ); 647 | } 648 | } else if (alignItem !== CSS_ALIGN_FLEX_START) { 649 | // The remaining space between the parent dimensions+padding and child 650 | // dimensions+margin. 651 | var/*float*/ remainingCrossDim = containerCrossAxis - 652 | getPaddingAndBorderAxis(node, crossAxis) - 653 | getDimWithMargin(child, crossAxis); 654 | 655 | if (alignItem === CSS_ALIGN_CENTER) { 656 | leadingCrossDim += remainingCrossDim / 2; 657 | } else { // CSS_ALIGN_FLEX_END 658 | leadingCrossDim += remainingCrossDim; 659 | } 660 | } 661 | } 662 | 663 | // And we apply the position 664 | child.layout[pos[crossAxis]] += linesCrossDim + leadingCrossDim; 665 | } 666 | } 667 | 668 | linesCrossDim += crossDim; 669 | linesMainDim = fmaxf(linesMainDim, mainDim); 670 | startLine = endLine; 671 | } 672 | 673 | // If the user didn't specify a width or height, and it has not been set 674 | // by the container, then we set it via the children. 675 | if (isUndefined(node.layout[dim[mainAxis]])) { 676 | node.layout[dim[mainAxis]] = fmaxf( 677 | // We're missing the last padding at this point to get the final 678 | // dimension 679 | boundAxis(node, mainAxis, linesMainDim + getPaddingAndBorder(node, trailing[mainAxis])), 680 | // We can never assign a width smaller than the padding and borders 681 | getPaddingAndBorderAxis(node, mainAxis) 682 | ); 683 | } 684 | 685 | if (isUndefined(node.layout[dim[crossAxis]])) { 686 | node.layout[dim[crossAxis]] = fmaxf( 687 | // For the cross dim, we add both sides at the end because the value 688 | // is aggregate via a max function. Intermediate negative values 689 | // can mess this computation otherwise 690 | boundAxis(node, crossAxis, linesCrossDim + getPaddingAndBorderAxis(node, crossAxis)), 691 | getPaddingAndBorderAxis(node, crossAxis) 692 | ); 693 | } 694 | 695 | // Calculate dimensions for absolutely positioned elements 696 | 697 | for (i = 0; i < node.children.length; ++i) { 698 | child = node.children[i]; 699 | if (getPositionType(child) === CSS_POSITION_ABSOLUTE) { 700 | // Pre-fill dimensions when using absolute position and both offsets for the axis are defined (either both 701 | // left and right or top and bottom). 702 | for (ii = 0; ii < 2; ii++) { 703 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 704 | if (!isUndefined(node.layout[dim[axis]]) && 705 | !isDimDefined(child, axis) && 706 | isPosDefined(child, leading[axis]) && 707 | isPosDefined(child, trailing[axis])) { 708 | child.layout[dim[axis]] = fmaxf( 709 | boundAxis(child, axis, node.layout[dim[axis]] - 710 | getPaddingAndBorderAxis(node, axis) - 711 | getMarginAxis(child, axis) - 712 | getPosition(child, leading[axis]) - 713 | getPosition(child, trailing[axis]) 714 | ), 715 | // You never want to go smaller than padding 716 | getPaddingAndBorderAxis(child, axis) 717 | ); 718 | } 719 | } 720 | for (ii = 0; ii < 2; ii++) { 721 | axis = (ii !== 0) ? CSS_FLEX_DIRECTION_ROW : CSS_FLEX_DIRECTION_COLUMN; 722 | if (isPosDefined(child, trailing[axis]) && 723 | !isPosDefined(child, leading[axis])) { 724 | child.layout[leading[axis]] = 725 | node.layout[dim[axis]] - 726 | child.layout[dim[axis]] - 727 | getPosition(child, trailing[axis]); 728 | } 729 | } 730 | } 731 | } 732 | } 733 | 734 | return { 735 | computeLayout: layoutNode, 736 | fillNodes: fillNodes, 737 | extractNodes: extractNodes 738 | }; 739 | })(); 740 | 741 | // UMD (Universal Module Definition) 742 | // See https://github.com/umdjs/umd for reference 743 | (function (root, factory) { 744 | if (typeof define === 'function' && define.amd) { 745 | // AMD. Register as an anonymous module. 746 | define([], factory); 747 | } else if (typeof exports === 'object') { 748 | // Node. Does not work with strict CommonJS, but 749 | // only CommonJS-like environments that support module.exports, 750 | // like Node. 751 | module.exports = factory(); 752 | } else { 753 | // Browser globals (root is window) 754 | root.returnExports = factory(); 755 | } 756 | }(this, function () { 757 | return computeLayout; 758 | })); 759 | 760 | -------------------------------------------------------------------------------- /list_item.js: -------------------------------------------------------------------------------- 1 | (function(CL){ 2 | 3 | var RenderLayer = CL.RenderLayer; 4 | var CanvasImage = CL.CanvasImage; 5 | var CanvasText = CL.CanvasText; 6 | 7 | 8 | function ListItem(opt){ 9 | if(!(this instanceof ListItem)){ 10 | return new ListItem(opt); 11 | } 12 | this.init(opt); 13 | }; 14 | 15 | ListItem.prototype = Object.create(RenderLayer.prototype); 16 | ListItem.prototype.constructor = RenderLayer; 17 | 18 | 19 | $.extend(ListItem.prototype,{ 20 | init:function(opt){ 21 | RenderLayer.prototype.init.apply(this,arguments); 22 | }, 23 | isOutOfView:function(){ 24 | var parent = this.parent; 25 | return this.drawTop + this.drawHeight < parent.scrollTop || this.drawTop > parent.scrollTop + parent.drawHeight; 26 | }, 27 | update:function(){ 28 | //不在可视范围内的不update 29 | if(this.isOutOfView()){ 30 | return; 31 | } 32 | RenderLayer.prototype.update.apply(this,arguments); 33 | }, 34 | draw:function(){ 35 | 36 | //不在可视范围内的不绘制 37 | if(this.isOutOfView()){ 38 | return; 39 | } 40 | 41 | RenderLayer.prototype.draw.apply(this,arguments); 42 | } 43 | }); 44 | 45 | CL.ListItem = ListItem; 46 | 47 | })(window.CanvasList = window.CanvasList || {}); -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var List = CanvasList.List; 3 | var ListItem = CanvasList.ListItem; 4 | var CanvasImage = CanvasList.CanvasImage; 5 | var CanvasText = CanvasList.CanvasText; 6 | var RenderLayer = CanvasList.RenderLayer; 7 | 8 | var canvasElement = $('.canvas-list'); 9 | 10 | var isLoading = false; 11 | 12 | //样式类集合 13 | var classMap = { 14 | 'active':{ 15 | backgroundColor:'blue' 16 | }, 17 | 'list':{ 18 | width:600, 19 | height:900, 20 | backgroundColor:'gray' 21 | }, 22 | 'list-item':{ 23 | padding:10, 24 | opacity:1, 25 | backgroundColor:'rgb(153, 198, 184)' 26 | }, 27 | 'info':{ 28 | marginLeft:80 29 | }, 30 | 'avatar':{ 31 | // left:10, 32 | // top:10, 33 | position:'absolute', 34 | width:56, 35 | height:56, 36 | borderRadius:28 37 | }, 38 | 'title':{ 39 | fontSize:28, 40 | lineHeight:28, 41 | width:280, 42 | height:40, 43 | backgroundColor:'green', 44 | color:'blue' 45 | }, 46 | 'brief':{ 47 | fontSize:24, 48 | width:100, 49 | height:40, 50 | color:'gray', 51 | backgroundColor:'red' 52 | }, 53 | 'btn':{ 54 | position:'absolute', 55 | right:4, 56 | top:20, 57 | fontSize:24, 58 | width:80, 59 | height:40, 60 | textAlign:'center', 61 | lineHeight:40, 62 | borderRadius:12, 63 | borderColor:'green' 64 | }, 65 | 'hide':{ 66 | position:'absolute', 67 | top:-9999999 68 | }, 69 | 'loading':{ 70 | height:60, 71 | textAlign:'center', 72 | fontSize:32, 73 | top:10 74 | } 75 | }; 76 | 77 | var loadingText; 78 | 79 | var list = new List({ 80 | canvasElement : canvasElement, 81 | className:'list', 82 | activeClassName:'active', 83 | classMap:classMap 84 | }); 85 | 86 | // list.addEventListener('touchstart',function(e){ 87 | // e.target.setStyle({ 88 | // backgroundColor:'blue' 89 | // }); 90 | // }); 91 | 92 | function processData(i,data,pageIndex){ 93 | 94 | var listItem = ListItem({ 95 | className:'list-item', 96 | useCache:true 97 | }); 98 | 99 | 100 | listItem.index = i; 101 | 102 | var container = RenderLayer({ 103 | className:'info' 104 | }); 105 | 106 | var btn = CanvasText({ 107 | className:'btn', 108 | content:'关注' 109 | 110 | }); 111 | 112 | btn.addEventListener('click',function(e){ 113 | e.stopPropagation(); 114 | alert('关注'); 115 | }); 116 | 117 | container.addChildren([ 118 | CanvasText({ 119 | className:'title', 120 | content:data.title 121 | }), 122 | CanvasText({ 123 | className:'brief', 124 | content:data.brief 125 | }), 126 | btn 127 | ]); 128 | 129 | listItem.addChildren([ 130 | CanvasImage({ 131 | className:'avatar', 132 | url:data.url 133 | }), 134 | container 135 | ]); 136 | 137 | 138 | 139 | list.insertBefore(listItem,loadingText); 140 | 141 | }; 142 | 143 | 144 | loadingText = CanvasText({ 145 | content:'加载中...', 146 | className:'loading' 147 | }); 148 | 149 | //加载中 150 | list.addChildren(loadingText); 151 | 152 | 153 | //增加子元素 154 | $.each(cgiData,function(i,data){ 155 | processData(i,data,0); 156 | }); 157 | 158 | var pageIndex = 0; 159 | var isEnd = false; 160 | 161 | 162 | //滚到到一半触发加载 163 | list.onScrollToHalf = function(){ 164 | if(isLoading || isEnd) return; 165 | 166 | isLoading = true; 167 | 168 | setTimeout(function(){ 169 | 170 | if(pageIndex == 3){ 171 | isEnd = true; 172 | loadingText.content = '已显示全部内容'; 173 | } 174 | 175 | 176 | $.each(cgiData,function(i,data){ 177 | processData(i,data,1); 178 | }); 179 | 180 | pageIndex ++; 181 | 182 | isLoading = false; 183 | 184 | },2000); 185 | 186 | }; 187 | 188 | 189 | list.run(); 190 | 191 | $('.change_size_btn').on('click',function(){ 192 | $(list.children).each(function(i,l){ 193 | l.setStyle({ 194 | 'height':400 195 | }); 196 | }); 197 | }); 198 | 199 | 200 | 201 | 202 | })(); -------------------------------------------------------------------------------- /render_layer.js: -------------------------------------------------------------------------------- 1 | (function(CL){ 2 | 3 | var CanvasUtil = CL.CanvasUtil; 4 | var CacheCanvasPool = CL.CacheCanvasPool; 5 | var devicePixelRatio = window.devicePixelRatio; 6 | 7 | //通用渲染层 8 | function RenderLayer(opt){ 9 | if(!(this instanceof RenderLayer)){ 10 | return new RenderLayer(opt); 11 | } 12 | this.init(opt); 13 | }; 14 | 15 | //布局相关样式列表 16 | var __layoutStyleList = ['left','top','width','height','minWidth','minHeight','maxWidth','maxHeight' 17 | ,'margin','marginLeft','marginRight','marginTop','marginBottom' 18 | ,'padding','paddingLeft','paddingRight','paddingTop','paddingBottom' 19 | ,'borderWidth','borderLeftWidth','borderRightWidth','borderTopWidth','borderBottomWidth','position' 20 | ,'flexDirection','justifyContent','alignItems','alignSelf','flex','flexWrap']; 21 | //绘制相关样式列表 22 | var __drawStyleList = ['borderColor','borderStyle','borderRadius','zIndex','backgroundColor','opacity','overflow']; 23 | //基础样式名集合 24 | var __styleList = __layoutStyleList.concat(__drawStyleList); 25 | 26 | 27 | 28 | RenderLayer.prototype = { 29 | init:function(opt){ 30 | //设置样式值 31 | var self = this; 32 | var style = this.style = this.style || {}; 33 | var optStyle = opt.style || {}; 34 | 35 | //尺寸 36 | $.each(__styleList,function(i,styleName){ 37 | 38 | var styleValue = optStyle[styleName]; 39 | 40 | if(typeof styleValue != 'undefined'){ 41 | style[styleName] = styleValue; 42 | } 43 | }); 44 | 45 | 46 | //最终计算的样式 47 | this.finalStyle = {}; 48 | this.classStyle = {}; 49 | this.id = opt.id || CanvasUtil.getRandomId(); 50 | this.useCache = opt.useCache; 51 | this.children = opt.children || []; 52 | this.parent = opt.parent; 53 | //首次更新标志 54 | this.firstUpdate = true; 55 | 56 | 57 | this.ctx = opt.ctx; 58 | 59 | 60 | //设置样式类 61 | if(opt.className){ 62 | this.className = opt.className; 63 | } 64 | else{ 65 | this.className = ''; 66 | } 67 | }, 68 | removeCache:function(){ 69 | CacheCanvasPool.remove(this.id); 70 | }, 71 | //寻找上层的可被缓存的层 72 | updateParentCacheableLayer:function(){ 73 | var parent = this; 74 | while(parent){ 75 | if(parent.useCache){ 76 | //删除缓存,方便下次重新绘制 77 | parent.removeCache(); 78 | } 79 | parent = parent.parent; 80 | } 81 | }, 82 | 83 | //某点是否在该元素内 84 | hitPoint:function(point){ 85 | var position = this.getDrawPositionInCanvas(); 86 | if(point.left >= position.left && point.left <= position.left + this.drawWidth){ 87 | if(point.top >= position.top && point.top <= position.top + this.drawHeight){ 88 | return true; 89 | } 90 | } 91 | return false; 92 | }, 93 | //添加事件监听 94 | addEventListener:function(type,handler){ 95 | if(!this.handlers){ 96 | this.handlers = {}; 97 | } 98 | if(!this.handlers[type]){ 99 | this.handlers[type] = []; 100 | } 101 | 102 | this.handlers[type].push(handler); 103 | 104 | }, 105 | //删除事件处理程序 106 | removeEventListener:function(type,handler){ 107 | if(!type){ 108 | this.handlers = {}; 109 | this.stopPropagationMap = {}; 110 | } 111 | else if(!handler){ 112 | this.handlers && (this.handlers[type] = []); 113 | this.stopPropagationMap && (this.stopPropagationMap[type] = null); 114 | } 115 | else if(this.handlers && this.handlers[type]){ 116 | this.handlers[type] = this.handlers[type].filter(function(i,h){ 117 | return h !== handler; 118 | }); 119 | } 120 | }, 121 | //禁止事件冒泡 122 | setStopEventPropagation:function(type){ 123 | if(!this.stopPropagationMap){ 124 | this.stopPropagationMap = {}; 125 | } 126 | this.stopPropagationMap[type] = true; 127 | }, 128 | //获取是否允许事件冒泡 129 | getStopEventPropagation:function(type){ 130 | if(this.stopPropagationMap){ 131 | return this.stopPropagationMap[type]; 132 | } 133 | }, 134 | //增加一个子对象 135 | addChildren:function(children,index){ 136 | var self = this; 137 | 138 | //增加子节点的同时 139 | var root = this.getRoot(); 140 | 141 | if(!$.isArray(children)){ 142 | children = [children]; 143 | } 144 | //添加父对象引用 145 | $.each(children,function(i,child){ 146 | child.parent = self; 147 | //从某个index开始插入元素 148 | if(index != null){ 149 | self.children.splice(index,0,child); 150 | } 151 | if(root){ 152 | //初始化新添加节点的样式 153 | child.initializeStyle(); 154 | } 155 | }); 156 | 157 | //从最后插入元素 158 | if(index == null){ 159 | this.children = this.children.concat(children); 160 | } 161 | 162 | if(root){ 163 | root.layoutChanged = true; 164 | 165 | //避免root在不适合的时机就initializeStyle 166 | if(!root.firstRun){ 167 | root.run(); 168 | } 169 | } 170 | 171 | 172 | 173 | }, 174 | //按zIndex排序prop 175 | sortChildren:function(){ 176 | 177 | // this.children = this.children.sort(function(c1,c2){ 178 | // return (c1.finalStyle.zIndex || 0) > (c2.finalStyle.zIndex || 0); 179 | // }); 180 | }, 181 | //是否存在改变layout的样式 182 | detectLayoutAndStyleValueChanged:function(preStyleObj,styleObj){ 183 | var layoutChanged; 184 | var styleValueChanged; 185 | $.each(styleObj,function(name,styleValue){ 186 | //更新和检测style的变更 187 | if(preStyleObj[name] != styleValue){ 188 | styleValueChanged = true; 189 | preStyleObj[name] = styleValue; 190 | } 191 | //改变的触发layout的属性 192 | if(__layoutStyleList.indexOf(name) > -1){ 193 | //todo:设置一样的样式值,不重新改变layout 194 | layoutChanged = true; 195 | } 196 | }); 197 | 198 | return { 199 | layoutChanged:layoutChanged, 200 | styleValueChanged:styleValueChanged 201 | }; 202 | }, 203 | //设置样式 204 | setStyle:function(styleObj,needRefreshCache){ 205 | if(needRefreshCache == null){ 206 | needRefreshCache = true; 207 | } 208 | //改变的对象检测 209 | var changedObj = this.detectLayoutAndStyleValueChanged(this.style,styleObj); 210 | 211 | var styleChanged = changedObj.styleValueChanged; 212 | var layoutChanged = changedObj.layoutChanged; 213 | 214 | if(styleChanged){ 215 | var root = this.getRoot(); 216 | //存在layout更新,让root重新计算layout 217 | if(root && layoutChanged){ 218 | root.layoutChanged = true; 219 | } 220 | //计算最终样式 221 | this.calculateFinalStyle(); 222 | 223 | //更新绘制缓存 224 | if(needRefreshCache){ 225 | this.updateParentCacheableLayer(); 226 | } 227 | 228 | root.run(); 229 | } 230 | }, 231 | //把样式准备好给第一次绘制 232 | initializeStyle:function(){ 233 | this.applyClassStyle(); 234 | //计算最终样式 235 | this.calculateFinalStyle(); 236 | //TODO:这里会导致重复计算,后面看看能否优化 237 | $.each(this.children,function(i,child){ 238 | child.initializeStyle(); 239 | }); 240 | }, 241 | //增加样式类 242 | addClass:function(className,needRefreshCache){ 243 | var layoutChanged; 244 | if(needRefreshCache == null){ 245 | needRefreshCache = true; 246 | } 247 | 248 | if(this.className == null) this.className = ''; 249 | 250 | if (!className || this.hasClass(className)){ 251 | return; 252 | } 253 | this.className += ' '+ className; 254 | 255 | var root = this.getRoot(); 256 | var rootClassMap = root.classMap; 257 | 258 | if(rootClassMap){ 259 | //改变的对象检测 260 | var changedObj = this.detectLayoutAndStyleValueChanged(this.classStyle,rootClassMap[className]); 261 | var classChanged = changedObj.styleValueChanged; 262 | var layoutChanged = changedObj.layoutChanged; 263 | 264 | if(classChanged){ 265 | //this.applyClassStyle(); 266 | //存在layout更新,让root重新计算layout 267 | if(layoutChanged){ 268 | root.layoutChanged = true; 269 | } 270 | 271 | //计算最终样式 272 | this.calculateFinalStyle(); 273 | //更新绘制缓存 274 | if(needRefreshCache){ 275 | this.updateParentCacheableLayer(); 276 | } 277 | 278 | root.run(); 279 | } 280 | } 281 | }, 282 | 283 | //删除样式类 284 | removeClass:function(className,needRefreshCache){ 285 | if (!className || !this.hasClass(className)) { 286 | return; 287 | } 288 | 289 | if(this.className == null) this.className = ''; 290 | 291 | if(needRefreshCache == null){ 292 | needRefreshCache = true; 293 | } 294 | 295 | this.className = this.className.replace(new RegExp('(?:^|\\s)' + className + '(?:\\s|$)'), ' '); 296 | //todo:删除class后样式不变不让classChanged为true 297 | this.applyClassStyle(); 298 | //计算最终样式 299 | this.calculateFinalStyle(); 300 | //更新绘制缓存 301 | if(needRefreshCache){ 302 | this.updateParentCacheableLayer(); 303 | } 304 | var root = this.getRoot(); 305 | root.run(); 306 | }, 307 | //是否有某个样式类 308 | hasClass:function(className){ 309 | if (!className) { 310 | return false; 311 | } 312 | return -1 < (' ' + this.className + ' ').indexOf(' ' + className + ' '); 313 | }, 314 | //寻找最近的有指定样式类的层 315 | closestClass:function(className){ 316 | var parent = this; 317 | 318 | while(parent && !parent.hasClass(className)){ 319 | parent = parent.parent; 320 | } 321 | 322 | return parent; 323 | }, 324 | //显示 325 | show:function(){ 326 | 327 | }, 328 | //隐藏 329 | hide:function(){ 330 | 331 | }, 332 | //获取某个子元素的索引 333 | getChildIndex:function(child){ 334 | for(var i = 0; i < this.children.length; i ++){ 335 | if(this.children[i] == child){ 336 | return i; 337 | } 338 | } 339 | }, 340 | //插入到某个元素前面 341 | insertBefore:function(layer,relativeLayer){ 342 | var index = this.getChildIndex(relativeLayer); 343 | this.addChildren(layer,index); 344 | }, 345 | //获取相对于canvas的绘制位置 346 | getDrawPositionInCanvas:function(){ 347 | var parent = this.parent; 348 | 349 | var left = this.drawLeft; 350 | var top = this.drawTop; 351 | var scrollTop = 0; 352 | 353 | while(parent){ 354 | left = left + parent.drawLeft; 355 | top = top + parent.drawTop; 356 | 357 | if(parent.scrollTop){ 358 | scrollTop = parent.scrollTop; 359 | } 360 | 361 | parent = parent.parent; 362 | 363 | 364 | } 365 | 366 | return { 367 | left:left, 368 | top:top - scrollTop 369 | }; 370 | 371 | }, 372 | getRoot:function(){ 373 | 374 | if(this.root) return this.root; 375 | 376 | var root; 377 | var parent = this.parent; 378 | 379 | while(parent){ 380 | root = parent; 381 | parent = parent.parent; 382 | } 383 | 384 | this.root = root; 385 | 386 | return this.root; 387 | }, 388 | //绘制可被子元素继承的基本属性 389 | drawInheritable:function(ctx){ 390 | var finalStyle = this.finalStyle; 391 | var opacity; 392 | if(finalStyle.opacity && finalStyle.opacity < 1){ 393 | //叠加父元素的透明属性 394 | if(ctx.globalAlpha){ 395 | opacity = finalStyle.opacity * ctx.globalAlpha; 396 | } 397 | ctx.globalAlpha = opacity; 398 | } 399 | 400 | //方便子元素相对于父元素位置进行绘制 401 | if(this.drawLeft || this.drawTop){ 402 | ctx.translate(Math.round(this.drawLeft), Math.round(this.drawTop)); 403 | } 404 | 405 | }, 406 | //绘制不可被继承的基本属性 407 | drawBase:function(ctx){ 408 | // var x = this.x; 409 | // var y = this.y; 410 | var finalStyle = this.finalStyle; 411 | var width = this.drawWidth; 412 | var height = this.drawHeight; 413 | var borderRadius = finalStyle.borderRadius; 414 | 415 | //绘制背景颜色 416 | if (finalStyle.backgroundColor) { 417 | ctx.fillStyle = finalStyle.backgroundColor; 418 | 419 | //以圆角边框轮廓绘制背景颜色 420 | if (finalStyle.borderRadius) { 421 | ctx.fill(); 422 | } 423 | //没有圆角边框,绘制矩形背景颜色 424 | else { 425 | ctx.fillRect(0, 0, Math.round(width), Math.round(height)); 426 | } 427 | } 428 | //圆角轮廓 429 | if (borderRadius) { 430 | 431 | CanvasUtil.setBorderRadiusContext(ctx,0,0,Math.round(width),Math.round(height),borderRadius); 432 | 433 | //有圆角并且有边框颜色 434 | if (finalStyle.borderColor) { 435 | ctx.lineWidth = finalStyle.borderWidth || 1; 436 | ctx.strokeStyle = finalStyle.borderColor; 437 | ctx.stroke(); 438 | } 439 | 440 | } 441 | 442 | //只有边框颜色,没有边框圆角,则绘制矩形边框 443 | else if (finalStyle.borderColor) { 444 | var halfLineWidth; 445 | ctx.strokeStyle = finalStyle.borderColor; 446 | 447 | //完整边框 448 | if(finalStyle.borderWidth){ 449 | ctx.lineWidth = finalStyle.borderWidth; 450 | //由于边框以中心为原点scale,所以这里位置要计算一下 451 | halfLineWidth = ctx.lineWidth / 2; 452 | 453 | ctx.strokeRect(halfLineWidth,halfLineWidth, Math.round(width - ctx.lineWidth), Math.round(height - halfLineWidth)); 454 | } 455 | else{ 456 | if(finalStyle.borderBottomWidth){ 457 | ctx.lineWidth = finalStyle.borderBottomWidth; 458 | halfLineWidth = ctx.lineWidth / 2; 459 | CanvasUtil.setSingleBorderContext(ctx,0,0,Math.round(width),Math.round(height),halfLineWidth,'bottom'); 460 | ctx.stroke(); 461 | } 462 | if(finalStyle.borderLeftWidth){ 463 | ctx.lineWidth = finalStyle.borderLeftWidth; 464 | halfLineWidth = ctx.lineWidth / 2; 465 | CanvasUtil.setSingleBorderContext(ctx,0,0,Math.round(width),Math.round(height),halfLineWidth,'left'); 466 | ctx.stroke(); 467 | } 468 | if(finalStyle.borderRightWidth){ 469 | ctx.lineWidth = finalStyle.borderRightWidth; 470 | halfLineWidth = ctx.lineWidth / 2; 471 | CanvasUtil.setSingleBorderContext(ctx,0,0,Math.round(width),Math.round(height),halfLineWidth,'right'); 472 | ctx.stroke(); 473 | } 474 | if(finalStyle.borderTopWidth){ 475 | ctx.lineWidth = finalStyle.borderTopWidth; 476 | halfLineWidth = ctx.lineWidth / 2; 477 | CanvasUtil.setSingleBorderContext(ctx,0,0,Math.round(width),Math.round(height),halfLineWidth,'top'); 478 | ctx.stroke(); 479 | } 480 | } 481 | 482 | } 483 | 484 | }, 485 | //应用类的样式 486 | applyClassStyle:function(){ 487 | var root = this.getRoot(); 488 | var classNameList = this.className.trim().split(' '); 489 | var rootClassMap = root.classMap; 490 | var classStyle = {}; 491 | 492 | $.each(classNameList,function(i,className){ 493 | 494 | if(rootClassMap[className]){ 495 | $.extend(classStyle,rootClassMap[className]); 496 | } 497 | }); 498 | 499 | this.classStyle = classStyle; 500 | 501 | }, 502 | //计算最终的样式 503 | calculateFinalStyle:function(){ 504 | this.finalStyle = $.extend({},this.classStyle,this.style); 505 | }, 506 | //待子类实现 507 | draw:function(ctx){ 508 | 509 | if(!this.ctx){ 510 | this.ctx = this.getRoot().ctx; 511 | } 512 | 513 | ctx = ctx || this.ctx; 514 | 515 | var cache; 516 | var isScrollingDown = CL.isScrollingDown; 517 | 518 | //是否使用缓存canvas绘制 519 | if(this.useCache){ 520 | cache = CacheCanvasPool.get(this); 521 | 522 | if(!cache){ 523 | //新建的缓存canvas 524 | cache = CacheCanvasPool.add(this,isScrollingDown); 525 | var cacheCtx = cache.context; 526 | cacheCtx.save(); 527 | cacheCtx.scale(devicePixelRatio,devicePixelRatio); 528 | //等下绘制的时候会首先产生该层的偏移,由于缓存canvas需要在原点开始绘制,所以这里预先移位一下 529 | cacheCtx.translate(-Math.round(this.drawLeft),-Math.round(this.drawTop)); 530 | this.drawDetail(cacheCtx); 531 | 532 | cacheCtx.restore(); 533 | 534 | console.log('new cache'); 535 | } 536 | 537 | 538 | 539 | //使用缓存canvas绘制 540 | ctx.save(); 541 | ctx.translate(Math.round(this.drawLeft),Math.round(this.drawTop)); 542 | ctx.drawImage(cache.canvas[0],0,0,cache.canvas[0].width,cache.canvas[0].height,0,0,Math.round(this.drawWidth),Math.round(this.drawHeight)); 543 | //ctx.drawImage(cache.canvas[0],0,0,1,1,0,0,1,1); 544 | ctx.restore(); 545 | } 546 | else{ 547 | this.drawDetail(ctx); 548 | } 549 | }, 550 | //绘制具体内容 551 | drawDetail:function(ctx){ 552 | ctx.save(); 553 | 554 | //绘制可被子元素继承的基本属性,这里设置的属性会被保留到到子元素的ctx 555 | this.drawInheritable(ctx); 556 | 557 | //绘制不可被继承的基本属性 558 | ctx.save(); 559 | this.drawBase(ctx); 560 | ctx.restore(); 561 | //绘制元素自身=独有的基本属性(不可被继承) 562 | ctx.save(); 563 | this.drawSelf && this.drawSelf(ctx); 564 | ctx.restore(); 565 | 566 | //绘制子元素前出发的事件 567 | this.onBeforeDrawChildren && this.onBeforeDrawChildren(); 568 | //overflow:hidden;的实现 569 | if(this.finalStyle.overflow == 'hidden'){ 570 | ctx.save(); 571 | ctx.rect(0,0,this.drawWidth,this.drawHeight); 572 | ctx.clip(); 573 | //先绘制自己再绘制子对象 574 | this.drawChildren(ctx); 575 | ctx.restore(); 576 | } 577 | else{ 578 | //先绘制自己再绘制子对象 579 | this.drawChildren(ctx); 580 | } 581 | 582 | //绘制子元素前出发的事件 583 | this.onAfterDrawChildren && this.onAfterDrawChildren(); 584 | 585 | ctx.restore(); 586 | 587 | }, 588 | drawChildren:function(ctx){ 589 | 590 | $.each(this.children,function(i,child){ 591 | child.draw(ctx); 592 | }); 593 | 594 | } 595 | 596 | }; 597 | 598 | CL.RenderLayer = RenderLayer; 599 | 600 | })(window.CanvasList = window.CanvasList || {}); -------------------------------------------------------------------------------- /scroller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Scroller 3 | * http://github.com/zynga/scroller 4 | * 5 | * Copyright 2011, Zynga Inc. 6 | * Licensed under the MIT License. 7 | * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt 8 | * 9 | * Based on the work of: Unify Project (unify-project.org) 10 | * http://unify-project.org 11 | * Copyright 2011, Deutsche Telekom AG 12 | * License: MIT + Apache (V2) 13 | */ 14 | 15 | var Scroller; 16 | 17 | (function() { 18 | var NOOP = function(){}; 19 | 20 | /** 21 | * A pure logic 'component' for 'virtual' scrolling/zooming. 22 | */ 23 | Scroller = function(callback, options) { 24 | 25 | this.__callback = callback; 26 | 27 | this.options = { 28 | 29 | /** Enable scrolling on x-axis */ 30 | scrollingX: true, 31 | 32 | /** Enable scrolling on y-axis */ 33 | scrollingY: true, 34 | 35 | /** Enable animations for deceleration, snap back, zooming and scrolling */ 36 | animating: true, 37 | 38 | /** duration for animations triggered by scrollTo/zoomTo */ 39 | animationDuration: 250, 40 | 41 | /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */ 42 | bouncing: true, 43 | 44 | /** Enable locking to the main axis if user moves only slightly on one of them at start */ 45 | locking: true, 46 | 47 | /** Enable pagination mode (switching between full page content panes) */ 48 | paging: false, 49 | 50 | /** Enable snapping of content to a configured pixel grid */ 51 | snapping: false, 52 | 53 | /** Enable zooming of content via API, fingers and mouse wheel */ 54 | zooming: false, 55 | 56 | /** Minimum zoom level */ 57 | minZoom: 0.5, 58 | 59 | /** Maximum zoom level */ 60 | maxZoom: 3, 61 | 62 | /** Multiply or decrease scrolling speed **/ 63 | speedMultiplier: 1, 64 | 65 | /** Callback that is fired on the later of touch end or deceleration end, 66 | provided that another scrolling action has not begun. Used to know 67 | when to fade out a scrollbar. */ 68 | scrollingComplete: NOOP, 69 | 70 | /** This configures the amount of change applied to deceleration when reaching boundaries **/ 71 | penetrationDeceleration : 0.03, 72 | 73 | /** This configures the amount of change applied to acceleration when reaching boundaries **/ 74 | penetrationAcceleration : 0.08 75 | 76 | }; 77 | 78 | for (var key in options) { 79 | this.options[key] = options[key]; 80 | } 81 | 82 | }; 83 | 84 | 85 | // Easing Equations (c) 2003 Robert Penner, all rights reserved. 86 | // Open source under the BSD License. 87 | 88 | /** 89 | * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) 90 | **/ 91 | var easeOutCubic = function(pos) { 92 | return (Math.pow((pos - 1), 3) + 1); 93 | }; 94 | 95 | /** 96 | * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) 97 | **/ 98 | var easeInOutCubic = function(pos) { 99 | if ((pos /= 0.5) < 1) { 100 | return 0.5 * Math.pow(pos, 3); 101 | } 102 | 103 | return 0.5 * (Math.pow((pos - 2), 3) + 2); 104 | }; 105 | 106 | 107 | var members = { 108 | 109 | /* 110 | --------------------------------------------------------------------------- 111 | INTERNAL FIELDS :: STATUS 112 | --------------------------------------------------------------------------- 113 | */ 114 | 115 | /** {Boolean} Whether only a single finger is used in touch handling */ 116 | __isSingleTouch: false, 117 | 118 | /** {Boolean} Whether a touch event sequence is in progress */ 119 | __isTracking: false, 120 | 121 | /** {Boolean} Whether a deceleration animation went to completion. */ 122 | __didDecelerationComplete: false, 123 | 124 | /** 125 | * {Boolean} Whether a gesture zoom/rotate event is in progress. Activates when 126 | * a gesturestart event happens. This has higher priority than dragging. 127 | */ 128 | __isGesturing: false, 129 | 130 | /** 131 | * {Boolean} Whether the user has moved by such a distance that we have enabled 132 | * dragging mode. Hint: It's only enabled after some pixels of movement to 133 | * not interrupt with clicks etc. 134 | */ 135 | __isDragging: false, 136 | 137 | /** 138 | * {Boolean} Not touching and dragging anymore, and smoothly animating the 139 | * touch sequence using deceleration. 140 | */ 141 | __isDecelerating: false, 142 | 143 | /** 144 | * {Boolean} Smoothly animating the currently configured change 145 | */ 146 | __isAnimating: false, 147 | 148 | 149 | 150 | /* 151 | --------------------------------------------------------------------------- 152 | INTERNAL FIELDS :: DIMENSIONS 153 | --------------------------------------------------------------------------- 154 | */ 155 | 156 | /** {Integer} Available outer left position (from document perspective) */ 157 | __clientLeft: 0, 158 | 159 | /** {Integer} Available outer top position (from document perspective) */ 160 | __clientTop: 0, 161 | 162 | /** {Integer} Available outer width */ 163 | __clientWidth: 0, 164 | 165 | /** {Integer} Available outer height */ 166 | __clientHeight: 0, 167 | 168 | /** {Integer} Outer width of content */ 169 | __contentWidth: 0, 170 | 171 | /** {Integer} Outer height of content */ 172 | __contentHeight: 0, 173 | 174 | /** {Integer} Snapping width for content */ 175 | __snapWidth: 100, 176 | 177 | /** {Integer} Snapping height for content */ 178 | __snapHeight: 100, 179 | 180 | /** {Integer} Height to assign to refresh area */ 181 | __refreshHeight: null, 182 | 183 | /** {Boolean} Whether the refresh process is enabled when the event is released now */ 184 | __refreshActive: false, 185 | 186 | /** {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */ 187 | __refreshActivate: null, 188 | 189 | /** {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */ 190 | __refreshDeactivate: null, 191 | 192 | /** {Function} Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */ 193 | __refreshStart: null, 194 | 195 | /** {Number} Zoom level */ 196 | __zoomLevel: 1, 197 | 198 | /** {Number} Scroll position on x-axis */ 199 | __scrollLeft: 0, 200 | 201 | /** {Number} Scroll position on y-axis */ 202 | __scrollTop: 0, 203 | 204 | /** {Integer} Maximum allowed scroll position on x-axis */ 205 | __maxScrollLeft: 0, 206 | 207 | /** {Integer} Maximum allowed scroll position on y-axis */ 208 | __maxScrollTop: 0, 209 | 210 | /* {Number} Scheduled left position (final position when animating) */ 211 | __scheduledLeft: 0, 212 | 213 | /* {Number} Scheduled top position (final position when animating) */ 214 | __scheduledTop: 0, 215 | 216 | /* {Number} Scheduled zoom level (final scale when animating) */ 217 | __scheduledZoom: 0, 218 | 219 | 220 | 221 | /* 222 | --------------------------------------------------------------------------- 223 | INTERNAL FIELDS :: LAST POSITIONS 224 | --------------------------------------------------------------------------- 225 | */ 226 | 227 | /** {Number} Left position of finger at start */ 228 | __lastTouchLeft: null, 229 | 230 | /** {Number} Top position of finger at start */ 231 | __lastTouchTop: null, 232 | 233 | /** {Date} Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */ 234 | __lastTouchMove: null, 235 | 236 | /** {Array} List of positions, uses three indexes for each state: left, top, timestamp */ 237 | __positions: null, 238 | 239 | 240 | 241 | /* 242 | --------------------------------------------------------------------------- 243 | INTERNAL FIELDS :: DECELERATION SUPPORT 244 | --------------------------------------------------------------------------- 245 | */ 246 | 247 | /** {Integer} Minimum left scroll position during deceleration */ 248 | __minDecelerationScrollLeft: null, 249 | 250 | /** {Integer} Minimum top scroll position during deceleration */ 251 | __minDecelerationScrollTop: null, 252 | 253 | /** {Integer} Maximum left scroll position during deceleration */ 254 | __maxDecelerationScrollLeft: null, 255 | 256 | /** {Integer} Maximum top scroll position during deceleration */ 257 | __maxDecelerationScrollTop: null, 258 | 259 | /** {Number} Current factor to modify horizontal scroll position with on every step */ 260 | __decelerationVelocityX: null, 261 | 262 | /** {Number} Current factor to modify vertical scroll position with on every step */ 263 | __decelerationVelocityY: null, 264 | 265 | 266 | 267 | /* 268 | --------------------------------------------------------------------------- 269 | PUBLIC API 270 | --------------------------------------------------------------------------- 271 | */ 272 | 273 | /** 274 | * Configures the dimensions of the client (outer) and content (inner) elements. 275 | * Requires the available space for the outer element and the outer size of the inner element. 276 | * All values which are falsy (null or zero etc.) are ignored and the old value is kept. 277 | * 278 | * @param clientWidth {Integer ? null} Inner width of outer element 279 | * @param clientHeight {Integer ? null} Inner height of outer element 280 | * @param contentWidth {Integer ? null} Outer width of inner element 281 | * @param contentHeight {Integer ? null} Outer height of inner element 282 | */ 283 | setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight) { 284 | 285 | var self = this; 286 | 287 | // Only update values which are defined 288 | if (clientWidth === +clientWidth) { 289 | self.__clientWidth = clientWidth; 290 | } 291 | 292 | if (clientHeight === +clientHeight) { 293 | self.__clientHeight = clientHeight; 294 | } 295 | 296 | if (contentWidth === +contentWidth) { 297 | self.__contentWidth = contentWidth; 298 | } 299 | 300 | if (contentHeight === +contentHeight) { 301 | self.__contentHeight = contentHeight; 302 | } 303 | 304 | // Refresh maximums 305 | self.__computeScrollMax(); 306 | 307 | // Refresh scroll position 308 | self.scrollTo(self.__scrollLeft, self.__scrollTop, true); 309 | 310 | }, 311 | 312 | 313 | /** 314 | * Sets the client coordinates in relation to the document. 315 | * 316 | * @param left {Integer ? 0} Left position of outer element 317 | * @param top {Integer ? 0} Top position of outer element 318 | */ 319 | setPosition: function(left, top) { 320 | 321 | var self = this; 322 | 323 | self.__clientLeft = left || 0; 324 | self.__clientTop = top || 0; 325 | 326 | }, 327 | 328 | 329 | /** 330 | * Configures the snapping (when snapping is active) 331 | * 332 | * @param width {Integer} Snapping width 333 | * @param height {Integer} Snapping height 334 | */ 335 | setSnapSize: function(width, height) { 336 | 337 | var self = this; 338 | 339 | self.__snapWidth = width; 340 | self.__snapHeight = height; 341 | 342 | }, 343 | 344 | 345 | /** 346 | * Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever 347 | * the user event is released during visibility of this zone. This was introduced by some apps on iOS like 348 | * the official Twitter client. 349 | * 350 | * @param height {Integer} Height of pull-to-refresh zone on top of rendered list 351 | * @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release. 352 | * @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled. 353 | * @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh. 354 | */ 355 | activatePullToRefresh: function(height, activateCallback, deactivateCallback, startCallback) { 356 | 357 | var self = this; 358 | 359 | self.__refreshHeight = height; 360 | self.__refreshActivate = activateCallback; 361 | self.__refreshDeactivate = deactivateCallback; 362 | self.__refreshStart = startCallback; 363 | 364 | }, 365 | 366 | 367 | /** 368 | * Starts pull-to-refresh manually. 369 | */ 370 | triggerPullToRefresh: function() { 371 | // Use publish instead of scrollTo to allow scrolling to out of boundary position 372 | // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled 373 | this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true); 374 | 375 | if (this.__refreshStart) { 376 | this.__refreshStart(); 377 | } 378 | }, 379 | 380 | 381 | /** 382 | * Signalizes that pull-to-refresh is finished. 383 | */ 384 | finishPullToRefresh: function() { 385 | 386 | var self = this; 387 | 388 | self.__refreshActive = false; 389 | if (self.__refreshDeactivate) { 390 | self.__refreshDeactivate(); 391 | } 392 | 393 | self.scrollTo(self.__scrollLeft, self.__scrollTop, true); 394 | 395 | }, 396 | 397 | 398 | /** 399 | * Returns the scroll position and zooming values 400 | * 401 | * @return {Map} `left` and `top` scroll position and `zoom` level 402 | */ 403 | getValues: function() { 404 | 405 | var self = this; 406 | 407 | return { 408 | left: self.__scrollLeft, 409 | top: self.__scrollTop, 410 | zoom: self.__zoomLevel 411 | }; 412 | 413 | }, 414 | 415 | 416 | /** 417 | * Returns the maximum scroll values 418 | * 419 | * @return {Map} `left` and `top` maximum scroll values 420 | */ 421 | getScrollMax: function() { 422 | 423 | var self = this; 424 | 425 | return { 426 | left: self.__maxScrollLeft, 427 | top: self.__maxScrollTop 428 | }; 429 | 430 | }, 431 | 432 | 433 | /** 434 | * Zooms to the given level. Supports optional animation. Zooms 435 | * the center when no coordinates are given. 436 | * 437 | * @param level {Number} Level to zoom to 438 | * @param animate {Boolean ? false} Whether to use animation 439 | * @param originLeft {Number ? null} Zoom in at given left coordinate 440 | * @param originTop {Number ? null} Zoom in at given top coordinate 441 | * @param callback {Function ? null} A callback that gets fired when the zoom is complete. 442 | */ 443 | zoomTo: function(level, animate, originLeft, originTop, callback) { 444 | 445 | var self = this; 446 | 447 | if (!self.options.zooming) { 448 | throw new Error("Zooming is not enabled!"); 449 | } 450 | 451 | // Add callback if exists 452 | if(callback) { 453 | self.__zoomComplete = callback; 454 | } 455 | 456 | // Stop deceleration 457 | if (self.__isDecelerating) { 458 | core.effect.Animate.stop(self.__isDecelerating); 459 | self.__isDecelerating = false; 460 | } 461 | 462 | var oldLevel = self.__zoomLevel; 463 | 464 | // Normalize input origin to center of viewport if not defined 465 | if (originLeft == null) { 466 | originLeft = self.__clientWidth / 2; 467 | } 468 | 469 | if (originTop == null) { 470 | originTop = self.__clientHeight / 2; 471 | } 472 | 473 | // Limit level according to configuration 474 | level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom); 475 | 476 | // Recompute maximum values while temporary tweaking maximum scroll ranges 477 | self.__computeScrollMax(level); 478 | 479 | // Recompute left and top coordinates based on new zoom level 480 | var left = ((originLeft + self.__scrollLeft) * level / oldLevel) - originLeft; 481 | var top = ((originTop + self.__scrollTop) * level / oldLevel) - originTop; 482 | 483 | // Limit x-axis 484 | if (left > self.__maxScrollLeft) { 485 | left = self.__maxScrollLeft; 486 | } else if (left < 0) { 487 | left = 0; 488 | } 489 | 490 | // Limit y-axis 491 | if (top > self.__maxScrollTop) { 492 | top = self.__maxScrollTop; 493 | } else if (top < 0) { 494 | top = 0; 495 | } 496 | 497 | // Push values out 498 | self.__publish(left, top, level, animate); 499 | 500 | }, 501 | 502 | 503 | /** 504 | * Zooms the content by the given factor. 505 | * 506 | * @param factor {Number} Zoom by given factor 507 | * @param animate {Boolean ? false} Whether to use animation 508 | * @param originLeft {Number ? 0} Zoom in at given left coordinate 509 | * @param originTop {Number ? 0} Zoom in at given top coordinate 510 | * @param callback {Function ? null} A callback that gets fired when the zoom is complete. 511 | */ 512 | zoomBy: function(factor, animate, originLeft, originTop, callback) { 513 | 514 | var self = this; 515 | 516 | self.zoomTo(self.__zoomLevel * factor, animate, originLeft, originTop, callback); 517 | 518 | }, 519 | 520 | 521 | /** 522 | * Scrolls to the given position. Respect limitations and snapping automatically. 523 | * 524 | * @param left {Number?null} Horizontal scroll position, keeps current if value is null 525 | * @param top {Number?null} Vertical scroll position, keeps current if value is null 526 | * @param animate {Boolean?false} Whether the scrolling should happen using an animation 527 | * @param zoom {Number?null} Zoom level to go to 528 | */ 529 | scrollTo: function(left, top, animate, zoom) { 530 | 531 | var self = this; 532 | 533 | // Stop deceleration 534 | if (self.__isDecelerating) { 535 | core.effect.Animate.stop(self.__isDecelerating); 536 | self.__isDecelerating = false; 537 | } 538 | 539 | // Correct coordinates based on new zoom level 540 | if (zoom != null && zoom !== self.__zoomLevel) { 541 | 542 | if (!self.options.zooming) { 543 | throw new Error("Zooming is not enabled!"); 544 | } 545 | 546 | left *= zoom; 547 | top *= zoom; 548 | 549 | // Recompute maximum values while temporary tweaking maximum scroll ranges 550 | self.__computeScrollMax(zoom); 551 | 552 | } else { 553 | 554 | // Keep zoom when not defined 555 | zoom = self.__zoomLevel; 556 | 557 | } 558 | 559 | if (!self.options.scrollingX) { 560 | 561 | left = self.__scrollLeft; 562 | 563 | } else { 564 | 565 | if (self.options.paging) { 566 | left = Math.round(left / self.__clientWidth) * self.__clientWidth; 567 | } else if (self.options.snapping) { 568 | left = Math.round(left / self.__snapWidth) * self.__snapWidth; 569 | } 570 | 571 | } 572 | 573 | if (!self.options.scrollingY) { 574 | 575 | top = self.__scrollTop; 576 | 577 | } else { 578 | 579 | if (self.options.paging) { 580 | top = Math.round(top / self.__clientHeight) * self.__clientHeight; 581 | } else if (self.options.snapping) { 582 | top = Math.round(top / self.__snapHeight) * self.__snapHeight; 583 | } 584 | 585 | } 586 | 587 | // Limit for allowed ranges 588 | left = Math.max(Math.min(self.__maxScrollLeft, left), 0); 589 | top = Math.max(Math.min(self.__maxScrollTop, top), 0); 590 | 591 | // Don't animate when no change detected, still call publish to make sure 592 | // that rendered position is really in-sync with internal data 593 | if (left === self.__scrollLeft && top === self.__scrollTop) { 594 | animate = false; 595 | } 596 | 597 | // Publish new values 598 | self.__publish(left, top, zoom, animate); 599 | 600 | }, 601 | 602 | 603 | /** 604 | * Scroll by the given offset 605 | * 606 | * @param left {Number ? 0} Scroll x-axis by given offset 607 | * @param top {Number ? 0} Scroll x-axis by given offset 608 | * @param animate {Boolean ? false} Whether to animate the given change 609 | */ 610 | scrollBy: function(left, top, animate) { 611 | 612 | var self = this; 613 | 614 | var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft; 615 | var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop; 616 | 617 | self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate); 618 | 619 | }, 620 | 621 | 622 | 623 | /* 624 | --------------------------------------------------------------------------- 625 | EVENT CALLBACKS 626 | --------------------------------------------------------------------------- 627 | */ 628 | 629 | /** 630 | * Mouse wheel handler for zooming support 631 | */ 632 | doMouseZoom: function(wheelDelta, timeStamp, pageX, pageY) { 633 | 634 | var self = this; 635 | var change = wheelDelta > 0 ? 0.97 : 1.03; 636 | 637 | return self.zoomTo(self.__zoomLevel * change, false, pageX - self.__clientLeft, pageY - self.__clientTop); 638 | 639 | }, 640 | 641 | 642 | /** 643 | * Touch start handler for scrolling support 644 | */ 645 | doTouchStart: function(touches, timeStamp) { 646 | 647 | // Array-like check is enough here 648 | if (touches.length == null) { 649 | throw new Error("Invalid touch list: " + touches); 650 | } 651 | 652 | if (timeStamp instanceof Date) { 653 | timeStamp = timeStamp.valueOf(); 654 | } 655 | if (typeof timeStamp !== "number") { 656 | throw new Error("Invalid timestamp value: " + timeStamp); 657 | } 658 | 659 | var self = this; 660 | 661 | // Reset interruptedAnimation flag 662 | self.__interruptedAnimation = true; 663 | 664 | // Stop deceleration 665 | if (self.__isDecelerating) { 666 | core.effect.Animate.stop(self.__isDecelerating); 667 | self.__isDecelerating = false; 668 | self.__interruptedAnimation = true; 669 | } 670 | 671 | // Stop animation 672 | if (self.__isAnimating) { 673 | core.effect.Animate.stop(self.__isAnimating); 674 | self.__isAnimating = false; 675 | self.__interruptedAnimation = true; 676 | } 677 | 678 | // Use center point when dealing with two fingers 679 | var currentTouchLeft, currentTouchTop; 680 | var isSingleTouch = touches.length === 1; 681 | if (isSingleTouch) { 682 | currentTouchLeft = touches[0].pageX; 683 | currentTouchTop = touches[0].pageY; 684 | } else { 685 | currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2; 686 | currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2; 687 | } 688 | 689 | // Store initial positions 690 | self.__initialTouchLeft = currentTouchLeft; 691 | self.__initialTouchTop = currentTouchTop; 692 | 693 | // Store current zoom level 694 | self.__zoomLevelStart = self.__zoomLevel; 695 | 696 | // Store initial touch positions 697 | self.__lastTouchLeft = currentTouchLeft; 698 | self.__lastTouchTop = currentTouchTop; 699 | 700 | // Store initial move time stamp 701 | self.__lastTouchMove = timeStamp; 702 | 703 | // Reset initial scale 704 | self.__lastScale = 1; 705 | 706 | // Reset locking flags 707 | self.__enableScrollX = !isSingleTouch && self.options.scrollingX; 708 | self.__enableScrollY = !isSingleTouch && self.options.scrollingY; 709 | 710 | // Reset tracking flag 711 | self.__isTracking = true; 712 | 713 | // Reset deceleration complete flag 714 | self.__didDecelerationComplete = false; 715 | 716 | // Dragging starts directly with two fingers, otherwise lazy with an offset 717 | self.__isDragging = !isSingleTouch; 718 | 719 | // Some features are disabled in multi touch scenarios 720 | self.__isSingleTouch = isSingleTouch; 721 | 722 | // Clearing data structure 723 | self.__positions = []; 724 | 725 | }, 726 | 727 | 728 | /** 729 | * Touch move handler for scrolling support 730 | */ 731 | doTouchMove: function(touches, timeStamp, scale) { 732 | 733 | // Array-like check is enough here 734 | if (touches.length == null) { 735 | throw new Error("Invalid touch list: " + touches); 736 | } 737 | 738 | if (timeStamp instanceof Date) { 739 | timeStamp = timeStamp.valueOf(); 740 | } 741 | if (typeof timeStamp !== "number") { 742 | throw new Error("Invalid timestamp value: " + timeStamp); 743 | } 744 | 745 | var self = this; 746 | 747 | // Ignore event when tracking is not enabled (event might be outside of element) 748 | if (!self.__isTracking) { 749 | return; 750 | } 751 | 752 | 753 | var currentTouchLeft, currentTouchTop; 754 | 755 | // Compute move based around of center of fingers 756 | if (touches.length === 2) { 757 | currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2; 758 | currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2; 759 | } else { 760 | currentTouchLeft = touches[0].pageX; 761 | currentTouchTop = touches[0].pageY; 762 | } 763 | 764 | var positions = self.__positions; 765 | 766 | // Are we already is dragging mode? 767 | if (self.__isDragging) { 768 | 769 | // Compute move distance 770 | var moveX = currentTouchLeft - self.__lastTouchLeft; 771 | var moveY = currentTouchTop - self.__lastTouchTop; 772 | 773 | // Read previous scroll position and zooming 774 | var scrollLeft = self.__scrollLeft; 775 | var scrollTop = self.__scrollTop; 776 | var level = self.__zoomLevel; 777 | 778 | // Work with scaling 779 | if (scale != null && self.options.zooming) { 780 | 781 | var oldLevel = level; 782 | 783 | // Recompute level based on previous scale and new scale 784 | level = level / self.__lastScale * scale; 785 | 786 | // Limit level according to configuration 787 | level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom); 788 | 789 | // Only do further compution when change happened 790 | if (oldLevel !== level) { 791 | 792 | // Compute relative event position to container 793 | var currentTouchLeftRel = currentTouchLeft - self.__clientLeft; 794 | var currentTouchTopRel = currentTouchTop - self.__clientTop; 795 | 796 | // Recompute left and top coordinates based on new zoom level 797 | scrollLeft = ((currentTouchLeftRel + scrollLeft) * level / oldLevel) - currentTouchLeftRel; 798 | scrollTop = ((currentTouchTopRel + scrollTop) * level / oldLevel) - currentTouchTopRel; 799 | 800 | // Recompute max scroll values 801 | self.__computeScrollMax(level); 802 | 803 | } 804 | } 805 | 806 | if (self.__enableScrollX) { 807 | 808 | scrollLeft -= moveX * this.options.speedMultiplier; 809 | var maxScrollLeft = self.__maxScrollLeft; 810 | 811 | if (scrollLeft > maxScrollLeft || scrollLeft < 0) { 812 | 813 | // Slow down on the edges 814 | if (self.options.bouncing) { 815 | 816 | scrollLeft += (moveX / 2 * this.options.speedMultiplier); 817 | 818 | } else if (scrollLeft > maxScrollLeft) { 819 | 820 | scrollLeft = maxScrollLeft; 821 | 822 | } else { 823 | 824 | scrollLeft = 0; 825 | 826 | } 827 | } 828 | } 829 | 830 | // Compute new vertical scroll position 831 | if (self.__enableScrollY) { 832 | 833 | scrollTop -= moveY * this.options.speedMultiplier; 834 | var maxScrollTop = self.__maxScrollTop; 835 | 836 | if (scrollTop > maxScrollTop || scrollTop < 0) { 837 | 838 | // Slow down on the edges 839 | if (self.options.bouncing) { 840 | 841 | scrollTop += (moveY / 2 * this.options.speedMultiplier); 842 | 843 | // Support pull-to-refresh (only when only y is scrollable) 844 | if (!self.__enableScrollX && self.__refreshHeight != null) { 845 | 846 | if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) { 847 | 848 | self.__refreshActive = true; 849 | if (self.__refreshActivate) { 850 | self.__refreshActivate(); 851 | } 852 | 853 | } else if (self.__refreshActive && scrollTop > -self.__refreshHeight) { 854 | 855 | self.__refreshActive = false; 856 | if (self.__refreshDeactivate) { 857 | self.__refreshDeactivate(); 858 | } 859 | 860 | } 861 | } 862 | 863 | } else if (scrollTop > maxScrollTop) { 864 | 865 | scrollTop = maxScrollTop; 866 | 867 | } else { 868 | 869 | scrollTop = 0; 870 | 871 | } 872 | } 873 | } 874 | 875 | // Keep list from growing infinitely (holding min 10, max 20 measure points) 876 | if (positions.length > 60) { 877 | positions.splice(0, 30); 878 | } 879 | 880 | // Track scroll movement for decleration 881 | positions.push(scrollLeft, scrollTop, timeStamp); 882 | 883 | // Sync scroll position 884 | self.__publish(scrollLeft, scrollTop, level); 885 | 886 | // Otherwise figure out whether we are switching into dragging mode now. 887 | } else { 888 | 889 | var minimumTrackingForScroll = self.options.locking ? 3 : 0; 890 | var minimumTrackingForDrag = 5; 891 | 892 | var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft); 893 | var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop); 894 | 895 | self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll; 896 | self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll; 897 | 898 | positions.push(self.__scrollLeft, self.__scrollTop, timeStamp); 899 | 900 | self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag); 901 | if (self.__isDragging) { 902 | self.__interruptedAnimation = false; 903 | } 904 | 905 | } 906 | 907 | // Update last touch positions and time stamp for next event 908 | self.__lastTouchLeft = currentTouchLeft; 909 | self.__lastTouchTop = currentTouchTop; 910 | self.__lastTouchMove = timeStamp; 911 | self.__lastScale = scale; 912 | 913 | }, 914 | 915 | 916 | /** 917 | * Touch end handler for scrolling support 918 | */ 919 | doTouchEnd: function(timeStamp) { 920 | 921 | if (timeStamp instanceof Date) { 922 | timeStamp = timeStamp.valueOf(); 923 | } 924 | if (typeof timeStamp !== "number") { 925 | throw new Error("Invalid timestamp value: " + timeStamp); 926 | } 927 | 928 | var self = this; 929 | 930 | // Ignore event when tracking is not enabled (no touchstart event on element) 931 | // This is required as this listener ('touchmove') sits on the document and not on the element itself. 932 | if (!self.__isTracking) { 933 | return; 934 | } 935 | 936 | // Not touching anymore (when two finger hit the screen there are two touch end events) 937 | self.__isTracking = false; 938 | 939 | // Be sure to reset the dragging flag now. Here we also detect whether 940 | // the finger has moved fast enough to switch into a deceleration animation. 941 | if (self.__isDragging) { 942 | 943 | // Reset dragging flag 944 | self.__isDragging = false; 945 | 946 | // Start deceleration 947 | // Verify that the last move detected was in some relevant time frame 948 | if (self.__isSingleTouch && self.options.animating && (timeStamp - self.__lastTouchMove) <= 100) { 949 | 950 | // Then figure out what the scroll position was about 100ms ago 951 | var positions = self.__positions; 952 | var endPos = positions.length - 1; 953 | var startPos = endPos; 954 | 955 | // Move pointer to position measured 100ms ago 956 | for (var i = endPos; i > 0 && positions[i] > (self.__lastTouchMove - 100); i -= 3) { 957 | startPos = i; 958 | } 959 | 960 | // If start and stop position is identical in a 100ms timeframe, 961 | // we cannot compute any useful deceleration. 962 | if (startPos !== endPos) { 963 | 964 | // Compute relative movement between these two points 965 | var timeOffset = positions[endPos] - positions[startPos]; 966 | var movedLeft = self.__scrollLeft - positions[startPos - 2]; 967 | var movedTop = self.__scrollTop - positions[startPos - 1]; 968 | 969 | // Based on 50ms compute the movement to apply for each render step 970 | self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60); 971 | self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60); 972 | 973 | // How much velocity is required to start the deceleration 974 | var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? 4 : 1; 975 | 976 | // Verify that we have enough velocity to start deceleration 977 | if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) { 978 | 979 | // Deactivate pull-to-refresh when decelerating 980 | if (!self.__refreshActive) { 981 | self.__startDeceleration(timeStamp); 982 | } 983 | } 984 | } else { 985 | self.options.scrollingComplete(); 986 | } 987 | } else if ((timeStamp - self.__lastTouchMove) > 100) { 988 | self.options.scrollingComplete(); 989 | } 990 | } 991 | 992 | // If this was a slower move it is per default non decelerated, but this 993 | // still means that we want snap back to the bounds which is done here. 994 | // This is placed outside the condition above to improve edge case stability 995 | // e.g. touchend fired without enabled dragging. This should normally do not 996 | // have modified the scroll positions or even showed the scrollbars though. 997 | if (!self.__isDecelerating) { 998 | 999 | if (self.__refreshActive && self.__refreshStart) { 1000 | 1001 | // Use publish instead of scrollTo to allow scrolling to out of boundary position 1002 | // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled 1003 | self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true); 1004 | 1005 | if (self.__refreshStart) { 1006 | self.__refreshStart(); 1007 | } 1008 | 1009 | } else { 1010 | 1011 | if (self.__interruptedAnimation || self.__isDragging) { 1012 | self.options.scrollingComplete(); 1013 | } 1014 | self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel); 1015 | 1016 | // Directly signalize deactivation (nothing todo on refresh?) 1017 | if (self.__refreshActive) { 1018 | 1019 | self.__refreshActive = false; 1020 | if (self.__refreshDeactivate) { 1021 | self.__refreshDeactivate(); 1022 | } 1023 | 1024 | } 1025 | } 1026 | } 1027 | 1028 | // Fully cleanup list 1029 | self.__positions.length = 0; 1030 | 1031 | }, 1032 | 1033 | 1034 | 1035 | /* 1036 | --------------------------------------------------------------------------- 1037 | PRIVATE API 1038 | --------------------------------------------------------------------------- 1039 | */ 1040 | 1041 | /** 1042 | * Applies the scroll position to the content element 1043 | * 1044 | * @param left {Number} Left scroll position 1045 | * @param top {Number} Top scroll position 1046 | * @param animate {Boolean?false} Whether animation should be used to move to the new coordinates 1047 | */ 1048 | __publish: function(left, top, zoom, animate) { 1049 | 1050 | var self = this; 1051 | 1052 | // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation 1053 | var wasAnimating = self.__isAnimating; 1054 | if (wasAnimating) { 1055 | core.effect.Animate.stop(wasAnimating); 1056 | self.__isAnimating = false; 1057 | } 1058 | 1059 | if (animate && self.options.animating) { 1060 | 1061 | // Keep scheduled positions for scrollBy/zoomBy functionality 1062 | self.__scheduledLeft = left; 1063 | self.__scheduledTop = top; 1064 | self.__scheduledZoom = zoom; 1065 | 1066 | var oldLeft = self.__scrollLeft; 1067 | var oldTop = self.__scrollTop; 1068 | var oldZoom = self.__zoomLevel; 1069 | 1070 | var diffLeft = left - oldLeft; 1071 | var diffTop = top - oldTop; 1072 | var diffZoom = zoom - oldZoom; 1073 | 1074 | var step = function(percent, now, render) { 1075 | 1076 | if (render) { 1077 | 1078 | self.__scrollLeft = oldLeft + (diffLeft * percent); 1079 | self.__scrollTop = oldTop + (diffTop * percent); 1080 | self.__zoomLevel = oldZoom + (diffZoom * percent); 1081 | 1082 | // Push values out 1083 | if (self.__callback) { 1084 | self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel); 1085 | } 1086 | 1087 | } 1088 | }; 1089 | 1090 | var verify = function(id) { 1091 | return self.__isAnimating === id; 1092 | }; 1093 | 1094 | var completed = function(renderedFramesPerSecond, animationId, wasFinished) { 1095 | if (animationId === self.__isAnimating) { 1096 | self.__isAnimating = false; 1097 | } 1098 | if (self.__didDecelerationComplete || wasFinished) { 1099 | self.options.scrollingComplete(); 1100 | } 1101 | 1102 | if (self.options.zooming) { 1103 | self.__computeScrollMax(); 1104 | if(self.__zoomComplete) { 1105 | self.__zoomComplete(); 1106 | self.__zoomComplete = null; 1107 | } 1108 | } 1109 | }; 1110 | 1111 | // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out 1112 | self.__isAnimating = core.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic); 1113 | 1114 | } else { 1115 | 1116 | self.__scheduledLeft = self.__scrollLeft = left; 1117 | self.__scheduledTop = self.__scrollTop = top; 1118 | self.__scheduledZoom = self.__zoomLevel = zoom; 1119 | 1120 | // Push values out 1121 | if (self.__callback) { 1122 | self.__callback(left, top, zoom); 1123 | } 1124 | 1125 | // Fix max scroll ranges 1126 | if (self.options.zooming) { 1127 | self.__computeScrollMax(); 1128 | if(self.__zoomComplete) { 1129 | self.__zoomComplete(); 1130 | self.__zoomComplete = null; 1131 | } 1132 | } 1133 | } 1134 | }, 1135 | 1136 | 1137 | /** 1138 | * Recomputes scroll minimum values based on client dimensions and content dimensions. 1139 | */ 1140 | __computeScrollMax: function(zoomLevel) { 1141 | 1142 | var self = this; 1143 | 1144 | if (zoomLevel == null) { 1145 | zoomLevel = self.__zoomLevel; 1146 | } 1147 | 1148 | self.__maxScrollLeft = Math.max((self.__contentWidth * zoomLevel) - self.__clientWidth, 0); 1149 | self.__maxScrollTop = Math.max((self.__contentHeight * zoomLevel) - self.__clientHeight, 0); 1150 | 1151 | }, 1152 | 1153 | 1154 | 1155 | /* 1156 | --------------------------------------------------------------------------- 1157 | ANIMATION (DECELERATION) SUPPORT 1158 | --------------------------------------------------------------------------- 1159 | */ 1160 | 1161 | /** 1162 | * Called when a touch sequence end and the speed of the finger was high enough 1163 | * to switch into deceleration mode. 1164 | */ 1165 | __startDeceleration: function(timeStamp) { 1166 | 1167 | var self = this; 1168 | 1169 | if (self.options.paging) { 1170 | 1171 | var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0); 1172 | var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0); 1173 | var clientWidth = self.__clientWidth; 1174 | var clientHeight = self.__clientHeight; 1175 | 1176 | // We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area. 1177 | // Each page should have exactly the size of the client area. 1178 | self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth; 1179 | self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight; 1180 | self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth; 1181 | self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight; 1182 | 1183 | } else { 1184 | 1185 | self.__minDecelerationScrollLeft = 0; 1186 | self.__minDecelerationScrollTop = 0; 1187 | self.__maxDecelerationScrollLeft = self.__maxScrollLeft; 1188 | self.__maxDecelerationScrollTop = self.__maxScrollTop; 1189 | 1190 | } 1191 | 1192 | // Wrap class method 1193 | var step = function(percent, now, render) { 1194 | self.__stepThroughDeceleration(render); 1195 | }; 1196 | 1197 | // How much velocity is required to keep the deceleration running 1198 | var minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1; 1199 | 1200 | // Detect whether it's still worth to continue animating steps 1201 | // If we are already slow enough to not being user perceivable anymore, we stop the whole process here. 1202 | var verify = function() { 1203 | var shouldContinue = Math.abs(self.__decelerationVelocityX) >= minVelocityToKeepDecelerating || Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating; 1204 | if (!shouldContinue) { 1205 | self.__didDecelerationComplete = true; 1206 | } 1207 | return shouldContinue; 1208 | }; 1209 | 1210 | var completed = function(renderedFramesPerSecond, animationId, wasFinished) { 1211 | self.__isDecelerating = false; 1212 | if (self.__didDecelerationComplete) { 1213 | self.options.scrollingComplete(); 1214 | } 1215 | 1216 | // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions 1217 | self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping); 1218 | }; 1219 | 1220 | // Start animation and switch on flag 1221 | self.__isDecelerating = core.effect.Animate.start(step, verify, completed); 1222 | 1223 | }, 1224 | 1225 | 1226 | /** 1227 | * Called on every step of the animation 1228 | * 1229 | * @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only! 1230 | */ 1231 | __stepThroughDeceleration: function(render) { 1232 | 1233 | var self = this; 1234 | 1235 | 1236 | // 1237 | // COMPUTE NEXT SCROLL POSITION 1238 | // 1239 | 1240 | // Add deceleration to scroll position 1241 | var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX; 1242 | var scrollTop = self.__scrollTop + self.__decelerationVelocityY; 1243 | 1244 | 1245 | // 1246 | // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE 1247 | // 1248 | 1249 | if (!self.options.bouncing) { 1250 | 1251 | var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft); 1252 | if (scrollLeftFixed !== scrollLeft) { 1253 | scrollLeft = scrollLeftFixed; 1254 | self.__decelerationVelocityX = 0; 1255 | } 1256 | 1257 | var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop); 1258 | if (scrollTopFixed !== scrollTop) { 1259 | scrollTop = scrollTopFixed; 1260 | self.__decelerationVelocityY = 0; 1261 | } 1262 | 1263 | } 1264 | 1265 | 1266 | // 1267 | // UPDATE SCROLL POSITION 1268 | // 1269 | 1270 | if (render) { 1271 | 1272 | self.__publish(scrollLeft, scrollTop, self.__zoomLevel); 1273 | 1274 | } else { 1275 | 1276 | self.__scrollLeft = scrollLeft; 1277 | self.__scrollTop = scrollTop; 1278 | 1279 | } 1280 | 1281 | 1282 | // 1283 | // SLOW DOWN 1284 | // 1285 | 1286 | // Slow down velocity on every iteration 1287 | if (!self.options.paging) { 1288 | 1289 | // This is the factor applied to every iteration of the animation 1290 | // to slow down the process. This should emulate natural behavior where 1291 | // objects slow down when the initiator of the movement is removed 1292 | var frictionFactor = 0.95; 1293 | 1294 | self.__decelerationVelocityX *= frictionFactor; 1295 | self.__decelerationVelocityY *= frictionFactor; 1296 | 1297 | } 1298 | 1299 | 1300 | // 1301 | // BOUNCING SUPPORT 1302 | // 1303 | 1304 | if (self.options.bouncing) { 1305 | 1306 | var scrollOutsideX = 0; 1307 | var scrollOutsideY = 0; 1308 | 1309 | // This configures the amount of change applied to deceleration/acceleration when reaching boundaries 1310 | var penetrationDeceleration = self.options.penetrationDeceleration; 1311 | var penetrationAcceleration = self.options.penetrationAcceleration; 1312 | 1313 | // Check limits 1314 | if (scrollLeft < self.__minDecelerationScrollLeft) { 1315 | scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft; 1316 | } else if (scrollLeft > self.__maxDecelerationScrollLeft) { 1317 | scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft; 1318 | } 1319 | 1320 | if (scrollTop < self.__minDecelerationScrollTop) { 1321 | scrollOutsideY = self.__minDecelerationScrollTop - scrollTop; 1322 | } else if (scrollTop > self.__maxDecelerationScrollTop) { 1323 | scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop; 1324 | } 1325 | 1326 | // Slow down until slow enough, then flip back to snap position 1327 | if (scrollOutsideX !== 0) { 1328 | if (scrollOutsideX * self.__decelerationVelocityX <= 0) { 1329 | self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration; 1330 | } else { 1331 | self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration; 1332 | } 1333 | } 1334 | 1335 | if (scrollOutsideY !== 0) { 1336 | if (scrollOutsideY * self.__decelerationVelocityY <= 0) { 1337 | self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration; 1338 | } else { 1339 | self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration; 1340 | } 1341 | } 1342 | } 1343 | } 1344 | }; 1345 | 1346 | // Copy over members to prototype 1347 | for (var key in members) { 1348 | Scroller.prototype[key] = members[key]; 1349 | } 1350 | 1351 | })(); 1352 | -------------------------------------------------------------------------------- /text.js: -------------------------------------------------------------------------------- 1 | (function(CL){ 2 | 3 | var RenderLayer = CL.RenderLayer; 4 | 5 | //图片对象 6 | function CanvasText(opt){ 7 | if(!(this instanceof CanvasText)){ 8 | return new CanvasText(opt); 9 | } 10 | this.init(opt); 11 | }; 12 | 13 | CanvasText.prototype = Object.create(RenderLayer.prototype); 14 | CanvasText.prototype.constructor = RenderLayer; 15 | 16 | $.extend(CanvasText.prototype,{ 17 | init:function(opt){ 18 | 19 | RenderLayer.prototype.init.apply(this,arguments); 20 | 21 | var style = this.style = this.style || {}; 22 | var optStyle = opt.style || {}; 23 | 24 | style.color = optStyle.color; 25 | style.fontSize = optStyle.fontSize; 26 | style.fontWeight = optStyle.fontWeight; 27 | style.lineHeight = optStyle.lineHeight; 28 | style.fontFamily = optStyle.fontFamily; 29 | style.textAlign = optStyle.textAlign; 30 | 31 | this.content = opt.content + '' || ''; 32 | 33 | }, 34 | drawSelf:function(ctx){ 35 | 36 | //绘制分行文字 37 | this.drawContentWithLines(ctx); 38 | 39 | }, 40 | beforeLayerLayoutComputed:function(layoutObj){ 41 | var parentLayoutObj = layoutObj.parentLayoutObj; 42 | var parentContentWidth; 43 | var ctx = this.getRoot().ctx; 44 | var finalStyle = this.finalStyle; 45 | var textHeight = finalStyle.lineHeight || 18; 46 | 47 | 48 | //不设定高度样式,并且有文本内容,使用文本的撑开高度作为绘制高度,父元素的宽度作为计算参考 49 | if(this.content){ 50 | if(finalStyle.height == null || finalStyle.width == null){ 51 | if(this.parent){ 52 | var parentFinalStyle = this.parent.finalStyle; 53 | //父容器内容宽度 54 | parentContentWidth = parentLayoutObj.width; 55 | } 56 | else{ 57 | parentContentWidth = window.innerWidth; 58 | } 59 | parentContentWidth = parentContentWidth- (parentFinalStyle.padding ? parentFinalStyle.padding * 2 : ((parentFinalStyle.paddingLeft || 0) + (parentFinalStyle.paddingRight || 0))) 60 | - (finalStyle.padding ? finalStyle.padding * 2 : ((finalStyle.paddingLeft || 0) + (finalStyle.paddingRight || 0))) 61 | - (finalStyle.margin ? finalStyle.margin * 2 : ((finalStyle.marginLeft || 0) + (finalStyle.marginRight || 0))) 62 | 63 | if(finalStyle.height == null){ 64 | //计算文本高度 65 | this.forEachTextLinePosition(ctx,parentContentWidth,function(lineContent,left,top){ 66 | textHeight += top; 67 | }); 68 | 69 | //应用文本高度 70 | layoutObj.style.height = textHeight + (finalStyle.padding ? finalStyle.padding * 2 : ((finalStyle.paddingTop || 0) + (finalStyle.paddingTop || 0))); 71 | } 72 | if(finalStyle.width == null){ 73 | 74 | layoutObj.style.width = Math.min(parentContentWidth,ctx.measureText(this.content).width) + 2; 75 | 76 | } 77 | } 78 | } 79 | 80 | }, 81 | //遍历每行字,给出该行字的内容和起点 82 | forEachTextLinePosition:function(ctx,width,callback){ 83 | var self = this; 84 | var testContent = ''; 85 | var lineHeight = this.finalStyle.lineHeight || 18; 86 | var left; 87 | var top = 0; 88 | var content = this.content; 89 | 90 | var finalStyle = this.finalStyle; 91 | 92 | ctx.font = (finalStyle.fontWeight || 'normal') + ' ' + (finalStyle.fontSize || 16) + 'px ' + (finalStyle.fontFamily || 'serif'); 93 | 94 | ctx.textBaseline = 'top'; 95 | 96 | $.each(content,function(i,font){ 97 | var nextFont = content[i + 1] || ''; 98 | //增加一个文字 99 | testContent += font; 100 | 101 | //满一行,绘制该行文字 102 | if(ctx.measureText(testContent + nextFont).width >= width || i == content.length - 1){ 103 | left = self.getTextStartPosition(testContent,ctx); 104 | callback(testContent, Math.round(left), Math.round(top)); 105 | top += lineHeight; 106 | testContent = ''; 107 | } 108 | 109 | }); 110 | }, 111 | //分行绘制文字 112 | drawContentWithLines:function(ctx){ 113 | 114 | var width = this.drawWidth; 115 | ctx.fillStyle = this.finalStyle.color || '#000'; 116 | 117 | //绘制每行文字内容 118 | this.forEachTextLinePosition(ctx,width,function(lineContent,left,top){ 119 | ctx.fillText(lineContent, Math.round(left), Math.round(top)); 120 | }); 121 | 122 | }, 123 | //根据textAlign获取文本起始位置 124 | getTextStartPosition:function(text,ctx){ 125 | var textAlign = this.finalStyle.textAlign || 'left'; 126 | var textWidth = ctx.measureText(text).width; 127 | var width = this.drawWidth; 128 | 129 | if(textAlign == 'left'){ 130 | return 0; 131 | } 132 | if(textAlign == 'right'){ 133 | return width - textWidth; 134 | } 135 | if(textAlign == 'center'){ 136 | return (width - textWidth) / 2; 137 | } 138 | }, 139 | onClick:function(){ 140 | 141 | } 142 | }); 143 | 144 | CL.CanvasText = CanvasText; 145 | 146 | })(window.CanvasList = window.CanvasList || {}); --------------------------------------------------------------------------------