摘要:为了实现移动视频监控,提出了一种基于智能手机的远程视频监控系统。介绍了监控系统的体系结构和硬件平台,阐述了嵌入式操作系统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 平台的体系架构和分辨率相关的术语,详细论述资源加载原理和多分辨率处理的详细流程,最后给出了测试应用程序是否适应多种分辨率的方法。结合本人实践经验进行讲述,内容深入浅出,较完整地论述了如何使应用程序尽可能多地适应多种分辨率屏幕。
评论
查看更多