網絡收音機資料收集
最近打算利用業余時間,編寫一個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裏面的圖片資源。
界面數據相關的就講到這裏,下一次會講解如何控制播放,也就是一個播放器的核心。
網絡收音機資料收集