1. 程式人生 > >網絡收音機資料收集

網絡收音機資料收集

缺點 才有 流媒體 temp 返回 過程 group getchild 當前

最近打算利用業余時間,編寫一個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裏面的圖片資源。

  界面數據相關的就講到這裏,下一次會講解如何控制播放,也就是一個播放器的核心。

網絡收音機資料收集