摘要:為了實現移動視頻監控,提出了一種基于智能手機的遠程視頻監控系統。介紹了監控系統的體系結構和硬件平臺,闡述了嵌入式操作系統Android 應用程序的開發方法,并結合實際的應用系統,重點論述了Android 平臺上視頻監控客戶端的設計思路。移植了音視頻解碼庫FFmpeg 進行H. 264 視頻解碼,并采用OpenGL ES 實現實時視頻顯示。在無線局域網絡的環境下對視頻監控終端進行測試,達到了利用手機進行移動視頻監控的目的。
隨著多媒體技術、視頻壓縮技術以及網絡傳輸技術的發展,視頻監控正朝著數字化、網絡化、智能化方向持續發展,并越來越廣泛地滲透到政府、教育、娛樂、醫療等領域。目前大部分的網絡視頻監控系統是基于WEB 服務器的, 監控終端為PC機,用戶使用瀏覽器獲取監控服務。由于互聯網接入地點的限制,普通的網絡視頻監控無法滿足用戶在任何時間、任何地點獲取監控信息的需求。
參閱相關系列文章
Android系統開發全攻略(一)
本文介紹了一種以Android 智能手機為終端的視頻監控系統,該系統將傳統的視頻監控與移動多媒體技術相結合,真正實現了移動視頻監控。
1系統的結構
本文中的視頻監控系統采用C/ S 體系結構。
如圖1 所示,該系統由視頻采集端( 攝像頭),視頻服務器以及監控客戶端等構成。
圖1視頻監控系統總體結構
視頻服務器是整個系統的核心部分,它將攝像頭采集到的原始模擬信號轉換為數字信號,并對視頻數據進行編碼壓縮,最后通過Internet 將壓縮后的數據傳送至客戶端。客戶端通過TCP/ IP 協議訪問服務器,通過對視頻數據的接收、解碼以及顯示,實現實時預覽功能。客戶端也可以根據用戶需求發送控制命令,實現對前端設備的控制操作,如云臺控制等。
服務器部分采用Hi3515 處理器芯片為硬件平臺,并移植了嵌入式操作系統Linux 作為整個系統運行的軟件環境。Hi3515 是一款基于ARM9 處理器內核以及視頻硬件加速引擎的高性能通信媒體處理器,具有H. 264 和MJPEG 多協議編解碼能力。
本文以基于Hi3515 的遠程視頻監控系統為例,重點介紹了Android 平臺上監控客戶端的設計過程。
2Android 開發介紹
Android 是基于Linux 開放性內核的操作系統,是Google 公司在2007 年11 月5 日公布的手機操作系統。Android 采用軟件堆層的架構,主要分為三部分:底層以Linux 核心為基礎,提供基本功能;中間層包括函數庫和虛擬機;最上層是各種應用軟件。
Android 平臺顯著的開放性使其擁有眾多的開發者,應用日益豐富,不僅應用于智能手機,也向平板電腦、智能MP4 方面急速擴張。
Android 應用程序用Java 語言編寫,每個應用程序都擁有一個獨立的Dalvik 虛擬機實例,這個實例駐留在一個由Linux 內核管理的進程中。Dalvik支持Java Native Interface(JNI)編程方式,Android 應用程序可以通過JNI 調用C/ C++開發的共享庫,實現“Java+C冶的編程方式。開發Android 應用程序最簡捷的方式是安裝Android SDK 和Eclipse IDE.
Eclipse 提供了一個豐富的Java 環境,Java 代碼通過編譯后,Android Developer Tools 會將它打包,用于安裝。
3 監控客戶端的設計與實現
基于Android 平臺的監控客戶端的總體框架如圖2 所示,分別由網絡通訊模塊、視頻解碼模塊以及視頻顯示模塊等構成。其中網絡通訊模塊接收來自服務器的所有數據,對數據進行解析,并將視頻數據存入到視頻緩沖區。視頻解碼模塊負責從視頻緩沖區中讀取數據并送入H. 264 解碼器進行解碼。最后,采用OpenGL 圖形庫將解碼后圖像繪制到屏幕上實現視頻播放。
圖2客戶端總體框架。
3. 1 H. 264 視頻解碼器的實現
在網絡視頻監控系統中,視頻的編碼壓縮是非常必要和關鍵的工作,沒有經過壓縮的海量數據對網絡傳輸系統來說是無法承受的[7] .H.264 是目前最先進的視頻壓縮算法,它由視頻編碼層VCL 和網絡提取層NAL 兩部分組成。其中,VCL 進行視頻編解碼,包括運動補償預測、變換編碼和熵編碼等;NAL 采用適當的格式對VCL 視頻數據進行封裝打包。H.264 標準對編碼效率和圖像質量進行了諸多改進,且抗丟包性能和抗誤碼性能好,適應各種網絡環境,非常適合于對壓縮率要求高,網絡環境復雜的移動視頻監控。
客戶端接收的數據是經過H.264 編碼壓縮后的數據,需要經過H.264 解碼還原視頻圖像后才能夠顯示,因此,H.264 解碼器是客戶端的關鍵部分。這里移植了開源的音視頻解碼庫FFmpeg 進行H.264 解碼。在Android 應用程序中使用FFmpeg 的步驟如下:
(1)在Linux 環境下安裝Android 原生開發工具包NDK.
(2) 創建jni 文件夾,將FFmpeg 工程復制到文件夾下。創建H264Decoder. c 源文件,提供Android程序使用的接口函數,文件需要包括JNI 的操作頭文件《jni. h 》, 且函數名有固定的形式, 如com_ipcamera_PreView_H264Decoder 表示com_ipcamera包下面PreView 類中H264Decoder 函數。
(3)創建Android. mk 文件,該文件包含正確構建和命名庫的MakeFile 說明。分別在LOCAL_SRC_FILES 和LOCAL_C_INCLUDES 項中添加編譯模塊所需源文件和頭文件目錄。
(4)執行NDK 開發包中的ndk鄄build 腳本,生成對應的。 so 共享庫,并復制到Android 工程下的libs/armeabi 目錄下。
(5) 在Android 程序中通過System. loadLibrary(”庫名稱冶)加載所需要的庫,加載成功后,應用程序就可以使用H264Decoder 函數進行H.264 的解碼。
3. 2 OpenGL ES 繪圖
為了提高繪圖的效率,客戶端使用OpenGL ES實現視頻圖像的顯示。OpenGL ES 是一個2D/3D輕量圖形庫,是跨平臺圖形庫OpenGL 的簡化版。
OpenGL ES 專門針對手機、PDA 和游戲主機等嵌入式設備而設計,目的是為了充分利用硬件加速,適合復雜的、圖形密集的程序。
Android 中使用GLSurfaceView 來顯示OpenGL視圖,該類繼承至SurfaceView 并包含了一個專門用于渲染3D 的接口Renderer,主要通過實現ON鄄DrawFrame、onSurfaceChanged 以及onSurfaceCreated等方法構建所需的Renderer.解碼器解碼一幀圖像后,調用GLSurfaceView 的requeSTRender 方法通知OpenGL ES 完成視頻圖像的顯示。使用OpenGL 繪圖的核心代碼如下:
3. 3多線程設計
視頻數據的接收和解碼都是復雜、持續的過程,如果其中一個過程出現阻塞會影響整個程序的運行,因此,客戶端使用多線程實現數據接收和視頻解碼的并行處理。在整個程序運行過程中,主線程響應用戶操作,負責屏幕刷新工作,并創建兩個子線程:數據接收和視頻解碼子線程,處理過程如圖3 所示。
圖3子線程處理流程。
在Java 中, 多線程的實現有兩種方式: 擴展java. lang. Thread 類或實現java. lang. Runnable 接口。這里通過繼承Thread 類并覆寫run()方法實現兩個子線程。在多線程的應用中關鍵是處理好線程之間的同步問題,以解決對共享存儲區的訪問沖突,避免引起線程甚至整個系統的死鎖。Java 多線程主要利用synchronized 關鍵字和wait( )、notify( ) 等方法實現線程間的同步。
4 結束語
目前,該系統已經在實驗室進行測試,服務器輸出15fps CIF 格式的H. 264 視頻數據,客戶端安裝在Android 手機上,通過WIFI 接入無線局域網中與服務器建立連接,用戶界面如圖4 所示,可實現遠程視頻預覽、云臺控制等操作。
圖4 監控客戶端
隨著3G 時代的到來,數據傳輸速度有了大幅提升,為移動實時視頻業務的實現創造更好的條件。
手機用戶可以直接接入3G 網絡訪問視頻監控服務器,實現移動在線的實時視頻監控。由此可見,手機視頻監控市場潛力巨大,具有很好的發展前景。
二、可動態布局的Android抽屜之完整篇
以前曾經介紹過《Android提高第十九篇之“多方向”抽屜》,當這個抽屜組件不與周圍組件發生壓擠的情況下(周圍組件布局不變),是比較好使的,但是如果需要對周圍組件擠壓,則用起來欠缺美觀了。
如下圖。在對周圍壓擠的情況下,抽屜是先把周圍的組件一次性壓擠,再通過動畫效果展開/收縮的,這種做法的好處是快速簡單,壞處是如果擠壓范圍過大,則效果生硬。
本文實現的自定義抽屜組件,主要針對這種壓擠效果做出改良,漸進式壓擠周圍組件,使得過渡效果更加美觀。如下圖。
本文實現的抽屜原理是醬紫:
1.抽屜組件主要在屏幕不可視區域,手柄在屏幕邊緣的可視區域。即 抽屜.rightMargin=-XXX + 手柄.width
2.指定一個周圍組件為可壓擠,即LayoutParams.weight=1;當然用戶也可以指定多個View.
3.使用AsyncTask來實現彈出/收縮的動畫,彈出:抽屜.rightMargin+=XX,收縮:抽屜.rightMargin-=XX
總結,本文的自定義抽屜雖然對壓擠周圍組件有過渡效果,但是比較耗資源,讀者可以針對不同的情況考慮使用。
本文的源碼可以到http://download.csdn.net/detail/hellogv/3615686 下載。
接下來貼出本文全部源代碼:
main.xml的源碼:
[html] view plaincopyprint?
《span style=“font-family:Comic Sans MS;font-size:18px;”》《?xml version=“1.0” encoding=“utf-8”?》
《LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“fill_parent” android:layout_height=“fill_parent”
android:id=“@+id/container”》
《GridView android:id=“@+id/gridview” android:layout_width=“fill_parent”
android:layout_height=“fill_parent” android:numColumns=“auto_fit”
android:verticalSpacing=“10dp” android:gravity=“center”
android:columnWidth=“50dip” android:horizontalSpacing=“10dip” /》
《/LinearLayout》《/span》
《span style=“font-family:Comic Sans MS;font-size:18px;”》《?xml version=“1.0” encoding=“utf-8”?》
《LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“fill_parent” android:layout_height=“fill_parent”
android:id=“@+id/container”》
《GridView android:id=“@+id/gridview” android:layout_width=“fill_parent”
android:layout_height=“fill_parent” android:numColumns=“auto_fit”
android:verticalSpacing=“10dp” android:gravity=“center”
android:columnWidth=“50dip” android:horizontalSpacing=“10dip” /》
《/LinearLayout》《/span》
GridView的Item.xml的源碼:
[html] view plaincopyprint?
《span style=“font-family:Comic Sans MS;font-size:18px;”》《?xml version=“1.0” encoding=“utf-8”?》
《RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_height=“wrap_content” android:paddingBottom=“4dip”
android:layout_width=“fill_parent”》
《ImageView android:layout_height=“wrap_content” android:id=“@+id/ItemImage”
android:layout_width=“wrap_content” android:layout_centerHorizontal=“true”》
《/ImageView》
《TextView android:layout_width=“wrap_content”
android:layout_below=“@+id/ItemImage” android:layout_height=“wrap_content”
android:text=“TextView01” android:layout_centerHorizontal=“true”
android:id=“@+id/ItemText”》
《/TextView》
《/RelativeLayout》 《/span》
《span style=“font-family:Comic Sans MS;font-size:18px;”》《?xml version=“1.0” encoding=“utf-8”?》
《RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_height=“wrap_content” android:paddingBottom=“4dip”
android:layout_width=“fill_parent”》
《ImageView android:layout_height=“wrap_content” android:id=“@+id/ItemImage”
android:layout_width=“wrap_content” android:layout_centerHorizontal=“true”》
《/ImageView》
《TextView android:layout_width=“wrap_content”
android:layout_below=“@+id/ItemImage” android:layout_height=“wrap_content”
android:text=“TextView01” android:layout_centerHorizontal=“true”
android:id=“@+id/ItemText”》
《/TextView》
《/RelativeLayout》 《/span》
Panel.java是本文核心,抽屜組件的源碼,這個抽屜只實現了從右往左的彈出/從左往右的收縮,讀者可以根據自己的需要修改源碼來改變抽屜動作的方向:
[java] view plaincopyprint?
《span style=“font-family:Comic Sans MS;font-size:18px;”》public class Panel extends LinearLayout{
public interface PanelClosedEvent {
void onPanelClosed(View panel);
}
public interface PanelOpenedEvent {
void onPanelOpened(View panel);
}
/**Handle的寬度,與Panel等高*/
private final static int HANDLE_WIDTH=30;
/**每次自動展開/收縮的范圍*/
private final static int MOVE_WIDTH=20;
private Button btnHandle;
private LinearLayout panelContainer;
private int mRightMargin=0;
private Context mContext;
private PanelClosedEvent panelClosedEvent=null;
private PanelOpenedEvent panelOpenedEvent=null;
/**
* otherView自動布局以適應Panel展開/收縮的空間變化
* @author GV
*
*/
public Panel(Context context,View otherView,int width,int height) {
super(context);
this.mContext=context;
//改變Panel附近組件的屬性
LayoutParams otherLP=(LayoutParams) otherView.getLayoutParams();
otherLP.weight=1;//支持壓擠
otherView.setLayoutParams(otherLP);
//設置Panel本身的屬性
LayoutParams lp=new LayoutParams(width, height);
lp.rightMargin=-lp.width+HANDLE_WIDTH;//Panel的Container在屏幕不可視區域,Handle在可視區域
mRightMargin=Math.abs(lp.rightMargin);
this.setLayoutParams(lp);
this.setOrientation(LinearLayout.HORIZONTAL);
//設置Handle的屬性
btnHandle=new Button(context);
btnHandle.setLayoutParams(new LayoutParams(HANDLE_WIDTH,height));
btnHandle.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
LayoutParams lp = (LayoutParams) Panel.this.getLayoutParams();
if (lp.rightMargin 《 0)// CLOSE的狀態
new AsynMove().execute(new Integer[] { MOVE_WIDTH });// 正數展開
else if (lp.rightMargin 》= 0)// OPEN的狀態
new AsynMove().execute(new Integer[] { -MOVE_WIDTH });// 負數收縮
}
});
//btnHandle.setOnTouchListener(HandleTouchEvent);
this.addView(btnHandle);
//設置Container的屬性
panelContainer=new LinearLayout(context);
panelContainer.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
this.addView(panelContainer);
}
/**
* 定義收縮時的回調函數
* @param event
*/
public void setPanelClosedEvent(PanelClosedEvent event)
{
this.panelClosedEvent=event;
}
/**
* 定義展開時的回調函數
* @param event
*/
public void setPanelOpenedEvent(PanelOpenedEvent event)
{
this.panelOpenedEvent=event;
}
/**
* 把View放在Panel的Container
* @param v
*/
public void fillPanelContainer(View v)
{
panelContainer.addView(v);
}
/**
* 異步移動Panel
* @author hellogv
*/
class AsynMove extends AsyncTask《Integer, Integer, Void》 {
@Override
protected Void doInBackground(Integer.。. params) {
int times;
if (mRightMargin % Math.abs(params[0]) == 0)// 整除
times = mRightMargin / Math.abs(params[0]);
else
// 有余數
times = mRightMargin / Math.abs(params[0]) + 1;
for (int i = 0; i 《 times; i++) {
publishProgress(params);
try {
Thread.sleep(Math.abs(params[0]));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return null;
}
@Override
protected void onProgressUpdate(Integer.。. params) {
LayoutParams lp = (LayoutParams) Panel.this.getLayoutParams();
if (params[0] 《 0)
lp.rightMargin = Math.max(lp.rightMargin + params[0],
(-mRightMargin));
else
lp.rightMargin = Math.min(lp.rightMargin + params[0], 0);
if(lp.rightMargin==0 && panelOpenedEvent!=null){//展開之后
panelOpenedEvent.onPanelOpened(Panel.this);//調用OPEN回調函數
}
else if(lp.rightMargin==-(mRightMargin) && panelClosedEvent!=null){//收縮之后
panelClosedEvent.onPanelClosed(Panel.this);//調用CLOSE回調函數
}
Panel.this.setLayoutParams(lp);
}
}
}
《/span》
《span style=“font-family:Comic Sans MS;font-size:18px;”》public class Panel extends LinearLayout{
public interface PanelClosedEvent {
void onPanelClosed(View panel);
}
public interface PanelOpenedEvent {
void onPanelOpened(View panel);
}
/**Handle的寬度,與Panel等高*/
private final static int HANDLE_WIDTH=30;
/**每次自動展開/收縮的范圍*/
private final static int MOVE_WIDTH=20;
private Button btnHandle;
private LinearLayout panelContainer;
private int mRightMargin=0;
private Context mContext;
private PanelClosedEvent panelClosedEvent=null;
private PanelOpenedEvent panelOpenedEvent=null;
/**
* otherView自動布局以適應Panel展開/收縮的空間變化
* @author GV
*
*/
public Panel(Context context,View otherView,int width,int height) {
super(context);
this.mContext=context;
//改變Panel附近組件的屬性
LayoutParams otherLP=(LayoutParams) otherView.getLayoutParams();
otherLP.weight=1;//支持壓擠
otherView.setLayoutParams(otherLP);
//設置Panel本身的屬性
LayoutParams lp=new LayoutParams(width, height);
lp.rightMargin=-lp.width+HANDLE_WIDTH;//Panel的Container在屏幕不可視區域,Handle在可視區域
mRightMargin=Math.abs(lp.rightMargin);
this.setLayoutParams(lp);
this.setOrientation(LinearLayout.HORIZONTAL);
//設置Handle的屬性
btnHandle=new Button(context);
btnHandle.setLayoutParams(new LayoutParams(HANDLE_WIDTH,height));
btnHandle.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0) {
LayoutParams lp = (LayoutParams) Panel.this.getLayoutParams();
if (lp.rightMargin 《 0)// CLOSE的狀態
new AsynMove().execute(new Integer[] { MOVE_WIDTH });// 正數展開
else if (lp.rightMargin 》= 0)// OPEN的狀態
new AsynMove().execute(new Integer[] { -MOVE_WIDTH });// 負數收縮
}
});
//btnHandle.setOnTouchListener(HandleTouchEvent);
this.addView(btnHandle);
//設置Container的屬性
panelContainer=new LinearLayout(context);
panelContainer.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
this.addView(panelContainer);
}
/**
* 定義收縮時的回調函數
* @param event
*/
public void setPanelClosedEvent(PanelClosedEvent event)
{
this.panelClosedEvent=event;
}
/**
* 定義展開時的回調函數
* @param event
*/
public void setPanelOpenedEvent(PanelOpenedEvent event)
{
this.panelOpenedEvent=event;
}
/**
* 把View放在Panel的Container
* @param v
*/
public void fillPanelContainer(View v)
{
panelContainer.addView(v);
}
/**
* 異步移動Panel
* @author hellogv
*/
class AsynMove extends AsyncTask《Integer, Integer, Void》 {
@Override
protected Void doInBackground(Integer.。. params) {
int times;
if (mRightMargin % Math.abs(params[0]) == 0)// 整除
times = mRightMargin / Math.abs(params[0]);
else
// 有余數
times = mRightMargin / Math.abs(params[0]) + 1;
for (int i = 0; i 《 times; i++) {
publishProgress(params);
try {
Thread.sleep(Math.abs(params[0]));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return null;
}
@Override
protected void onProgressUpdate(Integer.。. params) {
LayoutParams lp = (LayoutParams) Panel.this.getLayoutParams();
if (params[0] 《 0)
lp.rightMargin = Math.max(lp.rightMargin + params[0],
(-mRightMargin));
else
lp.rightMargin = Math.min(lp.rightMargin + params[0], 0);
if(lp.rightMargin==0 && panelOpenedEvent!=null){//展開之后
panelOpenedEvent.onPanelOpened(Panel.this);//調用OPEN回調函數
}
else if(lp.rightMargin==-(mRightMargin) && panelClosedEvent!=null){//收縮之后
panelClosedEvent.onPanelClosed(Panel.this);//調用CLOSE回調函數
}
Panel.this.setLayoutParams(lp);
}
}
}
《/span》
main.java是主控部分,演示了Panel的使用:
[java] view plaincopyprint?
《span style=“font-family:Comic Sans MS;font-size:18px;”》public class main extends Activity {
public Panel panel;
public LinearLayout container;
public GridView gridview;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
this.setTitle(““可動態布局”的抽屜組件之構建基礎-----hellogv”);
gridview = (GridView) findViewById(R.id.gridview);
container=(LinearLayout)findViewById(R.id.container);
panel=new Panel(this,gridview,200,LayoutParams.FILL_PARENT);
container.addView(panel);//加入Panel控件
//新建測試組件
TextView tvTest=new TextView(this);
tvTest.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,LayoutParams.FILL_PARENT));
tvTest.setText(“測試組件,紅字白底”);
tvTest.setTextColor(Color.RED);
tvTest.setBackgroundColor(Color.WHITE);
//加入到Panel里面
panel.fillPanelContainer(tvTest);
panel.setPanelClosedEvent(panelClosedEvent);
panel.setPanelOpenedEvent(panelOpenedEvent);
//往GridView填充測試數據
ArrayList《HashMap《String, Object》》 lstImageItem = new ArrayList《HashMap《String, Object》》();
for (int i = 0; i 《 100; i++) {
HashMap《String, Object》 map = new HashMap《String, Object》();
map.put(“ItemImage”, R.drawable.icon);
map.put(“ItemText”, “NO.” + String.valueOf(i));
lstImageItem.add(map);
}
SimpleAdapter saImageItems = new SimpleAdapter(this,
lstImageItem,
R.layout.item,
new String[] { “ItemImage”, “ItemText” },
new int[] { R.id.ItemImage, R.id.ItemText });
gridview.setAdapter(saImageItems);
gridview.setOnItemClickListener(new ItemClickListener());
}
PanelClosedEvent panelClosedEvent =new PanelClosedEvent(){
@Override
public void onPanelClosed(View panel) {
Log.e(“panelClosedEvent”,“panelClosedEvent”);
}
};
PanelOpenedEvent panelOpenedEvent =new PanelOpenedEvent(){
@Override
public void onPanelOpened(View panel) {
Log.e(“panelOpenedEvent”,“panelOpenedEvent”);
}
};
class ItemClickListener implements OnItemClickListener {
@Override
public void onItemClick(AdapterView《?》 arg0,View arg1, int arg2, long arg3) {
@SuppressWarnings(“unchecked”)
HashMap《String, Object》 item = (HashMap《String, Object》) arg0
.getItemAtPosition(arg2);
setTitle((String) item.get(“ItemText”));
}
}《/span》
這次就在基礎篇的基礎上加入拖拉功能。拖拉功能基于GestureDetector,GestureDetector的基本使用方式不是本文介紹的重點,有興趣的童鞋可以上網查詢相關的教程。
本文的抽屜控件相對于基礎篇的抽屜控件多了以下功能:
1.支持手勢拖拉
2.拖拉到一半時,可以自動展開或者收縮。
具體如下圖:
本文的源碼可以到這里下載:http://download.csdn.net/detail/hellogv/3642418
只貼出抽屜組件的源碼,其他源文件與基礎篇的一樣:
[java] view plaincopyprint?
《span style=“font-family:Comic Sans MS;font-size:18px;”》public class Panel extends LinearLayout implements GestureDetector.OnGestureListener{
public interface PanelClosedEvent {
void onPanelClosed(View panel);
}
public interface PanelOpenedEvent {
void onPanelOpened(View panel);
}
private final static int HANDLE_WIDTH=30;
private final static int MOVE_WIDTH=20;
private Button btnHandler;
private LinearLayout panelContainer;
private int mRightMargin=0;
private Context mContext;
private GestureDetector mGestureDetector;
private boolean mIsScrolling=false;
private float mScrollX;
private PanelClosedEvent panelClosedEvent=null;
private PanelOpenedEvent panelOpenedEvent=null;
public Panel(Context context,View otherView,int width,int height) {
super(context);
this.mContext=context;
//定義手勢識別
mGestureDetector = new GestureDetector(mContext,this);
mGestureDetector.setIsLongpressEnabled(false);
//改變Panel附近組件的屬性
LayoutParams otherLP=(LayoutParams) otherView.getLayoutParams();
otherLP.weight=1;
otherView.setLayoutParams(otherLP);
//設置Panel本身的屬性
LayoutParams lp=new LayoutParams(width, height);
lp.rightMargin=-lp.width+HANDLE_WIDTH;
mRightMargin=Math.abs(lp.rightMargin);
this.setLayoutParams(lp);
this.setOrientation(LinearLayout.HORIZONTAL);
//設置Handler的屬性
btnHandler=new Button(context);
btnHandler.setLayoutParams(new LayoutParams(HANDLE_WIDTH,height));
//btnHandler.setOnClickListener(handlerClickEvent);
btnHandler.setOnTouchListener(handlerTouchEvent);
this.addView(btnHandler);
//設置Container的屬性
panelContainer=new LinearLayout(context);
panelContainer.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
this.addView(panelContainer);
}
private View.OnTouchListener handlerTouchEvent=new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction()==MotionEvent.ACTION_UP && //onScroll時的ACTION_UP
mIsScrolling==true)
{
LayoutParams lp=(LayoutParams) Panel.this.getLayoutParams();
if (lp.rightMargin 》= (-mRightMargin/2)) {//往左超過一半
new AsynMove().execute(new Integer[] { MOVE_WIDTH });// 正數展開
}
else if (lp.rightMargin 《 (-mRightMargin/2)) {//往右拖拉
new AsynMove().execute(new Integer[] { -MOVE_WIDTH });// 負數收縮
}
}
return mGestureDetector.onTouchEvent(event);
}
};
/**
* 定義收縮時的回調函數
* @param event
*/
public void setPanelClosedEvent(PanelClosedEvent event)
{
this.panelClosedEvent=event;
}
/**
* 定義展開時的回調函數
* @param event
*/
public void setPanelOpenedEvent(PanelOpenedEvent event)
{
this.panelOpenedEvent=event;
}
/**
* 把View放在Panel的Container
* @param v
*/
public void fillPanelContainer(View v)
{
panelContainer.addView(v);
}
/**
* 異步移動Panel
* @author hellogv
*/
class AsynMove extends AsyncTask《Integer, Integer, Void》 {
@Override
protected Void doInBackground(Integer.。。 params) {
int times;
if (mRightMargin % Math.abs(params[0]) == 0)// 整除
times = mRightMargin / Math.abs(params[0]);
else
// 有余數
times = mRightMargin / Math.abs(params[0]) + 1;
for (int i = 0; i 《 times; i++) {
publishProgress(params);
try {
Thread.sleep(Math.abs(params[0]));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return null;
}
@Override
protected void onProgressUpdate(Integer.。。 params) {
LayoutParams lp = (LayoutParams) Panel.this.getLayoutParams();
if (params[0] 《 0)
lp.rightMargin = Math.max(lp.rightMargin + params[0],
(-mRightMargin));
else
lp.rightMargin = Math.min(lp.rightMargin + params[0], 0);
if(lp.rightMargin==0 && panelOpenedEvent!=null){//展開之后
panelOpenedEvent.onPanelOpened(Panel.this);//調用OPEN回調函數
}
else if(lp.rightMargin==-(mRightMargin) && panelClosedEvent!=null){//收縮之后
panelClosedEvent.onPanelClosed(Panel.this);//調用CLOSE回調函數
}
Panel.this.setLayoutParams(lp);
}
}
@Override
public boolean onDown(MotionEvent e) {
mScrollX=0;
mIsScrolling=false;
return false;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
LayoutParams lp = (LayoutParams) Panel.this.getLayoutParams();
if (lp.rightMargin 《 0)// CLOSE的狀態
new AsynMove().execute(new Integer[] { MOVE_WIDTH });// 正數展開
else if (lp.rightMargin 》= 0)// OPEN的狀態
new AsynMove().execute(new Integer[] { -MOVE_WIDTH });// 負數收縮
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
mIsScrolling=true;
mScrollX+=distanceX;
LayoutParams lp=(LayoutParams) Panel.this.getLayoutParams();
if (lp.rightMargin 《 -1 && mScrollX 》 0) {//往左拖拉
lp.rightMargin = Math.min((lp.rightMargin + (int) mScrollX),0);
Panel.this.setLayoutParams(lp);
Log.e(“onScroll”,lp.rightMargin+“”);
}
else if (lp.rightMargin 》 -(mRightMargin) && mScrollX 《 0) {//往右拖拉
lp.rightMargin = Math.max((lp.rightMargin + (int) mScrollX),-mRightMargin);
Panel.this.setLayoutParams(lp);
}
if(lp.rightMargin==0 && panelOpenedEvent!=null){//展開之后
panelOpenedEvent.onPanelOpened(Panel.this);//調用OPEN回調函數
}
else if(lp.rightMargin==-(mRightMargin) && panelClosedEvent!=null){//收縮之后
panelClosedEvent.onPanelClosed(Panel.this);//調用CLOSE回調函數
}
Log.e(“onScroll”,lp.rightMargin+“”);
return false;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {return false;}
@Override
public void onLongPress(MotionEvent e) {}
@Override
public void onShowPress(MotionEvent e) {}
}
《/span》
三、Android智能手機藍牙通信功能開發:BluetoothChat例程分析
1. 概述
Bluetooth 是幾乎現在每部手機標準配備的功能,多用于耳機 mic 等設備與手機的連接,除此之外,還可以多部手機之間建立 bluetooth 通信,本文就通過 SDK 中帶的一個聊天室的例程,來介紹一下 Android 上的 Bluetooth 的開發。
在 Android1.x 的時候,相關 API 非常不完善,還不能簡單的使用 Bluetooth 開發,有一個開源項目可以幫助程序員使用、開發藍牙,支持直接方法 bluetooth 協議棧。在 Android2 以后,框架提供了一些官方 API 來進行藍牙的通信,但目前的程序也比較不完善。本文主要討論 Android2 后的 Bluetooth 通信的 API 使用方法。
首先看聊天室的效果圖:
2. Bluetooth 通信 API 介紹
2.1. Bluetooth 通信過程
2.2. Bluetooth API 的主要方法
BluetoothAdapter 類
BluetoothAdapter.getDefaultAdapter() :得到本地默認的 BluetoothAdapter ,若返回為 null 則表示本地不支持藍牙;
isDiscovering() :返回設備是否正在發現周圍藍牙設備;
cancelDiscovery() :取消正在發現遠程藍牙設備的過程;
startDiscovery() :開始發現過程;
getScanMode() :得到本地藍牙設備的 Scan Mode ;
getBondedDevices() :得到已配對的設備;
isEnabled() :藍牙功能是否啟用。
當發現藍牙功能未啟用時,如下調用設置啟用藍牙:
如果發現當前設備沒有打開對外可見模式,則傳遞 Intent 來調用打開可發現模式,代碼如下:
BluetoothDevice 類,此為對應的遠程藍牙 Device
createRfcommSocketToServiceRecord() :創建該 Device 的 socket 。
BluetoothSocket 類
connect() :請求連接藍牙。
getInputStream() :得到輸入流,用于接收遠程方信息。
getOutputStream() :得到輸出流,發送給遠程方的信息。
close() :關閉藍牙連接。
InputStream 類:
read(byte[]) :以阻塞方式讀取輸入流。
OutputStream 類:
write(byte[]) :將信息寫入該輸出流,發送給遠程。
3. BluetoothChat 例程分析
Google 提供的關于 Bluetooth 開發的例程為 Bluetoothchat ,使用截圖可見本文一開始。除去配置及 ui 定義等文件,主程序文件共三個: BluetoothChat.java 、 BluetoothChatService.java 以及 DeviceListActivity.java ,詳細功能可見下面的描述。
3.1. 整體調用關系序列圖
3.2. BluetoothChat.java
例程的主 Activity 。 onCreate() 得到本地 BluetoothAdapter 設備,檢查是否支持。 onStart() 中檢查是否啟用藍牙,并請求啟用,然后執行 setupChat() 。 setupChat() 中先對界面中的控件進行初始化增加點擊監聽器等,然創建 BluetoothChatService 對象,該對象在整個應用過程中存在,并執行藍牙連接建立、消息發送接受等實際的行為。
3.3. BluetoothChatService.java
public synchronized void start() :
開啟 mAcceptThread 線程,由于樣例程序是僅 2 人的聊天過程,故之前先檢測 mConnectThread 和 mConnectedThread 是否運行,運行則先退出這些線程。
public synchronized void connect(BluetoothDevice device) :
取消 CONNECTING 和 CONNECTED 狀態下的相關線程,然后運行新的 mConnectThread 線程。
public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) :
開啟一個 ConnectedThread 來管理對應的當前連接。之前先取消任意現存的 mConnectThread 、 mConnectedThread 、 mAcceptThread 線程,然后開啟新 mConnectedThread ,傳入當前剛剛接受的 socket 連接。最后通過 Handler 來通知 UI 連接 OK 。
public synchronized void stop() :
停止所有相關線程,設當前狀態為 NONE 。
public void write(byte[] out) :
在 STATE_CONNECTED 狀態下,調用 mConnectedThread 里的 write 方法,寫入 byte 。
private void connectionFailed() :
連接失敗的時候處理,通知 ui ,并設為 STATE_LISTEN 狀態。
private void connectionLost() :
當連接失去的時候,設為 STATE_LISTEN 狀態并通知 ui 。
內部類:
private class AcceptThread extends Thread :
創建監聽線程,準備接受新連接。使用阻塞方式,調用 BluetoothServerSocket.accept() 。提供 cancel 方法關閉 socket 。
private class ConnectThread extends Thread :
這是定義的連接線程,專門用來對外發出連接對方藍牙的請求和處理流程。構造函數里通過 BluetoothDevice.createRfcommSocketToServiceRecord() ,從待連接的 device 產生 BluetoothSocket. 然后在 run 方法中 connect ,成功后調用 BluetoothChatSevice 的 connected() 方法。定義 cancel() 在關閉線程時能夠關閉相關 socket 。
private class ConnectedThread extends Thread :
這個是雙方藍牙連接后一直運行的線程。構造函數中設置輸入輸出流。 Run 方法中使用阻塞模式的 InputStream.read() 循環讀取輸入流, 然后 post 到 UI 線程中更新聊天消息。也提供了 write() 將聊天消息寫入輸出流傳輸至對方,傳輸成功后回寫入 UI 線程。最后 cancel() 關閉連接的 socket 。
3.4. DeviceListActivity.java
該類包含 UI 和操作的 Activity 類,作用是得到系統默認藍牙設備的已配對設備列表,以及搜索出的未配對的新設備的列表。然后提供點擊后發出連接設備請求的功能。
除了 RFCOMM 通信外, Android 上關于 Bluetooth 的還有 SDP 、 GAP 、耳機設備連接等內容,本文還未涉及,將會隨著藍牙相關 API 在新版本中的進一步完善來學習使用。
四、Android智能手機平臺多分辨率解決方案詳解
摘 要:近年來,智能手機的功能越來越強大,移動終端應用程序層出不窮,移動互聯網改變人們的生活。Android 系統是開放手機聯盟推出的一款開源的手機操作系統,正是由于其開放性,沒有采用Windows PhONe7 類似的硬件限定標準,目前基于Android系統的機型越來越多,一些硬件指標出現了混亂的局面,其中最明顯的就是屏幕分辨率的問題。如何使開發者的應用程序盡可能多地適應多種分辨率,正是本文要講述的問題。文章首先介紹Android 的系統架構,然后介紹Android 平臺中分辨率的相關術語,之后重點講述在開發過程中如何部署資源以及所應遵循的原則,最后給出測試多分辨率兼容性的方法。
0 引 言
2007 年11 月,Google 公司發布基于Linux2.6 內核的移動終端操作系統- Android, 由于其開源性, 得到很多手機廠商的追捧和應用開發者的青睞。近年來智能手機發展迅速,運行速度、存儲容量和可靠性等指標有了顯著提高[1],當今的智能手機用戶對應用軟件的舒適性和美觀性有了更大的期望,應用程序界面友好性已經越來越重要。但是由于Android 的開源性,硬件廠商屏幕分辨率不統一,據統計目前市場上Android系統手機的分辨率有10 余種,分辨率分布如此廣泛使得開發者在處理多分辨率適應方面遇到了不少難題。文章首先介紹Android 平臺的系統架構及資源管理方法,之后介紹目前開發者在處理多分辨率時采用的方法,而后重點分析Android 平臺資源加載機制并且結合實例給出多分辨率的處理步驟及技巧,最后介紹測試多分辨率效果的方法。
1 Android 平臺簡介
Android 是一個包括操作系統、中間件和關鍵應用的移動設備軟件堆[2],Android 系統和其他系統一樣,采用分層的架構。由下至上依此為Linux 操作系統和驅動、程序庫及Android 運行時環境、應用程序框架層、應用層。 Android 應用程序的基本組件有Activity、Intent、BroadcaSTReceiver、Service 四種,各個組件的配置信息以及權限管理、版本管理等配置信息都保存在AndroidManifest.xml 中。
1.1 Android 應用程序資源管理
手機界面上加載的圖片是Android 資源的一種,除此之外還有XML 資源(anim.xml layout.xml 等) 以及原數據文件( 音視頻文件等)[3]。新建一個HelloAndroid 的Android 應用程序,默認生成的文件架構包含src,gen,assets,res 等文件夾,以及AndroidManifest.xml 配置文件。src 文件夾中保存的是Android 源代碼,res 文件夾代表應用程序需要使用到的資源文件,gen 包中包含R.java 文件。Res 文件夾中包含的所有資源文件都對應在R.java 中。
當開發者在res/ 目錄中任何一個子目錄中添加相應類型的文件之后,ADT 會在R.java 文件中相應的匿名內部類中國自動生成一條靜態int 類型的常量,對添加的文件進行索引。
Android 系統采取這種架構使視圖等資源文件與控制代碼分離,實現松耦合。然而可以使用R.java 文件在代碼中對相應的資源文件進行存取,靈活操作。
1.2 一般多分辨率處理方法及其缺點
1.2.1 圖片縮放
基于當前屏幕的精度,平臺自動加載任何未經縮放的限定尺寸和精度的圖片。如果圖片不匹配,平臺會加載默認資源并且在放大或者縮小之后可以滿足當前界面的顯示要求。例如,當前為高精度屏幕,平臺會加載高精度資源(如HelloAndroid中drawable-hdpi 中的位圖資源),如果沒有,平臺會將中精度資源縮放至高精度,導致圖片顯示不清晰。
1.2.2 自動定義像素尺寸和位置
如果程序不支持多種精度屏幕,平臺會自動定義像素絕對位置和尺寸值等,這樣就能保證元素能和精度160 的屏幕上一樣能顯示出同樣尺寸的效果。例如,要讓WVGA 高精度屏幕和傳統的HVGA 屏幕一樣顯示同樣尺寸的圖片,當程序不支持時,系統會對程序慌稱屏幕分辨率為320×480,在(10,10)到(100,100)的區域內繪制圖形完成之后,系統會將圖形放大到(15,15)到(150,150)的屏幕顯示區域。
1.2.3 兼容更大尺寸的屏幕
當前屏幕超過程序所支持屏幕的上限時,定義supportsscreens元素,這樣超出顯示的基準線時,平臺在此顯示黑色的背景圖。例如,WVGA 中精度屏幕上,如程序不支持這樣的大屏幕,系統會謊稱是一個320×480 的,多余的顯示區域會被填充成黑色。
1.2.4 采用OpenGL 動態繪制圖片
Android 底層提供了OpenGL 的接口和方法,可以動態繪制圖片,但是這種方式對不熟悉計算機圖形學的開發者來講是一個很大的挑戰。一般開發游戲,采用OpenGL 方式。
1.2.5 多個apk 文件
Symbian 和傳統的J2ME 就是采用這種方式,為一款應用提供多個分辨率版本,用戶根據自己的需求下載安裝相應的可執行文件。針對每一種屏幕單獨開發應用程序不失為一種好方法,但是目前Google Market 對一個應用程序多個分辨率版本的支持還不完善,開發者還是需要盡可能使用一個apk 文件適應多個分辨率。
2 多分辨率處理方案詳解
2.1 基本術語介紹
2.1.1 屏幕尺寸
真正的物理尺寸,屏幕對角線的長度,單位是英寸。為了簡化起見,Android 把支持的所有物理尺寸分成了4 組:small,normal, large, extra large.
2.1.2 屏幕密度Density
一定物理范圍的像素的個數,單位通常是dpi(dots perinch), 即每英寸的點數。例如一個低分辨率屏幕相對于高分辨率屏幕在一定的物理區域內包含的像素點要少。為了簡化起見,Android 將所有的屏幕密度分成四組:low, medium,high 和extra high.
2.1.3 方向Orientation
從用戶視角來看的屏幕的方向,Portrait 縱向和Landscape 橫向。
2.1.4 分辨率Resolution
屏幕上所有的像素點數目,一般用480*800 的形式來表示。密度無關像素dp: Android 平臺中虛擬的像素單位,定義成一種密度無關的形式,像素px 和dp 的轉換公式為 px =dp*(dpi/160)。在界面開發中應使用dp 作為像素單位,從而保證在不同的屏幕密度上控件所占的實際px 因密度而自動調整。
2.2 手機屏幕的分類
Android 采用兩種標準對屏幕進行分類。按照屏幕尺寸分為四組small, normal, large, extra large;按照屏幕密度分為四組 low, medium ,high 和extra high,其分界線如圖1所示。
圖1 Android 中的屏幕分類
為了優化程序UI,讓其適應多種分辨率并能清晰顯示,一般情況下需要為不同屏幕大小密度提供不同的圖片文件和對應的布局文件,在運行的時候,Android 系統會根據當前設備的屏幕大小及密度等信息,選擇加載其中一套匹配的資源加以運行,從而達到適應多分辨率的效果。
2.3 Android 支持多分辨率原理及步驟
由以上分析,默認的加載方式都不能很好地適應不同的分辨率,Android 從1.6 開始支持多種分辨率的處理,原理簡而言之就是根據屏幕參數,動態加載資源文件。在Android 項目文件結構中,drawable 文件夾下包含三個子文件夾,分別為drawable-hdpi, drawable-mdpi, drawable-ldpi, 分別存放hdpi,mdpi,ldip 的位圖。應用程序運行時,Android 系統會根據當前設備的屏幕大小、分辨率、屏幕密度、方向、長寬比等信息,選擇相應文件夾進行加載。Android 配置修飾符的定義規則如下:
1)在res 文件夾下新建目錄,命名為《resources_name》-《qualifier》 這種格式,其中《resources_name》 為標準資源名稱,例如drawable 或者layout;《qualifier》 即修飾符,指定對應的屏幕參數,比如normal/small/large,hdpi/mdpi/ldpi,land/port,long/notlong 等。
2)在步驟1 新建的文件夾中存入相應的資源,比如位圖資源或者layout 資源,資源文件的名字必須與默認資源文件的名字相同。例如:
3)Android 系統支持多分辨率的機制離不開Android-Manifest.xml 文件的supports-screen 元素,若應用程序要適應多種分辨率,需要將anyDensity 設置為true.
2.4 界面設計技巧
前面的部分已經詳細講解了如何架構應用程序使其更好地適應多種分辨率屏幕,此外,在界面設計和控制中我們還應該掌握一些原則或者技巧,從而使應用程序界面友好、適應性強。
1)在XML layout 文件中定義長度的時候,最好使用wrap_content,fill_parent, 或者dp 進行描述,這樣可以保證在屏幕上面展示的時候有合適的大小。例如,一個view layout_width=“100dip”,在 HVGA@160 density 的設備上顯示100 個px,而在 WVGA@240 density 的設備上顯示150 個px,但是所占的物理尺寸時相同的。
2)在Activity 或者其他控制視圖加載的代碼處,不要使用像素單位的硬編碼。
3)不要使用AbsoluteLayout.絕對布局是由AndroidUI toolkit 提供的布局容器中的一種。但是與其他layout 不一樣的是,AbsoluteLayout 使用固定的位置表示,使得在不同的屏幕上面顯示效果不好,因此AbsoluteLayout 在sdk1.6 及以后的版本中被棄用了。
4)為不同屏幕密度的手機,提供不同的位圖資源,可以使得界面清晰無縮放。
3 多分辨率兼容性測試方案
在產品發行之前,要在所有的目標手機上進行全面的測試。Android SDK 包含了一套測試多分辨率的機制。可以自己定制avd 作為應用程序的測試環境,avd 會模擬真實機器的屏幕大小和密度。例如圖2 為模擬器的列表,可以將程序運行在這四個模擬器中進行多分辨率的測試。
圖2 虛擬機列表
4 結論
本文介紹Android 平臺的體系架構和分辨率相關的術語,詳細論述資源加載原理和多分辨率處理的詳細流程,最后給出了測試應用程序是否適應多種分辨率的方法。結合本人實踐經驗進行講述,內容深入淺出,較完整地論述了如何使應用程序盡可能多地適應多種分辨率屏幕。
評論
查看更多