美文网首页
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