实现原理很简单,大致分成3个步骤:
1. 使用RenderTexture.draw方法在显示对象displayObj上截取一张截图纹理,并将该纹理传给下面会介绍到的Animation对象,该对象的作用是在获取到现有截图纹理后生成一张看上去与displayObj相同的image覆盖在displayObj之上,从而遮挡displayObj将要产生的显示更新。
2. 更新displayObj,并用一个新的RenderTexture对象以同样的方法获取到新的截图纹理,并交给animation。
3. 在获取了新旧两张纹理之后,animation以这两张纹理为基础做计算而产生翻页效果,整个过程中displayObj始终会被animation遮挡而不可见。当animation的翻页动画完成时,animation将显示为新的最新的那张截图覆盖在displayObj智商,此时将animation.visible置为false,displayObj从而可见!
下面介绍下我的自定义类:
MixedImage.as
package utils { import flash.geom.Point; import starling.display.Sprite; import starling.textures.Texture; import starling.utils.deg2rad; /** * 混合图片,以三张图片的纹理拼合来描述一张被翻阅的图片 * @author JixiaoKe(363959579@qq.com) */ public class MixedImage extends Sprite { private var original:Texture; private var another:Texture; private var bookHeight:Number; private var bookWidth:Number; public function MixedImage(bookWidth:Number, bookHeight:Number) { this.bookWidth = bookWidth; this.bookHeight = bookHeight; } //旧面两页 private var cacheImage_0:QuadImage; private var cacheImage_1:QuadImage; //新面 private var cacheImage_2:QuadImage; public function set originalTexture(texture:Texture):void { if(original && original == texture) return; original = texture; if(!cacheImage_0) { cacheImage_0 = new QuadImage(original); this.addChild(cacheImage_0); cacheImage_1 = new QuadImage(original); this.addChild(cacheImage_1); cacheImage_0.touchable = cacheImage_1.touchable = false; }else { cacheImage_1.texture = original; cacheImage_0.texture = original; } } public function set anotherTexture(texture:Texture):void { if(another && another == texture) return; another = texture; if(!cacheImage_2) { cacheImage_2 = new QuadImage(another); this.addChild(cacheImage_2); }else { cacheImage_2.texture = texture; } } //拖动点 private var _dragPoint:Point = new Point(); //拖动衍生第四个点 private var _dragPointCopy:Point = new Point(); //纸张边缘点:下 private var _edgePoint:Point = new Point(); //纸张边缘点:上与右 private var _edgePointCopy:Point = new Point(); private var currentPointCount:int; /**重置四个顶点坐标*/ private function resetAllTexCoords(image:QuadImage):void { image.setTexCoordsTo(0, 0, 0); image.setTexCoordsTo(1, 1, 0); image.setTexCoordsTo(2, 0, 1); image.setTexCoordsTo(3, 1.0, 1.0); } public function readjustSizeHandler(image:QuadImage):void { image.readjustSize(); resetAllTexCoords(image); image.vertexDataChanged(); } private function readjustSize():void { readjustSizeHandler(cacheImage_0); readjustSizeHandler(cacheImage_1); readjustSizeHandler(cacheImage_2); } override public function dispose():void { this.original = null; this.another = null; if(cacheImage_0) cacheImage_0.removeFromParent(true); if(cacheImage_1) cacheImage_1.removeFromParent(true); if(cacheImage_2) cacheImage_2.removeFromParent(true); super.dispose(); } /** * * @param quadBatch * @param progress [0-1] 翻页进度(0为开始,至1为完成) * @param leftToRight * */ public function setLocation(progress:Number, pageUp:Boolean):void { var radius:Number; var angle:Number; //拖拽点相对于圆心的旋转角度 var degAngle:Number; //angle对应的弧度值 var tempAngle:Number //_edgePointCopy相对于圆心的旋转角度 var degTemp:Number; if(pageUp) { angle = -180 + progress*180; degAngle = deg2rad( angle ); tempAngle = -180 + progress*90; degTemp = deg2rad( tempAngle ); radius = bookWidth * progress / 2; _edgePoint.x = radius; _edgePoint.y = bookHeight; _dragPoint.x = _edgePoint.x + radius * Math.cos( degAngle ); _dragPoint.y = _edgePoint.y + radius * Math.sin( degAngle ); _edgePointCopy.y = radius * Math.tan( -degTemp ) + _edgePoint.y; if(_edgePointCopy.y < 0) //有四个有效点 { currentPointCount = 4; var d:Number; d = -_edgePointCopy.y; _edgePointCopy.x = radius * d / (d + bookHeight); _edgePointCopy.y = 0; _dragPointCopy.x = _edgePointCopy.x + _edgePointCopy.x * Math.cos( degAngle ); _dragPointCopy.y = _edgePointCopy.y + _edgePointCopy.x * Math.sin( degAngle ); } else { currentPointCount = 3; _edgePointCopy.x = 0; _dragPointCopy.x = _dragPointCopy.y = 0; } } else { angle = 0 - progress * 180; degAngle = deg2rad( angle ); tempAngle = 0 - progress*90; degTemp = deg2rad( tempAngle ); radius = bookWidth * progress / 2; _edgePoint.x = bookWidth - radius; _edgePoint.y = bookHeight; _dragPoint.x = _edgePoint.x + radius * Math.cos( degAngle ); _dragPoint.y = _edgePoint.y + radius * Math.sin( degAngle ); _edgePointCopy.y = _edgePoint.y + radius * Math.tan( degTemp ); if(_edgePointCopy.y < 0) { currentPointCount = 4; d = -_edgePointCopy.y; _edgePointCopy.x = bookWidth - radius * d / (d + bookHeight); _edgePointCopy.y = 0; _dragPointCopy.x = _edgePointCopy.x + (bookWidth - _edgePointCopy.x) * Math.cos( degAngle ); _dragPointCopy.y = _edgePointCopy.y + (bookWidth - _edgePointCopy.x) * Math.sin( degAngle ); } else { currentPointCount = 3; _edgePointCopy.x = bookWidth; _dragPointCopy.x = bookWidth; _dragPointCopy.y = 0; } } readjustSize(); createView(pageUp); } private function createView(pageUp:Boolean):void { if(currentPointCount == 3) cacheImage_1.visible = true; else cacheImage_1.visible = false; if(pageUp) { if(currentPointCount == 3) { cacheImage_0.setPosition(0, 0, 0); cacheImage_0.setTexCoordsTo(0, 0, 0); cacheImage_0.setPosition(1, bookWidth/2, 0); cacheImage_0.setTexCoordsTo(1, 1, 0); cacheImage_0.setPosition(2, 0, _edgePointCopy.y); cacheImage_0.setTexCoordsTo(2, 0, _edgePointCopy.y/bookHeight); cacheImage_0.setPosition(3, bookWidth/2, _edgePointCopy.y); cacheImage_0.setTexCoordsTo(3, 1, _edgePointCopy.y/bookHeight); cacheImage_0.vertexDataChanged(); cacheImage_1.setPosition(0, 0, _edgePointCopy.y); cacheImage_1.setTexCoordsTo(0, 0, _edgePointCopy.y/bookHeight); cacheImage_1.setPosition(1, bookWidth/2, _edgePointCopy.y); cacheImage_1.setTexCoordsTo(1, 1, _edgePointCopy.y/bookHeight); cacheImage_1.setPosition(2, _edgePoint.x, bookHeight); cacheImage_1.setTexCoordsTo(2, _edgePoint.x*2/bookWidth , 1); cacheImage_1.setPosition(3, bookWidth/2, bookHeight); cacheImage_1.vertexDataChanged(); cacheImage_2.setPosition(0, 0, _edgePointCopy.y); cacheImage_2.setTexCoordsTo(0, 1, _edgePointCopy.y/bookHeight); cacheImage_2.setPosition(1, _edgePointCopy.x, _edgePointCopy.y); cacheImage_2.setTexCoordsTo(1, 1, _edgePointCopy.y/bookHeight); cacheImage_2.setPosition(2, _edgePoint.x, _edgePoint.y); cacheImage_2.setTexCoordsTo(2, 1-_edgePoint.x*2/bookWidth, 1); cacheImage_2.setPosition(3, _dragPoint.x, _dragPoint.y); cacheImage_2.vertexDataChanged(); }else if(currentPointCount == 4) { cacheImage_0.setPosition(0, _edgePointCopy.x, 0); cacheImage_0.setTexCoordsTo(0, _edgePointCopy.x*2/bookWidth, 0); cacheImage_0.setPosition(1, bookWidth/2, 0); cacheImage_0.setPosition(2, _edgePoint.x, bookHeight); cacheImage_0.setTexCoordsTo(2, _edgePoint.x*2/bookWidth, 1); cacheImage_0.setPosition(3, bookWidth/2, bookHeight); cacheImage_0.vertexDataChanged(); cacheImage_2.setPosition(0, _edgePointCopy.x, 0); cacheImage_2.setTexCoordsTo(0, 1-_edgePointCopy.x*2/bookWidth, 0); cacheImage_2.setPosition(1, _dragPointCopy.x, _dragPointCopy.y); cacheImage_2.setPosition(2, _edgePoint.x, bookHeight); cacheImage_2.setTexCoordsTo(2, 1-_edgePoint.x*2/bookWidth, 1); cacheImage_2.setPosition(3, _dragPoint.x, _dragPoint.y); cacheImage_2.vertexDataChanged(); } } else { if(currentPointCount == 3) { cacheImage_0.setPosition(0, bookWidth/2, 0); cacheImage_0.setTexCoordsTo(0, 0, 0); cacheImage_0.setPosition(1, bookWidth, 0); cacheImage_0.setTexCoordsTo(1, 1, 0); cacheImage_0.setPosition(2, bookWidth/2, _edgePointCopy.y); cacheImage_0.setTexCoordsTo(2, 0, _edgePointCopy.y/bookHeight); cacheImage_0.setPosition(3, bookWidth, _edgePointCopy.y); cacheImage_0.setTexCoordsTo(3, 1, _edgePointCopy.y/bookHeight); cacheImage_0.vertexDataChanged(); cacheImage_1.setPosition(0, bookWidth/2, _edgePointCopy.y); cacheImage_1.setTexCoordsTo(0, 0, _edgePointCopy.y/bookHeight); cacheImage_1.setPosition(1, bookWidth, _edgePointCopy.y); cacheImage_1.setTexCoordsTo(1, 1, _edgePointCopy.y/bookHeight); cacheImage_1.setPosition(2, bookWidth/2, bookHeight); cacheImage_1.setPosition(3, _edgePoint.x, bookHeight); cacheImage_1.setTexCoordsTo(3, (_edgePoint.x - bookWidth/2)*2/bookWidth, 1); cacheImage_1.vertexDataChanged(); cacheImage_2.setPosition(0, bookWidth, _edgePointCopy.y); cacheImage_2.setTexCoordsTo(0, 0, _edgePointCopy.y/bookHeight); cacheImage_2.setPosition(1, bookWidth, _edgePointCopy.y); cacheImage_2.setTexCoordsTo(1, 0, _edgePointCopy.y/bookHeight); cacheImage_2.setPosition(2, _dragPoint.x, _dragPoint.y); cacheImage_2.setPosition(3, _edgePoint.x, bookHeight); cacheImage_2.setTexCoordsTo(3, 1 - (_edgePoint.x - bookWidth/2)*2/bookWidth, 1); cacheImage_2.vertexDataChanged(); } else if(currentPointCount == 4) { cacheImage_0.setPosition(0, bookWidth/2, 0); cacheImage_0.setPosition(1, _edgePointCopy.x, 0); cacheImage_0.setTexCoordsTo(1, (_edgePointCopy.x - bookWidth/2)*2/bookWidth, 0); cacheImage_0.setPosition(2, bookWidth/2, bookHeight); cacheImage_0.setPosition(3, _edgePoint.x, bookHeight); cacheImage_0.setTexCoordsTo(3, (_edgePoint.x - bookWidth/2)*2/bookWidth, 1); cacheImage_0.vertexDataChanged(); cacheImage_2.setPosition(0, _dragPointCopy.x, _dragPointCopy.y); cacheImage_2.setPosition(1, _edgePointCopy.x, 0); cacheImage_2.setTexCoordsTo(1, 1-(_edgePointCopy.x - bookWidth/2)*2/bookWidth, 0); cacheImage_2.setPosition(2, _dragPoint.x, _dragPoint.y); cacheImage_2.setPosition(3, _edgePoint.x, bookHeight); cacheImage_2.setTexCoordsTo(3, 1-(_edgePoint.x - bookWidth/2)*2/bookWidth, 1); cacheImage_2.vertexDataChanged(); } } } } } import flash.geom.Point; import starling.display.Image; import starling.textures.Texture; class QuadImage extends Image { public function QuadImage(texture:Texture) { super(texture); } /** * 更新顶点纹理坐标值 (范围 0-1). */ internal function setTexCoordsTo(vertexID:int, u:Number, v:Number):void { /* * 由于 starling.display.Image 自带的setTextCoordsTo()方法是在starling 1.4之后发布的api, * 而我在写这段代码时1.4还未发布,所以才有了这个自定义方法。也因此需要最下方自定义的vertexDataChanged()方法! * 虽然在1.4之前有setTexCoords方法,但要求第二个参数是一个point对象这一点很蛋疼!我很懒! */ this.mVertexData.setTexCoords(vertexID, u, v); } /** 更新顶点位置. */ internal function setPosition(vertexID:int, x:Number, y:Number):void { this.mVertexData.setPosition(vertexID, x, y); } /** 在手动改变'mVertexData'的内容后调用此方法*/ internal function vertexDataChanged():void { this.onVertexDataChanged(); } }
该类公开了三个自定义接口:
/** 用于指定该混合图片的正面纹理 */ public function set originalTexture(texture:Texture):void; /** 用于指定该混合图片的背面纹理 */ public function set anotherTexture(texture:Texture):void; /** 指定当前翻页进度,进度为 0 - 1之间。pageUp用于指定翻页进行的方向 */ public function setLocation(progress:Number, pageUp:Boolean):void;
animation对象持续调用mixImage.setLocation()方法来传入持续增长的progress参数,从而产生翻页动画。
Animation类:
package utils { import flash.geom.Rectangle; import starling.display.Image; import starling.display.Sprite; import starling.events.Event; import starling.textures.Texture; public class Animation extends Sprite { public function Animation(width:Number, height:Number) { bookWidth = width; bookHeight = height; // maxHeight = Math.ceil( Math.sqrt(bookWidth*bookWidth + bookHeight*bookHeight) ); initialize(); } private var bookWidth:Number = 0; private var bookHeight:Number = 0; private var maxHeight:Number; /** * 软页 */ private var softImage:MixedImage; private var cacheImageL:Image; private var cacheImageR:Image; protected function initialize():void { initSoftImage(); this.addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function initSoftImage():void { softImage = new MixedImage(bookWidth, bookHeight); this.addChild(softImage); } /** * 时间轴方法 */ private function onEnterFrame():void { if(!isActive) return; progress += this.velocity; if(progress >= 1) { progress = 1; softImage.setLocation(progress, pageUp); complete(); } else { softImage.setLocation(progress, pageUp); } } /** * 动画开关标记 */ private var isActive:Boolean = false; private var pageUp:Boolean = false; /** * 动画开始,播放前需使用setPageTexture指定相关纹理 * @param pageUp * @param velocity */ public function start(pageUp:Boolean = false, velocity = 0.05):void { softImage.originalTexture = (pageUp)?page_3:page_2; softImage.anotherTexture = (pageUp)?page_2:page_3; cacheImageL.texture = page_1; cacheImageR.texture = page_4; this.pageUp = pageUp; this.velocity = velocity; progress = 0; isActive = true; } /** * 动画播放进度[0 - 1] */ private var progress:Number; private var velocity:Number; /** * 动画播放完成 */ private function complete():void { this.stop(); dispatchEvent(new Event(Event.COMPLETE)); } /** * 动画停止 */ public function stop():void { isActive = false; } /** * 验证动画是否正在播放 * @return * */ public function isRunning():Boolean { return isActive; } //4页纹理 private var page_1:Texture; private var page_2:Texture; private var page_3:Texture; private var page_4:Texture; /** * 按页码由小到大顺序传入, * @param tx_1 * @param tx_2 */ public function setSoftPageTexture(tx_1:Texture, tx_2:Texture):void { page_1 = Texture.fromTexture( tx_1, new Rectangle(0, 0, bookWidth/2, bookHeight) ); page_2 = Texture.fromTexture( tx_1, new Rectangle(bookWidth/2, 0, bookWidth/2, bookHeight) ); page_3 = Texture.fromTexture( tx_2, new Rectangle(0, 0, bookWidth/2, bookHeight) );; page_4 = Texture.fromTexture( tx_2, new Rectangle(bookWidth/2, 0, bookWidth/2, bookHeight) ); } /** * 指定不变纹理,用于重绘固定显示页 * @param page_L * @param page_R * */ public function setFixPageTexture(page_L:Texture, page_R:Texture):void { if(!page_1 || (page_1 && page_1 != page_L)) { page_1 = page_L; if(cacheImageL) cacheImageL.texture = page_1; else { cacheImageL = new Image(page_1); this.addChildAt( cacheImageL, 0 ); cacheImageL.touchable = false; } } if(!page_2 || (page_2 && page_2 != page_R)) { page_2 = page_R; if(cacheImageR) cacheImageR.texture = page_2; else { cacheImageR = new Image(page_2); cacheImageR.x = this.bookWidth/2; this.addChildAt( cacheImageR, 0 ); cacheImageR.touchable = false; } } } override public function dispose():void { if(this.hasEventListener(Event.ENTER_FRAME)) this.removeEventListener(Event.ENTER_FRAME, onEnterFrame); if(softImage) softImage.removeFromParent(true); if(cacheImageL) cacheImageL.removeFromParent(true); if(cacheImageR) cacheImageR.removeFromParent(true); super.dispose(); } } }
该类公开了五个用于外部调用的自定义方法:
/** 用于判断当前动画是否已启动 */ public function isRunning():Boolean; /** 停止动画播放 */ public function stop():void; /** 获取到初始显示纹理后,通过该方法传入纹理,动画内部将显示一张图片覆盖原显示对象 */ public function setFixPageTexture(page_L:Texture, page_R:Texture):void; /** 新旧两张纹理获取之后,通过该按页码大小依次传入两张纹理后,调用start()方法即可播放动画 */ public function setSoftPageTexture(tx_1:Texture, tx_2:Texture):void; /** 动画开始播放,播放完成后将该类实例将派发starling.events.Event.COMPLETE事件!velocity 参数用来指定翻页速度 */ public function start(pageUp:Boolean = false, velocity = 0.05):void;
下面是测试用主类,starling的环境配置就不再赘述!
由于我使用的是air环境,所以加载方式跟web环境下的加载会有所不同!有兴趣自己完成吧!
package { import flash.geom.Rectangle; import starling.display.Image; import starling.display.Quad; import starling.display.Sprite; import starling.events.Event; import starling.events.Touch; import starling.events.TouchEvent; import starling.events.TouchPhase; import starling.text.TextField; import starling.textures.RenderTexture; import starling.textures.Texture; import starling.utils.AssetManager; import utils.Animation; public class Main extends Sprite { public function Main() { super(); init(); } private var assets:AssetManager; private var img:Image; //背景图片 private var vec:Vector.<Texture>; //纹理序列 private var ani:Animation; //动画对象 private var main:Sprite; //显示对象容器 private var t_l:TextField; //页码文本左 private var t_r:TextField; //页码文本右 private var crtRender:RenderTexture; //用于保存翻页前的纹理 private var crtL:Texture; private var crtR:Texture; private var tarRender:RenderTexture; //用于保存最终显示效果纹理,即翻页后的纹理 private var tarL:Texture; private var tarR:Texture; private function init():void { main = new Sprite(); this.addChild( main ); crtRender = new RenderTexture(800, 480); tarRender = new RenderTexture(800, 480); //加载图片资源 assets = new AssetManager(); //air下加载 assets.enqueue(File.applicationDirectory.resolvePath("assets/imgs")); assets.loadQueue( function(r:Number):void{ if(r == 1) loadComplete(); }); } private function loadComplete():void { trace("loadComplete"); vec = new Vector.<Texture>(); var tx:Texture; for(var i:int = 0;i<int.MAX_VALUE;i++) { tx = assets.getTexture(String(i)); if(!tx) break; vec[i] = tx; } //初始化现实图片 img = new Image(vec[0]); main.addChild( img ); //页码 t_l = new TextField(100, 30, "", "Verdana", 18, 0xffffff); t_r = new TextField(100, 30, "", "Verdana", 18, 0xffffff); t_l.x = 150, t_r.x = 550; t_l.y = t_r.y = 240; main.addChild( t_l ); main.addChild( t_r ); t_l.text = "Page 1"; t_r.text = "Page 2"; //添加一个比较粗糙的阴影 var quad:Quad = new Quad(100, 480, 0x000000); quad.x = 400; main.addChild( quad ); quad.setVertexAlpha(0, .8); quad.setVertexAlpha(1, 0); quad.setVertexAlpha(2, .8); quad.setVertexAlpha(3, 0); quad = new Quad(80, 480, 0x000000); quad.x = 400 - quad.width; main.addChild( quad ); quad.setVertexAlpha(0, 0); quad.setVertexAlpha(1, .8); quad.setVertexAlpha(2, 0); quad.setVertexAlpha(3, .8); //初始化翻页动画 ani = new Animation( 800, 480 ); this.addChild( ani ); ani.visible = false; ani.addEventListener(Event.COMPLETE, aniComplete); this.addEventListener(TouchEvent.TOUCH, onTouch); } //动画完成 private function aniComplete():void { //隐藏动画组件,于是就显示出了实际画面 ani.visible = false; crtRender.clear(); tarRender.clear(); } private function onTouch(e:TouchEvent):void { //动画播放中 if(ani.isRunning()) return; var touch:Touch = e.getTouch(this); if(touch) { if(touch.phase == TouchPhase.BEGAN) { startP = touch.getLocation(this).x; } else if(touch.phase == TouchPhase.ENDED) { var p:int = touch.getLocation(this).x; //判断翻动方向 if(p != startP) pageUp = (p > startP); else return; //判断页码是否超出 if( (pageUp && crt == 0) || ((!pageUp) && crt == vec.length-1)) return; //获取当前显示对象纹理数据 crtRender.draw( main ); crtL = Texture.fromTexture( crtRender, new Rectangle(0, 0, 400, 480)); crtR = Texture.fromTexture( crtRender, new Rectangle(400, 0, 400, 480)); //显示ani对象,遮挡img ani.setFixPageTexture( crtL, crtR ); ani.visible = true; //变更img纹理 crt += pageUp ? - 1 : 1; img.texture = vec[crt]; img.readjustSize(); //重写页码 t_l.text = "Page " + (crt*2+1); t_r.text = "Page " + 2*(crt+1); //获取用于替换旧纹理的新纹理数据 tarRender.draw( main ); tarL = Texture.fromTexture( tarRender, new Rectangle(0, 0, 400, 480)); tarR = Texture.fromTexture( tarRender, new Rectangle(400, 0, 400, 480)); if( pageUp ) ani.setSoftPageTexture( tarRender, crtRender ); // else ani.setSoftPageTexture( crtRender, tarRender ); // if( pageUp ) // ani.setSoftPageTexture( tarL, tarR, crtL, crtR ); // // else // ani.setSoftPageTexture( crtL, crtR ,tarL, tarR ); ani.start( pageUp, 0.05 ); } } } private var startP:int = 0; private var pageUp:Boolean; private var crt:int = 0; override public function dispose():void { //太麻烦了,不写了…… super.dispose(); } } }