最近在做一个音乐播放器,纯粹练手,前端使用FLex,后台使用JAVA,现在慢慢的在实现,主要涉及的技术还在不断学习中:
这里也一点一点记录下来和大家分享哈。
自定义组件:(左边是一个播放列表,右边是音乐播放控件)
自定义组件要继承SkinnableComponent类。主要有两部分组成,一个是组件的功能逻辑,一个是皮肤。
功能逻辑和普通的as写法一样,要用到皮肤就需要遵守皮肤的契约.看看下面的代码:
(音乐播放控件:PlayerControlBar.as):
1 package components 2 { 3 import events.PlayEvent; 4 import events.StopEvent; 5 6 import flash.events.Event; 7 import flash.media.Sound; 8 import flash.media.SoundChannel; 9 import flash.net.URLRequest; 10 11 import mx.controls.Alert; 12 import mx.controls.HSlider; 13 import mx.controls.sliderClasses.Slider; 14 import mx.events.SliderEvent; 15 import mx.messaging.AbstractConsumer; 16 17 import service.IPlayController; 18 import service.impl.PlayController; 19 20 import spark.components.Button; 21 import spark.components.TextArea; 22 import spark.components.supportClasses.SkinnableComponent; 23 24 [SkinState("stop")] 25 [SkinState("run")] 26 /**播放控制栏组件*/ 27 public class PlayerControlBar extends SkinnableComponent 28 { 29 [SkinPart(required="true")] 30 public var lyricText:TextArea; 31 [SkinPart(required="true")] 32 public var playSlider:HSlider; 33 [SkinPart(required="true")] 34 public var preButton:Button; 35 [SkinPart(required="true")] 36 public var stateButton:Button; 37 [SkinPart(required="true")] 38 public var nextButton:Button; 39 [SkinPart(required="true")] 40 public var stopButton:Button; 41 42 public function PlayerControlBar() 43 { 44 super(); 45 //添加播放状态更改的监听器 46 this.addEventListener(PlayEvent.PLAY, handleStateButtonClick); 47 this.addEventListener(StopEvent.STOP, handleStopButtonClick); 48 this.addEventListener(SliderEvent.CHANGE, handlePlaySilderChange); 49 } 50 51 /**是否在播放*/ 52 public var isStart:Boolean = false; 53 /**音乐播放控制器*/ 54 private var playController:IPlayController; 55 /**播放状态改变的处理函数*/ 56 private function handleStateButtonClick(event:PlayEvent):void 57 { 58 if(!isStart) 59 { 60 //加载音乐并开始播放 61 playController = new PlayController(this); 62 playController.start("gole.mp3"); 63 isStart = true; 64 //改变皮肤的状态 65 this.skin.currentState="stop"; 66 } 67 else if(this.skin.currentState == "stop") 68 { 69 //暂停播放音乐 70 playController.pause(); 71 this.skin.currentState="run"; 72 } 73 else if(this.skin.currentState == "run") 74 { 75 //开始音乐播放 76 playController.play(); 77 this.skin.currentState="stop"; 78 } 79 } 80 81 private function handleStopButtonClick(e:StopEvent):void 82 { 83 isStart = false; 84 this.skin.currentState = "run"; 85 if(playController) 86 playController.stop(true); 87 } 88 89 //活动条拉动的触发函数,从指定位置开始播放 90 private function handlePlaySilderChange(e:SliderEvent):void 91 { 92 if(isStart) 93 { 94 (playController as PlayController).clickPlay(); 95 if(this.skin.currentState == "run") 96 this.skin.currentState = "stop"; 97 } 98 } 99 } 100 }
看到24~25行为自定义组件加了两个SkinState标注,这个是皮肤的状态,
第29~40行为几个组件加了[SkinPart(required="true")]标注,这个是皮肤必须要拥有的控件
皮肤的契约如下图:
利用flex builder的功能可以为自定义组件添加皮肤,它会根据皮肤的契约自动生成提示:
如下:
(音乐播放控件的皮肤:PlayerControlBarSkin.mxml):
1 <?xml version="1.0" encoding="utf-8"?> 2 <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" 3 xmlns:s="library://ns.adobe.com/flex/spark" 4 xmlns:mx="library://ns.adobe.com/flex/mx" 5 creationComplete="this.currentState = 'run'" 6 > 7 <!-- host component --> 8 <fx:Metadata> 9 [HostComponent("components.PlayerControlBar")] 10 </fx:Metadata> 11 <fx:Script> 12 <![CDATA[ 13 import events.PlayEvent; 14 import events.StopEvent; 15 16 import mx.events.SliderEvent; 17 ]]> 18 </fx:Script> 19 20 <!-- states --> 21 <s:states> 22 <s:State id="runState" name="run"/> 23 <s:State id="stopState" name="stop" /> 24 </s:states> 25 26 <!-- SkinParts 27 name=lyricText, type=spark.components.TextArea, required=true 28 name=stateButton, type=spark.components.Button, required=true 29 name=nextButton, type=spark.components.Button, required=true 30 name=preButton, type=spark.components.Button, required=true 31 name=stopButton, type=spark.components.Button, required=true 32 --> 33 34 <s:Group width="700" height="600"> 35 <s:Image source="../asserts/img/background.jpg" alpha=".6"/> 36 37 <s:VGroup width="100%" height="100%" horizontalAlign="center" paddingTop="20"> 38 <s:Group width="60%" height="80%" horizontalCenter="0"> 39 <s:TextArea id="lyricText" width="100%" height="100%" alpha=".8" borderVisible="false"> 40 </s:TextArea> 41 </s:Group> 42 <s:HGroup width="55%" verticalAlign="middle"> 43 <mx:HSlider id="playSlider" width="100%" height="100%" minimum="0" maximum="100" 44 change="dispatchEvent(new SliderEvent(SliderEvent.CHANGE,true))"/> 45 </s:HGroup> 46 <s:HGroup width="60%" horizontalAlign="center" paddingBottom="10"> 47 <s:Button id="preButton" skinClass="skins.PreButtonSkin"/> 48 <s:Button left="15" id="stateButton" skinClass.run="skins.PlayButtonSkin" skinClass.stop="skins.PauseButtonSkin" click="dispatchEvent(new PlayEvent(PlayEvent.PLAY))"/> 49 <s:Button left="15" id="nextButton" skinClass="skins.NextButtonSkin"/> 50 <s:Button left="15" id="stopButton" skinClass="skins.StopButtonSkin" click="dispatchEvent(new StopEvent(StopEvent.STOP))"/> 51 </s:HGroup> 52 </s:VGroup> 53 </s:Group> 54 </s:Skin>
自定义组件的好处是将逻辑内部封装好,安全也便于维护,通过皮肤改变外观也是很方便的,需要对外的服务只要提供接口就可以了。
在主界面使用自定义组件:
1 <s:HGroup horizontalAlign="center" verticalAlign="middle" horizontalCenter="0" verticalCenter="0"> 2 <components:MusicList skinClass="skins.MusicListSkin" listContent="{musicList}" creationComplete="initMusicList()"/> 3 <components:PlayerControlBar skinClass="skins.PlayeControlBarSkin"/> 4 </s:HGroup>
运行效果(自定义组件PlayerControlBar):
自定义事件和事件派发:
事件流有三个阶段:捕获阶段--->目标阶段--->冒泡阶段
1.捕获阶段(从根节点到子节点,检测对象是否注册了监听器,是则调用监听函数)
2.目标阶段(调用目标对象本身注册的监听程序)
3.冒泡阶段(从目标节点到根节点,检测对象是否注册了监听器,是则调用监听函数)
注:事件发生后,每个节点可以有2个机会(2选1)响应事件,默认关闭捕获阶段。
从上到下(从根到目标)是捕获阶段,到达了目标后是目标阶段,然后从目标向上返回是冒泡阶段。
这里需要注意的是:如果派发事件的源(调用dispatchEvent方法)没有在一组容器里,那么这组容器里面的控件是监听不到这个派发事件的。
如下,在点击stateButton按钮的时候会派发一个自定义的事件PlayEvent, 然后在自定义组件PlayControlBar中添加
监听并作出相应处理:
<s:Button left="15" id="stateButton" skinClass.run="skins.PlayButtonSkin" skinClass.stop="skins.PauseButtonSkin" click="dispatchEvent(new PlayEvent(PlayEvent.PLAY))"/>
(自定义的事件:PlayEvent.as) :
1 package events 2 { 3 import flash.events.Event; 4 5 import mx.states.OverrideBase; 6 7 public class PlayEvent extends Event 8 { 9 public static const PLAY:String = "play"; 10 11 public function PlayEvent(type:String = "play", bubbles:Boolean=true, cancelable:Boolean=false) 12 { 13 super(type, bubbles, cancelable); 14 } 15 16 override public function clone():Event 17 { 18 return new PlayEvent(); 19 } 20 } 21 }
自定义事件要继承Event类和重写clone方法。构造函数的第二参数表示是否要执行冒泡,如果不冒泡,父容器就捕获不到事件
添加监听:
public function PlayerControlBar() { super(); //添加播放状态更改的监听器 this.addEventListener(PlayEvent.PLAY, handleStateButtonClick); ……………… }
事件处理函数:
1 private function handleStateButtonClick(event:PlayEvent):void 2 { 3 if(!isStart) 4 { 5 //加载音乐并开始播放 6 playController = new PlayController(this); 7 playController.start("gole.mp3"); 8 isStart = true; 9 //改变皮肤的状态 10 this.skin.currentState="stop"; 11 } 12 else if(this.skin.currentState == "stop") 13 { 14 //暂停播放音乐 15 playController.pause(); 16 this.skin.currentState="run"; 17 } 18 else if(this.skin.currentState == "run") 19 { 20 //开始音乐播放 21 playController.play(); 22 this.skin.currentState="stop"; 23 } 24 }
音频播放的处理:
Flex中音频的播放主要是靠Sound和SoundChannel两个类来实现的。
具体的使用Adobe的官方文档讲得非常详细,地址是:
http://help.adobe.com/zh_CN/FlashPlatform/reference/actionscript/3/flash/media/Sound.html
(控制音乐播放类:PlayController.as):
1 package service.impl 2 { 3 import components.PlayerControlBar; 4 5 import flash.display.DisplayObject; 6 import flash.events.Event; 7 import flash.events.TimerEvent; 8 import flash.media.Sound; 9 import flash.media.SoundChannel; 10 import flash.net.URLRequest; 11 import flash.utils.Timer; 12 13 import mx.managers.CursorManager; 14 15 import service.IPlayController; 16 17 /**音乐播放控制类*/ 18 public class PlayController implements IPlayController 19 { 20 private var sound:Sound; 21 private var soundChannel:SoundChannel; 22 private var _pausePosition:int; 23 private var _derectory:String = "../music/" 24 /**实时记录播放进度*/ 25 private var timer:Timer; 26 private var view:PlayerControlBar; 27 28 public function PlayController(view:DisplayObject) 29 { 30 this.view = view as PlayerControlBar; 31 } 32 33 34 /**音乐播放暂停位置*/ 35 public function get pausePosition():int 36 { 37 return _pausePosition; 38 } 39 40 /** 41 * @private 42 */ 43 public function set pausePosition(value:int):void 44 { 45 _pausePosition = value; 46 } 47 48 /**音乐存放的目录*/ 49 public function get derectory():String 50 { 51 return _derectory; 52 } 53 54 public function set derectory(value:String):void 55 { 56 _derectory = value; 57 } 58 59 public function start(music:String):void 60 { 61 sound = new Sound(); 62 timer = new Timer(1000); 63 var urlRequest:URLRequest = new URLRequest(derectory + music); 64 sound.addEventListener(Event.COMPLETE, function handleStart(e:Event):void 65 { 66 soundChannel = sound.play(); 67 //增加音乐播放完毕的监听器 68 soundChannel.addEventListener(Event.SOUND_COMPLETE, handlePlayEnd); 69 timer.start(); 70 sound.removeEventListener(Event.COMPLETE,handleStart); 71 } 72 ); 73 timer.addEventListener(TimerEvent.TIMER, handleTimmerWork); 74 sound.load(urlRequest); 75 } 76 77 /*音乐播放结束处理函数*/ 78 private function handlePlayEnd(e:Event):void 79 { 80 stop(true); 81 view.skin.currentState = "run"; 82 view.isStart = false; 83 soundChannel.removeEventListener(Event.SOUND_COMPLETE,handlePlayEnd); 84 } 85 86 /*每隔一秒,刷新进度条*/ 87 private function handleTimmerWork(e:TimerEvent):void 88 { 89 var estimatedLength:int = Math.ceil(sound.length / (sound.bytesLoaded / sound.bytesTotal)); 90 var playbackPercent:uint = Math.round(100 * (soundChannel.position / estimatedLength)); 91 view.playSlider.value = playbackPercent; 92 } 93 94 public function pause():void 95 { 96 if(soundChannel) 97 { 98 pausePosition = soundChannel.position; 99 stop(); 100 } 101 } 102 103 public function play():void 104 { 105 if(sound) 106 { 107 soundChannel = sound.play(pausePosition); 108 soundChannel.addEventListener(Event.SOUND_COMPLETE, handlePlayEnd); 109 } 110 if(!timer.running) 111 timer.start(); 112 } 113 114 public function stop(isExit:Boolean = false):void 115 { 116 if(soundChannel) 117 { 118 soundChannel.stop(); 119 soundChannel.removeEventListener(Event.SOUND_COMPLETE,handlePlayEnd); 120 } 121 if(timer.running) 122 timer.stop(); 123 if(isExit) 124 timer.removeEventListener(TimerEvent.TIMER,handleTimmerWork); 125 } 126 127 /**由Slider触发的播放*/ 128 public function clickPlay():void 129 { 130 //根据拖动的位置计算实际音乐播放的位置 131 var percent:Number = view.playSlider.value / view.playSlider.maximum; 132 var estimatedLength:uint = Math.ceil(sound.length / (sound.bytesLoaded / sound.bytesTotal)); 133 var position:uint = Math.round(percent * estimatedLength); 134 pause(); 135 pausePosition = position; 136 play(); 137 } 138 } 139 }
第59至75行是第一次点击播放时调用的方法,第64行Sound增加了一个事件监听,是在音乐加载完后执行的,
这个如果要边加载边播放的时候不适用,可以参考官方文档来解决这个问题。第94~101行中是点击暂停时调用
的方法,暂停的时候要把音乐播放的位置记录下来,如98行,这是为了要在继续播放的时候找到起点。第103
至112行是继续播放函数。
Timer类的使用实现播放进度实时更新:
当音乐播放的时候,这个播放进度条会每个同步的移动位置,拖动进度条,音乐也会播放到相应的位置。
进度条控件:
<s:HGroup width="55%" verticalAlign="middle"> <mx:HSlider id="playSlider" width="100%" height="100%" minimum="0" maximum="100" change="dispatchEvent(new SliderEvent(SliderEvent.CHANGE,true))"/> </s:HGroup>
当进度条通过拖动或者点击改变值的时候会派发自定义事件SliderEvent,这个在自定义组件PlayerControlBar中进行监听和处理.
public function PlayerControlBar() { super(); ………………this.addEventListener(SliderEvent.CHANGE, handlePlaySilderChange); }
处理函数:
//进度条拉动的触发函数,从指定位置开始播放 private function handlePlaySilderChange(e:SliderEvent):void { if(isStart) { (playController as PlayController).clickPlay(); if(this.skin.currentState == "run") this.skin.currentState = "stop"; } }
clickplay方法:
1 /**由Slider触发的播放*/ 2 public function clickPlay():void 3 { 4 //根据拖动的位置计算实际音乐播放的位置 5 var percent:Number = view.playSlider.value / view.playSlider.maximum; 6 var estimatedLength:uint = Math.ceil(sound.length / (sound.bytesLoaded / sound.bytesTotal)); 7 var position:uint = Math.round(percent * estimatedLength); 8 pause(); 9 pausePosition = position; 10 play(); 11 }
第5~7行是计算当前进度条拖动的进度对应的音乐播放位置。
实时更新播放进度条:
1 public function start(music:String):void 2 { 3 sound = new Sound(); 4 timer = new Timer(1000); 5 var urlRequest:URLRequest = new URLRequest(derectory + music); 6 sound.addEventListener(Event.COMPLETE, function handleStart(e:Event):void 7 { 8 soundChannel = sound.play(); 9 //增加音乐播放完毕的监听器 10 soundChannel.addEventListener(Event.SOUND_COMPLETE, handlePlayEnd); 11 timer.start(); 12 sound.removeEventListener(Event.COMPLETE,handleStart); 13 } 14 ); 15 timer.addEventListener(TimerEvent.TIMER, handleTimmerWork); 16 sound.load(urlRequest); 17 }
第4行,在点击音乐播放的时候新建一个Timer类,并规定1秒执行一次,第11行是音乐播放的时候开始启动这个timer,第15行
中添加timer触发的事件。处理函数如下:
/*每隔一秒,刷新进度条*/ private function handleTimmerWork(e:TimerEvent):void { var estimatedLength:int = Math.ceil(sound.length / (sound.bytesLoaded / sound.bytesTotal)); var playbackPercent:uint = Math.round(100 * (soundChannel.position / estimatedLength)); view.playSlider.value = playbackPercent; }
当停止音乐播放的时候也停止timer,开始播放音乐的时候启动timer:
1 public function stop(isExit:Boolean = false):void 2 { 3 if(soundChannel) 4 { 5 soundChannel.stop(); 6 soundChannel.removeEventListener(Event.SOUND_COMPLETE,handlePlayEnd); 7 } 8 if(timer.running) 9 timer.stop(); 10 if(isExit) 11 timer.removeEventListener(TimerEvent.TIMER,handleTimmerWork); 12 }
1 public function play():void 2 { 3 if(sound) 4 { 5 soundChannel = sound.play(pausePosition); 6 soundChannel.addEventListener(Event.SOUND_COMPLETE, handlePlayEnd); 7 } 8 if(!timer.running) 9 timer.start(); 10 }
前端(FLEX)和服务器端(JAVA)之间的通信:
这个是通过Socket来实现的.
JAVA端的Socket编程若要和Flex端通信并且传递对象,就需要用到AMF序列化,这个Adobe为我们实现了,
只需要调用接口就可以了。Adobe的这个框架叫做blazeds,在官网可以下载到,为了方便大家,这里给出了
下载地址:blazeds.zip BlazeDS开发者指南
java服务端:
1 public class MusicServer { 2 private ServerSocket serverSocket; 3 private Socket clientSocket; 4 5 public MusicServer(int port) 6 { 7 try { 8 serverSocket = new ServerSocket(port); 9 clientSocket = serverSocket.accept(); 10 ClientSocketManager.addClient(clientSocket); 11 MusicListService.sendMusicList(clientSocket); 12 } catch (IOException e) { 13 e.printStackTrace(); 14 } 15 } 16 17 18 public static void main(String[] args) { 19 new MusicServer(9000); 20 } 21 }
第11行是socket输入,输出流处理的类,主要是把服务端里面的所有音乐的文件名发送给客户端。
ClientSocketManager.java:
1 public class MusicListService { 2 3 public static void sendMusicList(Socket socket) 4 { 5 try { 6 InputStream input = socket.getInputStream(); 7 OutputStream outputStream = socket.getOutputStream(); 8 Amf3Output amfoutput = new Amf3Output(new SerializationContext()); 9 10 while(true) 11 { 12 int index = 0; 13 byte[] buffer = new byte[100]; 14 StringBuffer requestState = new StringBuffer(); 15 while(-1 != (index = input.read(buffer, 0, buffer.length))) 16 { 17 String value = new String(buffer, 0, index); 18 requestState.append(value); 19 20 ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); 21 DataOutputStream output = new DataOutputStream(byteOutput); 22 amfoutput.setOutputStream(output); 23 MusicList musicList = MusicListGet.getMusicList(); 24 amfoutput.writeObject(musicList); 25 output.flush(); 26 27 byte[] data = byteOutput.toByteArray(); 28 outputStream.write(data); 29 outputStream.flush(); 30 } 31 32 break; 33 } 34 } catch (IOException e) { 35 e.printStackTrace(); 36 }finally 37 { 38 try { 39 socket.close(); 40 ClientSocketManager.removeClient(socket); 41 } catch (IOException e) { 42 e.printStackTrace(); 43 } 44 } 45 } 46 }
第8行中导入了Amf3Output类,这个类就是blazeds.zip包下面的,导入到项目中就可以了。
Flex客户端:
1 public class SocketController extends EventDispatcher 2 { 3 private static var socket:Socket = null; 4 private var view:DisplayObject; 5 6 public function SocketController(host:String = null, port:int = 0, view:DisplayObject = null) 7 { 8 if(!socket) 9 socket = new Socket(); 10 this.view = view; 11 configureListener(); 12 if(host && port != 0) 13 { 14 socket.connect(host,port); 15 } 16 } 17 18 19 private function configureListener():void 20 { 21 socket.addEventListener(Event.CONNECT, handleConnect); 22 socket.addEventListener(Event.CLOSE, handleClose); 23 socket.addEventListener(ProgressEvent.SOCKET_DATA, handleRecieve); 24 } 25 26 private function handleConnect(e:Event):void 27 { 28 socket.writeUTFBytes(RequestState.REQUESTLIST); 29 socket.flush(); 30 } 31 32 private function handleClose(e:Event):void 33 { 34 if(socket.connected) 35 socket.close(); 36 socket = null; 37 } 38 39 private function handleRecieve(e:Event):void 40 { 41 var obj:Object = socket.readObject(); 42 if(socket.connected) 43 socket.close(); 44 var musicList:ArrayCollection = obj.musicNames; 45 if(view) 46 view.dispatchEvent(new MusicListEvent(MusicListEvent.LISTRECIEVE,musicList)); 47 } 48 49 }
Flex的socket都是异步方式来实现的,通过事件来处理,可以看到第21~23行为socket添加了几个事件监听,
第一个是建立连接成功的事件监听,第二个是连接关闭的监听,第三个是得到服务端返回消息的监听。
程序还在不断的完善,本人也在学习当中,如果有兴趣的朋友,可以告诉我好的学习资料,或有什么好的建议,也希望告诉我哦.
下面是程序的源码地址:
audioplayer.rar