創建實現用戶期望行為的富媒體應用
Capturing Photos
Taking Photos Simply
Taking Photos Simply
使用已有的拍照程序拍照.
Request Camera Permission
如果你的app必須有攝像硬件支持,則需要在Google Play上限制其安裝渠道.
<manifest ... >
<uses-feature android:name="android.hardware.camera"
android:required="true" />
</manifest>
如果你的app對于攝像硬件是可選的,則需要在運行時的時候檢測系統是否有攝像硬件:hasSystemFeature(PackageManager.FEATURE_CAMERA)
Take a Photo with the Camera App
Android請求其他app來完成一個action通常會使用Intent
.
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);
}
}
Get the Thumbnail
@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) extra.get("data");
mImageView.setImageBitmap(imageBitmap);
}
}
Save the Full-size Photo
如果提供一個具體的文件對象,Android拍照軟件會保存未壓縮的照片. 你必須提供一個完全限定的文件名稱.
通常,用戶拍攝的任何照片都應該存儲在外部公共存儲區域,以便所有app進行訪問.
getExternalStorageDirectory()
+DIRECTORY_PICTURES
: 共享照片的適宜存儲位置.
權限請求:<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
然而,如果你只想要照片可被你個人的app訪問,你需要改變存儲區域:getExternalFilesDir()
.
在Android 4.3及以下,將照片寫入該文件位置需要WRITE_EXTERNAL_STORAGE
權限.
從Android 4.4開始,不再需要該權限是因為該文件位置不能再被其它app訪問,所以你可以聲明讀權限的最高sdk版本: <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="18" />
你存儲在
getExternalFilesDir()
或者getFilesDir()
的文件會在用戶卸載你的app以后一同被刪除。
一旦你決定了存儲文件的位置,你需要創建沒有沖突的文件名稱。以下為一個簡單的小例子,演示如何生成獨一無二的文件名稱.
String mCurrentPhotoPath;
private File createImageFile() throws IOException {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(imageFileName, ".jpg", storageDir);
mCurrentPhotoPath = image.getAbsolutePath();
return image;
}
通過結合該方法,你可以通過Intent
來創建一個圖片文件:
static final int REQUEST_TAKE_PHOTO = 1;
private void dispatchPictureIntent() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = createImageFile();
} catch (IOException ex) {}
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(Context, String, File)
=> content://URI.在Android 7.0及以上的版本,使用file://URI
將會拋出FileUriExposedException
.因此,我們現在更常用FileProvider
來生成圖片的URI.
配置FileProvider
=> 在配置文件中添加一個provider
<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.name.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths">
</meta-data>
<provider>
</application>
確保android:authorities
與getUriForFile(Context, String, File)
第二個參數匹配.
在provider定義的meta-data部分,你可以看到provider提供了一個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>
當調用帶有Environment.DIRECTORY_PICTURES
的getExternalFilesDir()
時,路徑組件會返回定義好的路徑.
Add the Photo to a Gallery
當你通過intent創建了一張照片時,你應該知道圖片的存儲位置. 對于其他的人來說,訪問你創建的照片最簡單的方式就是使其可被系統的Media Provider訪問.
如果你將圖片存儲在了
getExternalFilesDir()
,media scanner不能訪問該圖片,因為其只對你的app可見.
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);
}
Decode a Scaled Image
在低內存下管理多個未壓縮圖片會很復雜. 如果你發現應用在展示了很少的圖片就內存溢出,你可以通過將JPEG擴展到已經縮放到與目標視圖大小匹配的內存數組,大大減少使用的動態堆的數量.
private void setPic() {
int targetW = mImageView.getWidth();
int targetH = mImageView.getHeight();
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
int scaleFactor = Math.min(photoW/targetW, photoH/targetH);
bmOptions.inJustDecodeBounds = false;
bmOptions.isSampleSize = scaleFactor;
bmOptions.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
mImageView.setImageBitmap(bitmap);
}
Recording Videos Simply
使用已有的相機應用錄制音頻.
Request Camera Permission
在 manifest 文件中使用<uses-feature>
標簽.
<manifest ... >
<uses-feature android:name="android.hardware.camera"
android:required="true" />
</manifest>
Record a Video with a Camera 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);
}
}
View the Video
在onActivityResult()
中Android相機應用將會返回視頻的Uri.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
if (requestCode == REQUEST_VIDEO_CAPTURE && resultCode == RESULT_OK) {
Uri videoUri = intent.getData();
mVideoView.setVideoURI(videoUri);
}
}
Controlling the Camera
Open the Camera Object
- 獲取
Camera
對象的一個實例,推薦在onCreate()
時創建. - 在
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;
}
}
在API 9 及以上,camera framework 開始支持多個相機. 如果你調用open()
不攜帶任何參數,你將會獲取到后置攝像頭的Camera
對象.
Create the Camera Preview
使用SurfaceView
顯示相機傳感器檢測到的圖像.
Preview Class
實現android.view.SurfaceHolder.Callback
接口—— 接收相機硬件的圖片數據到app上.
class Preview extends ViewGroup implements SurfaceHolder.Callback {
SurfaceView mSurfaceView;
SufaceHolder mHolder;
Preview(Context context) {
super(context);
mSurfaceView = new SurfaceView(context);
addView(mSurfaceView);
mHolder = mSurfaceView.getHolder();
mHolder.addCallback(this);
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
}
Set and Start the Preview
Camera實例和其相關的界面展示必須有確切的創建時序,先創建Camera實例.
在下面的代碼段中,初始化相機的過程被封裝. 任何時間用戶做了一些改變相機狀態(setCamera()
)的操作,Camera.startPreview()
都會被調用. 預覽也應該在surfaceChanged()
中重啟.
public void setCamera(Camera camera) {
if (mCamera == camera) { return; }
stopPreviewAndFreeCamera();
mCamera = camera;
if (mCamera != null) {
mCamera.setPreviewDisplay(mHolder);
} catch (IOException e) {
e.printStackTrace();
}
mCamera.startPreview();
}
Modify Camera Setting
Camera Settings 更改相機拍攝照片的方式,從縮放級別到曝光補償. 此示例僅更改預覽大小.
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
requestLayout();
mCamera.setParameters(parameters);
mCamera.startPreview();
}
Set the Preview Orientation
大多數相機應用將顯示鎖定為橫向模式,因為這是相機傳感器的自然方向. 此設置不會阻止你拍攝人像模式的照片,因為設備的方向記錄在EXIF標頭中.setCameraDisplayOrientation()
允許你修改預覽的顯示方式,而不影響圖像的記錄方式. 但是在API 14以下,如果要改變方向,必須先停止預覽,然后再更改方向,再重新啟動.
Take a Picture
調用Camera.takePicture()
拍攝照片在preview已經開始的前提下. 你可以創建Camera.PictureCallback
和Camera.ShutterCallback
,并將他們傳遞給Camera.takePicture()
.
如果你想延遲拍照,你可以創建Camera.PreviewCallback
,實現onPreviewFrame()
. 對于捕捉到的鏡頭數據,你可以僅捕獲所選的預覽幀,或設置一個延遲操作來調用takePicture()
.
Restart the Preview
@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;
}
shutterBtnConfig();
}
Stop the Preview and Release the Camera
一旦你的app不再使用相機,就應該將其處理掉. 尤其需要釋放掉Camera
對象,否則將可能造成其他app的crash,當然包括你自己的app.
你什么時候需要停止預覽釋放相機資源?當你的預覽窗口被摧毀的時候.
public void surfaceDestroyed(SurfaceHolder holder) {
if (mCamera != null) {
mCamera.stopPreview();
}
}
private void stopPreviewAndFreeCamera() {
if (mCamera != null) {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
Printing Content
Android 4.4 以上提供了直接顯示圖片和文件的框架.
Photos
使用PrintHelper
類來顯示一張圖片.
PrintHelper
: 來自Android v4 support library
Print an Image
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);
photoPrint.printBitmap("droids.jpg - test print", bitmap);
}
HTML Documents
在Android 4.4以上,WebView
類做了更新,支持顯示HTML內容. 該類允許你加載本地HTML或者從web端下載一個頁面并顯示.
Load an HTML Document
創建本地輸出視圖的主要步驟如下:
- 在HTML資源家在以后創建一個
WebViewClient
對象 - 使用
WebView
對象加載HTML資源
示例.
private WebView mWebView;
private void doWebViewPrint() {
WebView webView = new WebView(getActivity());
webView.setWebViewContent(new WebViewClient() {
public boolean shouldOverrideUriLoading(Webview webView, String url) {
return false;
}
@Override
public void onPageFinished(WebView view, String url) {
Log.i(TAG, "page finished loading " + url);
createWebPrintJob(view);
mWebView = null;
}
});
String htmlDocument = "<html><body><h1>Test Content</h1><p>Testing, testing, testing ...</p></body></html>";
webView.loadDataWithBaseURL(null, htmlDocument, "text/HTML", "utf-8", null);
mWebView = webView;
}
Note 一定要確認你在
WebViewClient.onPageFinished()
方法被調用以后再做資源輸出的工作.
Note 以上的示例代碼保存了對一個WebView對象實例的引用,所以在進行資源輸出之前不會對其進行垃圾回收. 你需要確保在自己的實現中也要使用同樣的思路來執行代碼,否則打印過程可能會失敗.
如果要在頁面中包含圖形,請將圖形文件放在項目的assets/
目錄中,并在loadDataWithBaseURL()
方法的第一個參數中指定基本URL.
webView.loadDataWithBaseURL("file://android_asset/images", htmlBody,
"text/HTML", "utf-8", null);
你也可以通過調用loadUrl()
加載網頁.
webView.loadUrl("http://developer.android.com/about/index.html");
當使用WebView
來創建輸出文件時,有如下幾條注意事項:
- 不能添加頭惑尾,包括頁碼等.
- HTML 文檔的打印選項不包括打印頁面范圍的功能,例如:不支持打印10頁HTML文檔的第2頁到第4頁
- 一個
WebView
一次只能支持一個輸出工作 - 不支持包含CSS打印屬性的HTML文檔
- 不能在HTML中使用 JavaScript
Note 包含在布局中的WebView對象的內容也可以在加載文檔后打印.
Create a Print Job
做完以上事情以后,終于要做最后一步工作了:訪問PrintManager
,創建打印適配器,最后創建打印作業.
private void createWebPrintJob(WebView webView) {
PrintManager printManager = (PrintManager) getActivity()
.getSystemService(Context.PRINT_SERVICE);
PrintDocumentAdapter printAdapter = webView.createPrintDocumentAdapter();
String jobName = getString(R.string.app_name) + "Document";
PrintJob printJob = printManager.print(jobName, printAdapter,
new PrintAttributes.Builder().build());
mPrintJobs.add(printJob);
}
Custom Documents
Connect to the Print Manager
當應用程序直接管理打印過程時,從用戶收到打印請求后的第一步是連接到Android打印框架并獲取PrintManager類的實例. 以下代碼示例顯示如何獲取打印管理器并開始打印過程.
private void doPrint() {
PrintManager printManager = (PrintManager) getActivity().getSystemService(Context.PRINT_SERVICE);
String jobName = getActivity().getString(R.string.app_name) + " Document";
printManager.print(jobName, new MyPrintDocumentAdapter(getActivity(), null));
}
Note
print()
最后一個參數需要是PrintAttribute
對象. 你可以使用此參數向打印框架提供提示,并根據先前的打印周期提供預設選項,從而改善用戶體驗. 你還可以使用此參數設置更適合正在打印的內容的選項,例如在打印處于該方向的照片時將方向設置為橫向.
Create a Print Adapter
輸出適配器與Android輸出框架交互,處理輸出流程. 該過程需要用戶選擇打印機和打印選項. 在打印過程中,用戶可以選擇取消打印行為,所以你的打印適配器需要監聽和處理取消請求。
PrintDocumentAdapter
抽象類有4個主要的回調函數,被用來設計處理打印生命周期.
-
onStart()
: 一次調用. 如果你的應用有任何一次性的準備工作,都可以寫在這里. 不是必須實現的方法. -
onLayout()
: 任何時間用戶的行為影響了輸出都會被調用. -
onWrite()
: 調用將打印的頁面轉換為要打印的文件. 在每次onLayout()
被調用后都必須必須被調用至少一次. -
onFinish()
: 在打印任務結束時被調用一次.
Note 這些適配器的方法在主線程中被調用. 如果你期望在執行這些方法時消耗大量的時間,需要在非UI線程執行實現函數.
Compute print document info
在實現PrintDocumentAdapter
的過程中,app必須能夠在打印作業中確認文件類型以及需要打印的頁數,和打印頁面的大小. 實現onLayout()
,計算打印作業相關信息,將期望參數通過PrintDocumentInfo
類傳輸.
@Override
public void onLayout(PrintAttributes oldAttributes,
PrintAttributes newAttributes,
CancellationSignal cancellationSignal,
Bundle metadata) {
mPdfDocument = new PrintedPdfDocument(getActivity(), newAttributes);
if (cancellationSignal.isCancelled()) {
callback.onLayoutCancelled();
return;
}
int pages = computPageCount(newAttributes);
if (pages > 0) {
PrintDocumentInfo info = new PrintDocumentInfo()
.Builder("print_output.pdf")
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
.setPageCount(pages)
.build();
callback.onLayoutFinished(info, true);
} else {
callback.onLayoutFailed("Page count calculation failed.");
}
}
執行onLayout()
方法會有三種可能的執行結果:完成、取消或者失敗. 你必須通過實現PrintDocumentAdapter.LayoutResultCallback
來處理執行結果.
Note
onLayoutFinished()
方法的布爾參數指示自上次請求以來布局內容是否實際已更改. 正確設置此參數允許打印框架避免不必要地調用onWrite()
方法,緩存先前寫入的打印文檔并提高性能.
以下的代碼示例展示如何根據打印方向計算頁面頁數:
private int computePageCount(PrintAttributes printAttributes) {
int itemsPerPage = 4;
MediaSize pageSize = printAttributes.getMediaSize();
if (!pageSize.isPortrait()) {
itemsPerPage = 6;
}
int printItemCount = getPrintItemCount();
return (int) Math.ceil(printItemCount / itemsPerPage);
}
Write a print document file
PrintDocumentAdapter.onWrite()
在要進行打印作業時調用.
onWrite(PageRange[] pageRanges, ParcelFileDescriptor destination, CancellationSignal cancellationSignal, WriteResultCallback callback)
當打印任務完成時,需要調用callback.onWriteFinished()
.
Note 每次
onLayout()
被調用以后,onWrite()
都會被調用至少一次. 因此如果輸出內容沒有改變的話,需要設置onLayoutFinished()
為false
,以避免不必要地重新打印.
Note
onLayoutFinished()
的boolean型參數指示輸出內容在上次調用時有沒有改變.
@Override
public void onWrite(final PageRange[] pageRanges,
final ParcelFileDescriptor destination,
final CancellationSignal cancellationSignal,
final WriteResultCallback callback) {
for (int i = 0; i < totalPages; i ++) {
if (containsPage(pageRanges, i)) {
writtenPagesArray.append(writtenPagesArray.size(), i);
PdfDocument.Page page = mPdfDocument.startPage(i);
if (cancellationSignal.isCancelled()) {
callback.onWriteCancelled();
mPdfDocument.close();
mPdfDocument = null;
return;
}
drawPage(page);
mPdfDocument.finishPage(page);
}
}
try {
mPdfDocument.writeTo(new FileOutputStream(
destination.getFileDescriptor()));
} catch (IOException e) {
callback.onWriteFailed(e.toString());
return;
} finally {
mPdfDocument.close();
mPdfDocument = null;
}
PageRange[] writtenPages = computeWrittenPages();
callback.onWriteFinished(writtenPages);
...
}
PrintDocumentAdapter.WriteResultCallback
監聽onWrite()
結果.
Drawing PDF Page Content
你的app在打印時必須生成一個PDF文件,并將其傳輸給Android打印框架. 你可以使用PrintedPdfDocument
來收入能夠承認那個PDF文件.
PrintedPdfDocument
使用Canvas
繪出PDF頁面.
private void drawPage(PdfDocument.Page page) {
Canvas canvas = page.getCanvas();
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 paragragh", leftMargin, titleBaseLine + 25, paint);
paint.setColor(Color.BLUE);
canvas.drawRect(100, 100, 172, 172, paint);
}