• 音乐播放器


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

    一颗平常心,踏踏实实,平静对待一切
     
  • 相关阅读:
    大数据HIve
    大数据笔记
    [Leetcode]653.Two Sum IV
    [Leetcode]652.Find Duplicate Subtrees
    [Leetcode]650.2 Keys Keyboard
    [Leetcode]648.Replace Words
    [Leetcode Weekly Contest]173
    [总结]最短路径算法
    [Leetcode]647.Palindromic Substrings
    [Leetcode]646.Maximum Length of Pair Chain
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/2873427.html
Copyright © 2020-2023  润新知