学会在App中控制音频,图片,相机,文末总结很详细
1. 学会自定义相机:
还讲解了使用系统Framework层API Print 来保存文件,逼格更高有木有!
管理音频播放
控制 app音频播放
使用流
使用音频设备通常使用的 位于AudioManager的STREAM_MUSIC流
使用音频键控制App音频播放
在Activity或Fragment中申明这个流
setVolumeControlStream(AudioManager.STREAM_MUSIC);
使用物理按键控制App的音频播放
从Google的代码里可以总结道:
通过以上代码就可以监听多媒体按钮了。
最后我们将广播塞到AudioManager中:
声明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. } } } }
先判用户是否发送了带有ACTION_MEDIA_BUTTON的Intent
通过intent获得KeyEvnent,学到了getParcelableExtra函数,和Intent.EXTRA_KEY_EVENT这个参数
通过event.getKeyCode()函数可以判断点击事件
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 playbackint 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”:
@Overrideprotected 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); } } }
我们使用getUriForFile() 返回 content://URI, 在最新的Android版本比如Android N中,将会返回file://URI,有可能造成FileUriExposedException异常,因此我们使用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
这里值得注意的有:
创建文件
如果只是创建一个文件,我就没有必要再这里加入代码了
接下来将演示如何为图片加入时间戳
即:一张图片创建的时候带有时间标记
FileProvider的用法和参数传值
intent中存储的键位名称为:MediaStore.EXTRA_OUTPUT
使用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的应用
浏览视频
@Overrideprotected 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();
@Overridepublic 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()中就好写了:
@Overridepublic 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对象