跟Google 学代码: Building Apps with Multimedia(自定义相机和PrintHelper用法)

引言

尊重博主原创,如需转载,请附上本文链接http://blog.csdn.net/chivalrousman/article/details/51890716

学会在App中控制音频,图片,相机,文末总结很详细

1. 学会自定义相机:

这里写图片描述

还讲解了使用系统Framework层API Print 来保存文件,逼格更高有木有!

PrintHelper保存一张图片

这里写图片描述

管理音频播放


控制 app音频播放

  1. 使用流

    使用音频设备通常使用的 位于AudioManager的STREAM_MUSIC流

  2. 使用音频键控制App音频播放

    在Activity或Fragment中申明这个流

    setVolumeControlStream(AudioManager.STREAM_MUSIC);
  3. 使用物理按键控制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的代码里可以总结道:

    1. 先判用户是否发送了带有ACTION_MEDIA_BUTTON的Intent

    2. 通过intent获得KeyEvnent,学到了getParcelableExtra函数,和Intent.EXTRA_KEY_EVENT这个参数

    3. 通过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:”前缀
    通过上述方法创建好图片文件后,我们现在可以创建相关的Intent

    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) {
                Uri photoURI = FileProvider.getUriForFile(this,
                                                      "com.example.android.fileprovider",
                                                      photoFile);
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
                startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO);
            }
        }
    }
    • 这里值得注意的有:
      1. FileProvider的用法和参数传值
      2. 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);
    }
    

    总结:

    1. 学会Intent的参数Intent.ACTION_MEDIA_SCANNER_SCAN_FILE

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

总结:

  1. 这里学到了 Intent的参数 MediaStore.ACTION_VIDEO_CAPTURE

  2. 复习了Intent的resolveActivity接口,来判断系统中是否存在可以接收当前Intent的应用

  3. 浏览视频

@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. 本人之前的博客 《史诗巨著内存优化》《最全图片压缩范例》 《保存数据》

2. 全文翻译自 Google Building Apps with Multimedia 课程

3. 自定义相机demoPrintHelper Demo

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值