How to implement PdfRenderer Zoom and Scroll support?

I used the idea of @yan-yankelevich and wrote the code in Java. Much problem was with finding proper zoom and corresponding Bitmap size values. Don't forget that PdfRenderer works only on API 21+.

Fragment with PDF Bitmap fragment_pdf_renderer.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical"
    tools:context=".PdfRendererFragment">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <HorizontalScrollView
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/image"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                android:contentDescription="@null" />
        </HorizontalScrollView>
    </ScrollView>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/from_divider_gray"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <Button
            android:id="@+id/previous"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/previous_page"
            android:textSize="13sp" />

        <Button
            android:id="@+id/next"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/next_page"
            android:textSize="13sp" />

        <ImageButton
            android:id="@+id/zoomout"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="0dp"
            android:padding="8dp"
            android:src="@drawable/ic_zoom_out_black_36dp" />

        <ImageButton
            android:id="@+id/zoomin"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="0dp"
            android:padding="8dp"
            android:src="@drawable/ic_zoom_in_black_36dp" />
    </LinearLayout>

</LinearLayout>

enter image description here

The PdfRendererFragment:

/**
 * This fragment has a big {@ImageView} that shows PDF pages, and 2
 * {@link android.widget.Button}s to move between pages. We use a
 * {@link android.graphics.pdf.PdfRenderer} to render PDF pages as
 * {@link android.graphics.Bitmap}s.
 */
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class PdfRendererFragment extends Fragment implements View.OnClickListener {

    /**
     * Key string for saving the state of current page index.
     */
    private static final String STATE_CURRENT_PAGE_INDEX = "current_page_index";

    /**
     * The filename of the PDF.
     */
    public String FILENAME;
    public String PURCHASE_ID;
    public int TICKETS_NUMBER;

    /**
     * File descriptor of the PDF.
     */
    private ParcelFileDescriptor mFileDescriptor;

    /**
     * {@link android.graphics.pdf.PdfRenderer} to render the PDF.
     */
    private PdfRenderer mPdfRenderer;

    /**
     * Page that is currently shown on the screen.
     */
    private PdfRenderer.Page mCurrentPage;

    /**
     * {@link android.widget.ImageView} that shows a PDF page as a {@link android.graphics.Bitmap}
     */
    private ImageView mImageView;

    /**
     * {@link android.widget.Button} to move to the previous page.
     */
    private Button mButtonPrevious;
    private ImageView mButtonZoomin;
    private ImageView mButtonZoomout;
    private Button mButtonNext;
    private float currentZoomLevel = 12;

    /**
     * PDF page index
     */
    private int mPageIndex;

    public PdfRendererFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_pdf_renderer, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // Retain view references.
        mImageView = (ImageView) view.findViewById(R.id.image);
        mButtonPrevious = (Button) view.findViewById(R.id.previous);
        mButtonNext = (Button) view.findViewById(R.id.next);
        mButtonZoomin = view.findViewById(R.id.zoomin);
        mButtonZoomout = view.findViewById(R.id.zoomout);

        // Bind events.
        mButtonPrevious.setOnClickListener(this);
        mButtonNext.setOnClickListener(this);
        mButtonZoomin.setOnClickListener(this);
        mButtonZoomout.setOnClickListener(this);

        mPageIndex = 0;
        // If there is a savedInstanceState (screen orientations, etc.), we restore the page index.
        if (null != savedInstanceState) {
            mPageIndex = savedInstanceState.getInt(STATE_CURRENT_PAGE_INDEX, 0);
        }
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        FILENAME = getActivity().getIntent().getExtras().getString("pdfFilename");
        TICKETS_NUMBER = getActivity().getIntent().getExtras().getInt("tickets_number");
        PURCHASE_ID = getActivity().getIntent().getExtras().getString("purchaseGuid");
    }

    @Override
    public void onStart() {
        super.onStart();
        try {
            openRenderer(getActivity());
            showPage(mPageIndex);
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(getActivity(), getString(R.string.ticket_file_not_found, FILENAME), Toast.LENGTH_SHORT).show();
            App app = (App) getActivity().getApplicationContext();
            TicketUtil.downloadTicket(app, PURCHASE_ID);
            getActivity().finish();
        }
    }

    @Override
    public void onStop() {
        try {
            closeRenderer();
        } catch (IOException e) {
            e.printStackTrace();
        }
        super.onStop();
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (null != mCurrentPage) {
            outState.putInt(STATE_CURRENT_PAGE_INDEX, mCurrentPage.getIndex());
        }
    }

    /**
     * Sets up a {@link android.graphics.pdf.PdfRenderer} and related resources.
     */
    private void openRenderer(Context context) throws IOException {
        // In this sample, we read a PDF from the assets directory.
        File file = TicketUtil.getTicketFile(context, PURCHASE_ID);
        if (!file.exists()) {
            // Since PdfRenderer cannot handle the compressed asset file directly, we copy it into
            // the cache directory.
            InputStream asset = context.getAssets().open(FILENAME);
            FileOutputStream output = new FileOutputStream(file);
            final byte[] buffer = new byte[1024];
            int size;
            while ((size = asset.read(buffer)) != -1) {
                output.write(buffer, 0, size);
            }
            asset.close();
            output.close();
        }
        mFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
        // This is the PdfRenderer we use to render the PDF.
        if (mFileDescriptor != null) {
            mPdfRenderer = new PdfRenderer(mFileDescriptor);
        }
    }

    /**
     * Closes the {@link android.graphics.pdf.PdfRenderer} and related resources.
     *
     * @throws java.io.IOException When the PDF file cannot be closed.
     */
    private void closeRenderer() throws IOException {
        if (null != mCurrentPage) {
            mCurrentPage.close();
            mCurrentPage = null;
        }
        if (null != mPdfRenderer) {
            mPdfRenderer.close();
        }
        if (null != mFileDescriptor) {
            mFileDescriptor.close();
        }
    }

    /**
     * Zoom level for zoom matrix depends on screen density (dpiAdjustedZoomLevel), but width and height of bitmap depends only on pixel size and don't depend on DPI
     * Shows the specified page of PDF to the screen.
     *
     * @param index The page index.
     */
    private void showPage(int index) {
        if (mPdfRenderer.getPageCount() <= index) {
            return;
        }
        // Make sure to close the current page before opening another one.
        if (null != mCurrentPage) {
            mCurrentPage.close();
        }
        // Use `openPage` to open a specific page in PDF.
        mCurrentPage = mPdfRenderer.openPage(index);
        // Important: the destination bitmap must be ARGB (not RGB).
        int newWidth = (int) (getResources().getDisplayMetrics().widthPixels * mCurrentPage.getWidth() / 72 * currentZoomLevel / 40);
        int newHeight = (int) (getResources().getDisplayMetrics().heightPixels * mCurrentPage.getHeight() / 72 * currentZoomLevel / 64);
        Bitmap bitmap = Bitmap.createBitmap(
                newWidth,
                newHeight,
                Bitmap.Config.ARGB_8888);
        Matrix matrix = new Matrix();

        float dpiAdjustedZoomLevel = currentZoomLevel * DisplayMetrics.DENSITY_MEDIUM / getResources().getDisplayMetrics().densityDpi;
        matrix.setScale(dpiAdjustedZoomLevel, dpiAdjustedZoomLevel);
//        Toast.makeText(getActivity(), "width " + String.valueOf(newWidth) + " widthPixels " + getResources().getDisplayMetrics().widthPixels, Toast.LENGTH_LONG).show();
//        matrix.postTranslate(-rect.left/mCurrentPage.getWidth(), -rect.top/mCurrentPage.getHeight());

        // Here, we render the page onto the Bitmap.
        // To render a portion of the page, use the second and third parameter. Pass nulls to get
        // the default result.
        // Pass either RENDER_MODE_FOR_DISPLAY or RENDER_MODE_FOR_PRINT for the last parameter.
        mCurrentPage.render(bitmap, null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
        // We are ready to show the Bitmap to user.
        mImageView.setImageBitmap(bitmap);
        updateUi();
    }

    /**
     * Updates the state of 2 control buttons in response to the current page index.
     */
    private void updateUi() {
        int index = mCurrentPage.getIndex();
        int pageCount = mPdfRenderer.getPageCount();
        if (pageCount == 1) {
            mButtonPrevious.setVisibility(View.GONE);
            mButtonNext.setVisibility(View.GONE);
        } else {
            mButtonPrevious.setEnabled(0 != index);
            mButtonNext.setEnabled(index + 1 < pageCount);
        }
        if (currentZoomLevel == 2) {
            mButtonZoomout.setActivated(false);
        } else {
            mButtonZoomout.setActivated(true);
        }
    }

    /**
     * Gets the number of pages in the PDF. This method is marked as public for testing.
     *
     * @return The number of pages.
     */
    public int getPageCount() {
        return mPdfRenderer.getPageCount();
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.previous: {
                // Move to the previous page
                currentZoomLevel = 12;
                showPage(mCurrentPage.getIndex() - 1);
                break;
            }
            case R.id.next: {
                // Move to the next page
                currentZoomLevel = 12;
                showPage(mCurrentPage.getIndex() + 1);
                break;
            }
            case R.id.zoomout: {
                // Move to the next page
                --currentZoomLevel;
                showPage(mCurrentPage.getIndex());
                break;
            }
            case R.id.zoomin: {
                // Move to the next page
                ++currentZoomLevel;
                showPage(mCurrentPage.getIndex());
                break;
            }
        }
    }
}

Bring your attention to the fact that zoom level depends on your screen density, but width and height of Bitmap (it is in pixels) depend only on your zoom level. Also, you need to tweak your sizes so that at default zoom (for me it was pdf rendered full screen and value was 12) you PDF bitmap takes no more and no less than needed in your View.

int newWidth = (int) (getResources().getDisplayMetrics().widthPixels * mCurrentPage.getWidth() / 72 * currentZoomLevel / 40);
int newHeight = (int) (getResources().getDisplayMetrics().heightPixels * mCurrentPage.getHeight() / 72 * currentZoomLevel / 64);
Bitmap bitmap = Bitmap.createBitmap(
                newWidth,
                newHeight,
                Bitmap.Config.ARGB_8888);
  1. I found out that zoom 12 fits my screen and 40 and 64 are coefficents that make Bitmap proper size.
  2. mCurrentPage.getWidth() returns width in Postscript points, where each pt is 1/72 inch.
  3. 72 (DPI) is the default PDF resolution.

PS. If you need simultaneous vertical and horizontal scroll Scrollview vertical and horizontal in android


I found a nicer answer here: PdfRendering zoom on page linked by CommonsWare: https://github.com/commonsguy/cw-omnibus/tree/v8.8/PDF/PdfRenderer . So based on soshial's answer, you have pinch zoom and can get rid of the zoom buttons and the constants:

import com.davemorrissey.labs.subscaleview.ImageSource;
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;

....

private void showPage(int index) {
    if (mPdfRenderer.getPageCount() <= index) {
        return;
    }
    // Make sure to close the current page before opening another one.
    if (null != mCurrentPage) {
        mCurrentPage.close();
    }
    // Use `openPage` to open a specific page in PDF.
    mCurrentPage = mPdfRenderer.openPage(index);
    if(mBitmap==null) {
        // Important: the destination bitmap must be ARGB (not RGB).
        int newWidth = (int) (getResources().getDisplayMetrics().densityDpi * mCurrentPage.getWidth() / 72);
        int newHeight = (int) (getResources().getDisplayMetrics().densityDpi * mCurrentPage.getHeight() / 72);
        mBitmap = Bitmap.createBitmap(
                newWidth,
                newHeight,
                Bitmap.Config.ARGB_8888);
    }
    mBitmap.eraseColor(0xFFFFFFFF);
    mCurrentPage.render(mBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
    // We are ready to show the Bitmap to user.
    mSubsamplingImageView.resetScaleAndCenter();
    mSubsamplingImageView.setImage(ImageSource.cachedBitmap(mBitmap));
    updateUi();
}

I also added bitmap recycle:

/**
 * Closes the {@link android.graphics.pdf.PdfRenderer} and related resources.
 *
 * @throws java.io.IOException When the PDF file cannot be closed.
 */
private void closeRenderer() throws IOException {
    if (null != mCurrentPage) {
        mCurrentPage.close();
        mCurrentPage = null;
    }
    if (null != mPdfRenderer) {
        mPdfRenderer.close();
    }
    if (null != mFileDescriptor) {
        mFileDescriptor.close();
    }
    if(mBitmap!=null)
    {
        mBitmap.recycle();
        mBitmap = null;
    }
}

And in the xml, instead of ImageView:

    <com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
        android:id="@+id/report_viewer_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
    />

The solution i used when confronted to this situation was :

  • Load the pdfRenderer page in a ImageView

  • Put my ImageView in a ScrollView (tadam scroll managed), and this ScrollView in a FrameLayout

  • Add two buttons (outside the scroll view) to manage zoom in and out (each button triggering a scale animation on my ImageView). You could also manage it with a gesture detector but i had hard time with the scrolling behaviour when doing so

  • Add two buttons to manage page changes (still outside the ScrollView)

  • For a nice effect i added FadeIn/FadeOut animations on my buttons, FadeIn triggering on OnTouchEvents (if no animation is playing), and FadeOut triggering when FadeIn animation is over

Hope i helped, aks me if you need more detailed informations, but you should know where to start now

Here is a code sample (wich do not inclue page navigation etc, but only zoom behaviour and scrolling, as the rest being in the google code sample you linked) Code : C# (but very easy to convert into Java)

private Button _zoomInButton;
private Button _zoomOutButton;
private ImageView _pdfViewContainer;
private float _currentZoomLevel;
private float _zoomFactor;
private float _maxZoomLevel;
private float _minZoomLevel;

private void Init(View view) // the content of this method must go in your OnViewCreated method, here the view being the frameLayout you will find in xml
{
     _zoomInButton = view.FindViewById<Button>(Resource.Id.PdfZoomInButton);
     _zoomOutButton = view.FindViewById<Button>(Resource.Id.PdfZoomOutButton);
     _pdfViewContainer = view.FindViewById<ImageView>(Resource.Id.PdfViewContainer);

    _zoomInButton.Click += delegate { ZoomIn(); }; //for you (in Java) this must looks like setOnClickListener(this); and in the onClick metghod you just have to add a case for R.id.PdfZoomInButton containing a call to ZoomIn();
    _zoomOutButton.Click += delegate { ZoomOut(); };

    _minZoomLevel = 0.9f;
    _maxZoomLevel = 1.2f;
    _zoomFactor = 0.1f;
}

private void ZoomIn()
{
    if (_currentZoomLevel + _zoomFactor < _maxZoomLevel)
    {
        ScaleAnimation scale = new ScaleAnimation(_currentZoomLevel, _currentZoomLevel + _zoomFactor, _currentZoomLevel, _currentZoomLevel + _zoomFactor, Dimension.RelativeToSelf, 0.5f, Dimension.RelativeToSelf, 0.5f);
        scale.Duration = 50;
        scale.FillAfter = true;
        _pdfViewContainer.StartAnimation(scale);
        _currentZoomLevel += _zoomFactor;
    }
}

private void ZoomOut()
{
    if (_currentZoomLevel - _zoomFactor > _minZoomLevel)
    {
        ScaleAnimation scale = new ScaleAnimation(_currentZoomLevel, _currentZoomLevel - _zoomFactor, _currentZoomLevel, _currentZoomLevel - _zoomFactor, Dimension.RelativeToSelf, 0.5f, Dimension.RelativeToSelf, 0.5f);
        scale.Duration = 50;
        scale.FillAfter = true;
        _pdfViewContainer.StartAnimation(scale);
        _currentZoomLevel -= _zoomFactor;
    }
}

XMl

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/PdfContainer">
    <ScrollView xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scrollbarAlwaysDrawVerticalTrack="true"
        android:id="@+id/PdfScrollView">
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitCenter"
            android:adjustViewBounds="true"
            android:scrollbars="vertical"
            android:src="@drawable/mediaIconPDF"
            android:id="@+id/PdfViewContainer" />
    </ScrollView>
    <LinearLayout
        android:id="@+id/PdfRightLayout"
        android:layout_gravity="right"
        android:orientation="vertical"
        android:gravity="center"
        android:layout_width="50dp"
        android:layout_height="match_parent"
        android:weightSum="1">
        <Button
            android:id="@+id/PdfZoomInButton"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:text="+" />
        <space
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="0.2" />
        <Button
            android:id="@+id/PdfZoomOutButton"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:text="-" />
    </LinearLayout>
    <LinearLayout
        android:id="@+id/PdfBottomLayout"
        android:layout_gravity="bottom"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/vogofTransparentGrey"
        android:weightSum="1">
        <Button
            android:id="@+id/PdfPreviousPage"
            android:layout_width="0dp"
            android:layout_weight="0.15"
            android:layout_height="match_parent"
            android:text="Prev" />
        <TextView
            android:id="@+id/PdfCurrentPageLabel"
            android:layout_width="0dp"
            android:layout_weight="0.7"
            android:gravity="center"
            android:layout_height="match_parent"
             />
        <Button
            android:id="@+id/PdfNextPage"
            android:layout_width="0dp"
            android:layout_weight="0.15"
            android:layout_height="match_parent"
            android:text="Next" />
    </LinearLayout>
</FrameLayout>

With this, some time to understand it and little efforts you should be able to get the desired result. Have a nice day