├── 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 = "
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 || {});
--------------------------------------------------------------------------------