pAdTy_-1 构建多媒体应用程序

2015.11.07-11.11
个人英文阅读练习笔记。原文地址:http://developer.android.com/training/building-multimedia.html

2015.11.07
此部分内容指引用户如何创建它们所期望的丰富的多媒体应用程序。

1. 管理音频播放

此部分内容包括:当硬件音频按键被按下、音频播放时要求焦点以及当音频焦点改变时如何做出合适的响应。

1.1 控制应用程序的音量和播放

一个好的用户体验包括可预控(测)这一方面。对于一个播放媒体的应用程序来说,用户能够通过硬件(如耳机)或软件(蓝牙)的方式来控制音量是非常重要的。

同理,应用程序需要能对合适有效的播放、停止、暂停、快进以及播放上一首等操作分别作出正确的响应。

(1) 确认使用哪一个音频流
创建可预控(测)用户体验的第一步是要确认应用程序将使用哪个音频流。

安卓使用独立的音频流来维护播放引用、警告音、通知、来电铃声、系统声音、再电声以及DTMF声调。这就让用户能够独立的控制每一个流。

以上的大多数流都首系统事件的限制,所以,除非是一个代替闹钟的应用程序,不然,大多数播放音频的应用程序都是使用STREAM_MUSIC流。

(2) 用硬件音量按键控制音频音量
默认情况下,按下音量控制键时会更改当前正在被使用的音频流的音量。如果应用程序没有播放任何媒体,那么会更改铃声(ringer)音量。

如果当前运行一个游戏或音乐播放器的应用程序,用户按下音量按键来控制游戏或音乐的音量是一个不错的机会,不管当前是否有音乐播放。

您可能会采用边按音量按键边听音量的方式来改变音频流的音量。抵制这种做法。安卓提供了一个方便的setVolumeControlStream()方法,此方法直接控制您所指定音频流的音量。

在已经确认应用程序会用的音频流之后,应该将此音频流设置为音频流目标。应该在应用程序中早点完成这一步 —- 因为在活动的生命周期中只需执行一次,典型的做法是在onCreate()方法中完成(在控制媒体的活动或碎片(Fragemt)中)。这样能够确保当应用程序对用户可见时,音量控制能够达到用户期望。

setVolumeControlStream(AudioManager.STREAM_MUSIC);

从以上这个语句看,当活动或碎片可见时,按下音量控制键会影响指定音频流(以上语句的音频流为“音乐”)的音量。

(3) 用硬件播放控制按键控制应用程序音频的播放
在一些手持设备上都有诸如播放、暂停、停止、跳过、上一个等媒体按钮,还支持有线或无线的耳机(耳机上可能也有这些按钮)。无论用户何时按下这些硬件按键,系统都会广播一个含ACTION_MEDIA_BUTTON动作的目的。

欲响应媒体按键,需要在清单文件中注册一个来监听按键动作的广播,代码如下:

<receiver android:name=".RemoteControlReceiver">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON" />
    </intent-filter>
</receiver>

需要在应用程序中获取是哪个按键被按下所引起的广播。目的包含下的EXTRA_KEY_EVENT按键,KeyEvent类中包含代表诸如 KEYCODE_MEDIA_PLAY_PAUSE和KEYCODE_MEDIA_NEXT的每个按键的静态常量。

以下代码片段演示如何获取媒体按钮被按下并根据按键相应的播放媒体:

public class RemoteControlReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
            KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
            if (KeyEvent.KEYCODE_MEDIA_PLAY == event.getKeyCode()) {
                // Handle key press.
            }
        }
    }
}

因为许多应用程序可能需要监听媒体按键被按下的状态,所以当程序需要时要用编程的方式控制接收媒体按键被按下的事件。

以下代码能够被应用于应用程序中来注册或注销用AudioManager接收的媒体按键事件。当注册后,广播接收是所有媒体按钮广播的例外:

AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
...

// Start listening for button presses
am.registerMediaButtonEventReceiver(RemoteControlReceiver);
...

// Stop listening for button presses
am.unregisterMediaButtonEventReceiver(RemoteControlReceiver);

一般来讲,当应用程序不再运行或可见(诸如处于onStop()回调函数中)时应当注销接收器。然而,对于媒体播放应用程序却不是这么简单,当媒体应用程序不再可见即在不能用屏幕用户界面的方式控制媒体按键时的处理异常重要。

一个比较好的注册和注销媒体按键接收事件的方式分别是当应用程序获得和失去音频焦点时。这将在下一节中详解。

2015.11.08

1.2 管理音频焦点

在同一个设备上可能有多个音频应用程序,考虑它们之间的交互变得十分重要。为避免多个音频应用程序同时播放,安卓用音频焦点来调节音频播放 —- 只有获取音频焦点的应用程序才能够播放音频。

在媒体应用程序启动之前就应该请求并接收音频焦点。而且,当媒体应用程序监听到失去音频焦点时也应该做出恰当的响应。

(1) 请求音频焦点
在应用程序开始播放音频之前,它应该先获取将使用音频流的音频焦点。通过调用requestAudioFocus()能完成这个过程,如果请求成功,此函数将返回AUDIOFOCUS_REQUEST_GRANTED。

必须指定当前所使用的音频流,还需要明确请求临时还是永久的音频焦点。当欲播放一段短时的音频(如播放导航说明)时可请求临时的音频焦点。当欲播放非短的音频(如播放音乐)时可请求永久的音频焦点。

以下代码片段请求了音乐音频流的永久的音频焦点。在开始播放前就应该请求到此音频焦点,如当用户按播放或为下一个游戏开始前点背景音乐时。

AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
...

// Request audio focus for playback
int result = am.requestAudioFocus(afChangeListener,
                                 // Use the music stream.
                                 AudioManager.STREAM_MUSIC,
                                 // Request permanent focus.
                                 AudioManager.AUDIOFOCUS_GAIN);

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    am.registerMediaButtonEventReceiver(RemoteControlReceiver);
    // Start playback.
}

一旦完成播放之后要调用abandonAudioFocus()。此方法将通知系统本应用程序不再需要焦点并会注销相应的AudioManager.OnAudioFocusChangeListener。在放弃临时焦点的情况下,其它被打断的应用程序会继续播放。

// Abandon audio focus when playback complete    
am.abandonAudioFocus(afChangeListener);

请求临时的音频焦点还有一种情况:回避(duck)。通常,一个好的音频应用程序失去音频焦点时它会立即停止播放。通过请求一个运行快进的音频焦点来告知其它的应用程序它们可以保持播放,在焦点返回给它们之前都降低音量播放。

// Request audio focus for playback
int result = am.requestAudioFocus(afChangeListener,
                             // Use the music stream.
                             AudioManager.STREAM_MUSIC,
                             // Request permanent focus.
                             AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback.
}

回避尤其适合间歇性使用音频流的应用程序,诸如驾驶导航音频应用程序。

其它应用程序无论何时像以上描述那样请求音频焦点,在请求焦点时它们都有监听所注册的临时(不管支持汇编即duck与否)和永久音频焦点的选择。

(2) 处理失去音频焦点
您的应用程序可以请求音频焦点,那么当其它应用程序也请求音频焦点时,相应地您的应用程序也会失去音频焦点。应用程序处理失去音频焦点基于失去音频焦点的方式。

当请求音频焦点收到描述焦点改变的事件后,音频焦点的回调方法onAudioFocusChange()会改变您所注册的监听者。特别地,焦点失去事件能够反射出丢失焦点的类型 —- 永久失去、临时失去还是允许回避(duck)式的失去。

通常来讲,临时的失去焦点会导致应用程序的音频流沉默,否则会维持相同状态。应该继续保持监控焦点的改变,当应用程序重新获得焦点时要从原来暂停的地方恢复播放。

如果音频焦点失去是永久的,应用程序应该认为有另外一个应用程序正在监听音频且失去焦点的应用程序自身应该有效的自动结束。实际上,这就意味着停止播放、移除媒体按钮监听器 — 允许一个新的音频播放器肚子操控这些事件 —- 并自动抛弃音频焦点。也就是说,在恢复音频播放前必须要有用户动作(在失去焦点的应用程序中按播放)。

在以下代码片段中,处理暂停播放或媒体播放器临失去音频焦点时的情况,当重新获得焦点时并恢复。如果焦点时永久失去,就注销媒体按钮事件接收器并监控音频焦点的变化。

OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
    public void onAudioFocusChange(int focusChange) {
        if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT
            // Pause playback
        } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
            // Resume playback 
        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
            am.unregisterMediaButtonEventReceiver(RemoteControlReceiver);
            am.abandonAudioFocus(afChangeListener);
            // Stop playback
        }
    }
};

在回避(ducking)被允许而让音频焦点临时失去的情况,可以用接下来的“回避(duck)”笔记中的方法处理。

(3) 回避(duck)
回避(ducking)是当应用程序临时失去音频焦点时降低音频流音量的过程。能简单的听见即可不用完全打乱您自己应用程序的音频。

以下代码片段功能:当媒体播放器暂时失去音频焦点时降低音量,当应用程序重新获取焦点后又恢复到之前状态。

OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
    public void onAudioFocusChange(int focusChange) {
        if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
            // Lower the volume
        } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
            // Raise it back to normal
        }
    }
};

音频焦点的丢失是需要响应的最重要的广播,但不是唯一的一个。系统有许多广播给应用程序的目的来警告用户改变用户的音频体验。下一节将演示如何监控它们来提升用户整体的体验。

2015.11.09

1.3 控制音频输出硬件

在安卓设备上有许多音频应用程序的选择。许多设备上有内建的扬声器、耳机插槽、支持A2DP音频的蓝牙连接。

(1) 检查正在使用的硬件
音频应用程序的行为可能会基于正在使用硬件的输出。

以下代码片段通过查询AudioManager来判断当前音频是被用在设备扬声器、有线耳机还是蓝牙设备:

if (isBluetoothA2dpOn()) {
    // Adjust output for Bluetooth.
} else if (isSpeakerphoneOn()) {
    // Adjust output for Speakerphone.
} else if (isWiredHeadsetOn()) {
    // Adjust output for headsets
} else { 
    // If audio plays and noone can hear it, is it still playing?
}

(2) 处理音频输出硬件的改变
当拔去耳机或者断开蓝牙设备时,音频流会自动切换到内建的扬声器上。如果播放的音乐处于一个高音量,在拔去耳机等操作后可能会给人一个“惊喜”的噪音。

幸运的是,当此情况发生,系统会广播一个ACTION_AUDIO_BECOMING_NOISY目的。在播放音频的应用程序中注册BroadcastReceiver来监听这种目的时一种好的实践。在这样的音乐播放应用程序中,用户往往期望播放能够暂停 —- 如果是运行的是游戏用户可能希望降低音量。

private class NoisyAudioStreamReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
            // Pause the playback
        }
    }
}

private IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);

private void startPlayback() {
    registerReceiver(myNoisyAudioStreamReceiver(), intentFilter);
}

private void stopPlayback() {
    unregisterReceiver(myNoisyAudioStreamReceiver);
}

2. 拍照

此部分内容包括:如何借助于用户设备上存在的照相机应用程序来拍照以及怎么直接控制摄像头硬件来构建一个照相机应用程序。

在丰富的媒体流行起来以前世界犹如一个凄凉、惨淡的地方。可还记得Gopher?我们都不记得。欲将应用程序成为用户生活的一部分,给他们提供一种将他们生活放进应用程序的方式。使用设备上的摄像头,可以制作用户能将它们周围所见到的增加到应用程序中的应用程序,通过此应用程序,用户可以做独特的留影、捕获某角落的某一刻或分享它们的经历。

此部分笔记将介绍一些超级简单的方法来借用已存在的相机应用程序。在后续笔记里,将会深入了解并学会直接控制摄像头硬件设备。

2015.11.10

2.1 简单地拍照

此部分内容包括:用少许代码来借助其它应用程序来捕获照片。

假设您正在实现一个通过混合设备所拍照片来制作全球气候图的人群天气服务应用程序。整理照片只是应用程序一小部分功能。您想通过借助于其他应用程序拍得照片而不是直接通过摄像头。庆幸的是,大多数安卓设备在一开始就至少有一个已经安装的照相机应用程序。在此部分笔记中,您将学习到怎么在应用程序中拍照片。

(1) 请求摄像头权限
如果拍照是您应用程序中必要的功能,那么此应用程序就需要设备之上要有摄像头。欲告知应用程序时基于有摄像头的设备,在清单文件中添加标签:

<manifest ... >
    <uses-feature android:name="android.hardware.camera"
                  android:required="true" />
    ...
</manifest>

如果应用程序会使用摄像头,但可以不请求摄像头(不使用关联摄像头部分的功能),那么将android:required设置为false。如此,谷歌Play就允许无摄像头的设备下载此类应用程序。在无摄像头的设备上运行此应用程序时,应用程序在运行时调用hasSystemFeature(PackageManager.FEATURE_CAMERA)来检查设备是否有可用的摄像头。如果无可用摄像头,那么应用程序应当关闭跟摄像头相关的部分功能。

(2) 用照相机应用程序拍照
安卓授权动作到其它应用程序是通过调用含开发者意图的目的完成的。这个过程涉及3个步骤:创建目的本身,启动其它应用程序中的活动,当焦点回到您的活动中时处理图片数据的代码。

以下函数调用目的来拍照:

static final int REQUEST_IMAGE_CAPTURE = 1;

private void dispatchTakePictureIntent() {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
    }
}

注意startActivityForResult()方法要受resolveActivity()方法的保护,resolveActivity()方法返回能够操控目的的之前的活动组件。判断resolveActivity()返回值是否为null很重要,因为如果startActivityForResult()使用的目的无应用程序可以操作,那么应用程序将会崩溃。只要返回值不为null,就可以放心的使用目的。

(3) 获取缩放图
如果简单的拍照并不是您应用程序的风格,那么在从拍照应用程序中获得照片后还定是想做些什么。

照相机应用程序将照片按照Bitmap格式编码作为目的的额外数据并将目的返回传递给onActivityResult()。以下代码片段检索图片并将其展示到ImageView中。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        Bundle extras = data.getExtras();
        Bitmap imageBitmap = (Bitmap) extras.get("data");
        mImageView.setImageBitmap(imageBitmap);
    }
}

注:保存在“数据”中的缩略图类型比较适合保存图标(icon)这样的图片。处理全尺寸的图片就需要花更多的功夫。

(4) 保存全尺寸照片
如果将照片当成一个文件保存,安卓照相机应用程序将会保存全尺寸的照片。此时必须要给所保存照片提供一个准确的路径。

通常,用设备相机拍的照片都应该保存到设备的外部存储器中以让所有的应用程序都可以访问。用来分享照片的目录由伴随 DIRECTORY_PICTURES参数的getExternalStoragePublicDirectory()提供。因为由此方法提供的目录可以供所有的应用程序分享,所以其它应用程序写或读此目录时需分别请求READ_EXTERNAL_STORAGE和 WRITE_EXTERNAL_STORAGE权限。写权限隐式的包含了读权限,如果应用程序要写外部存储器就只需要请求一个写权限:

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

然而,如果您想这些照片只供您的应用程序私有,那么就用getExternalFilesDir()方法来提供照片的保存目录。对于安卓4.3及更低的版本,写此目录需要请求WRITE_EXTERNAL_STORAGE权限。从安卓4.4开始就不再需要请求此权限,因为其它应用程序不能访问此目录。所以此权限只需要在低于某个版本的安卓系统中添加,此功能由maxSdkVersion属性完成:

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="18" />
    ...
</manifest>

注:当用户卸载应用程序,由此应用程序调用getExternalFilesDir()方法所提供目录下的文件也将会被删除。

一旦决定了文件的保存目录,就需要为各个文件创建不冲突的文件名。您可能还希望将路径保存在变量中以供以后使用。以下代码片段用以日期时间戳的照片返回唯一的文件名:

String mCurrentPhotoPath;

private File createImageFile() throws IOException {
    // Create an image file name
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
    String imageFileName = "JPEG_" + timeStamp + "_";
    File storageDir = Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES);
    File image = File.createTempFile(
        imageFileName,  /* prefix */
        ".jpg",         /* suffix */
        storageDir      /* directory */
    );

    // Save a file: path for use with ACTION_VIEW intents
    mCurrentPhotoPath = "file:" + image.getAbsolutePath();
    return image;
}

在用这种方法为照片创建文件后,就可以向以下这样创造并调用目的:

static final int REQUEST_TAKE_PHOTO = 1;

private void dispatchTakePictureIntent() {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    // Ensure that there's a camera activity to handle the intent
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        // Create the File where the photo should go
        File photoFile = null;
        try {
            photoFile = createImageFile();
        } catch (IOException ex) {
            // Error occurred while creating the File
            ...
        }
        // Continue only if the File was successfully created
        if (photoFile != null) {
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                    Uri.fromFile(photoFile));
            startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
        }
    }
}

(5) 将照片增加到图库
您应该知道通过目的创造的图片的位置,因为最开始是您决定图片位于哪里。对于其它的应用程序,最简单的访问图片的方式是从系统的媒体提供器那里获得访问权限。

注:如果照片放在getExternalFilesDir()提供的目录中,媒体扫描器不能访问图片,因为图片是应用程序所私有的。

以下代码样例演示如何调用系统媒体扫描器来将照片添加到媒体提供器的数据库中,即将图片放在安卓的图片应用程序中,其它的应用程序都可以访问。

private void galleryAddPic() {
    Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    File f = new File(mCurrentPhotoPath);
    Uri contentUri = Uri.fromFile(f);
    mediaScanIntent.setData(contentUri);
    this.sendBroadcast(mediaScanIntent);
}

(6) 解码缩放图片
在有限内存中同时管理全尺寸的图片可能比较困难。如果在展示几张图片后发现应用程序跑到了内存之外,您可以通过将JPEG图片扩展到已经缩放到刚好拍匹配图片尺寸的内存序列中以减小堆内存的使用。以下代码片段演示这项技术:

private void setPic() {
    // Get the dimensions of the View
    int targetW = mImageView.getWidth();
    int targetH = mImageView.getHeight();

    // Get the dimensions of the bitmap
    BitmapFactory.Options bmOptions = new BitmapFactory.Options();
    bmOptions.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
    int photoW = bmOptions.outWidth;
    int photoH = bmOptions.outHeight;

    // Determine how much to scale down the image
    int scaleFactor = Math.min(photoW/targetW, photoH/targetH);

    // Decode the image file into a Bitmap sized to fill the View
    bmOptions.inJustDecodeBounds = false;
    bmOptions.inSampleSize = scaleFactor;
    bmOptions.inPurgeable = true;

    Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
    mImageView.setImageBitmap(bitmap);
}

2015.11.11

2.2 录制视频

此部分内容包括:用少许代码来借助其它应用程序来录制视频。

整理视频只是应用程序一小部分功能。您想通过借助于其他应用程序录制视频而不是直接通过摄像头。庆幸的是,大多数安卓设备在一开始就至少有一个已经安装的摄像应用程序。在此部分笔记中,您将学习到怎么在应用程序中录制视频。

(1) 请求摄像头权限
欲告知应用程序基于摄像头,在应用程序的清单文件中放置标签:

<manifest ... >
    <uses-feature android:name="android.hardware.camera"
                  android:required="true" />
    ...
</manifest>

如果应用程序会使用摄像头,但可以不请求摄像头(不使用关联摄像头部分的功能),那么将android:required设置为false。如此,谷歌Play就允许无摄像头的设备下载此类应用程序。在无摄像头的设备上运行此应用程序时,应用程序在运行时调用hasSystemFeature(PackageManager.FEATURE_CAMERA)来检查设备是否有可用的摄像头。如果无可用摄像头,那么应用程序应当关闭跟摄像头相关的部分功能。

(2) 用摄像应用程序录制视频
安卓授权动作到其它应用程序是通过调用含开发者意图的目的完成的。这个过程涉及3个步骤:创建目的本身,启动其它应用程序中的活动,当焦点回到您的活动中时处理图片数据的代码。

以下函数调用目的来摄像:

static final int REQUEST_VIDEO_CAPTURE = 1;

private void dispatchTakeVideoIntent() {
    Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    if (takeVideoIntent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE);
    }
}

注意startActivityForResult()方法要受resolveActivity()方法的保护,resolveActivity()方法返回能够操控目的的之前的活动组件。判断resolveActivity()返回值是否为null很重要,因为如果startActivityForResult()使用的目的无应用程序可以操作,那么应用程序将会崩溃。只要返回值不为null,就可以放心的使用目的。

(3) 查看视频
安卓摄像应用程序将视频附在目的中并将目的作为指向视频位置的Uri传递给onActivityResult()。以下代码片段检索视频并将视频展示到VideoView。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_VIDEO_CAPTURE && resultCode == RESULT_OK) {
        Uri videoUri = intent.getData();
        mVideoView.setVideoURI(videoUri);
    }
}

2.3 控制相机

此部分内容包括:直接控制摄像头硬件并实现照相/摄像机应用程序。

比起用已存在的照相机应用程序来拍照或录取视频,直接控制设备的摄像头需要更多的代码。如果您想要构建一个特别的照相机应用程序或者想要将照相机功能并将此设计成一个UI集到应用程序中,此部分笔记将描述如何实现这样的应用程序。

(1) 打开摄像头对象
获取摄像头对象是直接控制摄像头应用程序的第一步。推荐在onCreate()方法中用一个单独的线程来打开摄像头对象,就像安卓系统自带的照相机应用程序那样。这是一个好的方法,因为获取摄像头对象会花些时间因而有可能阻碍用户界面线程的进度。作为另外一个基本的实现,也可以延后在onResum()方法中打开摄像头,以能够使代码重用和控制流更简单。

如果摄像头已被另外一个应用程序使用,调用Camera.open()会抛出一个异常,所以可以将其封包为一个try模块:

private boolean safeCameraOpen(int id) {
    boolean qOpened = false;

    try {
        releaseCameraAndPreview();
        mCamera = Camera.open(id);
        qOpened = (mCamera != null);
    } catch (Exception e) {
        Log.e(getString(R.string.app_name), "failed to open Camera");
        e.printStackTrace();
    }

    return qOpened;    
}

private void releaseCameraAndPreview() {
    mPreview.setCamera(null);
    if (mCamera != null) {
        mCamera.release();
        mCamera = null;
    }
}

从API level 9起,相机框架支持多个摄像头。如果用传统的API并调用无参数的open()方法时,默认获得后置摄像头。

(2) 创建相机预览
通常,在用户使用快门拍照前就应该能在摄像头中看到场景的预览。欲在摄像头中看到场景预览,可以用SurfaceView预览摄像头传感器所捕获到的场景。

[1] 预览类
需要用预览类来开始呈现场景预览。场景预览需要实现android.view.SurfaceHolder.Callback接口,此接口将摄像头硬件的图片数据传递到应用程序中。

class Preview extends ViewGroup implements SurfaceHolder.Callback {

    SurfaceView mSurfaceView;
    SurfaceHolder mHolder;

    Preview(Context context) {
        super(context);

        mSurfaceView = new SurfaceView(context);
        addView(mSurfaceView);

        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = mSurfaceView.getHolder();
        mHolder.addCallback(this);
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }
...
}

在呈现图片预览之前必须将预览类传递给摄像头对象,下一节即将描述此。

[2] 设置并开始预览
摄像头实例和其相关的预览必须按照指定顺序创建。以下代码片段,初始化摄像头的过程被封装,这样,无论用户何时需改变摄像头时就可以在setCamera()方法中调用Camera.startPreview()。预览也必须在预览类中的回调方法surfaceChanged()中重启。

public void setCamera(Camera camera) {
    if (mCamera == camera) { return; }

    stopPreviewAndFreeCamera();

    mCamera = camera;

    if (mCamera != null) {
        List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
        mSupportedPreviewSizes = localSizes;
        requestLayout();

        try {
            mCamera.setPreviewDisplay(mHolder);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Important: Call startPreview() to start updating the preview
        // surface. Preview must be started before you can take a picture.
        mCamera.startPreview();
    }
}

(3) 修改相机设置
相机设置能够影响照相机拍照方式,包括从变焦到曝光。以下示例只改变预览尺寸;可解析相机源码来获取更多的信息。

public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    // Now that the size is known, set up the camera parameters and begin
    // the preview.
    Camera.Parameters parameters = mCamera.getParameters();
    parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
    requestLayout();
    mCamera.setParameters(parameters);

    // Important: Call startPreview() to start updating the preview surface.
    // Preview must be started before you can take a picture.
    mCamera.startPreview();
}

(4) 设置预览方向
大多数的应用程序将预览锁在landscape模式下,因为这是摄像头传感器自然的方向。此设定不会阻止阻止用户拍肖像模式(portrait-mode)的照片,因为设备的方向被记录在EXIF头部内。setCameraDisplayOrientation()方法提供改变预览方向但不影响图片如何被保存。然而,在安卓的API level 14之前,在改变朝向之前必须停止预览,之后再预览时需重启。

(5) 拍照
一旦场景预览被开启,就可以调用Camera.takePicture()方法来拍照了。可以创建Camera.PictureCallback和Camera.ShutterCallback对象并将它们传递给Camera.takePicture()。

若欲连续捕获图像,可以创建Camera.PreviewCallback来实现onPreviewFrame()。可以只捕获所选择的预览的帧,也可以在takePicture()中设置一个延迟。

(6) 重启预览
在拍照之后,在用户拍下一张照片之前必须重启预览。在以下示例中,在重按快门键时预览重启已经完成:

@Override
public void onClick(View v) {
    switch(mPreviewState) {
    case K_STATE_FROZEN:
        mCamera.startPreview();
        mPreviewState = K_STATE_PREVIEW;
        break;

    default:
        mCamera.takePicture( null, rawCallback, null);
        mPreviewState = K_STATE_BUSY;
    } // switch
    shutterBtnConfig();
}

(7) 停止预览并释放相机
一旦应用程序使用完摄像头就到清理时间。特别的,必须释放摄像头对象,否则会让其它有崩溃的危险,还需要释放应用程序中的一些新实例。

什么时候该停止预览并释放摄像头?当预览画面被销毁时是停止预览并释放摄像头的时机,如预览类中所示:

public void surfaceDestroyed(SurfaceHolder holder) {
    // Surface will be destroyed when we return, so stop the preview.
    if (mCamera != null) {
        // Call stopPreview() to stop updating the preview surface.
        mCamera.stopPreview();
    }
}

/**
 * When this function returns, mCamera will be null.
 */
private void stopPreviewAndFreeCamera() {

    if (mCamera != null) {
        // Call stopPreview() to stop updating the preview surface.
        mCamera.stopPreview();

        // Important: Call release() to release the camera for use by other
        // applications. Applications should release the camera immediately
        // during onPause() and re-open() it during onResume()).
        mCamera.release();

        mCamera = null;
    }
}

在此笔记的前部分,这段代码是setCamera()方法的一部分,如此看来,初始化摄像头往往会以停止预览开始。

3. 打印

此部分内容包括:如何打印照片、HTML文档以及在应用程序中自定义的文档。

安卓用户在他们的设备上即可进行频繁的内容查看。但有些时候,一个屏幕不足以分享信息。应用程序能够打印给用户提供了一个更大的版本或者能够将打印内容分享给无此应用程序的用户。打印也能够给没有设备、无充足电量或无无线网络的用户一个信息的快照。

在安卓4.4(API level 19)或更高版本中,框架为应用程序提供了直接打印图片和文档的服务。此笔记记录怎么在应用程序中打印图片、HTML网页以及用户自定义的文档。

3.1 打印照片

拍照和分享照片是移动设备中最流行的应用之一。对于一个能拍照、展示图片并允许用户分享图片的应用程序来说,还应该在此应用程序中考虑加入打印的功能。Android Support Library为使能打印图片提供了方便的函数,只需用小量代码和对打印的布局选项一些简单的设置就可以实现图片的打印。

安卓支持库PrintHelper类提供了打印图片的简单方法。此类有一个布局选项,setScaleMode(),次方法允许以以下两种选择中的一种进行打印:
- SCALE_MODE_FIT - 此选项会约束图片的尺寸以让整张图片恰好在打印区域内。
- SCALE_MODE_FILL - 此选项缩放图片以让图片填充到页面中的打印区域。选择此选项就意味中图片的某些边缘有可能不会被打印。此选项是打印的默认选项。

在setScaleMode()中的两种选项都会保持图片的纵横比不变。以下代码示例演示如何创建PrintHelper类的实例、如何设置缩放选项以及如何开始打印:

private void doPhotoPrint() {
    PrintHelper photoPrinter = new PrintHelper(getActivity());
    photoPrinter.setScaleMode(PrintHelper.SCALE_MODE_FIT);
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
            R.drawable.droids);
    photoPrinter.printBitmap("droids.jpg - test print", bitmap);
}

此方法可以作为菜单栏目的触发函数。不会支持动作事件(如打印)的注意菜单栏目应该放在溢出菜单栏目中。更多信息见Action Bar设计手册。

在printBitmap()方法被调用后,应用程序就不再需要其它的动作。安卓打印的用户界面出现时,允许用户选择具体的打印机和打印选项。用户可以完成或取消图片的打印。如果用户选择打印图片,打印的任务被创建且系统条上会显示打印的通知。

如果想打印不仅是图片而包含其他内容的打印输出,必须构建打印文档。关于创建打印文档,见后续内容。

3.2 打印HTML文档

若打印内容不仅是图片就需要将文本和图片组合在打印文档中。安卓框架提供用HTML来组织文档并打印它的方式。

在安卓4.4(API level19)中,WebView类已更新至能够打印HTML文档。此类能够加载本地或网页上下载的HTML文档,并为之创建打印工序后将其打印出来。

此部分笔记将演示如何用WebView类来快速构建包含文本和图片的HTML文档并将其打印。

(1) 加载HTML文档
用WebView类来打印HTML文档包括加载或构建HTML文档字符串。此部分将描述如何构建HTML字符串并将它载入WebView中供打印。

查看对象为活动布局的一部分。然而,如果应用程序中并未用WebView,可以围此类创建一个实例来指定打印的意图。这主要包括创建打印自定义视图的步骤:
[1] 在HTML资源被加载后创建WebViewClient来开启打印工序。
[2] 将HTML资源加载到WebView对象中。

以下代码示例演示创建一个简单的WebViewClient和HTML文档的载入:

private WebView mWebView;

private void doWebViewPrint() {
    // Create a WebView object specifically for printing
    WebView webView = new WebView(getActivity());
    webView.setWebViewClient(new WebViewClient() {

            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                return false;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                Log.i(TAG, "page finished loading " + url);
                createWebPrintJob(view);
                mWebView = null;
            }
    });

    // Generate an HTML document on the fly:
    String htmlDocument = "<html><body><h1>Test Content</h1><p>Testing, " +
            "testing, testing...</p></body></html>";
    webView.loadDataWithBaseURL(null, htmlDocument, "text/HTML", "UTF-8", null);

    // Keep a reference to WebView object until you pass the PrintDocumentAdapter
    // to the PrintManager
    mWebView = webView;
}

注:确保产生打印工序的调用要在之前创建的WebViewClient中的onPageFinished()中。如果不等HTML加载结束,打印输出可能会不完整或空白,甚至会完全失败。

注:以上代码示例拥有WebView对象示例,这样可以确保在打印工作被创建前不会收集无用数据。要确保在您的应用程序中也是同样的实现,否则打印过程可能会失败。

如果想要在打印页面中包含图片,将图片放在工程的assets/目录下并在loadDataWithBaseURL()方法的第一个参数中为此指定一个基本的URL,如以下代码示例:

webView.loadDataWithBaseURL("file:///android_asset/images/", htmlBody,
        "text/HTML", "UTF-8", null);

将loadDataWithBaseURL()方法替换成loadUrl()就可以从加载网页来打印,如以下代码示例:

// Print an existing web page (remember to request INTERNET permission!):
webView.loadUrl("http://developer.android.com/about/index.html");

需意识到用WebView创建打印文档的一些限制:
- 不能为文档添加页眉或页脚,包括文档的页码。
- 打印HTML文档选项不包括打印指定范围的文档,如:打印10页中的第2到第4页是不支持的。
- WebView示例在同一时间只能处理一个打印工作。
- 不支持在HTML文档中包含的CSS属性,如landscape属性。
- 不能在HTML文档中用JavaScript来触发打印。

注:包含在布局中的WebView对象的内容也能够被打印,只要它加载了文档。

欲创建更多自定义的打印输出并且想拥有对页面内容的绝对控制权,跳转到下一节“打印自定义文档”。

(2) 创建打印工序
在创建WebView和加载HTML内容后,应用程序完成了打印过程中的一大部分。后续步骤需要访问PrintHelper,创建打印适配器,最后创建打印工序。以下代码演示如何完成这些步骤:

private void createWebPrintJob(WebView webView) {

    // Get a PrintManager instance
    PrintManager printManager = (PrintManager) getActivity()
            .getSystemService(Context.PRINT_SERVICE);

    // Get a print adapter instance
    PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter();

    // Create a print job with name and adapter instance
    String jobName = getString(R.string.app_name) + " Document";
    PrintJob printJob = printManager.print(jobName, printAdapter,
            new PrintAttributes.Builder().build());

    // Save the job object for later status checking
    mPrintJobs.add(printJob);
}

此例保存了一个供应用程序使用的PrintJob对象,此对象不需要权限请求。当打印开始后,应用程序可以用这个对象去跟踪打印工序的进度。此方法在欲监控打印工作的完成、失败或用户取消等状态十分有用。不需要在应用程序中构建通知模块,因为打印框架会自动为打印工作创造系统通知。

3.3 打印自定义文档

对于诸如画图、页面布局这样的应用程序主要比较关注图像输出,创造漂亮的打印页面是它们主要的特征。在这种情形下,仅打印图片或HTML文档已经不够了。此种类型的应用程序的输出需要对打印页面版本的精确控制,包括字体、文本流、页面空白、页眉、页脚以及图片元素。

为应用程序创造完全自定义的打印输出比之前讨论的内容需要更多的功夫。必须建立和打印框架交流的组件、调整打印机的设置、绘制页面元素以及管理多页面。

此部分笔记演示如何连接打印管理器、如何创建打印适配器以及如何构建打印的内容。

(1) 连接打印管理器
应用程序欲直接管理打印过程,在接收用户请求后的第一步是连接安卓的打印框架并获取PrintManager类的实例。此类能够初始化打印工作并开始打印的生命周期。以下代码示例演示如何获取打印管理并如何开始打印:

private void doPrint() {
    // Get a PrintManager instance
    PrintManager printManager = (PrintManager) getActivity()
            .getSystemService(Context.PRINT_SERVICE);

    // Set job name, which will be displayed in the print queue
    String jobName = getActivity().getString(R.string.app_name) + " Document";

    // Start a print job, passing in a PrintDocumentAdapter implementation
    // to handle the generation of a print document
    printManager.print(jobName, new MyPrintDocumentAdapter(getActivity()),
            null); //
}

以上代码演示如何命名一个打印工作并设置了管理打印生命周期的PrintDocumentAdapter类的实例。打印适配器类的实现将在下一节中讨论。

print()方法的最后一个参数为PrintAttributes对象。通过这个参数可以给打印框架提供一个提示并根据前一个打印周期来预设选项,从而提升用户体验。也可以使用此参数来为更适合打印的内容做设置,诸如在打印照片时将照片打印成所设置的landscape模式。

(2) 创建打印适配器
打印适配器能和安卓打印框架互动并能操纵打印过程的每一个阶段。在创建打印文档之前,此过程需要用户选择打印机和打印选项。这些选择能够影响最终的打印输出,诸如用户选择不同能力输出的打印机、选择不同尺寸的页面或不同的页面朝向。当选择好这些选项后,打印框架将会叫适配器布局并生产打印文档,并作为最终的输出。一旦用户按下打印按钮,打印框架带着最终的打印文档并把它传递给打印提供器打印输出。在打印过程中,用户可以选择取消打印操作,所以答应适配器还不洗监听并回应取消答应请求。

PrintDocumentAdapter类被设计用来操纵打印的生命周期,它主要有四个主要的回调方法。为了跟打印框架进行合适的互动必须实现这四个回调方法:
- onStart() - 在打印过程开始时调用一次。如果应用程序有任何只需要执行一次的任务,如获取即将打印数据的快照,将这些代码写在这里。不必在适配器中实现此方法。
- onLayout() - 每当用户修改影响打印输出的设置时此函数都会被调用一次来让应用程序有机会重新计算即将被打印的页面布局,如用户修改页面尺寸、页面朝向。此方法必须返回在打印文档中所期望打印的页数。
- onWrite() - 在将打印页面转换成文件来打印时此函数被调用。在调用onLayout()函数后此函数可能被调用一次或多次。
- onFinish() - 在打印结束时此函数被调用一次。如果应用程序有任何一次性销毁的任务需要执行,就调用此函数。不必在适配器中实现此方法。

以下部分将描述如何实现布局和写这两个方法,这两个方法是决定打印适配器功能的关键。

注:适配器中的这些方法被应用程序的主线程调用。如果期望您所实现的这些方法能够执行更多的时间,那么在其它独立的线程中执行这些方法。例如,可以封装布局和打印文档的写操作在独立的Async对象中。

[1] 计算打印文档信息
通过PrintDocumentAdapter类的实现,应用程序必须能够指定此类所创建的文档类型并要能为打印工作计算打印的总页数和打印的尺寸。适配器中onLayout()的实现使得这些计算和打印工作所期望提供的信息到了PrintDocumentInfo类中,包括页数和内容类型。以下代码示例演示了一个为PrintDocumentAdapter类实现的基本的onLayout():

@Override
public void onLayout(PrintAttributes oldAttributes,
                     PrintAttributes newAttributes,
                     CancellationSignal cancellationSignal,
                     LayoutResultCallback callback,
                     Bundle metadata) {
    // Create a new PdfDocument with the requested page attributes
    mPdfDocument = new PrintedPdfDocument(getActivity(), newAttributes);

    // Respond to cancellation request
    if (cancellationSignal.isCancelled() ) {
        callback.onLayoutCancelled();
        return;
    }

    // Compute the expected number of printed pages
    int pages = computePageCount(newAttributes);

    if (pages > 0) {
        // Return print information to print framework
        PrintDocumentInfo info = new PrintDocumentInfo
                .Builder("print_output.pdf")
                .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
                .setPageCount(pages);
                .build();
        // Content layout reflow is complete
        callback.onLayoutFinished(info, true);
    } else {
        // Otherwise report an error to the print framework
        callback.onLayoutFailed("Page count calculation failed.");
    }
}

执行onLayout()方法能够带来三个结果:完成,被取消或因计算布局不完全而失败。必须调用PrintDocumentAdapter.LayoutResultCallback对象中合适的方法来表明onLayout()执行后属于哪一种结果。

注:onLayoutFinished()方法的布尔型参数用来表明根据上次的布局内容此次布局内容是否有改变。合适的设置此参数能够避免架构对onWrite()无必要的调用,必要的缓存之前的写打印并试着提升性能。

onLayout()的主要工作是计算打印的页数,此页数会作为属性传递给打印机。怎么计算页数高度基于应用程序怎么布局。以下代码示例展示了计算页数的方法有打印朝向得来:

private int computePageCount(PrintAttributes printAttributes) {
    int itemsPerPage = 4; // default item count for portrait mode

    MediaSize pageSize = printAttributes.getMediaSize();
    if (!pageSize.isPortrait()) {
        // Six items per page in landscape orientation
        itemsPerPage = 6;
    }

    // Determine number of print items
    int printItemCount = getPrintItemCount();

    return (int) Math.ceil(printItemCount / itemsPerPage);
}

[2] 写打印文档文件
当到将打印内容写到文件中时,安卓打印架构将调用应用程序PrintDocumentAdapter类中实现的onWrite()方法。此方法的参数决定哪些页面将被写以及哪些输出文件将被使用。在此方法的实现中,必须包含将每个打印页面的内容都输入到一个多页的PDF文档中。当此过程完成后,就调用回调对象的onWriteFinish()方法。

注:安卓打印框架在调用onLayout()之后可能会调用一次或多次onWrite()。因此,当打印内容布局没有改变时将onlayoutFinished()方法的布尔参数设置为falsehen很重要,这样可以避免对打印文档不必要的重写操作。

以下代码演示了使用PrintedpdfDocument类来创建PDF文件的过程的基本机制:

@Override
public void onWrite(final PageRange[] pageRanges,
                    final ParcelFileDescriptor destination,
                    final CancellationSignal cancellationSignal,
                    final WriteResultCallback callback) {
    // Iterate over each page of the document,
    // check if it's in the output range.
    for (int i = 0; i < totalPages; i++) {
        // Check to see if this page is in the output range.
        if (containsPage(pageRanges, i)) {
            // If so, add it to writtenPagesArray. writtenPagesArray.size()
            // is used to compute the next output page index.
            writtenPagesArray.append(writtenPagesArray.size(), i);
            PdfDocument.Page page = mPdfDocument.startPage(i);

            // check for cancellation
            if (cancellationSignal.isCancelled()) {
                callback.onWriteCancelled();
                mPdfDocument.close();
                mPdfDocument = null;
                return;
            }

            // Draw page content for printing
            drawPage(page);

            // Rendering is complete, so page can be finalized.
            mPdfDocument.finishPage(page);
        }
    }

    // Write PDF document to file
    try {
        mPdfDocument.writeTo(new FileOutputStream(
                destination.getFileDescriptor()));
    } catch (IOException e) {
        callback.onWriteFailed(e.toString());
        return;
    } finally {
        mPdfDocument.close();
        mPdfDocument = null;
    }
    PageRange[] writtenPages = computeWrittenPages();
    // Signal the print framework the document is complete
    callback.onWriteFinished(writtenPages);

    ...
}

此代码示例展示渲染PDF页内容到drawPage()方法中,这个过程将会在下一节讨论。

注:为打印渲染文档可能是一个资源敏感的操作。为了避免阻碍应用程序的主用户界面线程,应该考虑将页面渲染以及写操作创建一个独立的线程,例如将这些操作写入AsyncTask中。更多关于像异步任务的线程见Processes and Threads。

(3) 绘制PDF页面内容
当应用程序打印时,应用程序必须生成PDF文档并将此PDF传递给安卓打印框架打印。可以使用任何的PDF生成库来生成PDF。此部分笔记展示如何使用PrintedpdfDocument类来为打印内容生成PDF文档。

PrintedpdfDocument类用Canvas对象来将元素绘制到PDF页中,类似绘制活动布局。通过使用Canvas的绘制方法可以将元素绘制到打印页上。以下代码演示如何使用这些方法来将一些简单的元素绘制到PDF文档页上:

private void drawPage(PdfDocument.Page page) {
    Canvas canvas = page.getCanvas();

    // units are in points (1/72 of an inch)
    int titleBaseLine = 72;
    int leftMargin = 54;

    Paint paint = new Paint();
    paint.setColor(Color.BLACK);
    paint.setTextSize(36);
    canvas.drawText("Test Title", leftMargin, titleBaseLine, paint);

    paint.setTextSize(11);
    canvas.drawText("Test paragraph", leftMargin, titleBaseLine + 25, paint);

    paint.setColor(Color.BLUE);
    canvas.drawRect(100, 100, 172, 172, paint);
}

在使用Canvas来绘制PDF页时,元素被划分为点来绘制,点为一英寸的1/72。确保使用点单元来指定页面的尺寸。为定位绘制的元素,系统的坐标0,0在页面的左上角。

提示:尽管Canvas对象能够在PDF文档边缘绘制元素,但是许多的打印机不能打印页面上的这些边缘元素。当用这个类来绘制打印文档时,确保那些打印机不能打印的区域。

[2015.11.11-21:11]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值