引言
尊重博主原创,如需转载,请附上本文链接http://blog.csdn.net/chivalrousman/article/details/51890716
学会在App中控制音频,图片,相机,文末总结很详细
1. 学会自定义相机:
还讲解了使用系统Framework层API Print 来保存文件,逼格更高有木有!
PrintHelper保存一张图片
管理音频播放
控制 app音频播放
使用流
使用音频设备通常使用的 位于AudioManager的STREAM_MUSIC流
使用音频键控制App音频播放
在Activity或Fragment中申明这个流
setVolumeControlStream(AudioManager.STREAM_MUSIC);
使用物理按键控制App的音频播放
声明ACTION_MEDIA_BUTTON
这个声明需要写在广播接收者中
<receiver android:name=".RemoteControlReceiver"> <intent-filter> <action android:name="android.intent.action.MEDIA_BUTTON" /> </intent-filter> </receiver
为了去响应媒体键的点击,你需要注册BroadcastReceiver
这个BroadcastReceiver需要做些什么呢?
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. } } } }
从Google的代码里可以总结道:
先判用户是否发送了带有ACTION_MEDIA_BUTTON的Intent
通过intent获得KeyEvnent,学到了getParcelableExtra函数,和Intent.EXTRA_KEY_EVENT这个参数
通过event.getKeyCode()函数可以判断点击事件
通过以上代码就可以监听多媒体按钮了。
最后我们将广播塞到AudioManager中:
AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
...
// Start listening for button presses
am.registerMediaButtonEventReceiver(RemoteControlReceiver);
...
为了节省资源,我们需要在onStop()中解绑广播回调。
// Stop listening for button presses
am.unregisterMediaButtonEventReceiver(RemoteControlReceiver);
管理音频焦点
1. 请求音频焦点
关键点有三:AudioManager,requestAudioFocus(),AudioManager.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.
}
考虑用户不需要音频焦点的情况,那么我们可以解绑OnAudioFocusChangeListener
// Abandon audio focus when playback complete
am.abandonAudioFocus(afChangeListener);
requestAudioFocus()的第三个参数有个额外选项,选择是否“ducking”
可以提供一种降低声音的效果:当app失去焦点时,后台播放音频的时候音量会降低
// 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.
}
2. 处理失去音频焦点
关键点有两个:
设置OnAudioFocusChangeListener之后重写的onAudioFocusChange()回调函数
以及focusChange的三个常量:AUDIOFOCUS_LOSS_TRANSIENT,AudioManager.AUDIOFOCUS_GAIN,AudioManager.AUDIOFOCUS_LOSS
AudioManager.OnAudioFocusChangeListener afChangeListener =
new AudioManager.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
}
}
};
3. 降低声音的效果!
当app失去焦点后,app会降低音频的声音
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
}
}
};
处理音频输出设备
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. 处理音频输出设备的改变
当手机插入耳机时,系统的音频流将自动调整音量
为什么考虑这种场景?
如果不调整音量,你手机正在播放高音量的音乐,插入耳机的时候,你会崩溃的
Google给出什么建议?
两个要点: 常量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);
}
捕获图片
捕获图片
接下来将展示如何使用已有的相机App拍摄图片
1. 请求权限
<manifest ... >
<uses-feature android:name="android.hardware.camera"
android:required="true" />
...
</manifest>
2. 使用相机类App捕图片
通过Intent启动其他应用,在启动之前先判断设备中是否存在App可以接收我们发出的Intent,那么如何判断呢?
答案是:Intent的resolveActivity()接口
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);
}
}
3. 获得一张缩略图
因为我们通过启动其他App拍摄图片,所以我们只需要在本应用中接收返回的图片即可
那就再onActivityResult中加入响应的代码
Android 相机解析图片流后,通过Intent传递至onActivityResult中,但注意,该Intent只能够传递一张缩略图,对应的键为“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);
}
}
4. 保存完整图片
我们当然需要保存一张完整的大图至手机内存中,
那么如何做呢?
加入权限
<manifest ...> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ... </manifest>
创建文件
如果只是创建一个文件,我就没有必要再这里加入代码了
接下来将演示如何为图片加入时间戳
即:一张图片创建的时候带有时间标记
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 = getExternalFilesDir(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; }
这里需要注意的是 File路径前 带有“file:”前缀
通过上述方法创建好图片文件后,我们现在可以创建相关的Intentstatic 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) { Uri photoURI = FileProvider.getUriForFile(this, "com.example.android.fileprovider", photoFile); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO); } } }
- 这里值得注意的有:
- FileProvider的用法和参数传值
- intent中存储的键位名称为:MediaStore.EXTRA_OUTPUT
我们使用getUriForFile() 返回 content://URI, 在最新的Android版本比如Android N中,将会返回file://URI,有可能造成FileUriExposedException异常,因此我们使用FileProvider一般的做法来存储图片:
- 加入FileProvider
<application> ... <provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.android.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"></meta-data> </provider> ... </application>
使用FileProvider的时候,还需要在res/xml/file_paths.xml中加入如下配置:
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="my_images" path="Android/data/com.example.package.name/files/Pictures" /> </paths>
总 结:
FileProvider可以用来保存文件
将图片保存到相册
在之前的博文《保存数据》中讲解过内置存储和外置存储的概念,
这里我们选择将图片保存至外置存储中,让其他应用也可以看的见。
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); }
总结:
学会Intent的参数Intent.ACTION_MEDIA_SCANNER_SCAN_FILE
Uri和File文件的转换,Uri.fromFile()
6. 节约内存,压缩图片
关于为什么压缩图片?如何压缩图片,在我之前的博文《长篇巨著内存优化》《最全图片压缩范例》 部分都有写,这里就不叙述了。
只
两个关键点:BitmapFactory.Options类和如何计算出inSampleSize的值
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);
}
记录视频
1. 请求权限
<manifest ... >
<uses-feature android:name="android.hardware.camera"
android:required="true" />
...
</manifest>
2. 使用相机app录制视频
录制相机的逻辑不在当前App中实现,因此我们通过Intent启动其他App来录制,并返回数据
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);
}
}
总结:
这里学到了 Intent的参数 MediaStore.ACTION_VIDEO_CAPTURE
复习了Intent的resolveActivity接口,来判断系统中是否存在可以接收当前Intent的应用
浏览视频
@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);
}
}
在自己的应用中使用别人家的App拍摄照片录制视频,总感觉不爽,下面咱试试使用framewordk层的api 直接控制Camera
控制相机
1. 创建Camera对象
每个android设备只有一个Camera对象,有可能Camera**被其他应用占用,或者上一次使用结束后,没有释放Camera对象**
因此:我们需要安全的使用Camera对象:
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;
}
}
2. 创建相机预览
我们可以通过SurfaceView去绘制预览图
这是一个简单的ViewGroup,包含 SurfaceView,实现了SurfaceHolder回调
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);
}
...
}
在这个PreView中,还需要加入一个函数,来启动播放预览视图
下面这个函数 主要是通过调用camera.setPreViewDisplay()和camera.startPreview()来关联预览视图和启动预览视图
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. 设置Camera参数
我们会有修改相机参数的需求,接下来只是演示一些如何改变预览视图的尺寸,想改变其他参数,请查看Camera的源码
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. 设置相机预览视图的方向
有人刚开始做的时候,相机的方向和预览视图的方向是反的,这里我们就得在PreView中的代码设置Camera方向
具体setCameraDisplayOrientation()
5. 捕获照片
ok,我们可以通过Preview展示相机所看到的图片了,接下来我们如何保存图片呢?
第一步创建 Camera.PictureCallback对象,重写对应的方法
第二步通过camera.takePicture,传入PictureCallback对象
别以为通过相机捕获到图片就完事了,当PictureCallback被回调的时候,会使得Looper阻塞住,应用也会进入“卡住不动”的状态
那么我们该如何解决呢?
6. 重启预览视图
上面的问题很简单,视图被”阻塞“了,
Google提供的解决办法也很机制:
重启预览视图呗:
再说细一点:
就是在捕获照片 mCamera.takePicture()之前先调用Camera的startPreview();
@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. 暂停和释放相机
最后,当我们退出当前activity或者不使用相机的时候,一定要释放Camera对象
为什么要释放?
不仅仅是内存泄漏的问题,是因为Camera是所有App共用的,你不释放掉,其他App使用的时候会crash异常,也有可能你使用的时候打不开相机crash掉
所以,请跟Google学,养成书写优秀代码的好习惯
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;
}
}
Printing Content(通过打印输出App的内容)
以下内容,皆在Android 4.4以上,API level 19以上
Printing a Photo (输出图片)
Android Support Libray 包含了PrintHelper类,它提供简单的打印图片的方式这里写代码片
PrintHelper photoPrinter=new PrintHelper(getActivity);
photoPrinter.printBitmap("droids.jpg - test print", bitmap);
Google完整的示例:
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);
}
在这里,注意到photoPrinter.setScaleMode(PrintHelper.SCALE_MODE_FIT);
,photoPrinter提供了缩放模式,有助于我们控制打印输出的图片以便符合需求
Printting HTML Documents(打印输出HTML文档)
Android 4.4 版本以后,webview提供了 打印HTML文档的接口
1. 载入HTML文档
之前在《跟Google 学代码 之 Web app》的课程已经有讲过webview的所有用法,在这里简单回忆一下:
- 创建webviewclient对象完成创建printJob
载入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; }
目前不支持打印输出带有javascript的HTML文档
如果这一部分看不懂没关系,请跳过去看下一节《Printing a Csutom Document(打印自定义文档)》,之后再返回来看本节
2. 在创建完webview并且载入HTML内容之后,app已经具备打印的能力了,下一步就是使用 PrintManager 来执行打印任务
- 获得
PrintManager
对象 - 创建
PrintDocumentAdapter
- 创建
PrintJob
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);
}
Printing Custom Documents(输出打印自定义文档)
连接 Print Manager
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); //
}
创建Print Adapter
PrintDocumentAdapter 这个抽象类设计的初衷就是来控制打印活动的生命周期。在适配器中处理想应得回调方法,就可以控制打印相关的framework层业务
首先我们得知道这个适配中有什么API我们能用:
- onStart() 执行打印的进程被启动时执行回调的第一个方法
- onLayout() 我们可以在这里改变一些打印参数设置,比如打印页面的尺寸,页面的方向,页面的数量等等
- onWrite() 系统将页面写入文件的时候会回调这个方法,onWrite()可以被onLayout()回调多次,因为通常有许多页面去打印
- onFinish() 当打印进程结束的时候被回调,可以在这里撤销一些任务,系统并未要求必须实现这个回调接口
接下来我们细细研究Google的代码,在上述四个回调接口中是如何实现的
绘制PDF 页面内容
1. 计算需要打印的文档信息
打印文档之前,我们需要知道如下参数
- 文档的每个页面的尺寸
文档的页面个数
ok知道我们需要什么,那么在onLyout()中就好写了:
@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回调接口中返回了几个比较重要的信息:
- oldAttributes
- newAttribute 用于创建PDF文档对象,计算页面信息
- cancellationSignal 用于决定是否响应用户 取消回调的请求
- callback 响应回调,比如onLayoutCancelled(),onLyoutFinished(),onLayoutFailed()
onLayout()通过computePageCount()来计算页面的数量,页面的尺寸大小
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. 输出打印文档文件
是时候去打印文档了,这里我们需要在回调onWrite()接口写业务逻辑
@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);
...
}
这里最关键的两句话:
- PdfCocument.Page page=mdfDocument.startPage(i);
drawPage(page)
第一句是告诉我们如何获得Page对象
第二局是教我们绘制Page页面
下面是drawPage()的实现:
第一句最为关键page.getCanvas()
Canvas是个非常牛掰的类,想了解Canvas请关注我后续博客
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);
}
总结
总结的思路遵循自顶向下的原则,倒着写,根据需求写思路,更人性化一点
1. 捕获图片:
- 相机捕获的图片一定要压缩
- 使用FileProvider来保存的文件可以带有时间戳,FileProvider.getUriForFile()返回URI
- 相机拍照返回的Bundle中,存储的是缩略图
- 为Intent设置MediaStore.ACTION.IMAGE_CAPTURE可以启动相机类应用
- 使用Intent启动外部应用时,需先通过Intent.resolveActivity()接口判断
2. 控制相机:
- 使用Camera一定要及时释放
- 小心相机预览视图的阻塞机制,遇到了要知道怎么做
- 在预览视图的生命周期中设置Camera参数
- 通过SurfaceView实现相机预览视图
- 通过videoview.setVideoURI()播放视频
- 使用相机前线通过Intent.resolveActivity()接口来判断系统中是否存在能接受当前Intent的应用
- 需要为Intent设置参数MediaStore.ACTION_VIDEO_CAPTURE来捕获视频
3. 通过PrintHelper.printBitmap()可以打印输出图片,保存的数据为PDF格式
4. 打印输出文档 :
- PrintManager.print()返回一个PrintJob,通过操作PrintJob完成最终打印,PrintManager调用print()时,该函数需要传入PrintDocumentAdapter对象
- 通过WebView.createPrintDocumentAdapter()返回PrintDocumentAdapter对象
- 通过WebView载入对应的HTML文档
5. 通过PrintDocumentAdapter打印输出PDF或自定义文档:
- 通过Canvas完成最终绘制
- 通过PdfDocument.Page对象的getCanvas()接口来获得Canvas对象
- 通过PdfDocument.startPage()接口返回Page对象
- onLayout()多次回调onWrite()完成输出打印
- 在onLaout()回调接口返回的后三个参数非常有用,其中,newAttribute参数用来计算文档尺寸,文档页面的数量,callback参数用来绑定回调对象,cancellationSignal参数用来取消回调
- 通过PrintManage.print()完成输出打印,print()需要传入PrintDocumentAdapter对象
- 通过getAcitivity.getSystemService()和系统常量Context.PRINT_SERVICE来获得PrintManager对象
参考
1. 本人之前的博客 《史诗巨著内存优化》《最全图片压缩范例》 《保存数据》