最近在做一個音樂播放器,純粹練手,前端使用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添加了幾個事件監聽,
第一個是建立連接成功的事件監聽,第二個是連接關閉的監聽,第三個是得到服務端返回消息的監聽。
程序還在不斷的完善,本人也在學習當中,如果有興趣的朋友,可以告訴我好的學習資料,或有什么好的建議,也希望告訴我哦.