• 网络收音机资料收集


       最近打算利用业余时间,编写一个Android的网络收音机。因为我自己偶尔也喜欢听听广播,所以打算用业余时间编写一个网络版收音机。说起收音机,其实在工作中已经编写过一个,不过那个收音机是需要硬件支持,也就是说需要有特定的收音机芯片才可以使用。因为这个要跟芯片通信,还涉及通信协议。所以无法通用,手机上更加使用不了。所以才打算编写一个网络版的收音机。

      因为这个项目打算利用业余空闲时间来做,所以进度可能会比较慢一些,目前想法是把它做得完善一些,网上有关网络收音机的开源项目貌似很少,我找了一下,没发现有成型的项目。等我把程序框架和基本功能实现了,打算把这个项目做成开源项目。下面是目前实现了简单的播放测试功能界面,这个只是测试用的第一版,后面会根据功能模块增加进行调整。

        

     

    1、网络接收模块

      做网络收音机最核心的功能模块就是如何接收解析网络数据,目前网络电台的广播协议主要是使用了mms、rtsp、http等多媒体流。Android上面对这些流媒体支持实在是不怎么样,不知道是不是竞争对手的原因,毕竟mms是微软的标准。因此只能找第三方库实现,最后选了两种方案:第一使用VLC播放器的解码库。前面我也写了一篇编译VLC的文章。不过还没时间分析VLC使用和播放核心。第二种是使用目前开放的第三方库。最后我暂时选了Vitamio作为解码库。

     

    2、功能需求

    • 支持mms、rtsp、http多媒体流播放
    • 支持国内绝大部分可以播放的网络电台以及国外部分著名电台
    • 支持收藏电台
    • 支持电台录音保存
    • 支持音效调节

     

    3、UI设计规划

    • 打算使用侧滑栏实现电台列表
    • 使用多级列表实现电台列表管理
    • UI界面和数据分离,界面数据由XML提供
    • 实现动态音效调节界面

     

    4、开发计划

    • 2013-7月:实现播放界面、电台列表、播放管理、后台播放
    • 2013-8月:实现收藏电台、电台录音、音效调节
    • 2013-9月:后续规则中。。。

     

     5、后语

      目前的UI还比较粗糙,界面还没有仔细进行调节,只是为了测试播放电台功能,先把功能加上去。后续会继续完善,播放部分模块现在还在调试,打算把播放部分编写成独立的模块,降低代码的耦合度方便以后更换播放和解码核心代码。界面部分尽量独立出来。

      虽然自己已经做过很多Android方面的程序,不过都是基于工作上的开发。自己开发一个业余项目还是第一次,所以希望能把这项目做得完善点,不过功能上不会加入太多其他不相关的东西进去,现在Android市场上的软件,一个软件集成太多不相关的东西,用着复杂,也浪费手机资源。这个收音机只会加入收音机相关功能,而且尽量做到简洁易用。大家如果有这方面兴趣,可以提提意见,看希望有什么功能。

      目前我本身的工作很忙,在开发一个新项目,经常要加班,回到家已经10点了。所以只能业余晚上回去才有时间做,所以进度安排比较慢。后续会把开发过程写成一个系列博客,用到的技术也会进行 一些分析讲解。

     

    上一篇文章总体规划了这个项目的情况,今天讲讲实现电台列表。今天其实主要想讲解的是SlidingMenu,也就是我们平时说的侧滑栏,现在很多应用都有用这种UI效果。SlidingMenu侧滑栏功能实现的方式很多,可以自己使用ViewGroup实现也可以自己绘图实现。我这里借用了一个开源项目SlidingMenu,因为我这里不是研究如何实现SlidingMenu,而且为了快速实现这种功能,所以就直接使用这个开源项目的成果。

     

     

     

      

    上面就是侧滑栏的效果,指定一边滑动,就可以拉出一个新的界面出来。

     

    1、工程里引用SlidingMenu

      首先我们说说这个开源工程如何使用,因为SlidingMenu是以工程库的形式使用,因此我们只需要在我们的工程里面应用这个工程就行。下面说说具体步骤:(这是给初学者看的哈,有经验的跳过)

     

      我们添加SlidingMenu后,可以查看该工程属性,其中有一个Library的属性,说明这是一个Library工程。然后只需要在我们使用的工程里添加应用这个外部库就可以。

     

    这里添加外部Library,因为我还引用了另外一个解码库Vitamio,所以会有两个外部库。

     

    2、初始化SlidingMenu

    复制代码
    //Edited by mythou
    //http://www.cnblogs.com/mythou/
      private void initChannelMenu()
        {
         //创建SlidingMenu对象 mChannelMenu = new SlidingMenu(this);
         //设置侧滑栏菜单位置,这里在左边。拉动菜单时,会从左边弹出 mChannelMenu.setMode(SlidingMenu.LEFT);
         //设置触摸的范围,这里设置全屏 mChannelMenu.setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN);
         //设置阴影的宽度,查看上面第二张效果图,靠右边的位置,有一个阴影过渡。就是这个东西 mChannelMenu.setShadowWidthRes(R.dimen.shadow_width);
         //这里是阴影效果,可以设置图片或者一个颜色过渡 mChannelMenu.setShadowDrawable(R.drawable.shadow);
         //设置后面间距,侧滑栏和原来界面间距 mChannelMenu.setBehindOffsetRes(R.dimen.slidingmenu_offset);
         //边框的角度,这里指边界地方 mChannelMenu.setFadeDegree(0.35f);
         //把侧滑栏关联到当前的Activity mChannelMenu.attachToActivity(this, SlidingMenu.SLIDING_CONTENT);
         //侧滑栏的布局文件 mChannelMenu.setMenu(R.layout.channel_slide_menu); }
    复制代码

      上面给出了SlidingMenu的详细初始化配置,我们使用的时候可以根据需要的实际效果配置,达到我们需要的效果。SlidingMenu这个开源工程接口实现很好,可以实现很复杂的配置,同时使用的过程也十分简单,如果你只是单纯需要这种功能,借用这个开源项目是不错的选择。如果需要自己实现,也可以借鉴一下这工程。

     

    3、弹出和收起SlidingMenu

      一帮情况下,只有设置了上面的mChannelMenu.setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN) ,我们只要在屏幕上面向右滑动就可以把SlidingMenu拉出来,不过有时候我们也需要实现点击某个按钮就可以把它拉出来,就像我们上面的“电台列表”按钮,点击一下会自动弹出SlidingMenu处理,要实现这功能很简单,只要调动SlidingMenu一个借口即可。

    复制代码
    //Edited by mythou
    //http://www.cnblogs.com/mythou/
    mOpenMenuButton.setOnClickListener(new OnClickListener()
    {
        @Override
        public void onClick(View v)
        {
        //调用SlidingMenu的显示菜单接口 mChannelMenu.showMenu(); } });
    复制代码

    上面通过调用showMenu()即可显示菜单。隐藏SlidingMenu同样只需要调用一个接口

    复制代码
    
    
    //Edited by mythou
    //http://www.cnblogs.com/mythou/
    @Override
    public void onBackPressed()
    {
         if (mChannelMenu.isMenuShowing()) 
         {
         //隐藏SlidingMenu,这里的Content就是我们的主Activity。 mChannelMenu.showContent(); } else { super.onBackPressed(); } }
    复制代码

    我这里放在了返回键的处理,按下返回键的时候,自动收起SlidingMenu菜单。

     

    4、总结

      SlidingMenu是一个在Git上很火也很实用的项目,通过使用该项目可以快速在我们的项目中使用侧滑栏功能,只要按照我上面配置在新工程引用SlidingMenu就可以。另外补充一点,SlidingMenu提供了一个例子,不过需要用到另外一个开源工程ActionBarSherlock ,如果要配置该例子,注意引用这工程。不过我配置的时候遇到不少问题,建议大家按照我上面方法直接新建一个工程,先看看SlidingMenu的使用和效果。

    上一篇文章说了使用SlidingMenu开源项目实现侧滑栏,今天主要是讲解多级列表ExpandableListView的使用,以及如何使用它实现电台分类管理。ExpandableListView是Android自带的一个实现多级列表的控件,可以理解为ListView的二维实现。下面将针对如何在项目里面使用ExpandableListView进行讲解。

     

    ExpandableListView效果图:

     

     

    1、引用ExpandableListView控件

    复制代码
    
    
    //Edited by mythou
    //http://www.cnblogs.com/mythou/
       <ExpandableListView 
            android:id="@+id/list"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_marginTop="0dp"
            android:background="@drawable/radio_play_bg2"   //配置外层列表的背景熟悉
            android:divider="@drawable/list_driver"     //外层列表分割线
            android:cacheColorHint="#00000000"        //这个用过listView的应该都知道,避免拖动黑屏
            android:listSelector="@drawable/listview_bg"  //外层List列表项效果
            android:childDivider="@drawable/list_driver"  //子列表分割线效果
            >
        </ExpandableListView>
    复制代码

      上面是我们平常在XML里面配置的属性,这个跟我们一般使用ListView很类似,只是它多了一个子列表项的配置,后面我们会使用代码配置其他一些属性。ExpandableListView用法跟我们普通ListView类似,只是分开了父类ListView和子类ListView。ExpandableListView还有其他的一些属性,有需要可以查看android官网文档。

     

    2、继承BaseExpandableListAdapter

    复制代码
    
    
    //Edited by mythou
    //http://www.cnblogs.com/mythou/
    public class ChannelListAdapter extends BaseExpandableListAdapter
    {
        //设置context
        Context mContext;
    
        //.............
    }
    复制代码

     

    继承BaseExpandableListAdapter,编写提供数据的适配器

     

     

    3、数据适配器

      我们使用ListView的时候也知道,ListView需要提供一个数据适配器提供数据,ExpandableListView也同样需要一个适配器,我们需要重载BaseExpandableListAdapter基类,编写我们的适配器。其中最主要的是getGroupView()和getChildView()方法,这两个方法分别返回父类列表和子类列表的列表项视图。

     

    复制代码
    
    
    //Edited by mythou
    //http://www.cnblogs.com/mythou/
       @Override
        public View getGroupView(int groupPosition, boolean isExpanded,
                View convertView, ViewGroup parent) 
        {
            //生成一个LinearLayout的布局,这里是外层列表的一个列表项视图
            LinearLayout ll = new LinearLayout(mContext);
         //设置LinearLayout方向 ll.setOrientation(0);
         //设置列表前面的Logo ImageView logo = new ImageView(mContext);
         //这里提供一个图片文件作为Logo logo.setImageResource(logos[groupPosition]);
         //设置边距 logo.setPadding(80, 0, 0, 0);
         //ImageView添加到LinearLayout里面 ll.addView(logo);
         //列表项标题 TextView textView = getTextView(); textView.setTextColor(Color.WHITE);
         //获取标题资源 textView.setText(getGroup(groupPosition).toString()); ll.addView(textView); return ll; }
    复制代码

     

      上面返回的View就是我们外层列表的一个列表项,主要包含一个ImageView和TextView控件,使用LinearLayout布局。当然,你也可以使用一个XML文件来布局然后通过Inflate生成一个View返回。两种方法都可以,还有一点需要补充的是,提供的图标资源和Text的文字,这里你可以固定在程序,也可以使用数组提供。我后面因为是关联了电台的数据,所以使用了XML来保存电台数据,然后通过解析XML返回一个ArraryList来提供数据,这个后面我讲电台数据的时候,会仔细讲解。

    复制代码
    
    
    //Edited by mythou
    //http://www.cnblogs.com/mythou/
      @Override
        public View getChildView(int groupPosition, int childPosition,
                boolean isLastChild, View convertView, ViewGroup parent) 
        {
            //创建布局使用的LinearLayout
            LinearLayout ll = new LinearLayout(mContext);
         //设置布局方向 ll.setOrientation(0);
         //Logo图片 ImageView generallogo = new ImageView(mContext); generallogo .setImageResource(generallogos[groupPosition][childPosition]); ll.addView(generallogo);
         //标题资源 TextView textView = getTextView(); textView.setText(getChild(groupPosition, childPosition) .toString()); ll.addView(textView);
         //返回LinearLayout作为子视图 return ll; }
    复制代码

      上面是二级列表的列表项的视图构造,跟外层列表的构造几乎一样,这里同样构造了一个LinearLayout的布局视图。你也可以编写一个XML的布局,然后加载获取一个View视图返回。同样,这里提供数据的方法,我后面也是改用了XML获取数据,这里先不多说。

      除了上面两个获取视图的方法,剩下几个获取选项的列表数目的方法同样也需要重写,为列表提供数据。

    复制代码
    
    
    //Edited by mythou
    //http://www.cnblogs.com/mythou/
       @Override
        public Object getGroup(int groupPosition) 
        {
            //返回外层列表的对象
            return generalsTypes[groupPosition];
        }
    
        @Override
        public long getGroupId(int groupPosition) 
        {
            //外层列表当前位置
            return groupPosition;
        }
    
        @Override
        public int getChildrenCount(int groupPosition) 
        {
            //子列表选项数目
            return generals[groupPosition].length;
        }
    
        @Override
        public Object getChild(int groupPosition, int childPosition) 
        {
            //获取子视图对象
            return generals[groupPosition][childPosition];
        }
    
        @Override
        public long getChildId(int groupPosition, int childPosition) 
        {
            //子列表当前的位置
            return childPosition;
        }
    复制代码

       这些方法都需要重写,提供我们列表正确的数据,这个跟一般的ListView的Adapter基本类似,只是多了一个父类列表。我们可以根据实际情况重写其他方法。不过上面提到的方法,基本都要重写。才能提供正常的数据给列表使用。

     

     4、总结

      今天主要是讲解一下电台列表的分类,这里主要是借助二级列表实现,第一层列表主要是代表电台分类,第二层列表就是具体每一个分类里面的电台数据。这样分级管理,逻辑上比较清新,而且用户也比较方便查找自己喜欢的电台。下一篇文章会讲解如何通过XML读取数据然后跟我们的电台列表绑定在一起。

     

    国内外的电台数据很多,起码有好几百,所以把这些数据都写到代码里面是不实际的。只能写成一个数据文件,程序启动的时候再去加载。保存这些简单数据,我们肯定会优先使用XML文件,今天讲讲如何读取XML里面的数据,然后填充到列表里面。

      再把这张老截图贴出来,方便后面对应查看XML的数据。

     

     

     

    1、Android解析XML方法

      Android里面读取XML文件有3种方法,其中两种是解析XML的常规方法:SAX和文档对象模型方法。以前我写C++的时候,最常用的是文档对象模型方法,因为这个方法遍历数据很方便,缺点是会把整个文件加载到内存,构建一个文档的树模型。对于数据量比较大的文件,比较耗内存。以前就经常使用TinyXML的解析库,我在博客园第一篇文章就是说如何使用TinyXML库,O(∩_∩)O哈哈~

      SAX方法是事件驱动模型,也就是解析到哪个节点会回调相应方法,你需要做的就是在相应的方法里面编写你的解析代码,这个有点是解析速度快,而且不耗内存,不过需要你解析完整个文件。查找灵活性没有文档对象模型方便。

      Android支持上述两种方法,TinyXML的解析库也集成在Android里面。除了上面两种方法,Android自己修改了一种新的方法来解析XML文件——XmlPullParser,这个新方法是基于SAX方法改进的。传统SAX是需要解析完整个XML文件,而XmlPullParser是可以中途中断,停止解析。也就是说只要你获取了你想要的信息,你就可以停止XML的解析工作,因此速度效率上都不错。

      既然是Android官方的方法,这次就使用XmlPullParser来作为XML的解析(我这里数据量不大,用任何一种方法差别不大)。

     

    2、定义XML数据格式

      首先我们需要定义我们保存电台数据的XML格式,这是我定义的一种保存电台类型,以及电台类型下面具体电台数据的XML格式,<ChannelType>标签代表是什么类型的电台,包含了ID、名称、图标、级别等信息。

      <RadioChannel>是具体的电台数据标签,保存了电台名称、图标、以及URL等重要信息。

    复制代码
    //Edited by mythou
    //http://www.cnblogs.com/mythou/
      <ChannelType 
            ID="1"
            name="推荐电台"
            Icon="fm_icon"
            Level="1">    
            
                <RadioChannel
                    ID="001"
                    name="猫扑电台"
                    Icon="default_channel_icon"
                    Level="2"
                    URL="mms://ting.mop.com/mopradio"
                    />
                    
                <RadioChannel
                    ID="002"
                    name="国际电台怀旧金曲"
                    Icon="default_channel_icon"
                    Level="2"
                    URL="mms://live.cri.cn/oldies/"
                    />
                    
                <RadioChannel
                    ID="003"
                    name="国际电台都市流行"
                    Icon="default_channel_icon"
                    Level="2"
                    URL="mms://live.cri.cn/pop/"
                    />
        </ChannelType>    
    复制代码

     

     

    3、使用Pull解析XML

    复制代码
    
    
    //Edited by mythou
    //http://www.cnblogs.com/mythou/
    public boolean getRadioListData(InputStream is, ArrayList<RadioChannelData> ChannelTypeList, 
                ArrayList<ArrayList<RadioChannelData>> finalChanneldata) throws Exception 
        {  
            Log.d(TAG, "parse InputStream="+is);
            //临时电台类型
            RadioChannelData tempChannelTypeItem = null;
            //临时电台数据对象
            RadioChannelData tempChannelItem = null;  
            //保存每个频道类型里面具体电台数据
            ArrayList<RadioChannelData> channelDataList = null;
            
            //显示ChannelType string
            String channelTypeString="";
              
            //由android.util.Xml创建一个XmlPullParser实例  
            XmlPullParser parser = Xml.newPullParser(); 
            //设置输入流 并指明编码方式  
            parser.setInput(is, "UTF-8");               
      
            int eventType = parser.getEventType();  
            while (eventType != XmlPullParser.END_DOCUMENT) 
            {
                switch (eventType) 
                {
                case XmlPullParser.START_DOCUMENT:  
                    //处理文档开始
                    break;  
                case XmlPullParser.START_TAG:  
                    if (parser.getName().equals("ChannelType")) 
                    {
                        //电台类型信息
                        tempChannelTypeItem = new RadioChannelData();  
                        tempChannelTypeItem.setChannelID(parser.getAttributeValue(0));
                        tempChannelTypeItem.setChannelName(parser.getAttributeValue(1));
                        tempChannelTypeItem.setChannelICON(parser.getAttributeValue(2));
                        tempChannelTypeItem.setLevel(Integer.valueOf(parser.getAttributeValue(3)));
                        
                        //创建每个类型下的列表
                        channelDataList = new ArrayList<RadioChannelData>();
                    } 
                    else if (parser.getName().equals("RadioChannel")) 
                    {
                        //具体电台信息
                        tempChannelItem = new RadioChannelData();
                        tempChannelItem.setChannelID(parser.getAttributeValue(0));
                        tempChannelItem.setChannelName(parser.getAttributeValue(1));
                        tempChannelItem.setChannelICON(parser.getAttributeValue(2));
                        tempChannelItem.setLevel(Integer.valueOf(parser.getAttributeValue(3)));
                        tempChannelItem.setChannelURL(parser.getAttributeValue(4));
                    }  
                    break;  
                case XmlPullParser.END_TAG:  
                    if (parser.getName().equals("ChannelType")) 
                    {  
                        //电台类型列表保存
                        ChannelTypeList.add(tempChannelTypeItem);  
                        tempChannelTypeItem = null;   
                        
                        //把每个类型电台列表加入到总数据列表
                        finalChanneldata.add(channelDataList);
                        channelDataList = null;
                    }  
                    else if (parser.getName().equals("RadioChannel"))
                    {
                        channelDataList.add(tempChannelItem);  
                        tempChannelItem = null;     
                    }
                    break;
                case XmlPullParser.END_DOCUMENT:
                        
                    break;  
                }  
                
                eventType = parser.next();  
            }  
            
            //Test mythou 打印读取的数据
            Log.d(TAG, "Print all radio channel Type----->"+ChannelTypeList.toString());
            Log.d(TAG, "Print all radio final channel data----->"+finalChanneldata.toString());
    
            return true;  
        }  
    复制代码

      上面代码是我用来解析XML的代码,解析的数据保存到对应的ArrayList里面,当然我在程序里面也定义了响应的数据格式类用来保存数据,使用Pull解析XML很方便也很简单,这里不做详细介绍,对此不了解的朋友可以去查看相关文档,个人感觉使用Pull解析文档比使用文档对象模型要方便,起码遍历一次数据要快捷很多。

       补充一点,我这里的图片文件只保存了图片文件的名字(不带后缀),我在程序里面会根据图片名称读取Drawable里面的图片资源。

      界面数据相关的就讲到这里,下一次会讲解如何控制播放,也就是一个播放器的核心。

  • 相关阅读:
    C# 视频监控系列:学习地址汇总
    【转】C# 视频监控系列(13):H264播放器——控制播放和截图
    【转】C# 视频监控系列(12):H264播放器——播放录像文件
    【转】C#播放H264裸码流
    采集音频和摄像头视频并实时H264编码及AAC编码
    IntelliJ-IDEA和Git、GitHub、Gitlab的使用
    退出app 退出应用程序
    Android Service+Socket 联网交互
    Android 对话框(Dialog)大全 建立你自己的对话框
    canvas.drawBitmap(bitmap, src, dst, paint)
  • 原文地址:https://www.cnblogs.com/ltb6w/p/8905730.html
Copyright © 2020-2023  润新知