在本章中,我们将涵盖以下主题:
- 使用 SoundPool 播放音效
- 使用 MediaPlayer 播放音频
- 响应应用中的硬件媒体控制
- 使用默认相机应用拍照
- 使用(旧的)相机应用编程接口拍照
- 使用相机 2(新)应用编程接口拍照
既然我们已经在前面的章节中探索了图形和动画,现在是时候看看安卓中可用的声音选项了。播放声音的两个最受欢迎的选项包括:
- 音池:这是给的短音片段
- MediaPlayer :这是专为较大的声音文件(比如音乐)和视频文件设计的
前两个食谱将着眼于使用这些库。我们还将了解如何使用与声音相关的硬件,例如音量控制和媒体回放控制(播放、暂停等通常在耳机上找到)。
本章的其余部分将集中于使用相机,既可以通过 Intents 间接使用(将相机请求传递给默认的相机应用),也可以直接使用相机 API。我们将检查安卓 5.0 棒棒糖(API 21)发布的新 Camera 2 API,但我们也将查看原始 Camera API,因为大约 75%的市场还没有棒棒糖。(为了帮助您利用 Camera2 API 中提供的新功能,我们将展示一种使用旧 Camera API 的新方法,以便在您自己的应用中更容易使用这两种 Camera API。)
当你的应用需要音效时,SoundPool 通常是一个很好的起点。
SoundPool 的有趣之处在于,它允许我们通过改变播放速率和允许多种声音同时播放来用我们的声音创建特殊效果。
支持的流行音频文件类型包括:
- 3gpp(“t0”)
- 3gpp(“t0”)
- FLAC (
.flac
) - MP3 (
.mp3
) - MIDI 类型 0 和 1 (
.mid
、.xmf
和.mxmf
- Ogg (
.ogg
) - 波浪(
.wav
)
参见支持的媒体格式链接,了解完整的列表,包括网络协议。
在安卓系统中很常见的是,操作系统的新版本会给应用接口带来变化。SoundPool
也不例外,原来的SoundPool
构造函数在棒棒糖(API 21)中被弃用。我们将实现新旧方法,并在运行时检查操作系统版本,以使用适当的方法,而不是将我们的最小应用编程接口设置为 21 或依赖于不推荐使用的代码(这可能会在某个时候停止工作)。
这个食谱将演示如何使用安卓SoundPool
库播放音效。为了演示同时播放声音,我们将创建两个按钮,当按下时,每个按钮都会播放声音。
在 Android Studio 中创建新项目,并将其称为:SoundPool
。使用默认的电话&平板选项,当提示输入活动类型时,选择空活动。
为了演示同时播放声音,我们需要项目中至少有两个音频文件。我们去了 sound beach . com(http://soundbible.com/royalty-free-sounds-5.html)找到了两个免版税的公共领域声音,包含在下载项目文件中:
第一个声音是较长的播放声音:
http://soundbible.com/2032-Water.html
第二个声音更短:
http://soundbible.com/1615-Metal-Drop.html
如前所述,我们需要两个音频文件来将包含在项目中。准备好声音文件后,请按照下列步骤操作:
-
创建新的原始文件夹(文件 | 新的 | 安卓资源目录)并在资源类型下拉列表中选择
raw
。 -
将您的声音文件复制到
res/raw
作为sound_1
和sound_2
。(保留其原始扩展名。) -
打开
activity_main.xml
并用以下按钮替换现有的【T1:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Play Sound 1" android:id="@+id/button1" android:layout_centerInParent="true" android:onClick="playSound1"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Play Sound 2" android:id="@+id/button2" android:layout_below="@+id/button1" android:layout_centerHorizontal="true" android:onClick="playSound2"/>
-
现在打开
ActivityMain.java
并添加以下全局变量:HashMap<Integer, Integer> mHashMap= null; SoundPool mSoundPool;
-
修改现有的
onCreate()
方法,如下:final Button button1=(Button)findViewById(R.id.button1); button1.setEnabled(false); final Button button2=(Button)findViewById(R.id.button2); button2.setEnabled(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { createSoundPoolNew(); }else{ createSoundPooolOld(); } mSoundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() { @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { button1.setEnabled(true); button2.setEnabled(true); } }); mHashMap = new HashMap<>(); mHashMap.put(1, mSoundPool.load(this, R.raw.sound_1, 1)); mHashMap.put(2, mSoundPool.load(this, R.raw.sound_2, 1));
-
添加
createSoundPoolNew()
方法:@TargetApi(Build.VERSION_CODES.LOLLIPOP) private void createSoundPoolNew() { AudioAttributes audioAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(); mSoundPool = new SoundPool.Builder() .setAudioAttributes(audioAttributes) .setMaxStreams(2) .build(); }
-
添加
createSoundPooolOld()
方法:@SuppressWarnings("deprecation") private void createSoundPooolOld(){ mSoundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0); }
-
添加按钮
onClick()
方法:public void playSound1(View view){ mSoundPool.play(mHashMap.get(1), 0.1f, 0.1f, 1, 0, 1.0f); } public void playSound2(View view){ mSoundPool.play(mHashMap.get(2), 0.9f, 0.9f, 1, 1, 1.0f); }
-
覆盖
onStop()
回调,如下所示:protected void onStop() { super.onStop(); mSoundPool.release(); }
-
在设备或模拟器上运行应用。
注意的第一个细节是我们如何构造物体本身。正如我们在介绍中提到的,SoundPool 构造函数在棒棒糖(API 21)中进行了更改。旧的构造函数被弃用,转而使用SoundPool.Builder()
。在像安卓这样不断变化的环境中,应用编程接口的变化是非常常见的,所以学习如何处理这些变化是一个好主意。如你所见,在这种情况下并不难。我们只需检查当前的操作系统版本并调用适当的方法。值得注意的是方法注释:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
以及:
@SuppressWarnings("deprecation")
创建 SoundPool 后,我们设置一个setOnLoadCompleteListener()
监听器。启用按钮主要是为了演示,说明 SoundPool 需要在声音资源可用之前加载它们。
使用 SoundPool 的最后一点是调用play()
。我们需要传入soundID
,这是我们使用load()
加载声音时返回的。Play()
给了我们几个选项,包括音量(左右)、循环计数和播放速率。为了展示灵活性,我们以较低的音量播放第一个声音(较长),以利用流水营造更多的背景效果。第二个声音以较高的音量播放,我们播放两次。
如果你只需要一个基本的音效,比如一个点击,可以使用 AudioManager playSoundEffect()
方法。这里有一个例子:
AudioManager audioManager =(AudioManager)
this.getSystemService(Context.AUDIO_SERVICE);
audioManager.playSoundEffect(SoundEffectConstants.CLICK);
您只能从SoundEffectConstants
中指定一个声音;您不能使用自己的声音文件。
-
Developer Docs: SoundPool
https://developer . Android . com/reference/Android/media/soundpool . html
-
Developer Docs:
https://developer . Android . com/reference/Android/media/audio manager . html
MediaPlayer 可能是为应用添加多媒体功能的最重要的类之一。它支持以下媒体源:
- 项目资源
- 本地文件
- 外部资源(如网址,包括流)
MediaPlayer 支持以下流行的音频文件:
- 3gpp(“t0”)
- 3gpp(“t0”)
- FLAC (
.flac
) - MP3 (
.mp3
) - MIDI 类型 0 和 1 (
.mid
、.xmf
和.mxmf
- Ogg (
.ogg
) - 波浪(
.wav
)
而这些流行的文件类型:
- 3gpp(“t0”)
- 水手(1230t0)
- webm(1230)
- MPEG-4 (
.mp4
、.m4a
)
参见支持的媒体格式链接,了解完整的列表,包括网络协议。
本食谱将演示如何在您的应用中设置 MediaPlayer 来播放项目中包含的声音。(有关 MediaPlayer 提供的全部功能的完整回顾,请参见本食谱末尾的开发人员文档链接。)
在 Android Studio 中创建新项目,并将其称为:MediaPlayer
。使用默认的电话&平板电脑选项,当提示输入活动类型时,选择空活动。
我们还需要一个声音为这个食谱,并将使用相同的更长的播放“水”的声音在以前的食谱中使用。
第一个音是较长的播放音:http://soundbible.com/2032-Water.html
如前所述,我们需要一个声音文件来包含在项目中。准备好声音文件后,请按照下列步骤操作:
-
创建新的原始文件夹(文件 | 新的 | 安卓资源目录)并在资源类型下拉列表中选择
raw
-
将您的声音文件复制到
res/raw
作为sound_1
。(保留原扩展名。) -
打开
activity_main.xml
并用以下按钮替换现有的【T1:<Button android:layout_width="100dp" android:layout_height="wrap_content" android:text="Play" android:id="@+id/buttonPlay" android:layout_above="@+id/buttonPause" android:layout_centerHorizontal="true" android:onClick="buttonPlay" /> <Button android:layout_width="100dp" android:layout_height="wrap_content" android:text="Pause" android:id="@+id/buttonPause" android:layout_centerInParent="true" android:onClick="buttonPause"/> <Button android:layout_width="100dp" android:layout_height="wrap_content" android:text="Stop" android:id="@+id/buttonStop" android:layout_below="@+id/buttonPause" android:layout_centerHorizontal="true" android:onClick="buttonStop"/>
-
现在打开
ActivityMain.java
并添加以下全局变量:MediaPlayer mMediaPlayer;
-
添加
buttonPlay()
方法:public void buttonPlay(View view){ if (mMediaPlayer==null) { mMediaPlayer = MediaPlayer.create(this, R.raw.sound_1); mMediaPlayer.setLooping(true); mMediaPlayer.start(); } else { mMediaPlayer.start(); } }
-
添加
buttonPause()
方法:public void buttonPause(View view){ if (mMediaPlayer!=null && mMediaPlayer.isPlaying()) { mMediaPlayer.pause(); } }
-
添加
buttonStop()
方法:public void buttonStop(View view){ if (mMediaPlayer!=null) { mMediaPlayer.stop(); mMediaPlayer.release(); mMediaPlayer = null; } }
-
最后,用以下代码覆盖
onStop()
回调:protected void onStop() { super.onStop(); if (mMediaPlayer!=null) { mMediaPlayer.release(); mMediaPlayer = null; } }
-
您已经准备好在设备或模拟器上运行应用。
这里的代码非常简单。我们用我们的声音创建 MediaPlayer,并开始播放声音。按钮将相应地重放、暂停和停止。
甚至这个基本的例子也说明了关于 MediaPlayer 的一个非常重要的概念,那就是状态。如果您正在认真使用 MediaPlayer,请查看下面提供的链接了解详细信息。
为了使我们的演示更容易理解,我们在所有操作中使用了用户界面线程。对于这个例子,使用项目中包含的短音频文件,我们不太可能遇到任何用户界面延迟。一般来说,在准备 MediaPlayer 时使用后台线程是个好主意。为了让这个常见的任务变得更容易,MediaPlayer 已经包含了一个名为prepareAsync()
的异步准备方法。下面的代码将创建一个OnPreparedListener()
监听器并使用prepareAsync()
方法:
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mMediaPlayer.start();
}
});
try {
mMediaPlayer.setDataSource(*//*URI, URL or path here*//*));
} catch (IOException e) {
e.printStackTrace();
}
mMediaPlayer.prepareAsync();
我们的例子是,意在当应用在前台时播放音频,并将在onStop()
回调中释放 MediaPlayer 资源。如果您正在创建一个音乐播放器,并且想要在后台播放音乐,即使用户正在使用另一个应用,该怎么办?在这种情况下,您将希望在服务中使用媒体播放器,而不是活动。您将以同样的方式使用 MediaPlayer 库;您只需要将信息(如声音选择)从 UI 传递给您的服务。
请注意,由于服务运行在与活动相同的用户界面线程中,您仍然不希望在服务中执行潜在的阻塞操作。MediaPlayer 确实处理后台线程,以防止阻塞您的用户界面线程,否则,您将希望自己执行线程。(有关线程和选项的更多信息,请参见第 14 章、为游戏商店准备好您的应用。)
如果您想要音量控制来控制应用中的音量,请使用setVolumeControlStream()
方法来指定应用的音频流,如下所示:
setVolumeControlStream(AudioManager.STREAM_MUSIC);
其他流选项见以下AudioManager
链接。
- 支持的媒体格式https://developer . Android . com/guide/附录/media-formats.html
- 开发者 文档:MediaPlayerhttp://Developer . Android . com/reference/Android/media/MediaPlayer . html
- 开发者 文档:音频管理器:https://Developer . Android . com/reference/Android/media/audio manager . html
让您的应用响应媒体控制,如播放、暂停、跳过等,是一个很好的接触,您的用户会欣赏。
安卓通过媒体库使这成为可能。就像之前的用音池播放音效一样,棒棒糖的发布改变了这一过程。与SoundPool
例子不同,这个配方能够利用另一种方法——兼容性库。
本食谱将向您展示如何设置MediaSession
来响应硬件按钮,这将适用于棒棒糖和以后的版本,以及以前使用MediaSessionCompat
库的Lollilop
版本。(兼容性库将自动检查操作系统版本并使用正确的应用编程接口调用。)
在 Android Studio 中创建新项目,并将其称为:HardwareMediaControls
。使用默认的电话&平板电脑选项,并在提示输入活动类型时选择空活动。
我们将只使用祝酒信息来响应硬件事件,因此不需要对活动布局进行任何更改。首先,打开ActivityMain.java
并按照以下步骤操作:
-
创建以下
mMediaSessionCallback
来响应媒体按钮:MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() { @Override public void onPlay() { super.onPlay(); Toast.makeText(MainActivity.this, "onPlay()", Toast.LENGTH_SHORT).show(); } @Override public void onPause() { super.onPause(); Toast.makeText(MainActivity.this, "onPause()", Toast.LENGTH_SHORT).show(); } @Override public void onSkipToNext() { super.onSkipToNext(); Toast.makeText(MainActivity.this, "onSkipToNext()", Toast.LENGTH_SHORT).show(); } @Override public void onSkipToPrevious() { super.onSkipToPrevious(); Toast.makeText(MainActivity.this, "onSkipToPrevious()", Toast.LENGTH_SHORT).show(); } };
-
将以下代码添加到现有的
onCreate()
回调中:MediaSessionCompat mediaSession = new MediaSessionCompat(this, getApplication().getPackageName()); mediaSession.setCallback(mMediaSessionCallback); mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS); mediaSession.setActive(true); PlaybackStateCompat state = new PlaybackStateCompat.Builder() .setActions( PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS).build(); mediaSession.setPlaybackState(state);
-
在带有媒体控件(如耳机)的设备或模拟器上运行应用,以查看吐司消息。
设置有四个步骤:
- 创建一个
MediaSession.Callback
并将其附加到媒体会话 - 设置媒体会话标志,表示我们需要媒体按钮
- 将
SessionState
设置为active
- 设定
PlayBackState
我们将要处理的动作
步骤 4 和 1 一起工作,因为回调将只获得在PlayBackState
中设置的事件。
由于在这个配方中,我们实际上并不控制任何回放,我们只是演示如何响应硬件事件。您将希望在PlayBackState
中实现实际功能,并在setActions()
调用之后包含对setState()
的调用。
这是一个很好的例子,说明了对应用编程接口的更改如何使事情变得更容易。由于新的MediaSession
和PlaybackState
被引入到兼容性库中,我们可以在旧版本的操作系统上利用这些新的应用编程接口。
如果想让你的 app 根据当前输出硬件做出不同的响应,可以使用AudioManager
进行检查。这里有一个例子:
AudioManager audioManager =(AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
if (audioManager.isBluetoothA2dpOn()) {
// Adjust output for Bluetooth.
} else if (audioManager.isSpeakerphoneOn()) {
// Adjust output for Speakerphone.
} else if (audioManager.isWiredHeadsetOn()) {
//Only checks if a wired headset is plugged in
//May not be the audio output
} else {
// Regular speakers?
}
-
Developer Docs: MediaSession
https://developer . Android . com/reference/Android/media/session/media session . html
-
Developer Docs: MediaSessionCompat
-
Developer Docs: PlaybackState
-
Developer Docs: PlaybackStateCompat
如果你的应用需要一个来自相机的图像,但不是相机的替换应用,那么允许“默认”相机应用拍照可能会更好。这也尊重用户对首选相机应用的选择。
当你拍照的时候,除非是专门针对你的应用,否则公开照片被认为是一种好的做法。(这允许它包含在用户的照片库中。)本食谱将演示如何使用默认的照片应用点击图片,将其保存到公共文件夹,并显示图像。
在 Android Studio 中创建新项目,并将其称为:UsingTheDefaultCameraApp
。使用默认的电话&平板电脑选项,当提示输入活动类型时,选择空活动。
我们将创建一个带有图像视图和按钮的布局。该按钮将创建一个启动默认相机应用的意图。相机应用完成后,我们的应用会得到一个回拨。首先打开安卓清单,然后按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-
打开
activity_main.xml
文件,将现有的TextView
替换为以下视图:<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/imageView" android:src="@mipmap/ic_launcher" android:layout_centerInParent="true"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Take Picture" android:id="@+id/button" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="takePicture"/>
-
打开
MainActivity.java
并将以下全局变量添加到MainActivity
类:final int PHOTO_RESULT=1; private Uri mLastPhotoURI=null;
-
添加以下方法为照片创建 URI:
private Uri createFileURI() { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(System.currentTimeMillis()); String fileName = "PHOTO_" + timeStamp + ".jpg"; return Uri.fromFile(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),fileName)); }
-
添加以下方法处理按钮点击:
public void takePicture(View view) { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_ CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { mLastPhotoURI = createFileURI(); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mLastPhotoURI); startActivityForResult(takePictureIntent, PHOTO_RESULT); } }
-
增加一个新的方法覆盖
onActivityResult()
,如下:@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == PHOTO_RESULT && resultCode == RESULT_OK ) { mImageView.setImageBitmap(BitmapFactory.decodeFile(mLastPhotoURI.getPath())); } }
-
您已经准备好在设备或仿真器上运行应用了。
使用默认相机应用有两个部分。首先是设置启动应用的意图。我们使用MediaStore.ACTION_IMAGE_CAPTURE
来创建意图,以表明我们想要一个照片应用。我们通过检查resolveActivity()
的结果来验证默认应用是否存在。只要它不为空,我们就知道有一个应用可以处理这个意图。(否则,我们的应用会崩溃。)我们创建一个文件名,并将其添加到意图中:putExtra(MediaStore.EXTRA_OUTPUT, mLastPhotoURI)
。
当我们在onActivityResult()
中获得回拨时,我们首先确定是PHOTO_RESULT
和RESULT_OK
(用户可以取消),然后我们在ImageView
中加载照片。
如果不在乎图片存放在哪里,可以不使用MediaStore.EXTRA_OUTPUT
额外调用意图。如果不指定输出文件,onActivityResult()
将在数据意向中包含图像的缩略图。以下是显示缩略图的方式:
if (data != null) {
imageView
.setImageBitmap((Bitmap) data.getExtras().get("data"));
}
下面是加载全分辨率图像的代码,使用在data Intent
中返回的 URI:
if (data != null) {
try {
imageView.setImageBitmap(
MediaStore.Images.Media. getBitmap(getContentResolver(),
Uri.parse(data.toUri(Intent.URI_ALLOW_UNSAFE))));
} catch (IOException e) {
e.printStackTrace();
}
}
如果你想让调用默认的视频采集应用,也是同样的过程。只需在步骤 5 中更改意图,如下所示:
Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
您可以在onActivityResult()
中获得视频的 URI,如下所示:
Uri videoUri = intent.getData();
- 第 9 章、图形和动画中的缩小大图像以避免内存不足异常方法。
之前的配方演示了如何使用一个意图来调用默认照片应用。如果你只需要一个快速的照片,意图可能是理想的解决方案。如果没有,并且你需要对相机有更多的控制,这个食谱会告诉你如何直接用相机 API 来使用相机。
实际上有两种使用相机应用编程接口的方法——一种是安卓 1.0(应用编程接口 1)中发布的原始相机应用编程接口,另一种是安卓 5.0(应用编程接口 21)中发布的相机 2 应用编程接口。我们将涵盖新的和旧的 API。理想情况下,您会希望将您的应用编写为可用的最新、最好的 API,但在撰写本文时,Android 5.0 (API 21)仅占大约 23%的市场份额。如果只使用 Camera2 API,就排除了超过 75%的市场。
编写您的应用,使用 Camera2 API 来利用可用的新功能,但仍然为您的其他用户提供使用原始 Camera API 的功能性应用。为了方便两者的使用,本食谱将利用安卓系统中较新的功能,特别是安卓 4.0 (API 14)中引入的TextureView
。我们将使用TextureView
,代替更传统的SurfaceView
,来显示相机预览。这将允许您使用与更新的 Camera2 应用编程接口相同的布局,因为它也使用TextureView
。(将最低应用编程接口设置为安卓 4.0(应用编程接口 14)及以上,市场份额超过 96%,并没有太多限制你的用户群。)
在安卓工作室新建一个项目,称之为CameraAPI
。\在的目标安卓设备对话框中,选择手机&平板电脑选项,并为最小软件开发工具包选择 API 14(或以上)。当提示输入活动类型时,选择空活动。
首先打开安卓清单,并按照以下步骤操作:
-
添加以下两个权限:
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-
现在打开
activity_main.xml
并用以下视图替换现有的文本视图:<TextureView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/textureView" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Take Picture" android:id="@+id/button" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="takePicture"/>
-
打开
MainActivity.java
修改MainActivity
类申报执行SurfaceTextureListener
,如下:public class MainActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener {
-
将以下全球申报添加到
MainActivity
:@Deprecated private Camera mCamera; private TextureView mTextureView;
-
创建以下
PictureCallback
来处理保存照片:Camera.PictureCallback pictureCallback = new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { try { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(System.currentTimeMillis()); String fileName = "PHOTO_" + timeStamp + ".jpg"; File pictureFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),fileName); FileOutputStream fileOutputStream =new FileOutputStream(pictureFile.getPath()); fileOutputStream.write(data); fileOutputStream.close(); Toast.makeText(MainActivity.this, "Picture Taken", Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); } } };
-
将以下代码添加到现有的
onCreate()
回调中:mTextureView = (TextureView)findViewById(R.id.textureView); mTextureView.setSurfaceTextureListener(this);
-
增加以下方法实现
SurfaceTextureListener
界面:public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mCamera = Camera.open(); if (mCamera!=null) { try { mCamera.setPreviewTexture(surface); mCamera.startPreview(); } catch (IOException e) { e.printStackTrace(); } } } public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { if (mCamera!=null) { mCamera.stopPreview(); mCamera.release(); } return true; } public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { // Unused } public void onSurfaceTextureUpdated(SurfaceTexture surface) { // Unused }
-
添加以下方法处理按钮点击:
public void takePicture(View view) { if (mCamera!=null) { mCamera.takePicture(null, null, pictureCallback); } }
-
在带摄像头的设备或模拟器上运行应用。
首先要注意的是,当您在 Android Studio 中查看这些代码时,您会看到许多带有以下警告的删除线代码:
'android.hardware.Camera' is deprecated
介绍中提到android.hardware.camera2
API 是在安卓 5.0 (API 19)中引入的,取代了android.hardware.camera
API。
您可以添加以下注释来抑制“不赞成”警告:
@SuppressWarnings("deprecation")
使用相机应用编程接口时有两个主要步骤:
- 设置预览
- 捕捉图像
我们从布局中获取TextureView
,然后使用以下代码将我们的活动(实现SurfaceTextureListener
)分配为监听器:
mTextureView.setSurfaceTextureListener(this);
当TextureView
曲面准备好后,我们得到onSurfaceTextureAvailable
回调,在这里我们用下面的代码设置预览曲面:
mCamera.setPreviewTexture(surface);
mCamera.startPreview();
下一步是按下按钮时拍照。我们用这个代码来实现:
mCamera.takePicture(null, null, pictureCallback);
当图片准备好后,我们在创建的Camera.PictureCallback
类中得到onPictureTaken()
回调。
请记住,这段代码旨在向您展示它是如何工作的,而不是创建一个完整的商业应用。正如大多数开发人员所知,编码的真正挑战是处理所有的问题案例。一些需要改进的地方包括添加切换摄像头的功能,因为该应用目前使用默认摄像头。此外,查看预览和保存图片时的设备方向。一个更复杂的应用会在后台线程上处理一些工作,以避免用户界面线程的延迟。(看看下一个食谱,看看我们如何在后台线程上进行一些相机处理。)
相机应用编程接口包括参数,允许我们调整相机设置。通过此示例,我们可以更改预览的大小:
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(mPreviewSize.width,
mPreviewSize.height);
mCamera.setParameters(parameters);
请记住,硬件也必须支持我们想要的设置。在这个例子中,我们希望首先查询硬件以获得所有可用的预览模式,然后设置符合我们的要求的模式。(在我们设置图片分辨率时,请参见下一个食谱中的示例。)参见相机文档链接中的getParameters()
。
- 下一个食谱:使用 Camera2(新)API 拍照
- 第 8 章、中的读取设备方向方法使用触摸屏和传感器检测当前设备方向的示例
- 开发者文档:构建相机应用网址:https://Developer . Android . com/guide/topics/media/Camera . html #自定义相机
- https://developer . Android . com/reference/Android/hardware/camera . html
现在我们已经查看了旧的 Camera API,是时候了解新的 Camera2 API 了。不幸的是,由于 API 的异步特性,它有点复杂。幸运的是,整体概念与之前的 Camera API 相同。
在 Android Studio 中创建新项目,并将其称为Camera2API
。在目标安卓设备对话框中,选择手机&平板电脑选项,并为最小软件开发工具包选择 API 21(或更高版本)。当提示输入活动类型时,选择空活动。
正如您将看到的,这个食谱有很多代码。首先打开安卓清单,并按照以下步骤操作:
-
添加以下两个权限:
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-
现在打开
activity_main.xml
并用以下视图替换现有的文本视图:<TextureView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/textureView" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Take Picture" android:id="@+id/button" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="takePicture"/>
-
现在打开
MainActivity.java
文件,将以下全局变量添加到MainActivity
类:private CameraDevice mCameraDevice = null; private CaptureRequest.Builder mCaptureRequestBuilder = null; private CameraCaptureSession mCameraCaptureSession = null; private TextureView mTextureView = null; private Size mPreviewSize = null;
-
添加以下
Comparator
类:static class CompareSizesByArea implements Comparator<Size> { @Override public int compare(Size lhs, Size rhs) { return Long.signum((long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); } }
-
增加以下
CameraDevice.StateCallback
:private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { mCameraDevice = camera; SurfaceTexture texture = mTextureView.getSurfaceTexture(); if (texture == null) { return; } texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); Surface surface = new Surface(texture); try { mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); } catch (CameraAccessException e){ e.printStackTrace(); } mCaptureRequestBuilder.addTarget(surface); try { mCameraDevice.createCaptureSession(Arrays.asList(surface), mPreviewStateCallback, null); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onError(CameraDevice camera, int error) {} @Override public void onDisconnected(CameraDevice camera) {} };
-
在
SurfaceTextureListener
后面加上:private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) {} @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {} @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return false; } @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { openCamera(); } };
-
增加以下
CameraCaptureSession.StateCallback
:private CameraCaptureSession.StateCallback mPreviewStateCallback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { startPreview(session); } @Override public void onConfigureFailed(CameraCaptureSession session) {} };
-
将以下代码添加到现有的
onCreate()
回调中:mTextureView = (TextureView) findViewById(R.id.textureView); mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
-
添加以下方法覆盖
onPause()
和onResume()
:@Override protected void onPause() { super.onPause(); if (mCameraDevice != null) { mCameraDevice.close(); mCameraDevice = null; } } @Override public void onResume() { super.onResume(); if (mTextureView.isAvailable()) { openCamera(); } else { mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); } }
-
添加
openCamera()
方法:
```java
private void openCamera() {
CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE);
try{
String cameraId = manager.getCameraIdList()[0];
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
mPreviewSize = map.getOutputSizes(SurfaceTexture.class) [0];
manager.openCamera(cameraId, mStateCallback, null);
} catch(CameraAccessException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
}
```
- 添加的
startPreview()
T3】方法:
```java
private void startPreview(CameraCaptureSession session) {
mCameraCaptureSession = session;
mCaptureRequestBuilder.set(CaptureRequest.CONTROL_MODE,CameraMetadata.CONTROL_MODE_AUTO);
HandlerThread backgroundThread = new HandlerThread("CameraPreview");
backgroundThread.start();
Handler backgroundHandler = new Handler(backgroundThread. getLooper());
try {
mCameraCaptureSession.setRepeatingRequest(mCaptureRequestBuilder.build(), null, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
```
- 添加
getPictureFile()
方法:
```java
private File getPictureFile() {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss"). format(System.currentTimeMillis());
String fileName = "PHOTO_" + timeStamp + ".jpg";
return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),fileName);
}
```
- 添加保存图像文件的
takePicture()
方法:
```java
protected void takePicture(View view) {
if (null == mCameraDevice) {
return;
}
CameraManager manager = (CameraManager)
getSystemService(Context.CAMERA_SERVICE);
try {
CameraCharacteristics characteristics = manager.getCameraCharacteristics(mCameraDevice.getId());
StreamConfigurationMap configurationMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (configurationMap == null) return;
Size largest = Collections.max(
Arrays.asList(configurationMap.getOutputSizes(ImageFormat.JPEG)),
new CompareSizesByArea());
ImageReader reader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, 1);
List < Surface > outputSurfaces = new ArrayList < Surface > (2);
outputSurfaces.add(reader.getSurface());
outputSurfaces.add(new Surface(mTextureView.getSurfaceTexture()));
final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_ CAPTURE);
captureBuilder.addTarget(reader.getSurface());
captureBuilder.set(CaptureRequest.CONTROL_MODE,
CameraMetadata.CONTROL_MODE_AUTO);
ImageReader.OnImageAvailableListener readerListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = null;
try {
image = reader.acquireLatestImage();
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.capacity()];
buffer.get(bytes);
OutputStream output = new FileOutputStream( get PictureFile());
output.write(bytes);
output.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (image != null) {
image.close();
}
}
}
};
HandlerThread thread = new HandlerThread("CameraPicture");
thread.start();
final Handler backgroudHandler = new Handler(thread.getLooper());
reader.setOnImageAvailableListener(readerListener, backgroudHandler);
final CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(
CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
Toast.makeText(MainActivity.this, "Picture Saved", Toast.LENGTH_SHORT).show();
startPreview(session);
}
};
mCameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
@Override
public vod onConfigured(CameraCaptureSession session) {
try {
session.capture(captureBuilder.build(), captureCallback, backgroudHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) { }
}, backgroudHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
```
- 在带摄像头的设备或模拟器上运行应用。
自从我们在之前的食谱中学习了关于纹理视图,我们可以跳转到新的 Camera2 应用编程接口信息。
虽然涉及的类更多,就像旧的摄像机应用编程接口一样,但有两个基本步骤:
- 设置预览
- 捕捉图像
下面是代码如何设置预览的概要:
- 首先,我们在
onCreate()
用setSurfaceTextureListener()
方法设置TextureView.SurfaceTextureListener
。 - 当我们得到
onSurfaceTextureAvailable()
回调时,我们打开摄像机。 - 我们将
CameraDevice.StateCallback
类传递给openCamera()
方法,该方法最终调用onOpened()
回调。 onOpened()
通过调用getSurfaceTexture()
获取预览的表面,并通过调用createCaptureSession()
将其传递给摄像机设备。- 最后调用
CameraCaptureSession.StateCallback onConfigured()
时,我们用setRepeatingRequest()
方法开始预览。
即使takePicture()
方法看起来是程序性的,捕捉图像也涉及几个类,并且依赖于回调。下面是代码如何拍照的详细说明:
- 用户点击拍照按钮。
- 然后查询相机以找到最大的可用图像尺寸。
- 然后创建一个
ImageReader
。 - 接下来,他/她设置
OnImageAvailableListener
,并将图像保存在onImageAvailable()
回调中。 - 然后,创建
CaptureRequest.Builder
并包括ImageReader
曲面。 - 接下来,创建
CameraCaptureSession.CaptureCallback
,定义onCaptureCompleted()
回调。捕获完成后,它会重新启动预览。 - 然后,调用
createCaptureSession()
方法,创建一个CameraCaptureSession.StateCallback
。这就是capture()
方法被调用的地方,传入了之前创建的CameraCaptureSession.CaptureCallback
。
与前面的相机示例一样,我们刚刚创建了基础代码来演示一个工作正常的相机应用。同样,也有需要改进的地方。首先,无论是预览还是保存图像,您都应该处理设备方向。(链接见上一个食谱。)此外,随着 Android 6.0 (API 23)现已可用,这将是开始使用新权限模型的好时机。与其像我们在openCamera()
方法中那样只检查异常,不如检查所需的权限。
- 之前的食谱:用(旧的)相机 API 拍照
- 第十四章 中全新的安卓 6.0 运行时权限模型,让你的应用为游戏商店做好准备
- 开发者文档:Camera2 API
- https://developer . Android . com/reference/Android/hardware/camera 2/package-summary . html