美文网首页
Chapter 11. 多媒体

Chapter 11. 多媒体

作者: GeekGray | 来源:发表于2018-10-12 21:09 被阅读9次

阅读原文

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" />

相关文章

网友评论

      本文标题:Chapter 11. 多媒体

      本文链接:https://www.haomeiwen.com/subject/kkfnaftx.html