Fork me on GitHub
音乐播放器

最近在做一个音乐播放器,纯粹练手,前端使用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

 

 

一颗平常心,踏踏实实,平静对待一切
 
posted on 2013-01-23 16:44  HackerVirus  阅读(292)  评论(0编辑  收藏  举报