Android 官方培训文档 笔记 (3)

3、Android 多媒体 

3.1.1管理音频播放
使用硬件音量键来控制应用的音量
在oncreate方法中调用 setVolumeControlStream(AudioManager.Stream_Music);

使用硬件的播放控制按键来控制应用的音频播放
许多线控或无线耳机都会有许多媒体播放控制按钮。无论用户按下设备商任意一个控制按钮,系统都会广播一个带有ACTION_MEDIA_BUTTON的Intent。为了正确地响应这些操作。需要在清单文件中注册一个该ACTION的BroadcastReceiver
<receiver android:name=".RemoteControlReceiver">
    <intent-filter>
        <action android:name="android.intent.action.MEDIA_BUTTON" />
    </intent-filter></receiver>
在Receiver的实现中,需要判断这个广播来自于哪一个按钮,Intent通过 EXTRA_KEY_EVENT 这一Key包含了该信息,另外, KeyEvent 类包含了一系列诸如 KEYCODE_MEDIA_* 的静态变量来表示不同的媒体按钮,例如 KEYCODE_MEDIA_PLAY_PAUSE  与  KEYCODE_MEDIA_NEXT

因为可能会有多个程序在监听与媒体按钮相关的事件,所以我们必须在代码中控制应用接收相关事件的时机。下面的例子显示了如何使用 AudioManager 来为我们的应用注册监听与取消监听媒体按钮事件,当Receiver被注册上时,它将是唯一一个能够响应媒体按钮广播的Receiver。

3.1.2管理音频焦点
为了防止多个音乐播放应用同时播放音频,安卓使用音频焦点来控制音频的播放——即只有获取到音频焦点的应用才能够播放音频。
在我们的应用开始播放音频之前,它需要获取将要使用的音频流的音频焦点。通过使用 requestAudioFocus() 方法可以获取我们希望得到的音频流焦点。如果请求成功,该方法会返回 AUDIOFOCUS_REQUEST_GRANTED

另外我们必须指定正在使用的音频流,而且需要确定所请求的音频焦点是短暂的还是永久的。
短暂的焦点锁定:当计划播放 一个短暂的音频时使用(比如播放导航指示)
永久的焦点锁定:当计划播放一个较长但时长可预期的音频使用(比如播放音乐)

下面的代码片段是一个在播放音乐时请求永久音频焦点的例子,我们必须在开始播放之前立即请求音频焦点,比如在用户点击播放或者游戏中下一关的背景音乐开始前。

AudioManager am = mContext.getSystemService(Context.AUDIO_SERVICE);
...

// Request audio focus for playbackint 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.
}

一旦结束了播放,需要确保调用了abandonAudioFocus()方法。这样相当于告知系统我们不再需要获取焦点并且注销所关联的AudioManager.OnAudioFocusChangeListener监听器。对于另一种释放短暂音频焦点的情况,这会允许任何被我们打断的应用可以继续播放。

// Abandon audio focus when playback complete   
am.abandonAudioFocus(afChangeListener);

当请求短暂音频焦点的时候,我们可以选择是否开启“Ducking”。通常情况下,一个应用在失去音频焦点时会立即关闭它的播放声音。如果我们选择在请求短暂音频焦点的时候开启了Ducking,那意味着其它应用可以继续播放,仅仅是在这一刻降低自己的音量,直到重新获取到音频焦点后恢复正常音量(译注:也就是说,不用理会这个短暂焦点的请求,这并不会打断目前正在播放的音频。比如在播放音乐的时候突然出现一个短暂的短信提示声音,此时仅仅是把歌曲的音量暂时调低,使得用户能够听到短信提示声,在此之后便立马恢复正常播放)。

// 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.
}
Ducking对于那些间歇性使用音频焦点的应用来说特别合适,比如语音导航。

如果应用A请求获取了音频焦点,那么在应用B请求获取音频焦点的时候,A获取到的焦点就会失去。如何响应失去焦点事件,取决于失去焦点的方式。

在音频焦点的监听器里面,当接受到描述焦点改变的事件时会触发onAudioFocusChange()回调方法。如之前提到的,获取焦点有三种类型,我们同样会有三种失去焦点的类型:永久失去,短暂失去,允许Ducking的短暂失去。

  • 失去短暂焦点:通常在失去短暂焦点的情况下,我们会暂停当前音频的播放或者降低音量,同时需要准备在重新获取到焦点之后恢复播放。

  • 失去永久焦点:假设另外一个应用开始播放音乐,那么我们的应用就应该有效地将自己停止。在实际场景当中,这意味着停止播放,移除媒体按钮监听,允许新的音频播放器可以唯一地监听那些按钮事件,并且放弃自己的音频焦点。此时,如果想要恢复自己的音频播放,我们需要等待某种特定用户行为发生(例如按下了我们应用当中的播放按钮)。

在下面的代码片段当中,如果焦点的失去是短暂型的,我们将音频播放对象暂停,并在重新获取到焦点后进行恢复。如果是永久型的焦点失去事件,那么我们的媒体按钮监听器会被注销,并且不再监听音频焦点的改变。

OnAudioFocusChangeListener afChangeListener = new 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
        }
    }
};

在使用Ducking时,正常播放的歌曲会降低音量来凸显这个短暂的音频声音,这样既让这个短暂的声音比较突出,又不至于打断正常的声音。

下面的代码片段让我们的播放器在暂时失去音频焦点时降低音量,并在重新获得音频焦点之后恢复原来音量。

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
        }
    }
};
3.1.3兼容音频输出设备
系统会在这种情况下广播带有 ACTION_AUDIO_BECOMING_NOISY 的Intent。无论何时播放音频,我们都应该注册一个 BroadcastReceiver 来监听这个Intent。在使用音乐播放器时,用户通常会希望此时能够暂停当前歌曲的播放。而在游戏当中,用户通常会希望可以减低音量。


3.2.1
简单的拍照
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
    }
在调用startacitivtyforresult()之前,先调用resolveactivity(),这个方法会返回能处理该Intent的第一个Activity。执行这个检查很重要。如果在调用startactivityforresult()时,没有应用能处理你的Intent,应用会崩。只要返回结果不为Null,使用该Intent就是安全的。

获取缩略图
安卓的相机应用会把拍好的照片编码为缩小的bitmap。使用extra value 的方法添加到返回的Intent中。并传送给onactivityresult(),对应的key为"data"。
 Bundle extras = data.getExtras();
        Bitmap imageBitmap = (Bitmap) extras.get("data");
        mImageView.setImageBitmap(imageBitmap);
保存全尺寸照片
如果我们提供了一个file对象给安卓的相机程序,它会保存这张全尺寸照片到给定的路径下。另外,我们必须提供存储图片所需要的含有后缀名形式的文件名。

一般而言,用户使用设备相机所以拍摄的任何照片都应该被存放在设备的公共外部存储中,这样它们就能被所有的应用访问。将DIRECTORY_PICTURES作为参数,传递给getExternalStoragePublicDirectory()方法,可以返回适用于存储公共图片的目录。由于该方法提供的目录被所有应用共享,因此对该目录进行读写操作分别需要权限。

如果希望照片对我们的应用而言是私有的,那么可以使用getexternalFilesDir()提供的目录。
注:所有存储在getExternalFilesDir()提供的目录中的文件会在用户卸载你的app后被删除。
一旦选定了存储文件的目录,我们还需要设计一个保证文件名不会冲突的命名规则。当然我们还可以将路径存储在一个成员变量以备将来使用。下面的例子使用日期,时间戳作为新照片的文件名。

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 = Environment.getExternalStoragePublicDirectory(
            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;
}
将照片添加到相册中
由于我们通过Intent创建了一张照片,因此图片的存储位置我们是知道的。对其他人来说,也许查看我们的照片最简单的方式是通过系统的Media Provider 。
注:如果我们将图片存储在getExternalFileDir()提供的目录中,Media Scanner将无法访问到我们的文件,因为它们隶属于应用的私有数据。
下面的例子演示了如何触发系统的MediaSacnner,将我们的照片添加到MediaProvider的数据库中。
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);
}
解码一副缩放图片
在有限的内存下,管理很多全尺寸的图片会很棘手。如果发现应用在展示了少量图片后消耗了所有内存,我们可以通过缩放图片到目标视图尺寸,之后再载入到内存中的方法,来显著降低内存的使用。
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);
}

轻松录制视频
使用相机程序来录制视频
利用一个描述了执行目的的Intent对象,安卓可以将某些执行任务委托给其他应用,整个过程包含三部分:Intent 本身,一个函数调用来启动外部的Activity,当焦点返回到Activity时,处理返回图像数据的代码。
下面的函数将会发送一个Intent来录制视频
private void dispatchTakeVideoIntent() {
    Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    if (takeVideoIntent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE);
    }
}
查看视频
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_VIDEO_CAPTURE && resultCode == RESULT_OK) {
        Uri videoUri = intent.getData();
        mVideoView.setVideoURI(videoUri);
    }
}
控制相机
打开一个Camera对象是直接控制相机的第一步,正如安卓自带的相机程序一样,比较好的访问相机的方式是在onCreat()方法里面另起一个线程来打开相机。可以打开相机侧操作延迟到onResume()方法里面去执行。这样可以使得代码更容易重用,还能保持控制流程更为简单。
如果我们在执行Camera.open()方法的时候相机被另外一个应用使用,那么函数会抛出一个Exception。

Preview 类
我们需要使用Preview类来显示预览界面,这个类需要实现android.view.surfaceholder.callback接口。用这个接口把相机硬件获取的数据传递给应用。

设置和启动Preview
一个Camera实例与它相关的Preview必须以特定的顺序来创建,其中Camere对象首先被创建。初始化Camera的动作被封装了起来,无论用户相对Camera做什么样的改变,Camera.startPreview()都会被setCamera()调用。另外,Preview对象必须在surfacechanged这一回调方法里面重新启用。

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

修改相机设置
相机设置可以改变拍照的方式,从缩放级别到曝光补偿。下面的例子仅仅演示了如何改变预览大小。
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();
}
设置预览方向
大多数相机程序会锁定预览为横屏状态,因为该方向是相机传感器的自然方向。设备的方向信息会被记录在EXIF头中。setCameraDisplayOrientation()可以让你在不影响照片拍摄过程的情况下,改变预览方向。在改变预览方向之前,我们必须停止预览,然后再去重启它。

拍摄照片
只要预览开始之后,可以使用Camera.takePicture()方法来拍摄照片。我们可以创建Camera.PitctureCallback与Camera.ShutterCallback对象并将他们传递到Camera.takePicture()中

如果我们想要进行连拍,可以创建一个Camera.PreviewCallback并实现onPreviewFrame()方法。

重启Preview
在拍摄好图片后,我们必须在用户拍下一张图片之前重启预览。

停止预览并释放相机
当应用好相机后,我们有必须进行清理操作。特别地,我们必须释放Camera对象,不然的话,可能会引起其他应用崩溃。
那么何时应该停止预览并释放相机呢?在预览的surface被摧毁之后,可以做停止预览与释放相机的操作。
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;
    }
}
3.3打印

3.3.1打印照片
安卓support Library的PrintHelper类提供了一种打印图片的简单方法。该类有一个单一的布局选项:setscaleMode();它允许我们使用下面的两个选项之一:
1、scale_mode_fit:该选项会调整图像的大小,这样整个图像就会在打印有效区域内全部显示出来(等比例缩放至长和宽都包含在纸张页面内)。
2、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);
    photoPrinter.printBitmap("droids.jpg - test print", bitmap);
}
在printBitmap()被调用之后,我们的应用就不再需要进行其他的操作了。之后的安卓打印界面就会出现,允许用户选择一个打印机和它的打印选项。用户可以打印图像或者取消这一操作。如果用户选择了打印图像,那么一个打印任务将会被创建,同时在系统的通知栏中会显示一个打印提醒通知。

打印HTML文档
如果要在安卓上打印比一副照片更丰富的内容,我们需要将文本和图片组合在一个待打印的文档中。
安卓框架提供了一种使用html语言来构建文档并进行打印的方法。
webview类可以打印html内容,该类允许我们加载一个本地html资源或者从网页下载一个页面。

1、在HTML资源加载外币后,创建一个WebViewClient用来启动一个打印任务。
2、加载HTML资源至WebView对象中。

下面的代码展示了如何创建一个简单的WebViewClient并且加载一个动态创建的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;
}
注:请确保在WebViewClient()中的onPageFinished()方法内调用创建打印任务的方法。如果没有等到页面加载完毕就进行打印,打印的输出可能会不完整或空白。
注:在上面的样例代码中,保留了一个Webview对象实例的引用,这样就能够确保它不会在打印任务创建之前被垃圾回收期所回收。

我们希望页面中包含图像,将这个图像文件放置在你的工程的“asserts/”目录中,并指定一个URL(Base URL),并将它作为loadDataWithBaseURL()方法的第一个参数,就像下面所显示的一样。
webview.loadDataWithBaseURL("file://android_assert/images",htmlBody,"text/html",""

我们也可以加载一个需要打印的网页,具体做法是将loadDateWithBaseURL()方法替换为loadUrl();
webView.loadUrl("http://developer.android.com/about/index.html");

当使用WebView创建打印文档时,你要注意下面的一些限制:

  • 不能为文档添加页眉和页脚,包括页号。
  • HTML文档的打印选项不包含选择打印的页数范围,例如:对于一个10页的HTMl文档,只打印2到4页是不可以的。
  • 一个WebView的实例只能在同一时间处理一个打印任务。
  • 若一个HTML文档包含CSS打印属性,比如一个landscape属性,这是不被支持的。
  • 不能通过一个HTML文档中的JavaScript脚本来激活打印。
Note:一旦在布局中包含的 WebView对象将文档加载完毕后,就可以打印 WebView对象的内容了。
创建一个打印任务
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);
}
我们不需要创建一个应用内置的通知,因为打印框架会自动的创建一个该打印任务的系统通知。


3.3.3打印自定义文档 

创建打印适配器
打印适配器负责与安卓打印框架交互并处理打印过程的每一步。这个过程需要用户在创建打印问当前选择打印机和打印选项。
一旦用户点击了打印按钮,框架会将最终的打印文档传递给PrintProvider进行打印输出。在打印过程中,用户可以选择取消打印,所以打印适配器必须监听并响应取消打印的请求。

PrintDocumentAdapter抽象类负责处理打印的生命周期,它有四个主要的回调方法。
1、onstart():一旦打印进程开始,该方法就会被调用。如果我们的应用有任何一次性的准备任务要执行。让它们在此处执行,这个回调方法不是必须实现的。
2、onLayout():每当用户改变了影响打印输出的设置,该函数将会被被调用,以此给我们的应用一个机会去重新计算打印页面的布局,另外该方法必须返回打印文档包含多少页面。
3、onWrite(),该方法调用后,会将打印页面渲染成一个待打印的文件。该方法可以在onLayout()方法被调用后调用一次或多次。
4、onFinish():一旦打印进程金属后,该方法将会被调用。

计算打印文档信息
在实现 PrintDocumentAdapter 类时,我们的应用必须能够指定出所创建文档的类型,计算出打印任务所需要打印的总页数,并提供打印页面的尺寸信息。在实现适配器的 onLayout() 方法时,我们执行这些计算,并提供与理想的输出相关的一些信息,这些信息可以在 PrintDocumentInfo 类中获取,包括页数和内容类型。
@OverridepublicvoidonLayout(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 requestif (cancellationSignal.isCancelled() ) {
        callback.onLayoutCancelled();
        return;
    }

    // Compute the expected number of printed pagesint 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() 方法的执行结果有三种:完成,取消或失败(计算布局无法顺利完成时会失败)。我们必须通过调用 PrintDocumentAdapter.LayoutResultCallback 对象中的适当方法来指出这些结果中的一个。

Note:onLayoutFinished()方法的布尔类型参数明确了这个布局内容是否和上一次打印请求相比发生了改变。恰当地设定了这个参数将避免打印框架不必要地调用onWrite()方法,缓存之前的打印文档,提升执行性能。

onLayout()的主要任务是计算打印文档的页数,并将它作为打印参数交给打印机。如何计算页数则高度依赖于应用是如何对打印页面进行布局的。下面的代码展示了页数是如何根据打印方向确定的:

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

将打印文档写入文件

当需要将打印内容输出到一个文件时,Android打印框架会调用PrintDocumentAdapter类的onWrite()方法。这个方法的参数指定了哪些页面要被写入以及要使用的输出文件。该方法的实现必须将每一个请求页的内容渲染成一个含有多个页面的PDF文件。当这个过程结束以后,你需要调用callback对象的onWriteFinished()方法。

Note: Android打印框架可能会在每次调用onLayout()后,调用onWrite()方法一次甚至更多次。请务必牢记:当打印内容的布局没有变化时,可以将onLayoutFinished()方法的布尔参数设置为“false”,以此避免对打印文档进行不必要的重写操作。

Note:onLayoutFinished()方法的布尔类型参数明确了这个布局内容是否和上一次打印请求相比发生了改变。恰当地设定了这个参数将避免打印框架不必要的调用onLayout()方法,缓存之前的打印文档,提升执行性能。

下面的代码展示了使用PrintedPdfDocument类创建了PDF文件的基本原理:

@OverridepublicvoidonWrite(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 cancellationif (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 filetry {
        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);

    ...
}

代码中将PDF页面递交给了drawPage()方法,这个方法会在下一部分介绍。

就布局而言,onWrite()方法的执行可以有三种结果:完成,取消或者失败(内容无法被写入)。我们必须通过调用PrintDocumentAdapter.WriteResultCallback对象中的适当方法来指明这些结果中的一个。

Note:渲染打印文档是一个可能耗费大量资源的操作。为了避免阻塞应用的主UI线程,我们应该考虑将页面的渲染和写操作放在另一个线程中执行,比如在AsyncTask中执行。关于更多异步任务线程的知识,可以阅读:Processes and Threads

绘制PDF页面内容

当我们的应用进行打印时,应用必须生成一个PDF文档并将它传递给Android打印框架以进行打印。我们可以使用任何PDF生成库来协助完成这个操作。本节将展示如何使用PrintedPdfDocument类将打印内容生成为PDF页面。

PrintedPdfDocument类使用一个Canvas对象来在PDF页面上绘制元素,这一点和在activity布局上进行绘制很类似。我们可以在打印页面上使用Canvas类提供的相关绘图方法绘制页面元素。下面的代码展示了如何使用这些方法在PDF页面上绘制一些简单的元素:

privatevoiddrawPage(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);
}

当使用Canvas在一个PDF页面上绘图时,元素通过单位“点(point)”来指定大小,一个点相当于七十二分之一英寸。在编写程序时,请确保使用该测量单位来指定页面上的元素大小。在定位绘制的元素时,坐标系的原点(即(0,0)点)在页面的最左上角。

Tip:虽然Canvas对象允许我们将打印元素放置在一个PDF文档的边缘,但许多打印机无法在纸张的边缘打印。所以当我们使用这个类构建一个打印文档时,需要考虑到那些无法打印的边缘区域。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值