Androidアプリ開発

PDF作成をPdfDocumentで実装する

この記事は約21分で読めます。
スポンサーリンク

こんにちは、まっさん(@Tera_Msaki)です。

この記事は Android スマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Java での開発経験、XML 構文規則、Android のアプリ開発経験がある方を対象としています。
Android のアプリ開発でお役にたててれば、嬉しいです。
(これから Android のアプリ開発や Java での開発を始めたい方への案内は、
記事の最後で紹介します)

この記事のテーマ


 AndroidアプリでPDFを作成する

下記の動画は、
GPSタイム計測アプリ(
Laps)でタイム計測結果のPDFを作成して、
作成した
PDFを PDFビューワ(Acrobat Reader)で表示している動画です。

PDF作成を PdfDocument で実装する

Android API の android.graphics.pdf というパッケージには、
PDFファイルを作成する PdfDocument クラスが用意されています。
今回は、PdfDocumentクラスを使用した PDF作成について、解説したいと思います。

◎PDF作成クラス
PDF を作成する基本的な流れとして、
インスタンス化した PdfDocumentから Page作成を開始、
作成した Pageから、Canvas を取得します。
Canvasに PDF に出力する内容を描画して、Page作成を終了します。
注意点としては、改ページの単位で、Page 作成の開始と終了する必要があります。
全ページ分の Canvasへの描画が完了した時点で、
PDF 出力先にあたる FileOutputStreamPdfDocumentに引き渡して、書き込みを行います。
PDF 出力後に PdfDocumentを閉じます。

public class LapsPdfDocument {
    private Context                 context;
    private final Bitmap            bitmap;
    private final int[]             bitmapSize = { 0, 0 };
    private final String            DATE;
    private final float             Y3 = 100;
    private final float             PITCH = 12;

    // コンストラクタ //
    public LapsPdfDocument(Context newContext, int image) {
        if (context == null) {
            context = newContext;
        } else if (!context.equals(newContext)) {
            context = newContext;
        }
        bitmap = BitmapFactory.decodeResource(context.getResources(), image);
        bitmapSize[0] = bitmap.getWidth();
        bitmapSize[1] = bitmap.getHeight();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
        DATE = String.format("DATE : %s", simpleDateFormat.format(System.currentTimeMillis()));
    }

    // PDF作成 //
    public void createPdfDocument(List<String> motionList, Uri uri) {
        PdfDocument         pdfDocument = new PdfDocument();
        PdfDocument.Page    page;
        Canvas              canvas;
        RectF               rectF;
        Matrix              matrix = new Matrix();
        Paint               paint1 = new Paint();
        Paint               paint2 = new Paint();
        Paint               paint3 = new Paint();
        Paint               paint4 = new Paint();
        int                 pos = 0;
        int                 pages = 0;
        // [1] 00:00:00.000
        paint1.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD_ITALIC));
        paint1.setColor(context.getColor(R.color.theme500));
        paint1.setTextSize(20);
        paint1.setTextAlign(Paint.Align.LEFT);
        // [2] Laps: HH:MM 距離: 000.0m
        // [3] 最低: 0.0Km/h 最高: 0.0Km/h 平均: 0.0Km/h
        paint2.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD));
        paint2.setColor(context.getColor(R.color.grey800));
        paint2.setTextSize(10);
        paint2.setTextAlign(Paint.Align.LEFT);
        // DATE : yyyy/MM/dd
        paint3.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD));
        paint3.setColor(context.getColor(R.color.grey800));
        paint3.setTextSize(10);
        paint3.setTextAlign(Paint.Align.RIGHT);
        // PANEL
        float LX = 15;
        float RX = 580;
        LinearGradient linearGradient = new LinearGradient(LX, 0, RX, 0,Color.argb(0,0,0,0),  context.getColor(R.color.grey002), Shader.TileMode.CLAMP);
        paint4.setShader(linearGradient);
        // A4横(72dpi)
        int a4X = 595;
        // A4縦(72dpi)
        int a4Y = 842;
        page = pdfDocument.startPage(new PdfDocument.PageInfo.Builder(a4X, a4Y, pages).create());
        canvas = page.getCanvas();
        // 背景
        matrix.postScale(0.381f,0.381f);
        canvas.drawBitmap(Bitmap.createBitmap(bitmap, 0, 0, bitmapSize[0], bitmapSize[1], matrix, true), 0, 0, null);
        // DATE : yyyy/MM/dd
        float y2 = 20;
        canvas.drawText(DATE, RX, y2, paint3);
        // 計測結果
        List<String> dataList = new ArrayList<>(motionList);
        Collections.reverse(dataList);
        for (String data : dataList) {
            String[]    items = data.split("\n");
            if (items.length > 1) {
                int      detail = items.length > 2 ? (items.length - 1) / 2 + 2: 2;
                String[] heads = new String[] {
                    items[0].substring(0, Math.max(items[0].indexOf("Laps"), 0)),    // [0] 00:00:00.000
                    items[0].substring(Math.max(items[0].indexOf("Laps"), 0))        // [1] Laps: HH:MM 距離: 000.0m
                };
                // 行数
                int LIMIT = 60;
                if (pos + detail > LIMIT) {
                    pdfDocument.finishPage(page);
                    // 改ページ
                    pages++;
                    page = pdfDocument.startPage(new PdfDocument.PageInfo.Builder(a4X, a4Y, pages).create());
                    canvas = page.getCanvas();
                    canvas.drawBitmap(Bitmap.createBitmap(bitmap, 0, 0, bitmapSize[0], bitmapSize[1], matrix, true), 1 , 1, null);
                    canvas.drawText(DATE, RX, y2, paint3);
                    pos = 0;
                }
                // PANEL
                rectF = new RectF(LX, Y3 + (pos * PITCH) + 6, RX, Y3 + ((pos + detail) * PITCH) + 3);
                canvas.drawRoundRect(rectF, 2, 2, paint4);
                // [0] 00:00:00.000
                drawText(heads[0], LX + 5, pos, paint1, canvas, 2);
                pos++;
                // [1] Laps: HH:MM 距離: 0.0m
                float CX = 120;
                drawText(heads[1], CX, pos, paint2, canvas, 1);
                float DX = 330;
                if (items.length > 2) {
                    pos++;
                    for (int i = 1; i < items.length; i = i + 2) {
                        // [1] [ 区間1 ] タイム: 00:00.000 距離: 0.0m
                        drawText(items[i], CX, pos, paint2, canvas, 1);
                        // [2] 最低: 0.0Km/h 最高: 0.0Km/h 平均: 0.0Km/h
                        drawText(items[i + 1], DX, pos, paint2, canvas, 1);
                        pos++;
                    }
                } else {
                    // [2] 最低: 0.0Km/h 最高: 0.0Km/h 平均: 0.0Km/h
                    drawText(items[1], DX, pos, paint2, canvas, 1);
                    pos++;
                }
            }
        }
        pdfDocument.finishPage(page);
        try {
            FileOutputStream fileOutputStream = (FileOutputStream) context.getContentResolver().openOutputStream(uri, "wt");
            pdfDocument.writeTo(fileOutputStream);
            Toast toast = Toast.makeText(context, context.getString(R.string.export_pdf), Toast.LENGTH_SHORT);
            toast.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
        pdfDocument.close();
    }

    // テキスト出力 //
    private void drawText(String text, float x, int pos, Paint paint, Canvas canvas, float size) {
        canvas.drawText(text, x, Y3 + ((pos + size) * PITCH), paint);
    }
}

今回、出力する内容(測定結果)を Stringのリストを引数として、
背景付きの A4サイズの PDF を作成します。
アプリで作成する PDF が1種類のため、
コンストラクタで、背景と作成日付を設定、
Stringのリストと PDF の出力先 Uriを引数とした
PDF作成( createPdfDocument )を 1つのクラスとして実装します。

◎出力する内容( String型のリスト)
測定結果( motionList )は改行コードで区切りられたマルチレコードの構成で、
PDF作成クラスでは、1レコード目のヘッダを2つに分解、
2レコード目以降の明細は、2レコードを1行として出力します。
明細が 1レコードしかない場合は、ヘッダと合わせて 1行として出力します。

測定結果( motionList )の内容

◎PDF作成クラスを使用する
メニューの「PDF出力」をタップで、
ストレージアクセスフレームワーク( SAF )を使用して、
PDFファイルの出力先( Uri)を取得します。
Context とリソースに登録した背景( R.drawable.background )を引数として、
インスタンス化した PDF作成クラスに、
取得した Uri と測定結果( motionList)を使って、
PDF作成( createPdfDocument )を実行します。

    :
    private final SimpleDateFormat      simpleDateFormat2 = new SimpleDateFormat("yyMMddHHmm", Locale.getDefault());
    private Handler                     handler;
    private Runnable                    runnable;
    private Context                     context;
    private ArrayList<String>           motionList = new ArrayList<>();
    private ActionMenuView              actionMenuView;
    private int                         itemId;
    :
    // ActivityResultLauncher //
    private final ActivityResultLauncher<Intent> activityResultLauncher= registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    if (result.getData() != null) {
                        switch (itemId) {
                             :
                            case 11:
                                // PDF出力
                                LapsPdfDocument lapsPdfDocument = new LapsPdfDocument(context, R.drawable.background);
                                new Thread(() -> handler.post(() -> lapsPdfDocument.createPdfDocument(motionList, result.getData().getData()))).start();
                                break;
                            default:
                        }
                    }
                }
            });

    // onCreate //
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        context = getApplicationContext();
        :
        // メニュー
        actionMenuView = findViewById(R.id.menu);
        :
        actionMenuView.getMenu().add(Menu.NONE, 11, Menu.NONE,context.getString(R.string.menu_pdf));
        :
    }

    // onResume //
    @Override
    protected void onResume() {
        super.onResume();
        : 
        // メニュー
        actionMenuView.setOnMenuItemClickListener(menuItem -> {
            itemId = menuItem.getItemId();
            Intent intentMenu;
            switch (itemId) {
                : 
                case 11: // PDF出力
                    if (motionList.size() > 0) {
                        intentMenu = new Intent(Intent.ACTION_CREATE_DOCUMENT);
                        intentMenu.addCategory(Intent.CATEGORY_OPENABLE);
                        intentMenu.setType("application/pdf");
                        intentMenu.putExtra(Intent.EXTRA_TITLE, String.format("%s%s.pdf", context.getString(R.string.app_name), simpleDateFormat2.format(System.currentTimeMillis())));
                        intentMenu.putExtra(DocumentsContract.EXTRA_INITIAL_URI, String.format("%s.pdf", context.getString(R.string.app_name)));
                        activityResultLauncher.launch(intentMenu);
                        overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
                    } else
                        toastMessage(R.string.pdf_error);
                    break;
                    :
            }
            return false;
        });
        :

ストレージアクセスフレームワーク( SAF )について

使用する string や color は、string.xml または、color.xml に指定します。

<resources>
    <string name="app_name">Laps</string>
    <string name="pdf_error">PDF出力するデータがありません</string>
    <string name="menu_pdf">PDF出力</string>
    <string name="export_pdf">PDFファイルを出力しました</string>
    : 
</resources>
    <resources>
        <color name="theme500">#FF5722</color>
        <color name="grey800">#404040</color>
        <color name="grey002">#20FEFEFE</color>
        : 
    </resources>

PDF作成をPdfDocumentで実装している Androidアプリです。

今回は、ここまでです。

参考 : Android Developers > Docs > Reference

誤字脱字、意味不明でわかりづらい、
もっと詳しく知りたいなどのご意見は、
このページの最後にある
コメントか、
こちら
から、お願いいたします♪

サイズが大きいPDFファイルの中身はイメージです。
OCRでテキスト化しても、どうしても誤認識があるので、
最終的に人のチェックが必要です。
PDFだけでなく、
手書きのホワイトボードを撮影した画像からの
テキスト化(文字起こし)も可能な代行サービスを使えば、
一発解決ですね♪

ポチッとして頂けると、
次のコンテンツを作成する励みになります♪

ブログランキング・にほんブログ村へ

◎これからAndroidのアプリ開発やJavaでの開発を始めたい方へ

初めての Android のアプリ開発では、アプリケーション開発経験がない方や、
アプリケーション開発経験がある方でも、Java や C# などのオブジェクト指向言語が初めての方は、
書籍などによる独学ではアプリ開発できるようになるには、
かなりの時間がかかりますので、オンラインスクールでの習得をおススメします。

未経験者からシステムエンジニアを目指すのに最適かと、
まずは無料相談から♪

無料でJava言語を学べるのは、かなり魅力的♪
でも、応募資格は35歳以下です

未経験者からプログラマーを目指すのに最適かと、
まずは無料カウンセリングから♪

カリキュラムとサポートがしっかりしています、
お得なキャンペーンとかいろいろやっています♪

ゲーム系に強いスクール、UnityやUnrealEngineを習得するのに最適かと、
まずは無料オンライン相談から♪

コメント欄

タイトルとURLをコピーしました