Build Apps with Multidata

创建实现用户期望行为的富媒体应用

Capturing Photos

Taking Photos Simply

Taking Photos Simply

使用已有的拍照程序拍照.

Request Camera Permission

如果你的app必须有摄像硬件支持,则需要在Google Play上限制其安装渠道.


    

如果你的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: 共享照片的适宜存储位置.

权限请求:

然而,如果你只想要照片可被你个人的app访问,你需要改变存储区域:getExternalFilesDir().
在Android 4.3及以下,将照片写入该文件位置需要WRITE_EXTERNAL_STORAGE权限.
从Android 4.4开始,不再需要该权限是因为该文件位置不能再被其它app访问,所以你可以声明读权限的最高sdk版本:

你存储在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


    
        
        
    

确保android:authoritiesgetUriForFile(Context, String, File)第二个参数匹配.
在provider定义的meta-data部分,你可以看到provider提供了一个xml文件来配置合格的路径:



    

当调用带有Environment.DIRECTORY_PICTURESgetExternalFilesDir()时,路径组件会返回定义好的路径.

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 文件中使用标签.


    

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

  1. 获取Camera对象的一个实例,推荐在onCreate()时创建.
  2. 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.PictureCallbackCamera.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

创建本地输出视图的主要步骤如下:

  1. 在HTML资源家在以后创建一个WebViewClient对象
  2. 使用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 = "

Test Content

Testing, testing, testing ...

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

你可能感兴趣的:(Build Apps with Multidata)