如何实现 PdfRenderer 缩放和滚动支持?

How to implement PdfRenderer Zoom and Scroll support?

我正在尝试使用 PdfRenderer,要求是缩放和滚动可用,但在 AndroidPdfRenderer 中不提供任何缩放和滚动支持,只有页面导航支持可用.

但我想缩放和滚动支持可以实现,因为 PdfRenderer 使用位图来显示使用 imageview 的内容。

How to implement Zoom and Scroll support with Google PdfRenderer sample?

PS:我正在使用 Google、https://github.com/googlesamples/android-PdfRendererBasic

提供的这个 PdfRenderer 示例

遇到这种情况我使用的解决方案是:

  • 在 ImageView 中加载 pdfRenderer 页面

  • 将我的 ImageView 放在 ScrollView 中(tadam 滚动管理),并将此 ScrollView 放在 FrameLayout

  • 添加两个按钮(在滚动视图之外)来管理放大和缩小(每个按钮在我的 ImageView 上触发缩放动画)。您也可以使用手势检测器来管理它,但是我在这样做时很难处理滚动行为

  • 添加两个按钮来管理页面变化(仍在ScrollView之外)

  • 为了获得不错的效果,我在按钮上添加了 FadeIn/FadeOut 动画,在 OnTouchEvents 上触发 FadeIn(如果没有播放动画),在 FadeIn 动画结束时触发 FadeOut

希望我能帮到你,如果你需要更详细的信息可以问我,但你现在应该知道从哪里开始

这是一个代码示例(不包括页面导航等,只包括缩放行为和滚动,其余部分在您链接的 google 代码示例中) 代码 : C#(但很容易转换成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>

有了这个,花点时间去理解它,稍微努力一下,你应该就能得到想要的结果。祝你有美好的一天

我借鉴了@yan-yankelevich的思路,把代码写在了Java。很多问题在于找到合适的缩放比例和相应的位图大小值。不要忘记 PdfRenderer 仅适用于 API 21+.

带有 PDF 位图的片段 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>

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

请注意缩放级别取决于屏幕密度,但位图的宽度和高度(以像素为单位)仅取决于缩放级别。此外,您需要调整尺寸,以便在默认缩放时(对我来说,它是全屏渲染的 pdf,值为 12),PDF 位图在您的视图中占用的空间不多不少。

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. 我发现缩放 12 适合我的屏幕,40 和 64 是使位图大小合适的系数。
  2. mCurrentPage.getWidth() returns 后记点宽度,其中每个 pt 是 1/72 英寸。
  3. 72 (DPI) 是默认的 PDF 分辨率。

PS。如果你需要同时垂直和水平滚动Scrollview vertical and horizontal in android

从这个解决方案开始:我找到了一个很好的方法来避免初始缩放系数和其他固定系数,只改变了这个方法:

private void showPage(int index) {

    if (mPdfRenderer.getPageCount() <= index) {
        return;
    }
    if (null != mCurrentPage) {
        mCurrentPage.close();
    }
    mCurrentPage = mPdfRenderer.openPage(index);

    int newWidth = (int) (mVerticalScrollView.getWidth() * 
    currentZoomLevel);
    int newHeight = (int) (newWidth * 
    ((float)mCurrentPage.getHeight()/(float)mCurrentPage.getWidth()));

    Bitmap bitmap = Bitmap.createBitmap(
        newWidth,
        newHeight,
        Bitmap.Config.ARGB_8888);

   mCurrentPage.render(bitmap, null, null, 
   PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
   mImageView.setImageBitmap(bitmap);

   updateUi();

}

有了这个解决方案,currentZoomLevel 从 1.0f 开始到 xxx(你决定一个限制)并且缩放 1.0f 的渲染图像适合滚动视图并且比例保持......

我在这里找到了一个更好的答案:PdfRendering zoom on page linked by CommonsWare: https://github.com/commonsguy/cw-omnibus/tree/v8.8/PDF/PdfRenderer。所以根据 soshial 的回答,你有捏缩放并且可以摆脱缩放按钮和常量:

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

我还添加了位图回收:

/**
 * 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;
    }
}

并且在 xml 中,而不是 ImageView:

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