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