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>
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);
- I found out that zoom 12 fits my screen and 40 and 64 are coefficents that make Bitmap proper size.
mCurrentPage.getWidth()
returns width in Postscript points, where eachpt
is 1/72 inch.- 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