11.1 多媒体
Android系统在这方面也做得非常出色,它提供了一系列的API,开发者可以利用这些API调用手机的多媒体资源,从而开发出丰富多彩的应用程序
11.2 MediaPlayer播放音频
在Android中,MediaPlayer用于播放音频和视频。MediaPlayer支持多种格式的音频文件并提供了非常全面的控制方法,从而使得播放音乐的工作变得十分简单。
11.2.1 MediaPlayer的使用
常用方法名称 | 功能描述 |
---|---|
setAudioStreamType() | 指定音频文件的类型必须在prepare()方法之前调用 |
setDataSource() | 设置要播放的音频文件的位置 |
prepare() | 在开始播放之前调用这个方法完成准备工作 |
start() | 开始或继续播放音频 |
pause() | 暂停播放音频 |
reset() | 将MediaPlayer对象重置到刚刚创建的状态 |
seekTo() | 从指定的位置开始播放音频 |
release() | 释放掉与MediaPlayer对象相关的资源 |
isPlaying() | 判断当前MediaPlayer是否正在播放音频 |
getDuration() | 获取载入音频文件的时长 |
getCurrentPosition() | 获取当前播放音频文件的位置 |
示例代码:
MediaPlayer mediaPlayer=new MediaPlayer();//创建MediaPlayer
meidaPlayer.setAudioStreamType(AudiaManager.STREAM_MUSIC);//设置声音流的类型
MediaPlayer接收的声音类型有如下几种:
-
AudioManager.STREAM_MUSIC:音乐
-
AudioManager.STREAM_RING:响铃
-
AudioManager.STREAM_ALARM:闹钟
-
AudioManager.STREAM_NOTIFICTION:提示音
需要注意的是,不同流的类型底层申请的内存空间是不一样的,例如当短信来到时发出的较短提示音占用的内存最少,播放音乐占用的内存最大。合理非配内存可以更好的优化项目
设置数据源
设置数据源的三种方式,分别是设置播放应用自带的音频文件,设置播放SD卡中的音频文件、设置播放网络音频文件,具体如下:
//播放应用中res/raw目录下自带的音频文件
mediaPlayer.create(this,R.raw.xxx);
//放SD卡中的音频文件
mediaPlayer.setDataSource("mmt/sdcard/xxx.mp3");
//播放网络音频文件
mediaPlayer.setDataSource("http://www.xxx.mp3");
播放音乐
播放本地音乐与播放网络文件有所不同,当准备播放本地文件时使用的是prepare()方法通知底层框架准备播放音乐,而播放网络音频文件使用prepareAsync()方法,具体代码如下:
mediaPlayer.prepare();//播放本地音乐文件
mediaPlayer.start();//执行start()开始播放音乐
播放网络文件:
mediaPlayer.prepareAsync();//播放网络音乐文件
mediaPlayer.setOnPreparedListener(new OnPreparedListener)
{
public void onPrepared(MediaPlayer player)
{
mediaPlayer.start();
}
}
上述代码用到了prepare()方法和prepareAsync()方法,这两个方法有一些区别,具体如下:
-
prepare()是同步操作,在主线程中执行,它会对音频文件进行解码,当prepare()执行完成之后才会向下执行。
-
prepareAsync()是子线程中执行的异步操作,不管它有没有执行完成都不影响主线程操作。但是,如果音频文件没有解码完毕就执行start()方法就会播放失败。因此,这里要监听音频准备好的监听器OnPreparedListener。当音频解码完成可以播放时会执行onPreparedListener()中的onPrepared()方法,在该方法中执行播放音乐的操作即可。
需要注意的是,当播放网络中的音频文件时,需要添加访问网络的权限,具体如下:
<uses-permission android:name="android.permission.INTERNET">
暂停播放
暂停播放使用的是pause()方法,但是在暂停播放之前先要判断MediaPlayer对象是否存在,以及是否正在播放音乐。具体代码如下:
if(meidaPlayer!=null && mediaPlayer.isPlaying())
{
mediaPlayer.pause();
}
重新播放
重新播放使用的是seekTo()方法,该方法时MediaPlayer中快进快退的方法,它接收时间的参数表示毫秒值,代表要把播放时间定位到哪一毫秒,这里定位到0毫秒就是从头开始播放,具体代码如下:
//播放状态下进行重播
if(mediaPlayer!=null && mediaPlayer.isPlaying())
{
mediaPlayer.seekTo(0);
return;
}
//暂停状态下进行重播,要手动调用start();
if(mediaPlayer!=null)
{
mediaPlayer.seekTo(0);
mediaPlayer.start();
}
停止播放
停止播放音频使用的是stop()方法,停止播放之后还要调用MediaPlayer的release()方法将占有的资源释放掉并将MediaPlayer置为空,具体代码:
if(mediaPlayer!=null && mediaPlayer.isPlaying())
{
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer=null;
}
11.3 SoundPool播放音频
在Android开发中经常使用MediaPlayer来播放音频文件,但是MediaPlyer存在一些不足,例如,资源占用量较高、延迟时间长、不支持多个音频同时播放等。这些缺点决定了MediaPlayer在某些场合的使用情况不会很理想,例如在对时间精准度要求较高的游戏开发中。
在游戏开发中经常需要播放一些游戏音效(比如炸弹爆炸、物体撞击等),这些音效的共同特点是短促、密集、延迟程度小。在这样的场景下,可以使用SoundPool代替MediaPlayer来播放这些音效。下面分步骤讲解额如何使用SoundPool播放音频。
11.3.1 创建SoundPool对象
SoundPool的构造方法有三个参数,分别是maxStream、streamType、SRCQuality。具体代码如下
SoundPool soundPool=new SoundPool(int maxStream,int streamType,int srcQuality);
参数含义:
-
maxStreams:同时播放的流的最大数量
-
streamType:流的类型,一般为AudioManger.STREAM_MUSIC
-
srcQuality:采样率转化质量,当前无效果可以使用0作为默认值
将多个声音添加到一个Map中,具体代码如下:
Map<Integer,Integer>soundPoolMap=new HashMap<Integer,Integer>();
soundPoolMap.put(0,soundPool.load(this,R.raw.dingdong,1));
soundPoolMap.put(1,soundPool.load(this,R.raw.didu,1));
soundPool.setOnLoadCompleteListener(new OnLoadCompleteListener()
{
public void onLoadComplete(SouondPool soundPool,int sampleId,int status)
{
play(soundPoolMap.get(0),(float)1,(float)1,0,0,(float)1.2);
play(soundPoolMap.get(1),(float)1,(float)1,0,0,(float)1.2);
}
});
soundPool.load()方法,该方法为SoundPool对象添加音乐文件,其中第一个参数表示上下文,第二个参数表示加载指定的音频文件资源,第三个参数表示文件加载的优先级。
使用SoundPool加载音频时,必须要等到音频文件加载完成才能播放,否则在播放可能会产生一些问题。为了防止这种情况出现,Android中提供了一个soundPool.setOnLoadCompleteListener接口,该接口中有一个nLoadComplete(SouondPool soundPool,int sampleId,int status)方法,当音频文件载入完成后会执行该方法,一次可以将播放音频的操作放入该方法中执行。
play方法接收了6个参数,这6个参数都很重要,其中第一个参数表示获取当前播放的id,该id从0开始,第二个参数表示左音量eftVolume,第三个参数表示右音量rightVolume,第四个参数表示优先级priority,第五个参数表示循环次数loop,第六个参数表示速率rate,速度速率最低为0.5,最高为2,1代表正常 。
SoundPool与MediaPlayer相比,使用SoundPool载入引用文件时使用的是独立线程,不会阻塞UI线程,而且SoundPool还可以同时播放多个音乐文件。由于SoundPool最高只能申请1MB内存空间,因此只能通过使用SoundPool播放一些提示音或者很短的声音片段。
由于SoundPool只能播放声音较短的音频,如果音频文件较大则会造成Heap size overflow 内存溢出异常。SoundPool暂停音频的播放除了pause()方法外还有一个stop方法,但这些方法建议不要轻易使用,因为有些时候会使线程莫名其妙地终止,并且有时会延迟,按下暂停或停止后会多播放一秒,很影响体验。
11.4 VideoView播放视频
与播放音频相比,视频的播放需要使用视觉组件将影像展示出来。在Android中,播放视频主要使用VideoView或者SurfaceView,其中VideoView组件播放视频最简单,它将视频的显示和控制集于一身
11.4.1 VideoView的常用方法
常用方法名称 | 功能描述 |
---|---|
setVideoPath() | 设置要播放的视频文件的位置 |
start() | 开始或继续播放视频 |
pause() | 暂停播放视频 |
resume() | 将视频重头开始播放 |
seekTo() | 从指定的位置开始播放视频 |
isPlaying() | 判断当前是否正在播放视频 |
getDuration() | 获取载入视频文件的时长 |
1.创建VideoView
不同于音乐播放器,视频需要在界面中显示,因此首先要在布局文件中创建VideoView控件,具体代码如下:
<VideoView
android:id="@+id/videoview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
2. 视频的播放
使用VideoView播放视频和音频一样,既可以播放本地视频,也可以播放网络中的视频,具体代码如下:
VideoView videoview=(VideoView)findViewById(R.id.videoview);
//播放本地视频
videoview.setVideoPath("mmt/sdcard/apple.avi");//加载视频地址
//加载网络视频
videoview.setVideoURI("http://www.xxx.avi");
video.start();
加载网络地址非常简单,不需要做额外处理,使用setVideoURI()方法传入网络视频地址即可,不过VideoView应该播放不了avi类型的视频文件
需要注意的是,播放网络视频时需要添加访问网络权限,具体代码如下:
<uses-permission android:name="android.permission.INTERNET"/>
3. 为VideoView添加控制器
使用VideoView播放视频时可以为它添加一个控制器MdiaController,它是一个包含多媒体播放器(MediaPlayer)控件的视图。包含了一些典型的按钮,如播放/暂停(Play/Pause)/倒带(Rewind)、快进(Fast Forward)与进度滑动器(Progress Slider)。它管理媒体播发器(MediaController)的状态以保持控件的同步。具体代码如下:
MediaController controller=new MediaController(context);
videoview.setMediaController(controller);//为VideoView绑定控制器
11.4.2 VideoView播放视频案例
VideoView.xml 布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:orientation="vertical"
tools:context=".MainActivity" >
<RelativeLayout
android:id="@+id/rl"
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<EditText
android:id="@+id/et_path"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/bt_play"
android:hint="请输入视频 文件的路径"
android:text="/sdcard/oppo.mp4" />
<ImageView
android:id="@+id/bt_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:src="@android:drawable/ic_media_play" />
</RelativeLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal" >
<VideoView
android:id="@+id/sv"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
</LinearLayout>
界面交互代码
public class VideoViewActivity extends Activity implements OnClickListener
{
private EditText et_path;
private ImageView bt_play;
private VideoView videoView;
private MediaController controller;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
this.requestWindowFeature(Window.FEATURE_NO_TITLE);// 去掉标题栏
setContentView(R.layout.videoview);
et_path = (EditText) findViewById(R.id.et_path);
bt_play = (ImageView) findViewById(R.id.bt_play);
videoView = (VideoView) findViewById(R.id.sv);
controller = new MediaController(this);
videoView.setMediaController(controller);
bt_play.setOnClickListener(this);
}
@Override
public void onClick(View v)
{
switch (v.getId())
{
case R.id.bt_play:
play();
break;
}
}
/**
* 播放视频
*
* @param currentPosition
*/
private void play()
{
if (videoView != null && videoView.isPlaying())
{
bt_play.setImageResource(android.R.drawable.ic_media_play);
videoView.stopPlayback();
return;
}
videoView.setVideoPath(et_path.getText().toString());
videoView.start();
bt_play.setImageResource(android.R.drawable.ic_media_pause);
videoView.setOnCompletionListener(new OnCompletionListener()
{
@Override
public void onCompletion(MediaPlayer mp)
{
bt_play.setImageResource(android.R.drawable.ic_media_play);
}
});
}
}
11.5 MediaPlayer和SurfaceView播放视频
使用VideoView播放视频虽然方便,但不利于扩展,当开发者需要根据自己的需求自定义视频播放器时,使用VideoView就会很麻烦。为此,Android系统中还提供另一种播放视频的方式,就是MediaPlayer和SurfaceView一起结合使用。MediaPlayer可以播放视频,只不过它在播放视频的时候没有图像输出,因此需要使用SurfaceView组件。
SurfaceView是继承自View用于显示图像的组件。SurfaceView最大的特点就是它的双缓冲技术,所谓的双缓冲技术是它内部有两个线程,例如线程A和线程B。当线程A更新界面时,线程B进行后台计算操作,当两个线程都完成各自的任务时,它们会相互交换。线程A进行后台计算,线程B进行更新界面,两个线程就这样无限循环交替更新和计算。由于SurfaceView的这种特性可以避免画图任务繁重而造成主线程阻塞,从而提高了程序的反应速度,因此在游戏开发中多用到SurfaceView,例如游戏中的背景、人物、动画等。
1. 创建SurfaceView控件
SurfaceView是一个控件,使用时首先需要在布局文件中定义,具体代码:
<SurfaceView
android:id="@+id.sv"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
2. 获取界面显示容器并设置类型
在代码总通过id找到该控件并得到SurfaceView的容器SurfaceHolder,
SurfaceView view=(SurfaceView)findViewById(R.id.sv);
SurfaceHolder holder=view.getHolder();
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
SurfaceHolder是一个接口类型,它用于维护和管理显示的内容,也就相当于SurfaceView的管理器。通过SurfaceHolder对象控制SurfaceView的大小和像素格式,监视控件中的内容变化。
需要注意的是,在进行游戏开发使用SurfaceView需要开发者自己手动创建维护两个线程进行双缓冲区的管理,而播放视频时是使用MediaPlayer框架,它是通过底层代码去管理和维护音视频文件。因此,需要添加SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS参数不让SurfaceView自己维护双缓冲区,而是交给MediaPlayer底层去管理。虽然API已经过时,但是在Android4.0版本以下的系统中必须添加该参数。
3. 为SurfaceHolder添加回调
如果在onCreate()方法执行时,SurfaceHolder还没有完全创建好。这时候播放视频就会出现异常,因此,需要添加SurfaceHolder的回调函数Callback,在surfaceCreated()方法中执行视频的播放。具体代码如下:
holder.addCallback(new Callback()
{
@override
public void surfaceDestroyed(SurfaceHolder holder)
{
Log.i("surfaceview的holder被销毁了");
}
@override
public void surfaceCreted(SurfaceHolder holder,int fromat,int width,int height)
{
Log.i("surfaceview的大小发生变化");
}
});
Callback接口一共有三个回调方法,
-
surfaceDestoryed():SurfaceView的holder被销毁
-
surfaceCreated():SurfaceView的holder被创建
-
surfaceChanged():SurfaceView的大小发生变化。
11.5.1 案例---------SurfaceView+MediaPlayer视频播放器
1. 布局文件
使用FrameLayout布局,在布局下方放置一个SurfaceView控件,在SurfaceView上方添加一个SeekBar用于控制视频的进度,添加一个ImageView用于控制视频的播放与暂停
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<SurfaceView
android:id="@+id/sv"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
<RelativeLayout
android:id="@+id/rl"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:visibility="visible" >
<SeekBar
android:id="@+id/sbar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:max="100"
android:progress="0" />
<ImageView
android:id="@+id/play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:onClick="click"
android:src="@android:drawable/ic_media_pause" />
</RelativeLayout>
</FrameLayout>
2. 界面交互
在主界面中控制视频的播放与暂停,控制视频视频时间随着进度条拖动变化、点击屏幕出现精度条及按钮,3秒不操作屏幕进度条和按钮自动隐藏。具体代码如下:
public class MainActivity extends Activity implements OnSeekBarChangeListener, Callback
{
private SurfaceView sv;
private SurfaceHolder holder;
private MediaPlayer mediaplayer;
private int position;
private RelativeLayout rl;
private Timer timer;
private TimerTask task;
private SeekBar sbar;
private ImageView play;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sbar = (SeekBar) findViewById(R.id.sbar);
play = (ImageView) findViewById(R.id.play);
sbar.setOnSeekBarChangeListener(this);
sv = (SurfaceView) findViewById(R.id.sv);
// 初始化计时器
timer = new Timer();
task = new TimerTask()
{
@Override
public void run()
{
if (mediaplayer != null && mediaplayer.isPlaying())
{
int progress = mediaplayer.getCurrentPosition();
int total = mediaplayer.getDuration();
sbar.setMax(total);
sbar.setProgress(progress);
}
}
};
timer.schedule(task, 500, 500);
rl = (RelativeLayout) findViewById(R.id.rl);
holder = sv.getHolder();// 得到SurfaceView的容器,界面内容是显示在容器里面的。
// 过时的api,必须写,如果4.0以上的系统,不写完全没问题, 4.0一下的系统必须要写
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
// surfaceView 被创建是需要花费一定的时间的。
// 在oncreate方法执行的时候 surfaceViewHolder还没有完全创建出来。
holder.addCallback(this);
}
// 屏幕触摸事件
@Override
public boolean onTouchEvent(MotionEvent event)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
if (rl.getVisibility() == View.INVISIBLE)
{
rl.setVisibility(View.VISIBLE);
// 倒计时3秒
CountDownTimer cdt = new CountDownTimer(3000, 3000)
{
@Override
public void onTick(long millisUntilFinished)
{
System.out.println(millisUntilFinished);
}
@Override
public void onFinish()
{
rl.setVisibility(View.INVISIBLE);
}
};
cdt.start();
}
else if (rl.getVisibility() == View.VISIBLE)
{
rl.setVisibility(View.INVISIBLE);
}
break;
}
return super.onTouchEvent(event);
}
// Activity注销时把Timer和TimerTask对象置为空
@Override
protected void onDestroy()
{
timer.cancel();
task.cancel();
timer = null;
task = null;
super.onDestroy();
}
// 播放暂停按钮的点击事件
public void click(View view)
{
if (mediaplayer != null && mediaplayer.isPlaying())
{
mediaplayer.pause();
play.setImageResource(android.R.drawable.ic_media_play);
}
else
{
mediaplayer.start();
play.setImageResource(android.R.drawable.ic_media_pause);
}
}
// 进度发生变化时触发
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)
{
}
// 进度条开始拖动时触发
@Override
public void onStartTrackingTouch(SeekBar seekBar)
{
}
// 进度条拖动停止时触发
@Override
public void onStopTrackingTouch(SeekBar seekBar)
{
int position = seekBar.getProgress();
if (mediaplayer != null && mediaplayer.isPlaying())
{
mediaplayer.seekTo(position);
}
}
// SurfaceHolder创建完成时触发
@Override
public void surfaceCreated(SurfaceHolder holder)
{
try
{
mediaplayer = new MediaPlayer();
mediaplayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaplayer.setDataSource("/sdcard/fengjing.f4v");
mediaplayer.setDisplay(holder);
mediaplayer.prepareAsync();
mediaplayer.setOnPreparedListener(new OnPreparedListener()
{
@Override
public void onPrepared(MediaPlayer mp)
{
mediaplayer.start();
if (position > 0)
{
mediaplayer.seekTo(position);
}
}
});
}
catch (Exception e)
{
Toast.makeText(MainActivity.this, "播放失败", 0).show();
e.printStackTrace();
}
}
// SurfaceHolder大小变化时触发
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
}
// SurfaceHolder注销时触发
@Override
public void surfaceDestroyed(SurfaceHolder holder)
{
position = mediaplayer.getCurrentPosition();// 记录上次播放的位置,然后停止。
mediaplayer.stop();
mediaplayer.release();
mediaplayer = null;
}
}
上述代码,OnSeekBarChangeListener接口用于SeekBar滑块位置变化,由于视频播放器需要实时更新播放进度,因此需要在TimeTask里面获取视频播放进度。其中onpProgressChanged()方法是进度发生变化时调用,onStartTrackingTouch()方法开始在拖动SeekBar时调用,onStopTrackingTouch()方法是在SeekBar拖动完成调用,在该方法中记录SeekBar拖动的位置,并把视频的时间设置与SeekBar同步。
onTouchEvent()方法。该方法会在手指触摸屏幕时调用,当进度条显示时点击屏幕使进度条隐藏,当进度条隐藏时点击屏幕则使其显示。其中用到了CountDownTimer对象,该对象是倒计时类,在这里的作用是让进度条显示3s后自动隐藏。
SurfaceHolder方法,在SurfaceHolder加载完成之后会调用SurfaceCreated()方法,一般在该方法中播放视频;SurfaceHolder停止时调用SurfaceDestoryed()方法,一般在该方法中停止视频播放并把MediaPlayer置空。
11.5.2 CountDownTimer
它内部结合Handler方法异步处理线程,CountDownTimer是Android中用于倒计时的类,具体用法如下:
CountDownTimer cdt=new CountDownTimer((3000,1000))
{
@override
public void onTick(long millisUntilFinished)
{
Log.i("TAG","每隔1s执行一次");
}
@override
public void onFinish()
{
Log.i("TAG","3s之后执行");
}
};
cdt.start();
在CountDownTimer构造方法中接收两个long类型的参数,具体含义如下:
-
第一个参数:设置从调用start()方法到执行onFinish()方法的时间间隔,(倒计时时间,单位毫秒)
-
第二个参数:回调onTick(long)方法的时间间隔(单位毫秒)
由于CountDownTimer是抽象类,因此需要重写它的两个抽象方法(这种说法似乎不是那么准确,有抽象方法的类一定是抽象类,但抽象类中不一定要有抽象方法):onTick()和onFinish().上述代码的意思是3s之后执行onFinish()方法,在3s中每隔1s执行一次onTick()方法。要使定义好的倒计时器运行,只需要执行CountDownTimer对象的start()方法即可。
11.6 传感器
传感器是一种物理装置,它能探测、感知外界信号(如物理条件、化学组成),并将探知的信息传递给其他装置。也可以将传感器理解为生物器官,当器官探知信息时,就会将该信息传递给大脑。
11.6.1 Android传感器简介
Android手机通常都会支持多种类型的传感器,如光照传感器、加速度传感器、地磁传感器、压力传感器、温度传感器等。Android系统负责将这些传感器所输出的信息传递给开发者,开发者可以利用这些信息开发很多应用。例如,市场上的赛车游戏使用的就是重力传感器,微信的摇一摇使用的是加速度传感器,手机指南针使用的是地磁传感器等。
Android系统提供了一个类android.hardware.Sensor代表传感器,该类将不同的传感器封装成了常量,常用的传感器对应的常量表值如下表
传感器类型常量 | 内部整数值 | 中文名称 |
---|---|---|
Sensor.TYPE_ACCELEROMETER | 1 | 加速度传感器 |
Sensor.TYPE_MAGNETIG_FIELD | 2 | 磁力传感器 |
Sensor.TYPE_ORIENTATION | 3 | 方向传感器(放弃,但依然可用) |
Sensor.TYPE_GYROSCOPE | 4 | 陀螺仪传感器 |
Sensor.TYPE_LIGHT | 5 | 环境光照传感器 |
Sensor.TYPE_PRESSURE | 6 | 压力传感器 |
Sensor.TYPE_TEMPERATURE | 7 | 温度传感器(放弃,但依然可用) |
Sensor.TYPE_PROXIMITY | 8 | 距离传感器 |
Sensor.TYPE_GRAVITY | 9 | 重力传感器 |
Sensor.TYPE_LINER_ACCELERATION | 10 | 线性加速度 |
Sensor.TYPE_ROTATION_VERTOR | 11 | 旋转矢量 |
Sensor.TYPE_RELATIVE_HUMIDITY | 12 | 湿度传感器 |
Sensor.TYPE_AMBIENT_TEMPERATURE | 13 | 温度传感器(android4.0之后代替TYPE_TEMPERATURE) |
11.6.2 传感器的使用
由于传感器并不是所有手机都支持(或者手机不一定支持所有的传感器),因此使用传感器之前要先查手机集成了那些传感器,然后再使用指定的传感器。模拟器一般都不支持传感器!
1. 获取所有传感器
//获取传感器管理器
SensorManager sm=(SensorManager)getSystemService(Context.SENSOR_SERVICE);
//从传感器管理器中获得全部传感器列表
List<Sensor>allSensors=sm.getSensorList(Sensor.TYPE_ALL);
//显示一共有多少个传感器
allSensors.size();
//获取到传感器列表之后,可以使用for循环查看每一个传感器的详细信息
for(Sensor s:allSensors)
{
s.getName();//传感器名称
......
}
2. 获取指定传感器
如果要获取指定的传感器,在拿到SensorManager管理器之后可以使用getDefaultSensor(int type)方法获取,如下:
//获取传感器管理器
SensorManager sm=(SensorManager)getSystemService(Context.SENSOR_SERVICE);
//从传感器管理器中获得指定的传感器
Sensro sensor=sm.getDefaultSensor(Sensor.TYPE_GRAVITY);
if(sensor!=null)
{
//重力传感器存在
sensor.getName();
//获取传感器供应商
sensor.getvendeor();
}
else
{
//重力传感器不存在
}
调用SensorManager对象的getDefaultSensor()方法,可以得到封装了传感器信息的Sensor对象,可以在该方法里传入相应的传感器参数,如果没有该传感器则会返回null。例如Sensor.TYPE_GRAVITY,如果设备不存在重力传感,则会返回null。
Sensor对象封装了传感器的信息,可以通过调用Sensor对象的方法,获取相应传感器的信息。如表所示:
方法名称 | 功能描述 |
---|---|
getName() | 传感器名称 |
getVersion() | 传感器设备版本 |
getvendor() | 传感器制造商名称 |
getType() | 传感器类型 |
getPower | 传感器的功率 |
3.为传感器注册监听事件
在实际开发中,经常需要实时获取传感器的数据变化,因此在得到了指定的传感器之后,需要为该传感器注册监听事件,具体代码如下:
sm.registerListener(SensorEventListener listener,Sensor sensor,int rate);
上述这行代码通过管理器的registerListener()方法为传感器注册了监听,该方法接收三个参数,具体如下:
-
SensorEventListener listener:传感器事件的监听器接口,该接口有2个方法,分别是onSensorChanged(SensorEvent event)方法时在传感器数据发生变化时调用,例如注册加速度传感器,当加速度方向发生变化时,就可以通过该方法中的event对象获取数据。onAccracyChanged(Sensor sensor,int accuracy)方法是当精确度发生变化时调用,例如在坐地铁是使用磁场传感器,由于地铁中对磁场干扰比较强导致判断不准确,当离开地忒后磁场传感器恢复正常,这时候就会调用这个方法。
-
Sensor sensor:表示传感器对象,例如重力传感器,加速度传感器、地磁场传感器。
-
int rate:表示传感器数据变化的采样率,该采样率支持4种类型,具体如下:
SensorManager.SENSOR_DELAY_FASTEST:延迟10ms。int数值为0
SensorManager.SENSOR_DELAY_GAME:延迟20ms,适合游戏的频率。int数值为1
SensorManager.SENSOR_DELAY_UI:延迟60ms,适合普通界面的频率。int数值为2
SensorManager.SENSOR_DELAY_NORMAL:延迟200ms,正常频率。int数值为3
需要注意的是,如果采样频率越高手机就越费电,对于用户来说体验度不好,一般在实际开发中选择默认的SensorManager.SENSOR_DELAY_NORMAL参数就可以。开发游戏时选择SensorManager.SENSOR_DELAY_GAME参数。没有特殊需求的情况下,最好不要使用SensorManager.SENSOR_DELAY_FASTEST参数,以免影响用户体验
4. 注销传感器
由于Android系统中的传感器管理服务是系统底层服务,即使应用程序关闭后也会一直在后台运行,而且传感器时刻都在采集数据,每秒都有大量数据产生,这样对设备电量造成极大的消耗。因此,在不使用传感器时要注销传感器的监听。注销传感器监听的方法如下所示:
@override
protected void onDestroy()
{
suoer.onDestroy();
sm.unregisterListener(listener);
listener=null;
}
注销时需要传入SensorListener接口,该接口就是前面进行传感器注册时创建的接口,注销完成后把接口置为空。完整代码如下所示:
public class MainActivity extends Activity
{
private SensorManager sm;
private MyListener listener;
@override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
sm=(SensorManager)getSystemService(Context.SENSOR_SERVICE);
//参数Sensor.TYPE_GPAVITY也可以写int数值9
listener=new Mylistener();
sm.registerListener(listener,sensor,SensorManager.SENSOR_DELAY_NORMAL);
}
@override
protected void onDestroy()
{
super.onDestory();
sm.unregisterListener(listener);
listener=null;
}
private class MyListener implements SensorEventListener
{
@override
public void onAccuracyChanged(Sensor sensor,int accuracy)
{
}
@override
public void onSensorChanged(SensorEvent event)
{
}
}
}
上述代码中首先通过getDefaultSensor()方法获取到了重力传感器得到Sensor对象,把Sensor对象传入registerListener()方法第二个参数中,该方法第三个参数传入了SensorManager.SENSOR_DELAY_NORMAL值,这里也可以用int数值3代替
为了节省手机电量,传感器不能一直运行,当Activity不在前台时应该注销传感器,所以传感器的注册和注销推荐写在Activity的onResume()和onPause()方法中,这样会极大地节省手机电量。
11.6.3 案例-----摇一摇
1.布局文件
首先在页面的顶端防止一个RelativeLayout,其中放置两个Button和一个TextView。在屏幕的下方放置了一个RelativeLayout,在RelativeLayout中放置一个ImageView,该ImageView被覆盖在下方,显示摇一摇后的图片,同时该RelativeLayout又嵌套了一个LinearLayout,在此LinearLayout中嵌套了两个RelativeLayout,这两个RelativeLayout分别是手势图片的上下两部分(屏幕中的摇一摇图片不是完整的一张,而是分为上下两张图片组合而成)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#111"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_centerInParent="true" >
<ImageView
android:id="@+id/shakeBg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/cz_shakehideimg_man2" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical" >
<RelativeLayout
android:id="@+id/shakeImgUp"
android:layout_width="fill_parent"
android:layout_height="190dp"
android:background="#111" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:src="@drawable/cz_shake_logo_up" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/shakeImgDown"
android:layout_width="fill_parent"
android:layout_height="190dp"
android:background="#111" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:src="@drawable/cz_shake_logo_down" />
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/shake_title_bar"
android:layout_width="fill_parent"
android:layout_height="45dp"
android:background="@drawable/cz_title_bar"
android:gravity="center_vertical" >
<Button
android:layout_width="70dp"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:background="@drawable/cz_title_back_btn"
android:onClick="shake_activity_back"
android:text="返回"
android:textColor="#fff"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="摇一摇"
android:textColor="#ffffff"
android:textSize="20sp" />
<ImageButton
android:layout_width="67dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="5dp"
android:background="@drawable/cz_title_menu_bg"
android:onClick="linshi"
android:src="@drawable/cz_title_menu_btn" />
</RelativeLayout>
</RelativeLayout>
2. 界面交互
首先在onCreate()方法中调用init()方法初始化数据,然后调用Utils类的Utils.loadSound()方法把assets目录下的音频资源文件添加到map中。最后用加速度传感器的接口,在接口中处理手机摇晃时的逻辑
当手机摇晃时需要做的是:播放储存在Map中第一个位置的音乐、开启
手机振动功能并且开始播放动画,这时候要暂停加速度传感器的监听;当动画播放完毕后要把手机震动关闭、播放存在Map中的第二个音乐并使用Toast显示提示信息,最后重新开启加速度传感器的监听。
需要注意的是,最后一定要在主界面的onPause()方法中注销传感器监听、以免浪费电量和内存。
public class ShakeActivity extends Activity
{
ShakeListener mShakeListener = null;
Vibrator mVibrator;
private RelativeLayout mImgUp;
private RelativeLayout mImgDn;
private SoundPool sndPool;
private Map<Integer, Integer> loadSound;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.shake_activity);
// 初始化数据
init();
// 调用工具类方法把assets目录下的声音存放在map中,返回一个HashMap
loadSound = Utils.loadSound(sndPool, this);
// 创建加速度监听器的对象
}
@Override
protected void onResume()
{
super.onResume();
mShakeListener = new ShakeListener(this);
// 加速度传感器,达到速度阀值,播放动画
mShakeListener.setOnShakeListener(new OnShakeListener()
{
public void onShake()
{
Utils.startAnim(mImgUp, mImgDn); // 开始 摇一摇手掌动画
mShakeListener.stop();// 停止加速度传感器
sndPool.play(loadSound.get(0), (float) 1, (float) 1, 0, 0, (float) 1.2);// 摇一摇时播放map中存放的第一个声音
startVibrato();// 震动
new Handler().postDelayed(new Runnable()
{
public void run()
{
sndPool.play(loadSound.get(1), (float) 1, (float) 1, 0, 0, (float) 1.0);// 摇一摇结束后播放map中存放的第二个声音
Toast.makeText(getApplicationContext(), "抱歉,暂时没有找到\n在同一时刻摇一摇的人。\n再试一次吧!", 10).show();
mVibrator.cancel();// 震动关闭
mShakeListener.start();// 再次开始检测加速度传感器值
}
}, 2000);
}
});
}
@Override
protected void onPause()
{
super.onPause();
if (mShakeListener != null)
{
mShakeListener.stop();
}
}
private void init()
{
mVibrator = (Vibrator) getApplication().getSystemService(VIBRATOR_SERVICE);
mImgUp = (RelativeLayout) findViewById(R.id.shakeImgUp);
mImgDn = (RelativeLayout) findViewById(R.id.shakeImgDown);
sndPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 5);
}
public void startVibrato()
{ // 定义震动
mVibrator.vibrate(new long[] { 500, 200, 500, 200 }, -1); // 第一个{}里面是节奏数组,
}
public void shake_activity_back(View v)
{ // 标题栏 返回按钮
this.finish();
}
public void linshi(View v)
{ // 标题栏
Utils.startAnim(mImgUp, mImgDn);
}
}
创建工具类utils.java
为了使代码逻辑清晰,把其中用到的功能性代码单独抽出来放置在一个工具类Utils中,当使用的时候直接调用即可。这样会降低代码的耦合度,并且使程序更易于阅读。Utils类中的代码如下:
public class Utils
{
public static void startAnim(RelativeLayout mImgUp, RelativeLayout mImgDn)
{ // 定义摇一摇动画动画
AnimationSet animUp = new AnimationSet(true);
TranslateAnimation start0 = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF,
0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, -0.5f);
start0.setDuration(1000);
TranslateAnimation start1 = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF,
0f, Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, +0.5f);
start1.setDuration(1000);
start1.setStartOffset(1000);
animUp.addAnimation(start0);
animUp.addAnimation(start1);
mImgUp.startAnimation(animUp);
AnimationSet animDn = new AnimationSet(true);
TranslateAnimation end0 = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f,
Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, +0.5f);
end0.setDuration(1000);
TranslateAnimation end1 = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, 0f,
Animation.RELATIVE_TO_SELF, 0f, Animation.RELATIVE_TO_SELF, -0.5f);
end1.setDuration(1000);
end1.setStartOffset(1000);
animDn.addAnimation(end0);
animDn.addAnimation(end1);
mImgDn.startAnimation(animDn);
}
/**
* 把assets目录下的声音资源添加到map中
*
*/
public static Map<Integer, Integer> loadSound(final SoundPool pool, final Activity context)
{
final Map<Integer, Integer> soundPoolMap = new HashMap<Integer, Integer>();
new Thread()
{
public void run()
{
try
{
soundPoolMap.put(0, pool.load(context.getAssets().openFd("sound/shake_sound_male.mp3"), 1));
soundPoolMap.put(1, pool.load(context.getAssets().openFd("sound/shake_match.mp3"), 1));
}
catch (IOException e)
{
e.printStackTrace();
}
}
}.start();
return soundPoolMap;
}
}
4. 创建传感器
创建一个类实现SensorEventListener接口并重写接口中的onSensorChanged()和onAccuracyChanged()方法。该类创建了一个有参的构造方法,在主界面的onCreate()中调用,首先执行了start()方法创建出加速度传感器对象并注册了监听。
当应用打开后,onSensorChanged()方法不停地执行判断手机是否摇晃的频率超过设定的值,当频率达到要求时就调用自定义接口OnShakeListener的onShake()方法。在主界面中实现该自定义接口并在onShake()方法中执行摇晃后的逻辑即可
/**
* 一个检测手机摇晃的监听器
*/
public class ShakeListener implements SensorEventListener
{
private static final int SPEED_SHRESHOLD = 2000; // 速度阈值,当摇晃速度达到这值后产生作用
private static final int UPTATE_INTERVAL_TIME = 70; // 两次检测的时间间隔
private SensorManager sensorManager; // 传感器管理器
private Sensor sensor; // 传感器
private OnShakeListener onShakeListener; // 加速度感应监听器
private Context mContext; // 上下文
private long lastUpdateTime; // 上次检测时间
// 手机上一个位置时加速度感应坐标
private float lastX;
private float lastY;
private float lastZ;
public ShakeListener(Context c)
{
// 获得监听对象
mContext = c;
start();
}
public void start()
{
// 获得传感器管理器
sensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
if (sensorManager != null)
{
// 获得加速度传感器
sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
// 注册
if (sensor != null)
{
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME);
}
else
{
Toast.makeText(mContext, "您的手机不支持该功能", 0).show();
}
}
// 加速度感应器感应获得变化数据
public void onSensorChanged(SensorEvent event)
{
long currentUpdateTime = System.currentTimeMillis(); // 当前检测时间
long timeInterval = currentUpdateTime - lastUpdateTime; // 两次检测的时间间隔
// 判断是否达到了检测时间间隔
if (timeInterval < UPTATE_INTERVAL_TIME)
return;
// 现在的时间变成last时间
lastUpdateTime = currentUpdateTime;
// 获得x,y,z坐标
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
// 获得x,y,z的变化值
float deltaX = x - lastX;
float deltaY = y - lastY;
float deltaZ = z - lastZ;
// 将现在的坐标变成last坐标
lastX = x;
lastY = y;
lastZ = z;
double speed = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) / timeInterval * 10000;
// 达到速度阀值,发出提示
if (speed >= SPEED_SHRESHOLD)
{
onShakeListener.onShake();
}
}
// 摇晃监听接口
public interface OnShakeListener
{
public void onShake();
}
// 停止检测
public void stop()
{
sensorManager.unregisterListener(this);
}
public void onAccuracyChanged(Sensor sensor, int accuracy)
{
} // 设置重力感应监听器
public void setOnShakeListener(OnShakeListener listener)
{
onShakeListener = listener;
}
}
5. 配置清单文件
由于使用到了手机震动和加速度传感器,因此需要在清单文件中配置相关权限。具体如下所示:
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.hardware.sensor.accelerometer" />
网友评论