一、管理音频播放

app能够以预期方式播放音频很重要。同样app能获取音频焦点很重要,这样才能确保不会在同一时刻出现多个app的声音。

1、控制音量与音频播放

1)鉴别使用的是哪个音频流

app用到的音频流:Android为播放音乐,闹钟,通知铃,来电声音,系统声音,打电话声音与DTMF频道分别维护了一个隔离的音频流。这是我们能控制不同音频的前提。其中大多数都是被系统限制的,不能乱用。除了你的app需要做替换闹钟的铃声的操作,那么几乎其他的播放音频操作都是“STREAM_MUSIC”音频流。

2.1)使用硬件音量键来控制app的音量

默认情况,按下音量控制键会调节当前激活的音频流,如果你的app没有播放任何声音,则会调节响铃的声音。如果是一个游戏或音乐程序,需要在不管是否目前正在播放歌曲或游戏目前是否发出声音的时候,按硬件的音量键都会有对应的音量调节。我们需要监听音量键是否被按下:通过setVolumeControlStream()来直接控制指定的音频流。在鉴别出app会使用哪个音频流之后,需要在activity或Fragment创建的时候就设置音量控制。以确保app是否可见,音频控制功能都能正常工作。

setVolumeControlStream(AudioManager.STREAM_MUSIC);
2.2)使用硬件播放控制按键来控制app的音频播放

线控、耳麦或其他无线控制设备,无论用户按下上面任何设备上的控制按钮,系统都会广播一个带有ACTION_MEDIA_BUTTON的intent。注册广播来捕获它吧,皮卡丘。

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

Receiver需要判断这个广播是来自哪个按钮的操作,intent在EXTRA_KEY_EVENT中包含了KEY的信息,同样KeyEvent类包含了一列KEYCODE_MEDIA_的静态变量来表示不同的媒体按钮。如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.
}
}
}
}
2、管理音频焦点

由于有多个应用可播放音频,考虑他们该如何交互非常重要。为了防止多个音乐app同时播放音频,Android使用音频焦点(audio focus)来控制音频的播放--只有获取到audio focus的app才能播放音频。

播放音频原理:发出请求->接收请求->音频焦点锁定的过程。同样,它需要知道如何监听失去音频焦点的事件并进行合适的响应。

1)请求获取音频焦点

在你的app开始播放音频之前,它需要获取它将要使用的音频流的音频焦点(通过requestAudioFocus()来获取你想要的音频流焦点,成功则返回AUDIOFOCUS_REQUEST_GRANTED)。参数:一个指定使用哪个音频流,一个指定请求的是短暂的还是永久的audio focus。

短暂的焦点锁定:当期待播放一个短暂的音频的时候

永久的焦点锁定:当机会播放可预期到的较长的音频的时候

示例:

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的监听。在释放短暂音频焦点的情况下,这会允许你打断的app继续播放。

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


note:当请求短暂音频焦点时,可选择是否开启“ducking”。Ducking是一个特殊的机制使得允许音频间歇性的短暂播放。通常,一个好的app在失去焦点时会立刻保持安静。如果我们选择在请求短暂音频焦点时开启了ducking,那意味着其他app可以继续播放,仅在这一刻降低自己的音量,在短暂重新获取到音频焦点后恢复正常音量。

//Request audio focus for playback
int result = am.requestAudioFocus(afChangeListener,
		//Use the music stream.
		AudioManager.STREAM_MUSIC,
		//Request permanent focus.
		管理音频焦点
		请求获取音频焦点(Request the Audio Focus)
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
	//Start playback.
}
2)处理失去音频焦点

比如A程序请求获取音频焦点,B程序也请求获取,此时A获取的音频焦点就会丢失。此时该如何处理?

对于xxx,如何处理这个问题?标准思路是,谁来处理,何时来处理,该怎么处理。基于面向对象的考虑~一般谁来处理都是由其本身来处理,所以解决后两个问题即可。

何时处理:onAudioFocusChanged()会捕获该事件。怎么处理:对于获取焦点的三种类型,同样有失去焦点的三种类型:永久失去,短暂失去,允许Ducking的短暂失去。

失去短暂焦点:通常在失去这种焦点时,我们会暂停当前音频播放或调低音量,同时准备在重新获取到焦点之后重新播放。

失去永久焦点:实用的做法是停止播放,移除监听,并且放弃自己的音频焦点。

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[闪避]

下面代码让我们在暂时失去音频焦点时降低音量,并在重新获得音频焦点之后恢复原来音量。

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
		}
	}
};
3、兼容音频输出设备

用户在播放音乐的时候有多个选择,可使用内置扬声器,有线耳机或者支持A2DP的蓝牙耳机。

1)检测目前正在使用的硬件设备
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的intent。怎么处理:暂停或降低声音。

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);
}
二、拍照
1、简单的拍照
1)使用相机进行拍照
static final int REQUEST_IMAGE_CAPTURE = 1;
private void dispatchTakePictureIntent(int actionCode) {
	Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
	if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
		startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
	}
}

resolveActivity()用来检查是否有能处理这个intent的activity。

2)获取缩略图

Android的Camera程序会把拍好的照片编码为小的Bitmap,使用extra values的方式添加到返回的intent中,并传给onActivityResult(),对应的key为data。

@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);
	}
}
3)保存全尺寸照片

怎么实现:给Camera程序提供一个file对象(全路径名)即可。

图片存放位置即可在设备的公共外部存储中,或者外部私有空间。

一旦选定了你的文件目录,你需要创建一个不会冲突的文件名。下面是一个使用日期时间戳为新照片生成唯一文件名的范例解决方案:

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;
}

启动intent:

String mCurrentPhotoPath;
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);
		}
	}
}
4)添加照片到相册


note:如果你的照片存放在外部私有空间,则通过系统的媒体扫描器不能访问到你的文件。


下面例子演示如何触发系统的Media Scanner来添加你的照片到Media Provider数据库中,这是Android相册和其他程序就能读取这些照片了。

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);
}
5)解码缩放照片

多个全尺寸图片会导致OOM,可通过缩放图片到目标视图尺寸,之后再再入内存的方法来显著降低内存的使用:

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;
	添加照片到相册(Add the Photo to a Gallery)
	解码缩放图片(Decode a Scaled Image)
	bmOptions.inPurgeable = true;
	Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
	mImageView.setImageBitmap(bitmap);
}
2、简单的录像
1)启用相机录制视频
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);
	}
}
2)查看视频

Camera会把指向视频存储地址Uri添加到intent中,并传给onActivityResult()。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
	if (requestCode == REQUEST_VIDEO_CAPTURE && resultCode == RESULT_OK) {
		Uri videoUri = intent.getData();
		简单的录像
		请求相机权限(Request Camera Permission)
		使用相机程序来录制视频(Record a Video with a Camera App)
		查看视频(View the Video)
		mVideoView.setVideoURI(videoUri);
	}
}
3、控制相机硬件

通过使用framework的APIs来直接控制相机硬件。

1)打开相机对象

推荐的方式是在onCreate()另起一个Thread来打开Camera。可避免因打开工作比较费时导致的ANR。也可放到onResume(),这样使得代码更容易重用。Camera正被另一个程序使用时去执行Camera.open()会抛出一个异常。

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;
	}
}

note:子api level9开始,camera的framework可支持多个cameras。


2)创建相机预览界面

可用SurfaceView来展现照相机采集的画面。

a.Preview Class

为了展示一个预览界面,你需要一个Preview类。这个类需要实现android.view.SurfaceHolder.Callback接口,用这个接口把camera硬件获取的数据传递给程序。

class Preview extends ViewGroup implements SurfaceHolder.Callback {
	SurfaceView mSurfaceView;
	SurfaceHolder mHolder;
	Preview(Context context) {
		控制相机硬件
		打开相机对象(Open the Camera Object)
		创建相机预览界面(Create the Camera Preview)
		Preview Class
		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);
	}
	...
}

这个Preview类必须在图片预览开始之前传递给Camera对象。

b.设置和启动预览

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();
}

a.设置预览方向:setCameraDisplayOrientation()。api14之前,必须在改变方向之前,先停止你的预览,然后才能去重启它。

b.拍一张照片:Camera.takePicture()。想要连拍,可创建一个Camera.PreviewCallback并实现onPreviewFrame()你还可选择几个预览帧来进行拍照或延迟拍照。
c.重启预览:图片被获取后,必须在用户拍下一张图片之前重启预览。通过重载快门按钮可实现:

@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();
}

d.停止预览并释放相机


何时:在预览的surface被摧毁之后。


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;
	}
}

跟前面代码有点像,因为初始化一个camera的动作,总是从停止预览开始的。


更多信息参考Camera源码。

三、打印

Android4.4(API Level19)及更高版本中,框架提供了直接从Android应用程序打印图片和文字的服务。

1、打印图片

Android Support Library中的PrintHelper类提供了一种打印图片的方法。该类有一个单一的布局选项:setScaleMode()。它能允许你使用下面两个选项之一:

SCALE_MODE_FIT:等比例缩放至长和宽都包含在纸张页面内。

SCALE_MODE_FILL:让图像充满整个纸张页面。如果选择一个选项,那么图片的一部分(顶部和底部,或左侧和右侧)将无法打印出来。默认是这种模式。

设置布局并开始打印:

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);
}



2、打印HTML文档

3、打印自定义文档