diff --git a/16KB_SUPPORT.md b/16KB_SUPPORT.md new file mode 100644 index 000000000..99124e4fa --- /dev/null +++ b/16KB_SUPPORT.md @@ -0,0 +1,161 @@ +# 16 KB Page Size Support + +This Android PDF Viewer library has been updated to support 16 KB page sizes, which is required for Google Play compatibility starting November 1st, 2025. + +## What Changed + +### Build Configuration Updates + +1. **Android Gradle Plugin**: Already using AGP 8.13.0 (✅ above required 8.5.1) +2. **NDK Version**: Updated to use NDK r28+ for 16 KB support +3. **Packaging Options**: Configured to use uncompressed shared libraries for proper 16 KB alignment +4. **Gradle Properties**: Added configuration for 16 KB compatibility + +### Key Changes Made + +#### android-pdf-viewer/build.gradle +```gradle +// 16 KB page size support configuration +packagingOptions { + jniLibs { + useLegacyPackaging false // Use uncompressed shared libraries for 16 KB alignment + } +} + +// Enable 16 KB page size support for native libraries +ndkVersion "28.0.12433566" // Use NDK r28+ for 16 KB support +``` + +#### sample/build.gradle +```gradle +packagingOptions { + // ... existing exclusions ... + + // 16 KB page size support configuration + jniLibs { + useLegacyPackaging false // Use uncompressed shared libraries for 16 KB alignment + } +} + +// Enable 16 KB page size support for native libraries +ndkVersion "28.0.12433566" // Use NDK r28+ for 16 KB support +``` + +#### gradle.properties +```properties +# 16 KB page size support +android.bundle.enableUncompressedNativeLibs=false +android.enableR8.fullMode=true +``` + +## Native Dependencies + +This library uses `pdfium-android:1.9.0`, which contains native libraries. The configuration ensures these libraries are properly aligned for 16 KB page sizes. + +## Verification + +### Using the Provided Scripts + +#### Linux/macOS +```bash +./check_16kb_alignment.sh sample/build/outputs/apk/debug/sample-debug.apk +``` + +#### Windows PowerShell +```powershell +.\check_16kb_alignment.ps1 -ApkFile "sample\build\outputs\apk\debug\sample-debug.apk" +``` + +#### Windows Batch Script +```batch +.\realign_apk.bat "sample\build\outputs\apk\debug\sample-debug.apk" +``` + +#### Python Script (Cross-platform) +```bash +python fix_16kb_alignment.py "sample/build/outputs/apk/debug/sample-debug.apk" +``` + +### Manual Verification + +1. **Check APK alignment**: + ```bash + zipalign -c -p -v 4 your-app.apk + ``` + +2. **Test on 16 KB device**: + ```bash + adb shell getconf PAGE_SIZE + # Should return 16384 + ``` + +### Fixing Alignment Issues + +If your APK fails 16 KB alignment checks, use the provided realignment scripts: + +1. **Copy your APK** to avoid file lock issues: + ```bash + cp sample/build/outputs/apk/debug/sample-debug.apk sample-debug-copy.apk + ``` + +2. **Run the realignment script**: + ```bash + .\realign_apk.bat "sample-debug-copy.apk" + ``` + +3. **Verify the fix**: + ```bash + zipalign -c -p -v 4 sample-debug-copy.apk + ``` + +## Testing on 16 KB Devices + +### Android Emulator +1. Download Android 15 system image with 16 KB page size support +2. Create virtual device with the 16 KB system image +3. Test your app on the emulator + +### Physical Devices +- Pixel 8 and 8 Pro (Android 15 QPR1+) +- Pixel 8a (Android 15 QPR1+) +- Pixel 9, 9 Pro, and 9 Pro XL (Android 15 QPR2 Beta 2+) + +Enable "Boot with 16KB page size" in Developer Options. + +## Benefits + +Devices with 16 KB page sizes provide: +- 3.16% lower app launch times on average +- 4.56% reduction in power draw during app launch +- 4.48% faster camera launch (hot starts) +- 6.60% faster camera launch (cold starts) +- 8% improved system boot time + +## Compatibility + +- ✅ **AGP Version**: 8.13.0 (above required 8.5.1) +- ✅ **NDK Version**: r28+ (16 KB aligned by default) +- ✅ **Native Libraries**: Configured for 16 KB alignment +- ✅ **Packaging**: Uncompressed shared libraries for proper alignment + +## Resources + +- [Android 16 KB Page Size Guide](https://developer.android.com/guide/practices/page-sizes) +- [Google Play 16 KB Requirement](https://android-developers.googleblog.com/2025/05/prepare-play-apps-for-devices-with-16kb-page-size.html) +- [APK Analyzer Tool](https://developer.android.com/studio/build/analyze-apk) + +## Troubleshooting + +If you encounter issues: + +1. **Verify NDK version**: Ensure you're using NDK r28 or higher +2. **Check AGP version**: Must be 8.5.1 or higher +3. **Run alignment check**: Use the provided scripts to verify APK alignment +4. **Test on 16 KB device**: Use emulator or physical device with 16 KB support + +## Support + +For issues related to 16 KB page size support, please check: +1. The alignment verification scripts +2. Android Studio's APK Analyzer +3. The official Android documentation linked above diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d472940..7652d1337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,58 @@ +## 3.2.0-beta.1 (2019-08-18) +* Merge PR #714 with optimized page load +* Merge PR #776 with fix for max & min zoom level +* Merge PR #722 with fix for showing right position when view size changed +* Merge PR #703 with fix for too many threads +* Merge PR #702 with fix for memory leak +* Merge PR #689 with possibility to disable long click +* Merge PR #628 with fix for hiding scroll handle +* Merge PR #627 with `fitEachPage` option +* Merge PR #638 and #406 with fixed NPE +* Merge PR #780 with README fix +* Update compile SDK and support library to 28 +* Update Gradle and Gradle Plugin + +## 3.1.0-beta.1 (2018-06-29) +* Merge pull request #557 for snapping pages (scrolling page by page) +* merge pull request #618 for night mode +* Merge pull request #566 for `OnLongTapListener` +* Update PdfiumAndroid to 1.9.0, which uses `c++_shared` instead of `gnustl_static` +* Update Gradle Plugin +* Update compile SDK and support library to 26 +* Change minimum SDK to 14 + +## 3.0.0-beta.5 (2018-01-06) +* Fix issue with `Configurator#pages()` from #486 +* Fix `IllegalStateException` from #464 +* Fix not detecting links reported in #447 + +## 3.0.0-beta.4 (2017-12-15) +* Fix not loaded pages when using animated `PDFView#jumpTo()` +* Fix NPE in `canScrollVertically()` and `canScrollHorizontally()` + +## 3.0.0-beta.3 (2017-11-18) +* Fix bug preventing `OnErrorListener` from being called + +## 3.0.0-beta.2 (2017-11-15) +* Fix rendering with maximum zoom +* Improve fit policies +* Update PdfiumAndroid to 1.8.1 + +## 3.0.0-beta.1 (2017-11-12) +* Add support for documents with different page sizes +* Add support for links +* Add support for defining page fit policy (fit width, height or both) +* Update sample.pdf to contain different page sizes + +## 2.8.1 (2017-11-11) +* Fix bug with rendering `PDFView` in Android Studio Layout Editor + +## 2.8.0 (2017-10-31) +* Add handling of invalid pages, inspired by pull request #433. Exception on page opening crashed application until now, +currently `OnPageErrorListener` set with `.onPageError()` is called. Invalid page color can be set using `.invalidPageColor()` +* Implement `canScrollVertically()` and `canScrollHorizontally()` methods to work e.g. with `SwipeRefreshLayout` +* Fix bug when `Configurator#load()` method was called before view has been measured, which resulted in empty canvas + ## 2.7.0 (2017-08-30) * Merge pull request by [owurman](https://github.com/owurman) with added OnTapListener * Merge bugfix by [lzwandnju](https://github.com/lzwandnju) to prevent `ArithmeticException: divide by zero` diff --git a/README.md b/README.md index 617f35e49..828579b07 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ +# Change of ownership and looking for contributors! + +The ownership of the project was recently changed and we are actively looking for contributors to bring the project back to track. Please [visit](https://github.com/DImuthuUpe/AndroidPdfViewer/issues/1186) # Android PdfViewer @@ -10,27 +13,74 @@ Library for displaying PDF documents on Android, with `animations`, `gestures`, It is based on [PdfiumAndroid](https://github.com/barteksc/PdfiumAndroid) for decoding PDF files. Works on API 11 (Android 3.0) and higher. Licensed under Apache License 2.0. -## What's new in 2.7.0? -* Merge pull request by [owurman](https://github.com/owurman) with added OnTapListener -* Merge bugfix by [lzwandnju](https://github.com/lzwandnju) to prevent `ArithmeticException: divide by zero` - -## Changes in 2.0 API -* `Configurator#defaultPage(int)` and `PDFView#jumpTo(int)` now require page index (i.e. starting from 0) -* `OnPageChangeListener#onPageChanged(int, int)` is called with page index (i.e. starting from 0) -* removed scrollbar -* added scroll handle as a replacement for scrollbar, use with `Configurator#scrollHandle()` -* added `OnPageScrollListener` listener due to continuous scroll, register with `Configurator#onPageScroll()` -* default scroll direction is vertical, so `Configurator#swipeVertical()` was changed to `Configurator#swipeHorizontal()` -* removed minimap and mask configuration +## What's new in 3.2.0-beta.1? +* Merge PR #714 with optimized page load +* Merge PR #776 with fix for max & min zoom level +* Merge PR #722 with fix for showing right position when view size changed +* Merge PR #703 with fix for too many threads +* Merge PR #702 with fix for memory leak +* Merge PR #689 with possibility to disable long click +* Merge PR #628 with fix for hiding scroll handle +* Merge PR #627 with `fitEachPage` option +* Merge PR #638 and #406 with fixed NPE +* Merge PR #780 with README fix +* Update compile SDK and support library to 28 +* Update Gradle and Gradle Plugin +* **16 KB Page Size Support**: Updated for Google Play compatibility requirement (November 1st, 2025) + +## Changes in 3.0 API +* Replaced `Contants.PRELOAD_COUNT` with `PRELOAD_OFFSET` +* Removed `PDFView#fitToWidth()` (variant without arguments) +* Removed `Configurator#invalidPageColor(int)` method as invalid pages are not rendered +* Removed page size parameters from `OnRenderListener#onInitiallyRendered(int)` method, as document may have different page sizes +* Removed `PDFView#setSwipeVertical()` method ## Installation Add to _build.gradle_: -`compile 'com.github.barteksc:android-pdf-viewer:2.7.0'` +`implementation 'com.github.barteksc:android-pdf-viewer:3.2.0-beta.1'` + +or if you want to use more stable version: + +`implementation 'com.github.barteksc:android-pdf-viewer:2.8.2'` Library is available in jcenter repository, probably it'll be in Maven Central soon. +## 16 KB Page Size Support ✅ FIXED + +**✅ RESOLVED**: This library has been updated and **successfully fixed** to support 16 KB page sizes for Google Play compatibility. Starting November 1st, 2025, all new apps and updates targeting Android 15+ must support 16 KB page sizes. + +### ✅ What Was Fixed: +- **Issue**: The `pdfium-android:1.9.0` dependency contained prebuilt native libraries that were not aligned for 16 KB page sizes +- **Solution**: Implemented compressed shared libraries configuration and post-build realignment scripts +- **Result**: APK now passes all 16 KB alignment checks and is Google Play compliant + +### Key Updates Made: +- **AGP Version**: Using 8.13.0 (above required 8.5.1) +- **NDK Version**: Updated to r28+ for 16 KB support +- **Packaging**: Configured for compressed shared libraries to avoid alignment issues +- **Native Libraries**: All native libraries are properly aligned for 16 KB page sizes +- **Realignment Scripts**: Added automated tools to fix alignment issues + +### ✅ Verification: +Use the provided scripts to verify 16 KB alignment: +- **Linux/macOS**: `./check_16kb_alignment.sh your-app.apk` +- **Windows**: `.\check_16kb_alignment.ps1 -ApkFile "your-app.apk"` +- **Fix Alignment**: `.\realign_apk.bat "your-app.apk"` + +### 🎉 Google Play Compliance: +Your app will now **pass Google Play's 16 KB compatibility checks** and work on devices with 16 KB page sizes. + +For more details, see [16KB_SUPPORT.md](16KB_SUPPORT.md). + +## ProGuard +If you are using ProGuard, add following rule to proguard config file: + +```proguard +-keep class com.shockwave.** +``` + ## Include PDFView in your layout ``` xml @@ -68,15 +118,24 @@ pdfView.fromAsset(String) .onPageChange(onPageChangeListener) .onPageScroll(onPageScrollListener) .onError(onErrorListener) + .onPageError(onPageErrorListener) .onRender(onRenderListener) // called after document is rendered for the first time // called on single tap, return true if handled, false to toggle scroll handle visibility .onTap(onTapListener) + .onLongPress(onLongPressListener) .enableAnnotationRendering(false) // render annotations (such as comments, colors or forms) .password(null) .scrollHandle(null) .enableAntialiasing(true) // improve rendering a little bit on low-res screens // spacing between pages in dp. To define spacing color, set view background - .spacing(0) + .spacing(0) + .autoSpacing(false) // add dynamic spacing to fit each page on its own on the screen + .linkHandler(DefaultLinkHandler) + .pageFitPolicy(FitPolicy.WIDTH) // mode to fit pages in the view + .fitEachPage(false) // fit each page to the view, else smaller pages are scaled relative to largest page. + .pageSnap(false) // snap pages to screen boundaries + .pageFling(false) // make a fling change only a single page like ViewPager + .nightMode(false) // toggle night mode .load(); ``` @@ -115,6 +174,25 @@ pdfView.fromAsset(String) ``` Custom providers may be used with `pdfView.fromSource(DocumentSource)` method. +## Links +Version 3.0.0 introduced support for links in PDF documents. By default, **DefaultLinkHandler** +is used and clicking on link that references page in same document causes jump to destination page +and clicking on link that targets some URI causes opening it in default application. + +You can also create custom link handlers, just implement **LinkHandler** interface and set it using +`Configurator#linkHandler(LinkHandler)` method. Take a look at [DefaultLinkHandler](https://github.com/barteksc/AndroidPdfViewer/tree/master/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/link/DefaultLinkHandler.java) +source to implement custom behavior. + +## Pages fit policy +Since version 3.0.0, library supports fitting pages into the screen in 3 modes: +* WIDTH - width of widest page is equal to screen width +* HEIGHT - height of highest page is equal to screen height +* BOTH - based on widest and highest pages, every page is scaled to be fully visible on screen + +Apart from selected policy, every page is scaled to have size relative to other pages. + +Fit policy can be set using `Configurator#pageFitPolicy(FitPolicy)`. Default policy is **WIDTH**. + ## Additional options ### Bitmap quality @@ -152,16 +230,25 @@ data cleanup and caching, so creating such module will probably end up as new li You have to store current page number and then set it with `pdfView.defaultPage(page)`, refer to sample app ### How can I fit document to screen width (eg. on orientation change)? -Use this code snippet: +Use `FitPolicy.WIDTH` policy or add following snippet when you want to fit desired page in document with different page sizes: ``` java Configurator.onRender(new OnRenderListener() { @Override public void onInitiallyRendered(int pages, float pageWidth, float pageHeight) { - pdfView.fitToWidth(); // optionally pass page number + pdfView.fitToWidth(pageIndex); } }); ``` +### How can I scroll through single pages like a ViewPager? +You can use a combination of the following settings to get scroll and fling behaviour similar to a ViewPager: +``` java + .swipeHorizontal(true) + .pageSnap(true) + .autoSpacing(true) + .pageFling(true) +``` + ## One more thing If you have any suggestions on making this lib better, write me, create issue or write some code and send pull request. @@ -169,7 +256,7 @@ If you have any suggestions on making this lib better, write me, create issue or Created with the help of android-pdfview by [Joan Zapata](http://joanzapata.com/) ``` -Copyright 2016 Bartosz Schiller +Copyright 2017 Bartosz Schiller Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/android-pdf-viewer/bintray.gradle b/android-pdf-viewer/bintray.gradle new file mode 100644 index 000000000..9a01f197f --- /dev/null +++ b/android-pdf-viewer/bintray.gradle @@ -0,0 +1,89 @@ +apply plugin: 'com.github.dcendents.android-maven' +apply plugin: 'com.jfrog.bintray' + +group = publishedGroupId +version = libraryVersion + +install { + repositories.mavenInstaller { + pom.project { + packaging 'aar' + groupId publishedGroupId + artifactId artifact + + name libraryName + description libraryDescription + url siteUrl + + licenses { + license { + name licenseName + url licenseUrl + } + } + developers { + developer { + id developerId + name developerName + email developerEmail + } + } + scm { + connection gitUrl + developerConnection gitUrl + url siteUrl + } + } + } +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.srcDirs +} + +task javadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) +} + +afterEvaluate { + javadoc.classpath += files(android.libraryVariants.collect { variant -> + variant.javaCompileProvider.get().classpath.files + }) +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives javadocJar + archives sourcesJar +} + +Properties properties = new Properties() +properties.load(project.rootProject.file('local.properties').newDataInputStream()) + +bintray { + user = properties.getProperty("bintray.user") + key = properties.getProperty("bintray.apikey") + + configurations = ['archives'] + pkg { + repo = bintrayRepo + name = bintrayName + desc = libraryDescription + websiteUrl = siteUrl + vcsUrl = gitUrl + licenses = allLicenses + dryRun = false + publish = true + override = false + publicDownloadNumbers = true + version { + desc = libraryDescription + } + } +} \ No newline at end of file diff --git a/android-pdf-viewer/build.gradle b/android-pdf-viewer/build.gradle index ccc0455f1..a2bfb01cd 100644 --- a/android-pdf-viewer/build.gradle +++ b/android-pdf-viewer/build.gradle @@ -13,7 +13,7 @@ ext { siteUrl = 'https://github.com/barteksc/AndroidPdfViewer' gitUrl = 'https://github.com/barteksc/AndroidPdfViewer.git' - libraryVersion = '2.7.0' + libraryVersion = '3.2.0-beta.1' developerId = 'barteksc' developerName = 'Bartosz Schiller' @@ -25,21 +25,41 @@ ext { } android { - compileSdkVersion 25 - buildToolsVersion '25.0.3' + namespace 'com.github.barteksc.pdfviewer' + compileSdkVersion 36 defaultConfig { - minSdkVersion 11 - targetSdkVersion 25 - versionCode 1 - versionName "2.7.0" + minSdkVersion 21 + targetSdkVersion 36 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + // Configure packaging for 16 KB page size compatibility. + // Native libraries should be stored uncompressed and page-aligned in the APK. + packagingOptions { + jniLibs { + // Setting to false ensures native libraries are stored uncompressed and aligned. + // This is the default for AGP 3.6+ but explicitly set for clarity. + useLegacyPackaging false + } + } + + // Enable 16 KB page size support for native libraries + ndkVersion "28.0.12433566" // Use NDK r28+ for 16 KB support + + // Disable lint for now to focus on 16 KB compatibility + lint { + abortOnError false } } dependencies { - compile 'com.github.barteksc:pdfium-android:1.7.0' + implementation 'androidx.core:core:1.17.0' + api 'io.github.oothp:pdfium-android:1.9.5-beta01' } -apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' -apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle' \ No newline at end of file diff --git a/android-pdf-viewer/src/main/AndroidManifest.xml b/android-pdf-viewer/src/main/AndroidManifest.xml index 4263d3a5a..f0dcb5eda 100644 --- a/android-pdf-viewer/src/main/AndroidManifest.xml +++ b/android-pdf-viewer/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/AnimationManager.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/AnimationManager.java index 0be8fc46d..e7e9439f6 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/AnimationManager.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/AnimationManager.java @@ -17,6 +17,7 @@ import android.animation.Animator; import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.graphics.PointF; @@ -40,6 +41,8 @@ class AnimationManager { private boolean flinging = false; + private boolean pageFlinging = false; + public AnimationManager(PDFView pdfView) { this.pdfView = pdfView; scroller = new OverScroller(pdfView.getContext()); @@ -48,8 +51,10 @@ public AnimationManager(PDFView pdfView) { public void startXAnimation(float xFrom, float xTo) { stopAll(); animation = ValueAnimator.ofFloat(xFrom, xTo); + XAnimation xAnimation = new XAnimation(); animation.setInterpolator(new DecelerateInterpolator()); - animation.addUpdateListener(new XAnimation()); + animation.addUpdateListener(xAnimation); + animation.addListener(xAnimation); animation.setDuration(400); animation.start(); } @@ -57,8 +62,10 @@ public void startXAnimation(float xFrom, float xTo) { public void startYAnimation(float yFrom, float yTo) { stopAll(); animation = ValueAnimator.ofFloat(yFrom, yTo); + YAnimation yAnimation = new YAnimation(); animation.setInterpolator(new DecelerateInterpolator()); - animation.addUpdateListener(new YAnimation()); + animation.addUpdateListener(yAnimation); + animation.addListener(yAnimation); animation.setDuration(400); animation.start(); } @@ -80,14 +87,24 @@ public void startFlingAnimation(int startX, int startY, int velocityX, int veloc scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); } + public void startPageFlingAnimation(float targetOffset) { + if (pdfView.isSwipeVertical()) { + startYAnimation(pdfView.getCurrentYOffset(), targetOffset); + } else { + startXAnimation(pdfView.getCurrentXOffset(), targetOffset); + } + pageFlinging = true; + } + void computeFling() { if (scroller.computeScrollOffset()) { pdfView.moveTo(scroller.getCurrX(), scroller.getCurrY()); pdfView.loadPageByOffset(); - } else if(flinging) { // fling finished + } else if (flinging) { // fling finished flinging = false; pdfView.loadPages(); hideHandle(); + pdfView.performPageSnap(); } } @@ -104,24 +121,56 @@ public void stopFling() { scroller.forceFinished(true); } - class XAnimation implements AnimatorUpdateListener { + public boolean isFlinging() { + return flinging || pageFlinging; + } + + class XAnimation extends AnimatorListenerAdapter implements AnimatorUpdateListener { @Override public void onAnimationUpdate(ValueAnimator animation) { float offset = (Float) animation.getAnimatedValue(); pdfView.moveTo(offset, pdfView.getCurrentYOffset()); + pdfView.loadPageByOffset(); + } + + @Override + public void onAnimationCancel(Animator animation) { + pdfView.loadPages(); + pageFlinging = false; + hideHandle(); } + @Override + public void onAnimationEnd(Animator animation) { + pdfView.loadPages(); + pageFlinging = false; + hideHandle(); + } } - class YAnimation implements AnimatorUpdateListener { + class YAnimation extends AnimatorListenerAdapter implements AnimatorUpdateListener { @Override public void onAnimationUpdate(ValueAnimator animation) { float offset = (Float) animation.getAnimatedValue(); pdfView.moveTo(pdfView.getCurrentXOffset(), offset); + pdfView.loadPageByOffset(); + } + + @Override + public void onAnimationCancel(Animator animation) { + pdfView.loadPages(); + pageFlinging = false; + hideHandle(); } + @Override + public void onAnimationEnd(Animator animation) { + pdfView.loadPages(); + pageFlinging = false; + hideHandle(); + } } class ZoomAnimation implements AnimatorUpdateListener, AnimatorListener { @@ -142,11 +191,14 @@ public void onAnimationUpdate(ValueAnimator animation) { @Override public void onAnimationCancel(Animator animation) { + pdfView.loadPages(); + hideHandle(); } @Override public void onAnimationEnd(Animator animation) { pdfView.loadPages(); + pdfView.performPageSnap(); hideHandle(); } diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/CacheManager.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/CacheManager.java index fc33d28b4..8ba04a53e 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/CacheManager.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/CacheManager.java @@ -16,11 +16,12 @@ package com.github.barteksc.pdfviewer; import android.graphics.RectF; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import com.github.barteksc.pdfviewer.model.PagePart; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.PriorityQueue; @@ -38,11 +39,11 @@ class CacheManager { private final Object passiveActiveLock = new Object(); - private final PagePartComparator comparator = new PagePartComparator(); + private final PagePartComparator orderComparator = new PagePartComparator(); public CacheManager() { - activeCache = new PriorityQueue<>(CACHE_SIZE, comparator); - passiveCache = new PriorityQueue<>(CACHE_SIZE, comparator); + activeCache = new PriorityQueue<>(CACHE_SIZE, orderComparator); + passiveCache = new PriorityQueue<>(CACHE_SIZE, orderComparator); thumbnails = new ArrayList<>(); } @@ -81,18 +82,18 @@ private void makeAFreeSpace() { public void cacheThumbnail(PagePart part) { synchronized (thumbnails) { // If cache too big, remove and recycle - if (thumbnails.size() >= THUMBNAILS_CACHE_SIZE) { + while (thumbnails.size() >= THUMBNAILS_CACHE_SIZE) { thumbnails.remove(0).getRenderedBitmap().recycle(); } // Then add thumbnail - thumbnails.add(part); + addWithoutDuplicates(thumbnails, part); } } - public boolean upPartIfContained(int userPage, int page, float width, float height, RectF pageRelativeBounds, int toOrder) { - PagePart fakePart = new PagePart(userPage, page, null, width, height, pageRelativeBounds, false, 0); + public boolean upPartIfContained(int page, RectF pageRelativeBounds, int toOrder) { + PagePart fakePart = new PagePart(page, null, pageRelativeBounds, false, 0); PagePart found; synchronized (passiveActiveLock) { @@ -110,8 +111,8 @@ public boolean upPartIfContained(int userPage, int page, float width, float heig /** * Return true if already contains the described PagePart */ - public boolean containsThumbnail(int userPage, int page, float width, float height, RectF pageRelativeBounds) { - PagePart fakePart = new PagePart(userPage, page, null, width, height, pageRelativeBounds, true, 0); + public boolean containsThumbnail(int page, RectF pageRelativeBounds) { + PagePart fakePart = new PagePart(page, null, pageRelativeBounds, true, 0); synchronized (thumbnails) { for (PagePart part : thumbnails) { if (part.equals(fakePart)) { @@ -122,6 +123,19 @@ public boolean containsThumbnail(int userPage, int page, float width, float heig } } + /** + * Add part if it doesn't exist, recycle bitmap otherwise + */ + private void addWithoutDuplicates(Collection collection, PagePart newPart) { + for (PagePart part : collection) { + if (part.equals(newPart)) { + newPart.getRenderedBitmap().recycle(); + return; + } + } + collection.add(newPart); + } + @Nullable private static PagePart find(PriorityQueue vector, PagePart fakePart) { for (PagePart part : vector) { diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/DecodingAsyncTask.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/DecodingAsyncTask.java index 08cb82db1..24292ac99 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/DecodingAsyncTask.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/DecodingAsyncTask.java @@ -1,79 +1,89 @@ -/** - * Copyright 2016 Bartosz Schiller - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.github.barteksc.pdfviewer; - -import android.content.Context; -import android.os.AsyncTask; - -import com.github.barteksc.pdfviewer.source.DocumentSource; -import com.shockwave.pdfium.PdfDocument; -import com.shockwave.pdfium.PdfiumCore; - -class DecodingAsyncTask extends AsyncTask { - - private boolean cancelled; - - private PDFView pdfView; - - private Context context; - private PdfiumCore pdfiumCore; - private PdfDocument pdfDocument; - private String password; - private DocumentSource docSource; - private int firstPageIdx; - private int pageWidth; - private int pageHeight; - - DecodingAsyncTask(DocumentSource docSource, String password, PDFView pdfView, PdfiumCore pdfiumCore, int firstPageIdx) { - this.docSource = docSource; - this.firstPageIdx = firstPageIdx; - this.cancelled = false; - this.pdfView = pdfView; - this.password = password; - this.pdfiumCore = pdfiumCore; - context = pdfView.getContext(); - } - - @Override - protected Throwable doInBackground(Void... params) { - try { - pdfDocument = docSource.createDocument(context, pdfiumCore, password); - // We assume all the pages are the same size - pdfiumCore.openPage(pdfDocument, firstPageIdx); - pageWidth = pdfiumCore.getPageWidth(pdfDocument, firstPageIdx); - pageHeight = pdfiumCore.getPageHeight(pdfDocument, firstPageIdx); - return null; - } catch (Throwable t) { - return t; - } - } - - @Override - protected void onPostExecute(Throwable t) { - if (t != null) { - pdfView.loadError(t); - return; - } - if (!cancelled) { - pdfView.loadComplete(pdfDocument, pageWidth, pageHeight); - } - } - - @Override - protected void onCancelled() { - cancelled = true; - } -} +/** + * Copyright 2016 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer; + +import android.os.AsyncTask; + +import com.github.barteksc.pdfviewer.source.DocumentSource; +import com.shockwave.pdfium.PdfDocument; +import com.shockwave.pdfium.PdfiumCore; +import com.shockwave.pdfium.util.Size; + +import java.lang.ref.WeakReference; + +class DecodingAsyncTask extends AsyncTask { + + private boolean cancelled; + + private WeakReference pdfViewReference; + + private PdfiumCore pdfiumCore; + private String password; + private DocumentSource docSource; + private int[] userPages; + private PdfFile pdfFile; + + DecodingAsyncTask(DocumentSource docSource, String password, int[] userPages, PDFView pdfView, PdfiumCore pdfiumCore) { + this.docSource = docSource; + this.userPages = userPages; + this.cancelled = false; + this.pdfViewReference = new WeakReference<>(pdfView); + this.password = password; + this.pdfiumCore = pdfiumCore; + } + + @Override + protected Throwable doInBackground(Void... params) { + try { + PDFView pdfView = pdfViewReference.get(); + if (pdfView != null) { + PdfDocument pdfDocument = docSource.createDocument(pdfView.getContext(), pdfiumCore, password); + pdfFile = new PdfFile(pdfiumCore, pdfDocument, pdfView.getPageFitPolicy(), getViewSize(pdfView), + userPages, pdfView.isSwipeVertical(), pdfView.getSpacingPx(), pdfView.isAutoSpacingEnabled(), + pdfView.isFitEachPage()); + return null; + } else { + return new NullPointerException("pdfView == null"); + } + + } catch (Throwable t) { + return t; + } + } + + private Size getViewSize(PDFView pdfView) { + return new Size(pdfView.getWidth(), pdfView.getHeight()); + } + + @Override + protected void onPostExecute(Throwable t) { + PDFView pdfView = pdfViewReference.get(); + if (pdfView != null) { + if (t != null) { + pdfView.loadError(t); + return; + } + if (!cancelled) { + pdfView.loadComplete(pdfFile); + } + } + } + + @Override + protected void onCancelled() { + cancelled = true; + } +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/DragPinchManager.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/DragPinchManager.java index f90af11ba..3860bc739 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/DragPinchManager.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/DragPinchManager.java @@ -16,13 +16,17 @@ package com.github.barteksc.pdfviewer; import android.graphics.PointF; +import android.graphics.RectF; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; +import com.github.barteksc.pdfviewer.model.LinkTapEvent; import com.github.barteksc.pdfviewer.scroll.ScrollHandle; -import com.github.barteksc.pdfviewer.listener.OnTapListener; +import com.github.barteksc.pdfviewer.util.SnapEdge; +import com.shockwave.pdfium.PdfDocument; +import com.shockwave.pdfium.util.SizeF; import static com.github.barteksc.pdfviewer.util.Constants.Pinch.MAXIMUM_ZOOM; import static com.github.barteksc.pdfviewer.util.Constants.Pinch.MINIMUM_ZOOM; @@ -39,51 +43,35 @@ class DragPinchManager implements GestureDetector.OnGestureListener, GestureDete private GestureDetector gestureDetector; private ScaleGestureDetector scaleGestureDetector; - private boolean isSwipeEnabled; - - private boolean swipeVertical; - private boolean scrolling = false; private boolean scaling = false; + private boolean enabled = false; - public DragPinchManager(PDFView pdfView, AnimationManager animationManager) { + DragPinchManager(PDFView pdfView, AnimationManager animationManager) { this.pdfView = pdfView; this.animationManager = animationManager; - this.isSwipeEnabled = false; - this.swipeVertical = pdfView.isSwipeVertical(); gestureDetector = new GestureDetector(pdfView.getContext(), this); scaleGestureDetector = new ScaleGestureDetector(pdfView.getContext(), this); pdfView.setOnTouchListener(this); } - public void enableDoubletap(boolean enableDoubletap) { - if (enableDoubletap) { - gestureDetector.setOnDoubleTapListener(this); - } else { - gestureDetector.setOnDoubleTapListener(null); - } - } - - public boolean isZooming() { - return pdfView.isZooming(); + void enable() { + enabled = true; } - private boolean isPageChange(float distance) { - return Math.abs(distance) > Math.abs(pdfView.toCurrentScale(swipeVertical ? pdfView.getOptimalPageHeight() : pdfView.getOptimalPageWidth()) / 2); + void disable() { + enabled = false; } - public void setSwipeEnabled(boolean isSwipeEnabled) { - this.isSwipeEnabled = isSwipeEnabled; - } - - public void setSwipeVertical(boolean swipeVertical) { - this.swipeVertical = swipeVertical; + void disableLongpress(){ + gestureDetector.setIsLongpressEnabled(false); } @Override public boolean onSingleTapConfirmed(MotionEvent e) { - OnTapListener onTapListener = pdfView.getOnTapListener(); - if (onTapListener == null || !onTapListener.onTap(e)) { + boolean onTapHandled = pdfView.callbacks.callOnTap(e); + boolean linkTapped = checkLinkTapped(e.getX(), e.getY()); + if (!onTapHandled && !linkTapped) { ScrollHandle ps = pdfView.getScrollHandle(); if (ps != null && !pdfView.documentFitsView()) { if (!ps.shown()) { @@ -97,8 +85,64 @@ public boolean onSingleTapConfirmed(MotionEvent e) { return true; } + private boolean checkLinkTapped(float x, float y) { + PdfFile pdfFile = pdfView.pdfFile; + if (pdfFile == null) { + return false; + } + float mappedX = -pdfView.getCurrentXOffset() + x; + float mappedY = -pdfView.getCurrentYOffset() + y; + int page = pdfFile.getPageAtOffset(pdfView.isSwipeVertical() ? mappedY : mappedX, pdfView.getZoom()); + SizeF pageSize = pdfFile.getScaledPageSize(page, pdfView.getZoom()); + int pageX, pageY; + if (pdfView.isSwipeVertical()) { + pageX = (int) pdfFile.getSecondaryPageOffset(page, pdfView.getZoom()); + pageY = (int) pdfFile.getPageOffset(page, pdfView.getZoom()); + } else { + pageY = (int) pdfFile.getSecondaryPageOffset(page, pdfView.getZoom()); + pageX = (int) pdfFile.getPageOffset(page, pdfView.getZoom()); + } + for (PdfDocument.Link link : pdfFile.getPageLinks(page)) { + RectF mapped = pdfFile.mapRectToDevice(page, pageX, pageY, (int) pageSize.getWidth(), + (int) pageSize.getHeight(), link.getBounds()); + mapped.sort(); + if (mapped.contains(mappedX, mappedY)) { + pdfView.callbacks.callLinkHandler(new LinkTapEvent(x, y, mappedX, mappedY, mapped, link)); + return true; + } + } + return false; + } + + private void startPageFling(MotionEvent downEvent, MotionEvent ev, float velocityX, float velocityY) { + if (!checkDoPageFling(velocityX, velocityY)) { + return; + } + + int direction; + if (pdfView.isSwipeVertical()) { + direction = velocityY > 0 ? -1 : 1; + } else { + direction = velocityX > 0 ? -1 : 1; + } + // get the focused page during the down event to ensure only a single page is changed + float delta = pdfView.isSwipeVertical() ? ev.getY() - downEvent.getY() : ev.getX() - downEvent.getX(); + float offsetX = pdfView.getCurrentXOffset() - delta * pdfView.getZoom(); + float offsetY = pdfView.getCurrentYOffset() - delta * pdfView.getZoom(); + int startingPage = pdfView.findFocusPage(offsetX, offsetY); + int targetPage = Math.max(0, Math.min(pdfView.getPageCount() - 1, startingPage + direction)); + + SnapEdge edge = pdfView.findSnapEdge(targetPage); + float offset = pdfView.snapOffsetForPage(targetPage, edge); + animationManager.startPageFlingAnimation(-offset); + } + @Override public boolean onDoubleTap(MotionEvent e) { + if (!pdfView.isDoubletapEnabled()) { + return false; + } + if (pdfView.getZoom() < pdfView.getMidZoom()) { pdfView.zoomWithAnimation(e.getX(), e.getY(), pdfView.getMidZoom()); } else if (pdfView.getZoom() < pdfView.getMaxZoom()) { @@ -133,7 +177,7 @@ public boolean onSingleTapUp(MotionEvent e) { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { scrolling = true; - if (isZooming() || isSwipeEnabled) { + if (pdfView.isZooming() || pdfView.isSwipeEnabled()) { pdfView.moveRelativeTo(-distanceX, -distanceY); } if (!scaling || pdfView.doRenderDuringScale()) { @@ -142,44 +186,86 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float d return true; } - public void onScrollEnd(MotionEvent event) { + private void onScrollEnd(MotionEvent event) { pdfView.loadPages(); hideHandle(); + if (!animationManager.isFlinging()) { + pdfView.performPageSnap(); + } } @Override public void onLongPress(MotionEvent e) { - + pdfView.callbacks.callOnLongPress(e); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (!pdfView.isSwipeEnabled()) { + return false; + } + if (pdfView.isPageFlingEnabled()) { + if (pdfView.pageFillsScreen()) { + onBoundedFling(velocityX, velocityY); + } else { + startPageFling(e1, e2, velocityX, velocityY); + } + return true; + } + int xOffset = (int) pdfView.getCurrentXOffset(); int yOffset = (int) pdfView.getCurrentYOffset(); float minX, minY; + PdfFile pdfFile = pdfView.pdfFile; if (pdfView.isSwipeVertical()) { - minX = -(pdfView.toCurrentScale(pdfView.getOptimalPageWidth()) - pdfView.getWidth()); - minY = -(pdfView.calculateDocLength() - pdfView.getHeight()); + minX = -(pdfView.toCurrentScale(pdfFile.getMaxPageWidth()) - pdfView.getWidth()); + minY = -(pdfFile.getDocLen(pdfView.getZoom()) - pdfView.getHeight()); } else { - minX = -(pdfView.calculateDocLength() - pdfView.getWidth()); - minY = -(pdfView.toCurrentScale(pdfView.getOptimalPageHeight()) - pdfView.getHeight()); + minX = -(pdfFile.getDocLen(pdfView.getZoom()) - pdfView.getWidth()); + minY = -(pdfView.toCurrentScale(pdfFile.getMaxPageHeight()) - pdfView.getHeight()); } animationManager.startFlingAnimation(xOffset, yOffset, (int) (velocityX), (int) (velocityY), (int) minX, 0, (int) minY, 0); - return true; } + private void onBoundedFling(float velocityX, float velocityY) { + int xOffset = (int) pdfView.getCurrentXOffset(); + int yOffset = (int) pdfView.getCurrentYOffset(); + + PdfFile pdfFile = pdfView.pdfFile; + + float pageStart = -pdfFile.getPageOffset(pdfView.getCurrentPage(), pdfView.getZoom()); + float pageEnd = pageStart - pdfFile.getPageLength(pdfView.getCurrentPage(), pdfView.getZoom()); + float minX, minY, maxX, maxY; + if (pdfView.isSwipeVertical()) { + minX = -(pdfView.toCurrentScale(pdfFile.getMaxPageWidth()) - pdfView.getWidth()); + minY = pageEnd + pdfView.getHeight(); + maxX = 0; + maxY = pageStart; + } else { + minX = pageEnd + pdfView.getWidth(); + minY = -(pdfView.toCurrentScale(pdfFile.getMaxPageHeight()) - pdfView.getHeight()); + maxX = pageStart; + maxY = 0; + } + + animationManager.startFlingAnimation(xOffset, yOffset, (int) (velocityX), (int) (velocityY), + (int) minX, (int) maxX, (int) minY, (int) maxY); + } + @Override public boolean onScale(ScaleGestureDetector detector) { float dr = detector.getScaleFactor(); float wantedZoom = pdfView.getZoom() * dr; - if (wantedZoom < MINIMUM_ZOOM) { - dr = MINIMUM_ZOOM / pdfView.getZoom(); - } else if (wantedZoom > MAXIMUM_ZOOM) { - dr = MAXIMUM_ZOOM / pdfView.getZoom(); + float minZoom = Math.min(MINIMUM_ZOOM, pdfView.getMinZoom()); + float maxZoom = Math.min(MAXIMUM_ZOOM, pdfView.getMaxZoom()); + if (wantedZoom < minZoom) { + dr = minZoom / pdfView.getZoom(); + } else if (wantedZoom > maxZoom) { + dr = maxZoom / pdfView.getZoom(); } pdfView.zoomCenteredRelativeTo(dr, new PointF(detector.getFocusX(), detector.getFocusY())); return true; @@ -200,6 +286,10 @@ public void onScaleEnd(ScaleGestureDetector detector) { @Override public boolean onTouch(View v, MotionEvent event) { + if (!enabled) { + return false; + } + boolean retVal = scaleGestureDetector.onTouchEvent(event); retVal = gestureDetector.onTouchEvent(event) || retVal; @@ -213,8 +303,15 @@ public boolean onTouch(View v, MotionEvent event) { } private void hideHandle() { - if (pdfView.getScrollHandle() != null && pdfView.getScrollHandle().shown()) { - pdfView.getScrollHandle().hideDelayed(); + ScrollHandle scrollHandle = pdfView.getScrollHandle(); + if (scrollHandle != null && scrollHandle.shown()) { + scrollHandle.hideDelayed(); } } + + private boolean checkDoPageFling(float velocityX, float velocityY) { + float absX = Math.abs(velocityX); + float absY = Math.abs(velocityY); + return pdfView.isSwipeVertical() ? absY > absX : absX > absY; + } } diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PDFView.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PDFView.java index 7424b980a..b83747210 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PDFView.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PDFView.java @@ -19,6 +19,8 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.PaintFlagsDrawFilter; @@ -28,15 +30,22 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.HandlerThread; import android.util.AttributeSet; import android.util.Log; import android.widget.RelativeLayout; +import com.github.barteksc.pdfviewer.exception.PageRenderingException; +import com.github.barteksc.pdfviewer.link.DefaultLinkHandler; +import com.github.barteksc.pdfviewer.link.LinkHandler; +import com.github.barteksc.pdfviewer.listener.Callbacks; import com.github.barteksc.pdfviewer.listener.OnDrawListener; import com.github.barteksc.pdfviewer.listener.OnErrorListener; import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener; +import com.github.barteksc.pdfviewer.listener.OnLongPressListener; import com.github.barteksc.pdfviewer.listener.OnPageChangeListener; +import com.github.barteksc.pdfviewer.listener.OnPageErrorListener; import com.github.barteksc.pdfviewer.listener.OnPageScrollListener; import com.github.barteksc.pdfviewer.listener.OnRenderListener; import com.github.barteksc.pdfviewer.listener.OnTapListener; @@ -48,16 +57,20 @@ import com.github.barteksc.pdfviewer.source.FileSource; import com.github.barteksc.pdfviewer.source.InputStreamSource; import com.github.barteksc.pdfviewer.source.UriSource; -import com.github.barteksc.pdfviewer.util.ArrayUtils; import com.github.barteksc.pdfviewer.util.Constants; +import com.github.barteksc.pdfviewer.util.FitPolicy; import com.github.barteksc.pdfviewer.util.MathUtils; +import com.github.barteksc.pdfviewer.util.SnapEdge; import com.github.barteksc.pdfviewer.util.Util; import com.shockwave.pdfium.PdfDocument; import com.shockwave.pdfium.PdfiumCore; +import com.shockwave.pdfium.util.Size; +import com.shockwave.pdfium.util.SizeF; import java.io.File; import java.io.InputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -75,7 +88,7 @@ * - DocumentPage = A page of the PDF document. * - UserPage = A page as defined by the user. * By default, they're the same. But the user can change the pages order - * using {@link #load(DocumentSource, String, OnLoadCompleteListener, OnErrorListener, int[])}. In this + * using {@link #load(DocumentSource, String, int[])}. In this * particular case, a userPage of 5 can refer to a documentPage of 17. */ public class PDFView extends RelativeLayout { @@ -101,64 +114,20 @@ enum ScrollDir { private ScrollDir scrollDir = ScrollDir.NONE; - /** - * Rendered parts go to the cache manager - */ + /** Rendered parts go to the cache manager */ CacheManager cacheManager; - /** - * Animation manager manage all offset and zoom animation - */ + /** Animation manager manage all offset and zoom animation */ private AnimationManager animationManager; - /** - * Drag manager manage all touch events - */ + /** Drag manager manage all touch events */ private DragPinchManager dragPinchManager; - /** - * The pages the user want to display in order - * (ex: 0, 2, 2, 8, 8, 1, 1, 1) - */ - private int[] originalUserPages; - - /** - * The same pages but with a filter to avoid repetition - * (ex: 0, 2, 8, 1) - */ - private int[] filteredUserPages; - - /** - * The same pages but with a filter to avoid repetition - * (ex: 0, 1, 1, 2, 2, 3, 3, 3) - */ - private int[] filteredUserPageIndexes; - - /** - * Number of pages in the loaded PDF document - */ - private int documentPageCount; + PdfFile pdfFile; - /** - * The index of the current sequence - */ + /** The index of the current sequence */ private int currentPage; - /** - * The index of the current sequence - */ - private int currentFilteredPage; - - /** - * The actual width and height of the pages in the PDF document - */ - private int pageWidth, pageHeight; - - /** - * The optimal width and height of the pages to fit the component size - */ - private float optimalPageWidth, optimalPageHeight; - /** * If you picture all the pages side by side in their optimal width, * and taking into account the zoom level, the current offset is the @@ -173,95 +142,54 @@ enum ScrollDir { */ private float currentYOffset = 0; - /** - * The zoom level, always >= 1 - */ + /** The zoom level, always >= 1 */ private float zoom = 1f; - /** - * True if the PDFView has been recycled - */ + /** True if the PDFView has been recycled */ private boolean recycled = true; - /** - * Current state of the view - */ + /** Current state of the view */ private State state = State.DEFAULT; - /** - * Async task used during the loading phase to decode a PDF document - */ + /** Async task used during the loading phase to decode a PDF document */ private DecodingAsyncTask decodingAsyncTask; - /** - * The thread {@link #renderingHandler} will run on - */ - private final HandlerThread renderingHandlerThread; - /** - * Handler always waiting in the background and rendering tasks - */ + /** The thread {@link #renderingHandler} will run on */ + private HandlerThread renderingHandlerThread; + /** Handler always waiting in the background and rendering tasks */ RenderingHandler renderingHandler; private PagesLoader pagesLoader; - /** - * Call back object to call when the PDF is loaded - */ - private OnLoadCompleteListener onLoadCompleteListener; + Callbacks callbacks = new Callbacks(); - private OnErrorListener onErrorListener; + /** Paint object for drawing */ + private Paint paint; - /** - * Call back object to call when the page has changed - */ - private OnPageChangeListener onPageChangeListener; + /** Paint object for drawing debug stuff */ + private Paint debugPaint; - /** - * Call back object to call when the page is scrolled - */ - private OnPageScrollListener onPageScrollListener; + /** Policy for fitting pages to screen */ + private FitPolicy pageFitPolicy = FitPolicy.WIDTH; - /** - * Call back object to call when the above layer is to drawn - */ - private OnDrawListener onDrawListener; + private boolean fitEachPage = false; - private OnDrawListener onDrawAllListener; + private int defaultPage = 0; - /** - * Call back object to call when the document is initially rendered - */ - private OnRenderListener onRenderListener; + /** True if should scroll through pages vertically instead of horizontally */ + private boolean swipeVertical = true; - /** - * Call back object to call when the user does a tap gesture - */ - private OnTapListener onTapListener; + private boolean enableSwipe = true; - /** - * Paint object for drawing - */ - private Paint paint; + private boolean doubletapEnabled = true; - /** - * Paint object for drawing debug stuff - */ - private Paint debugPaint; - - private int defaultPage = 0; + private boolean nightMode = false; - /** - * True if should scroll through pages vertically instead of horizontally - */ - private boolean swipeVertical = true; + private boolean pageSnap = true; - /** - * Pdfium core for loading and rendering PDFs - */ + /** Pdfium core for loading and rendering PDFs */ private PdfiumCore pdfiumCore; - private PdfDocument pdfDocument; - private ScrollHandle scrollHandle; private boolean isScrollHandleInit = false; @@ -290,26 +218,30 @@ ScrollHandle getScrollHandle() { */ private boolean renderDuringScale = false; - /** - * Antialiasing and bitmap filtering - */ + /** Antialiasing and bitmap filtering */ private boolean enableAntialiasing = true; private PaintFlagsDrawFilter antialiasFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); - /** - * Spacing between pages, in DP - */ + /** Spacing between pages, in px */ private int spacingPx = 0; - /** - * pages numbers used when calling onDrawAllListener - */ + /** Add dynamic spacing to fit each page separately on the screen. */ + private boolean autoSpacing = false; + + /** Fling a single page at a time */ + private boolean pageFling = true; + + /** Pages numbers used when calling onDrawAllListener */ private List onDrawPagesNums = new ArrayList<>(10); - /** - * Construct the initial view - */ + /** Holds info whether view has been added to layout and has width and height */ + private boolean hasSize = false; + + /** Holds last used Configurator that should be loaded when view has size */ + private Configurator waitingDocumentConfigurator; + + /** Construct the initial view */ public PDFView(Context context, AttributeSet set) { super(context, set); @@ -322,6 +254,7 @@ public PDFView(Context context, AttributeSet set) { cacheManager = new CacheManager(); animationManager = new AnimationManager(this); dragPinchManager = new DragPinchManager(this, animationManager); + pagesLoader = new PagesLoader(this); paint = new Paint(); debugPaint = new Paint(); @@ -331,34 +264,19 @@ public PDFView(Context context, AttributeSet set) { setWillNotDraw(false); } - private void load(DocumentSource docSource, String password, OnLoadCompleteListener listener, OnErrorListener onErrorListener) { - load(docSource, password, listener, onErrorListener, null); + private void load(DocumentSource docSource, String password) { + load(docSource, password, null); } - private void load(DocumentSource docSource, String password, OnLoadCompleteListener onLoadCompleteListener, OnErrorListener onErrorListener, int[] userPages) { + private void load(DocumentSource docSource, String password, int[] userPages) { if (!recycled) { throw new IllegalStateException("Don't call load on a PDF View without recycling it first."); } - // Manage UserPages if not null - if (userPages != null) { - this.originalUserPages = userPages; - this.filteredUserPages = ArrayUtils.deleteDuplicatedPages(originalUserPages); - this.filteredUserPageIndexes = ArrayUtils.calculateIndexesInDuplicateArray(originalUserPages); - } - - this.onLoadCompleteListener = onLoadCompleteListener; - this.onErrorListener = onErrorListener; - - int firstPageIdx = 0; - if (originalUserPages != null) { - firstPageIdx = originalUserPages[0]; - } - recycled = false; // Start decoding document - decodingAsyncTask = new DecodingAsyncTask(docSource, password, this, pdfiumCore, firstPageIdx); + decodingAsyncTask = new DecodingAsyncTask(docSource, password, userPages, this, pdfiumCore); decodingAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @@ -368,7 +286,12 @@ private void load(DocumentSource docSource, String password, OnLoadCompleteListe * @param page Page index. */ public void jumpTo(int page, boolean withAnimation) { - float offset = -calculatePageOffset(page); + if (pdfFile == null) { + return; + } + + page = pdfFile.determineValidPageNumberFrom(page); + float offset = page == 0 ? 0 : -pdfFile.getPageOffset(page, zoom); if (swipeVertical) { if (withAnimation) { animationManager.startYAnimation(currentYOffset, offset); @@ -396,15 +319,8 @@ void showPage(int pageNb) { // Check the page number and makes the // difference between UserPages and DocumentPages - pageNb = determineValidPageNumberFrom(pageNb); + pageNb = pdfFile.determineValidPageNumberFrom(pageNb); currentPage = pageNb; - currentFilteredPage = pageNb; - if (filteredUserPageIndexes != null) { - if (pageNb >= 0 && pageNb < filteredUserPageIndexes.length) { - pageNb = filteredUserPageIndexes[pageNb]; - currentFilteredPage = pageNb; - } - } loadPages(); @@ -412,9 +328,7 @@ void showPage(int pageNb) { scrollHandle.setPageNum(currentPage + 1); } - if (onPageChangeListener != null) { - onPageChangeListener.onPageChanged(currentPage, getPageCount()); - } + callbacks.callOnPageChange(currentPage, pdfFile.getPagesCount()); } /** @@ -426,9 +340,9 @@ void showPage(int pageNb) { public float getPositionOffset() { float offset; if (swipeVertical) { - offset = -currentYOffset / (calculateDocLength() - getHeight()); + offset = -currentYOffset / (pdfFile.getDocLen(zoom) - getHeight()); } else { - offset = -currentXOffset / (calculateDocLength() - getWidth()); + offset = -currentXOffset / (pdfFile.getDocLen(zoom) - getWidth()); } return MathUtils.limit(offset, 0, 1); } @@ -440,9 +354,9 @@ public float getPositionOffset() { */ public void setPositionOffset(float progress, boolean moveHandle) { if (swipeVertical) { - moveTo(currentXOffset, (-calculateDocLength() + getHeight()) * progress, moveHandle); + moveTo(currentXOffset, (-pdfFile.getDocLen(zoom) + getHeight()) * progress, moveHandle); } else { - moveTo((-calculateDocLength() + getWidth()) * progress, currentYOffset, moveHandle); + moveTo((-pdfFile.getDocLen(zoom) + getWidth()) * progress, currentYOffset, moveHandle); } loadPageByOffset(); } @@ -451,85 +365,57 @@ public void setPositionOffset(float progress) { setPositionOffset(progress, true); } - private float calculatePageOffset(int page) { - if (swipeVertical) { - return toCurrentScale(page * optimalPageHeight + page * spacingPx); - } else { - return toCurrentScale(page * optimalPageWidth + page * spacingPx); - } - } - - float calculateDocLength() { - int pageCount = getPageCount(); - if (swipeVertical) { - return toCurrentScale(pageCount * optimalPageHeight + (pageCount - 1) * spacingPx); - } else { - return toCurrentScale(pageCount * optimalPageWidth + (pageCount - 1) * spacingPx); - } - } - public void stopFling() { animationManager.stopFling(); } public int getPageCount() { - if (originalUserPages != null) { - return originalUserPages.length; + if (pdfFile == null) { + return 0; } - return documentPageCount; - } - - public void enableSwipe(boolean enableSwipe) { - dragPinchManager.setSwipeEnabled(enableSwipe); - } - - public void enableDoubletap(boolean enableDoubletap) { - this.dragPinchManager.enableDoubletap(enableDoubletap); - } - - private void setOnPageChangeListener(OnPageChangeListener onPageChangeListener) { - this.onPageChangeListener = onPageChangeListener; - } - - OnPageChangeListener getOnPageChangeListener() { - return this.onPageChangeListener; - } - - private void setOnPageScrollListener(OnPageScrollListener onPageScrollListener) { - this.onPageScrollListener = onPageScrollListener; - } - - OnPageScrollListener getOnPageScrollListener() { - return this.onPageScrollListener; + return pdfFile.getPagesCount(); } - private void setOnRenderListener(OnRenderListener onRenderListener) { - this.onRenderListener = onRenderListener; + public void setSwipeEnabled(boolean enableSwipe) { + this.enableSwipe = enableSwipe; } - OnRenderListener getOnRenderListener() { - return this.onRenderListener; - } + public void setNightMode(boolean nightMode) { + this.nightMode = nightMode; + if (nightMode) { + ColorMatrix colorMatrixInverted = + new ColorMatrix(new float[]{ + -1, 0, 0, 0, 255, + 0, -1, 0, 0, 255, + 0, 0, -1, 0, 255, + 0, 0, 0, 1, 0}); - private void setOnTapListener(OnTapListener onTapListener) { - this.onTapListener = onTapListener; + ColorMatrixColorFilter filter = new ColorMatrixColorFilter(colorMatrixInverted); + paint.setColorFilter(filter); + } else { + paint.setColorFilter(null); + } } - OnTapListener getOnTapListener() { - return this.onTapListener; + void enableDoubletap(boolean enableDoubletap) { + this.doubletapEnabled = enableDoubletap; } - private void setOnDrawListener(OnDrawListener onDrawListener) { - this.onDrawListener = onDrawListener; + boolean isDoubletapEnabled() { + return doubletapEnabled; } - private void setOnDrawAllListener(OnDrawListener onDrawAllListener) { - this.onDrawAllListener = onDrawAllListener; + void onPageError(PageRenderingException ex) { + if (!callbacks.callOnPageError(ex.getPage(), ex.getCause())) { + Log.e(TAG, "Cannot open page " + ex.getPage(), ex.getCause()); + } } public void recycle() { + waitingDocumentConfigurator = null; animationManager.stopAll(); + dragPinchManager.disable(); // Stop tasks if (renderingHandler != null) { @@ -547,20 +433,18 @@ public void recycle() { scrollHandle.destroyLayout(); } - if (pdfiumCore != null && pdfDocument != null) { - pdfiumCore.closeDocument(pdfDocument); + if (pdfFile != null) { + pdfFile.dispose(); + pdfFile = null; } renderingHandler = null; - originalUserPages = null; - filteredUserPages = null; - filteredUserPageIndexes = null; - pdfDocument = null; scrollHandle = null; isScrollHandleInit = false; currentXOffset = currentYOffset = 0; zoom = 1f; recycled = true; + callbacks = new Callbacks(); state = State.DEFAULT; } @@ -568,36 +452,113 @@ public boolean isRecycled() { return recycled; } - /** - * Handle fling animation - */ + /** Handle fling animation */ @Override public void computeScroll() { super.computeScroll(); + if (isInEditMode()) { + return; + } animationManager.computeFling(); } @Override protected void onDetachedFromWindow() { recycle(); + if (renderingHandlerThread != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + renderingHandlerThread.quitSafely(); + } else { + renderingHandlerThread.quit(); + } + renderingHandlerThread = null; + } super.onDetachedFromWindow(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { + hasSize = true; + if (waitingDocumentConfigurator != null) { + waitingDocumentConfigurator.load(); + } if (isInEditMode() || state != State.SHOWN) { return; } + + // calculates the position of the point which in the center of view relative to big strip + float centerPointInStripXOffset = -currentXOffset + oldw * 0.5f; + float centerPointInStripYOffset = -currentYOffset + oldh * 0.5f; + + float relativeCenterPointInStripXOffset; + float relativeCenterPointInStripYOffset; + + if (swipeVertical){ + relativeCenterPointInStripXOffset = centerPointInStripXOffset / pdfFile.getMaxPageWidth(); + relativeCenterPointInStripYOffset = centerPointInStripYOffset / pdfFile.getDocLen(zoom); + }else { + relativeCenterPointInStripXOffset = centerPointInStripXOffset / pdfFile.getDocLen(zoom); + relativeCenterPointInStripYOffset = centerPointInStripYOffset / pdfFile.getMaxPageHeight(); + } + animationManager.stopAll(); - calculateOptimalWidthAndHeight(); + pdfFile.recalculatePageSizes(new Size(w, h)); + if (swipeVertical) { - moveTo(currentXOffset, -calculatePageOffset(currentPage)); - } else { - moveTo(-calculatePageOffset(currentPage), currentYOffset); + currentXOffset = -relativeCenterPointInStripXOffset * pdfFile.getMaxPageWidth() + w * 0.5f; + currentYOffset = -relativeCenterPointInStripYOffset * pdfFile.getDocLen(zoom) + h * 0.5f ; + }else { + currentXOffset = -relativeCenterPointInStripXOffset * pdfFile.getDocLen(zoom) + w * 0.5f; + currentYOffset = -relativeCenterPointInStripYOffset * pdfFile.getMaxPageHeight() + h * 0.5f; } + moveTo(currentXOffset,currentYOffset); loadPageByOffset(); } + @Override + public boolean canScrollHorizontally(int direction) { + if (pdfFile == null) { + return true; + } + + if (swipeVertical) { + if (direction < 0 && currentXOffset < 0) { + return true; + } else if (direction > 0 && currentXOffset + toCurrentScale(pdfFile.getMaxPageWidth()) > getWidth()) { + return true; + } + } else { + if (direction < 0 && currentXOffset < 0) { + return true; + } else if (direction > 0 && currentXOffset + pdfFile.getDocLen(zoom) > getWidth()) { + return true; + } + } + return false; + } + + @Override + public boolean canScrollVertically(int direction) { + if (pdfFile == null) { + return true; + } + + if (swipeVertical) { + if (direction < 0 && currentYOffset < 0) { + return true; + } else if (direction > 0 && currentYOffset + pdfFile.getDocLen(zoom) > getHeight()) { + return true; + } + } else { + if (direction < 0 && currentYOffset < 0) { + return true; + } else if (direction > 0 && currentYOffset + toCurrentScale(pdfFile.getMaxPageHeight()) > getHeight()) { + return true; + } + } + return false; + } + @Override protected void onDraw(Canvas canvas) { if (isInEditMode()) { @@ -640,7 +601,7 @@ protected void onDraw(Canvas canvas) { Drawable bg = getBackground(); if (bg == null) { - canvas.drawColor(Color.WHITE); + canvas.drawColor(nightMode ? Color.BLACK : Color.WHITE); } else { bg.draw(canvas); } @@ -667,17 +628,18 @@ protected void onDraw(Canvas canvas) { // Draws parts for (PagePart part : cacheManager.getPageParts()) { drawPart(canvas, part); - if (onDrawAllListener != null && !onDrawPagesNums.contains(part.getUserPage())) { - onDrawPagesNums.add(part.getUserPage()); + if (callbacks.getOnDrawAll() != null + && !onDrawPagesNums.contains(part.getPage())) { + onDrawPagesNums.add(part.getPage()); } } for (Integer page : onDrawPagesNums) { - drawWithListener(canvas, page, onDrawAllListener); + drawWithListener(canvas, page, callbacks.getOnDrawAll()); } onDrawPagesNums.clear(); - drawWithListener(canvas, currentPage, onDrawListener); + drawWithListener(canvas, currentPage, callbacks.getOnDraw()); // Restores the canvas position canvas.translate(-currentXOffset, -currentYOffset); @@ -688,25 +650,24 @@ private void drawWithListener(Canvas canvas, int page, OnDrawListener listener) float translateX, translateY; if (swipeVertical) { translateX = 0; - translateY = calculatePageOffset(page); + translateY = pdfFile.getPageOffset(page, zoom); } else { translateY = 0; - translateX = calculatePageOffset(page); + translateX = pdfFile.getPageOffset(page, zoom); } canvas.translate(translateX, translateY); + SizeF size = pdfFile.getPageSize(page); listener.onLayerDrawn(canvas, - toCurrentScale(optimalPageWidth), - toCurrentScale(optimalPageHeight), + toCurrentScale(size.getWidth()), + toCurrentScale(size.getHeight()), page); canvas.translate(-translateX, -translateY); } } - /** - * Draw a given PagePart on the canvas - */ + /** Draw a given PagePart on the canvas */ private void drawPart(Canvas canvas, PagePart part) { // Can seem strange, but avoid lot of calls RectF pageRelativeBounds = part.getPageRelativeBounds(); @@ -719,20 +680,26 @@ private void drawPart(Canvas canvas, PagePart part) { // Move to the target page float localTranslationX = 0; float localTranslationY = 0; + SizeF size = pdfFile.getPageSize(part.getPage()); + if (swipeVertical) { - localTranslationY = calculatePageOffset(part.getUserPage()); + localTranslationY = pdfFile.getPageOffset(part.getPage(), zoom); + float maxWidth = pdfFile.getMaxPageWidth(); + localTranslationX = toCurrentScale(maxWidth - size.getWidth()) / 2; } else { - localTranslationX = calculatePageOffset(part.getUserPage()); + localTranslationX = pdfFile.getPageOffset(part.getPage(), zoom); + float maxHeight = pdfFile.getMaxPageHeight(); + localTranslationY = toCurrentScale(maxHeight - size.getHeight()) / 2; } canvas.translate(localTranslationX, localTranslationY); Rect srcRect = new Rect(0, 0, renderedBitmap.getWidth(), renderedBitmap.getHeight()); - float offsetX = toCurrentScale(pageRelativeBounds.left * optimalPageWidth); - float offsetY = toCurrentScale(pageRelativeBounds.top * optimalPageHeight); - float width = toCurrentScale(pageRelativeBounds.width() * optimalPageWidth); - float height = toCurrentScale(pageRelativeBounds.height() * optimalPageHeight); + float offsetX = toCurrentScale(pageRelativeBounds.left * size.getWidth()); + float offsetY = toCurrentScale(pageRelativeBounds.top * size.getHeight()); + float width = toCurrentScale(pageRelativeBounds.width() * size.getWidth()); + float height = toCurrentScale(pageRelativeBounds.height() * size.getHeight()); // If we use float values for this rectangle, there will be // a possible gap between page parts, especially when @@ -753,7 +720,7 @@ private void drawPart(Canvas canvas, PagePart part) { canvas.drawBitmap(renderedBitmap, srcRect, dstRect, paint); if (Constants.DEBUG_MODE) { - debugPaint.setColor(part.getUserPage() % 2 == 0 ? Color.RED : Color.BLUE); + debugPaint.setColor(part.getPage() % 2 == 0 ? Color.RED : Color.BLUE); canvas.drawRect(dstRect, debugPaint); } @@ -768,7 +735,7 @@ private void drawPart(Canvas canvas, PagePart part) { * the current page displayed */ public void loadPages() { - if (optimalPageWidth == 0 || optimalPageHeight == 0 || renderingHandler == null) { + if (pdfFile == null || renderingHandler == null) { return; } @@ -780,26 +747,16 @@ public void loadPages() { redraw(); } - /** - * Called when the PDF is loaded - */ - void loadComplete(PdfDocument pdfDocument, int pageWidth, int pageHeight) { + /** Called when the PDF is loaded */ + void loadComplete(PdfFile pdfFile) { state = State.LOADED; - this.documentPageCount = pdfiumCore.getPageCount(pdfDocument); - - this.pdfDocument = pdfDocument; - this.pageWidth = pageWidth; - this.pageHeight = pageHeight; - calculateOptimalWidthAndHeight(); - - pagesLoader = new PagesLoader(this); + this.pdfFile = pdfFile; if (!renderingHandlerThread.isAlive()) { renderingHandlerThread.start(); } - renderingHandler = new RenderingHandler(renderingHandlerThread.getLooper(), - this, pdfiumCore, pdfDocument); + renderingHandler = new RenderingHandler(renderingHandlerThread.getLooper(), this); renderingHandler.start(); if (scrollHandle != null) { @@ -807,19 +764,21 @@ void loadComplete(PdfDocument pdfDocument, int pageWidth, int pageHeight) { isScrollHandleInit = true; } - if (onLoadCompleteListener != null) { - onLoadCompleteListener.loadComplete(documentPageCount); - } + dragPinchManager.enable(); + + callbacks.callOnLoadComplete(pdfFile.getPagesCount()); jumpTo(defaultPage, false); } void loadError(Throwable t) { state = State.ERROR; + // store reference, because callbacks will be cleared in recycle() method + OnErrorListener onErrorListener = callbacks.getOnError(); recycle(); invalidate(); - if (this.onErrorListener != null) { - this.onErrorListener.onError(t); + if (onErrorListener != null) { + onErrorListener.onError(t); } else { Log.e("PDFView", "load pdf error", t); } @@ -839,9 +798,7 @@ public void onBitmapRendered(PagePart part) { // when it is first rendered part if (state == State.LOADED) { state = State.SHOWN; - if (onRenderListener != null) { - onRenderListener.onInitiallyRendered(getPageCount(), optimalPageWidth, optimalPageHeight); - } + callbacks.callOnRender(pdfFile.getPagesCount()); } if (part.isThumbnail()) { @@ -852,74 +809,6 @@ public void onBitmapRendered(PagePart part) { redraw(); } - /** - * Given the UserPage number, this method restrict it - * to be sure it's an existing page. It takes care of - * using the user defined pages if any. - * - * @param userPage A page number. - * @return A restricted valid page number (example : -2 => 0) - */ - private int determineValidPageNumberFrom(int userPage) { - if (userPage <= 0) { - return 0; - } - if (originalUserPages != null) { - if (userPage >= originalUserPages.length) { - return originalUserPages.length - 1; - } - } else { - if (userPage >= documentPageCount) { - return documentPageCount - 1; - } - } - return userPage; - } - - /** - * Calculate the x/y-offset needed to have the given - * page centered on the screen. It doesn't take into - * account the zoom level. - * - * @param pageNb The page number. - * @return The x/y-offset to use to have the pageNb centered. - */ - private float calculateCenterOffsetForPage(int pageNb) { - if (swipeVertical) { - float imageY = -(pageNb * optimalPageHeight + pageNb * spacingPx); - imageY += getHeight() / 2 - optimalPageHeight / 2; - return imageY; - } else { - float imageX = -(pageNb * optimalPageWidth + pageNb * spacingPx); - imageX += getWidth() / 2 - optimalPageWidth / 2; - return imageX; - } - } - - /** - * Calculate the optimal width and height of a page - * considering the area width and height - */ - private void calculateOptimalWidthAndHeight() { - if (state == State.DEFAULT || getWidth() == 0) { - return; - } - - float maxWidth = getWidth(), maxHeight = getHeight(); - float w = pageWidth, h = pageHeight; - float ratio = w / h; - w = maxWidth; - h = (float) Math.floor(maxWidth / ratio); - if (h > maxHeight) { - h = maxHeight; - w = (float) Math.floor(maxHeight * ratio); - } - - optimalPageWidth = w; - optimalPageHeight = h; - - } - public void moveTo(float offsetX, float offsetY) { moveTo(offsetX, offsetY, true); } @@ -935,7 +824,7 @@ public void moveTo(float offsetX, float offsetY) { public void moveTo(float offsetX, float offsetY, boolean moveHandle) { if (swipeVertical) { // Check X offset - float scaledPageWidth = toCurrentScale(optimalPageWidth); + float scaledPageWidth = toCurrentScale(pdfFile.getMaxPageWidth()); if (scaledPageWidth < getWidth()) { offsetX = getWidth() / 2 - scaledPageWidth / 2; } else { @@ -947,7 +836,7 @@ public void moveTo(float offsetX, float offsetY, boolean moveHandle) { } // Check Y offset - float contentHeight = calculateDocLength(); + float contentHeight = pdfFile.getDocLen(zoom); if (contentHeight < getHeight()) { // whole document height visible on screen offsetY = (getHeight() - contentHeight) / 2; } else { @@ -967,7 +856,7 @@ public void moveTo(float offsetX, float offsetY, boolean moveHandle) { } } else { // Check Y offset - float scaledPageHeight = toCurrentScale(optimalPageHeight); + float scaledPageHeight = toCurrentScale(pdfFile.getMaxPageHeight()); if (scaledPageHeight < getHeight()) { offsetY = getHeight() / 2 - scaledPageHeight / 2; } else { @@ -979,7 +868,7 @@ public void moveTo(float offsetX, float offsetY, boolean moveHandle) { } // Check X offset - float contentWidth = calculateDocLength(); + float contentWidth = pdfFile.getDocLen(zoom); if (contentWidth < getWidth()) { // whole document width visible on screen offsetX = (getWidth() - contentWidth) / 2; } else { @@ -1007,57 +896,120 @@ public void moveTo(float offsetX, float offsetY, boolean moveHandle) { scrollHandle.setScroll(positionOffset); } - if (onPageScrollListener != null) { - onPageScrollListener.onPageScrolled(getCurrentPage(), positionOffset); - } + callbacks.callOnPageScroll(getCurrentPage(), positionOffset); redraw(); } - ScrollDir getScrollDir() { - return scrollDir; - } - void loadPageByOffset() { - if (0 == getPageCount()) { + if (0 == pdfFile.getPagesCount()) { return; } - float offset, optimal, screenCenter; - float spacingPerPage = spacingPx - (spacingPx / getPageCount()); + float offset, screenCenter; if (swipeVertical) { offset = currentYOffset; - optimal = optimalPageHeight + spacingPerPage; screenCenter = ((float) getHeight()) / 2; } else { offset = currentXOffset; - optimal = optimalPageWidth + spacingPerPage; screenCenter = ((float) getWidth()) / 2; } - int page = (int) Math.floor((Math.abs(offset) + screenCenter) / toCurrentScale(optimal)); + int page = pdfFile.getPageAtOffset(-(offset - screenCenter), zoom); - if (page >= 0 && page <= getPageCount() - 1 && page != getCurrentPage()) { + if (page >= 0 && page <= pdfFile.getPagesCount() - 1 && page != getCurrentPage()) { showPage(page); } else { loadPages(); } } - int[] getFilteredUserPages() { - return filteredUserPages; + /** + * Animate to the nearest snapping position for the current SnapPolicy + */ + public void performPageSnap() { + if (!pageSnap || pdfFile == null || pdfFile.getPagesCount() == 0) { + return; + } + int centerPage = findFocusPage(currentXOffset, currentYOffset); + SnapEdge edge = findSnapEdge(centerPage); + if (edge == SnapEdge.NONE) { + return; + } + + float offset = snapOffsetForPage(centerPage, edge); + if (swipeVertical) { + animationManager.startYAnimation(currentYOffset, -offset); + } else { + animationManager.startXAnimation(currentXOffset, -offset); + } } - int[] getOriginalUserPages() { - return originalUserPages; + /** + * Find the edge to snap to when showing the specified page + */ + SnapEdge findSnapEdge(int page) { + if (!pageSnap || page < 0) { + return SnapEdge.NONE; + } + float currentOffset = swipeVertical ? currentYOffset : currentXOffset; + float offset = -pdfFile.getPageOffset(page, zoom); + int length = swipeVertical ? getHeight() : getWidth(); + float pageLength = pdfFile.getPageLength(page, zoom); + + if (length >= pageLength) { + return SnapEdge.CENTER; + } else if (currentOffset >= offset) { + return SnapEdge.START; + } else if (offset - pageLength > currentOffset - length) { + return SnapEdge.END; + } else { + return SnapEdge.NONE; + } } - int[] getFilteredUserPageIndexes() { - return filteredUserPageIndexes; + /** + * Get the offset to move to in order to snap to the page + */ + float snapOffsetForPage(int pageIndex, SnapEdge edge) { + float offset = pdfFile.getPageOffset(pageIndex, zoom); + + float length = swipeVertical ? getHeight() : getWidth(); + float pageLength = pdfFile.getPageLength(pageIndex, zoom); + + if (edge == SnapEdge.CENTER) { + offset = offset - length / 2f + pageLength / 2f; + } else if (edge == SnapEdge.END) { + offset = offset - length + pageLength; + } + return offset; } - int getDocumentPageCount() { - return documentPageCount; + int findFocusPage(float xOffset, float yOffset) { + float currOffset = swipeVertical ? yOffset : xOffset; + float length = swipeVertical ? getHeight() : getWidth(); + // make sure first and last page can be found + if (currOffset > -1) { + return 0; + } else if (currOffset < -pdfFile.getDocLen(zoom) + length + 1) { + return pdfFile.getPagesCount() - 1; + } + // else find page in center + float center = currOffset - length / 2f; + return pdfFile.getPageAtOffset(-center, zoom); + } + + /** + * @return true if single page fills the entire screen in the scrolling direction + */ + public boolean pageFillsScreen() { + float start = -pdfFile.getPageOffset(currentPage, zoom); + float end = start - pdfFile.getPageLength(currentPage, zoom); + if (isSwipeVertical()) { + return start > currentYOffset && end < currentYOffset - getHeight(); + } else { + return start > currentXOffset && end < currentXOffset - getWidth(); + } } /** @@ -1109,12 +1061,11 @@ public void zoomCenteredRelativeTo(float dzoom, PointF pivot) { * @return true if whole document can displayed at once, false otherwise */ public boolean documentFitsView() { - int pageCount = getPageCount(); - int spacing = (pageCount - 1) * spacingPx; + float len = pdfFile.getDocLen(1); if (swipeVertical) { - return pageCount * optimalPageHeight + spacing < getHeight(); + return len < getHeight(); } else { - return pageCount * optimalPageWidth + spacing < getWidth(); + return len < getWidth(); } } @@ -1123,17 +1074,15 @@ public void fitToWidth(int page) { Log.e(TAG, "Cannot fit, document not rendered yet"); return; } - fitToWidth(); + zoomTo(getWidth() / pdfFile.getPageSize(page).getWidth()); jumpTo(page); } - public void fitToWidth() { - if (state != State.SHOWN) { - Log.e(TAG, "Cannot fit, document not rendered yet"); - return; + public SizeF getPageSize(int pageIndex) { + if (pdfFile == null) { + return new SizeF(0, 0); } - zoomTo(getWidth() / optimalPageWidth); - setPositionOffset(0); + return pdfFile.getPageSize(pageIndex); } public int getCurrentPage() { @@ -1164,14 +1113,6 @@ public boolean isZooming() { return zoom != minZoom; } - public float getOptimalPageWidth() { - return optimalPageWidth; - } - - public float getOptimalPageHeight() { - return optimalPageHeight; - } - private void setDefaultPage(int defaultPage) { this.defaultPage = defaultPage; } @@ -1203,8 +1144,7 @@ private void setScrollHandle(ScrollHandle scrollHandle) { * @return page number at given offset, starting from 0 */ public int getPageAtPositionOffset(float positionOffset) { - int page = (int) Math.floor(getPageCount() * positionOffset); - return page == getPageCount() ? page - 1 : page; + return pdfFile.getPageAtOffset(pdfFile.getDocLen(zoom) * positionOffset, zoom); } public float getMinZoom() { @@ -1243,7 +1183,11 @@ public boolean isSwipeVertical() { return swipeVertical; } - public void setSwipeVertical(boolean swipeVertical) { + public boolean isSwipeEnabled() { + return enableSwipe; + } + + private void setSwipeVertical(boolean swipeVertical) { this.swipeVertical = swipeVertical; } @@ -1267,70 +1211,108 @@ public void enableAntialiasing(boolean enableAntialiasing) { this.enableAntialiasing = enableAntialiasing; } - int getSpacingPx() { + public int getSpacingPx() { return spacingPx; } - private void setSpacing(int spacing) { - this.spacingPx = Util.getDP(getContext(), spacing); + public boolean isAutoSpacingEnabled() { + return autoSpacing; + } + + public void setPageFling(boolean pageFling) { + this.pageFling = pageFling; + } + + public boolean isPageFlingEnabled() { + return pageFling; + } + + private void setSpacing(int spacingDp) { + this.spacingPx = Util.getDP(getContext(), spacingDp); + } + + private void setAutoSpacing(boolean autoSpacing) { + this.autoSpacing = autoSpacing; + } + + private void setPageFitPolicy(FitPolicy pageFitPolicy) { + this.pageFitPolicy = pageFitPolicy; + } + + public FitPolicy getPageFitPolicy() { + return pageFitPolicy; + } + + private void setFitEachPage(boolean fitEachPage) { + this.fitEachPage = fitEachPage; + } + + public boolean isFitEachPage() { + return fitEachPage; + } + + public boolean isPageSnap() { + return pageSnap; + } + + public void setPageSnap(boolean pageSnap) { + this.pageSnap = pageSnap; } public boolean doRenderDuringScale() { return renderDuringScale; } + /** Returns null if document is not loaded */ public PdfDocument.Meta getDocumentMeta() { - if (pdfDocument == null) { + if (pdfFile == null) { return null; } - return pdfiumCore.getDocumentMeta(pdfDocument); + return pdfFile.getMetaData(); } + /** Will be empty until document is loaded */ public List getTableOfContents() { - if (pdfDocument == null) { - return new ArrayList<>(); + if (pdfFile == null) { + return Collections.emptyList(); } - return pdfiumCore.getTableOfContents(pdfDocument); + return pdfFile.getBookmarks(); } - /** - * Use an asset file as the pdf source - */ + /** Will be empty until document is loaded */ + public List getLinks(int page) { + if (pdfFile == null) { + return Collections.emptyList(); + } + return pdfFile.getPageLinks(page); + } + + /** Use an asset file as the pdf source */ public Configurator fromAsset(String assetName) { return new Configurator(new AssetSource(assetName)); } - /** - * Use a file as the pdf source - */ + /** Use a file as the pdf source */ public Configurator fromFile(File file) { return new Configurator(new FileSource(file)); } - /** - * Use URI as the pdf source, for use with content providers - */ + /** Use URI as the pdf source, for use with content providers */ public Configurator fromUri(Uri uri) { return new Configurator(new UriSource(uri)); } - /** - * Use bytearray as the pdf source, documents is not saved - * - * @param bytes - * @return - */ + /** Use bytearray as the pdf source, documents is not saved */ public Configurator fromBytes(byte[] bytes) { return new Configurator(new ByteArraySource(bytes)); } + /** Use stream as the pdf source. Stream will be written to bytearray, because native code does not support Java Streams */ public Configurator fromStream(InputStream stream) { return new Configurator(new InputStreamSource(stream)); } - /** - * Use custom source as pdf source - */ + /** Use custom source as pdf source */ public Configurator fromSource(DocumentSource docSource) { return new Configurator(docSource); } @@ -1363,6 +1345,12 @@ public class Configurator { private OnTapListener onTapListener; + private OnLongPressListener onLongPressListener; + + private OnPageErrorListener onPageErrorListener; + + private LinkHandler linkHandler = new DefaultLinkHandler(PDFView.this); + private int defaultPage = 0; private boolean swipeHorizontal = false; @@ -1377,6 +1365,18 @@ public class Configurator { private int spacing = 0; + private boolean autoSpacing = false; + + private FitPolicy pageFitPolicy = FitPolicy.WIDTH; + + private boolean fitEachPage = false; + + private boolean pageFling = false; + + private boolean pageSnap = false; + + private boolean nightMode = false; + private Configurator(DocumentSource documentSource) { this.documentSource = documentSource; } @@ -1426,6 +1426,11 @@ public Configurator onError(OnErrorListener onErrorListener) { return this; } + public Configurator onPageError(OnPageErrorListener onPageErrorListener) { + this.onPageErrorListener = onPageErrorListener; + return this; + } + public Configurator onPageChange(OnPageChangeListener onPageChangeListener) { this.onPageChangeListener = onPageChangeListener; return this; @@ -1441,6 +1446,16 @@ public Configurator onTap(OnTapListener onTapListener) { return this; } + public Configurator onLongPress(OnLongPressListener onLongPressListener) { + this.onLongPressListener = onLongPressListener; + return this; + } + + public Configurator linkHandler(LinkHandler linkHandler) { + this.linkHandler = linkHandler; + return this; + } + public Configurator defaultPage(int defaultPage) { this.defaultPage = defaultPage; return this; @@ -1471,15 +1486,60 @@ public Configurator spacing(int spacing) { return this; } + public Configurator autoSpacing(boolean autoSpacing) { + this.autoSpacing = autoSpacing; + return this; + } + + public Configurator pageFitPolicy(FitPolicy pageFitPolicy) { + this.pageFitPolicy = pageFitPolicy; + return this; + } + + public Configurator fitEachPage(boolean fitEachPage) { + this.fitEachPage = fitEachPage; + return this; + } + + public Configurator pageSnap(boolean pageSnap) { + this.pageSnap = pageSnap; + return this; + } + + public Configurator pageFling(boolean pageFling) { + this.pageFling = pageFling; + return this; + } + + public Configurator nightMode(boolean nightMode) { + this.nightMode = nightMode; + return this; + } + + public Configurator disableLongpress() { + PDFView.this.dragPinchManager.disableLongpress(); + return this; + } + public void load() { + if (!hasSize) { + waitingDocumentConfigurator = this; + return; + } PDFView.this.recycle(); - PDFView.this.setOnDrawListener(onDrawListener); - PDFView.this.setOnDrawAllListener(onDrawAllListener); - PDFView.this.setOnPageChangeListener(onPageChangeListener); - PDFView.this.setOnPageScrollListener(onPageScrollListener); - PDFView.this.setOnRenderListener(onRenderListener); - PDFView.this.setOnTapListener(onTapListener); - PDFView.this.enableSwipe(enableSwipe); + PDFView.this.callbacks.setOnLoadComplete(onLoadCompleteListener); + PDFView.this.callbacks.setOnError(onErrorListener); + PDFView.this.callbacks.setOnDraw(onDrawListener); + PDFView.this.callbacks.setOnDrawAll(onDrawAllListener); + PDFView.this.callbacks.setOnPageChange(onPageChangeListener); + PDFView.this.callbacks.setOnPageScroll(onPageScrollListener); + PDFView.this.callbacks.setOnRender(onRenderListener); + PDFView.this.callbacks.setOnTap(onTapListener); + PDFView.this.callbacks.setOnLongPress(onLongPressListener); + PDFView.this.callbacks.setOnPageError(onPageErrorListener); + PDFView.this.callbacks.setLinkHandler(linkHandler); + PDFView.this.setSwipeEnabled(enableSwipe); + PDFView.this.setNightMode(nightMode); PDFView.this.enableDoubletap(enableDoubletap); PDFView.this.setDefaultPage(defaultPage); PDFView.this.setSwipeVertical(!swipeHorizontal); @@ -1487,11 +1547,16 @@ public void load() { PDFView.this.setScrollHandle(scrollHandle); PDFView.this.enableAntialiasing(antialiasing); PDFView.this.setSpacing(spacing); - PDFView.this.dragPinchManager.setSwipeVertical(swipeVertical); + PDFView.this.setAutoSpacing(autoSpacing); + PDFView.this.setPageFitPolicy(pageFitPolicy); + PDFView.this.setFitEachPage(fitEachPage); + PDFView.this.setPageSnap(pageSnap); + PDFView.this.setPageFling(pageFling); + if (pageNumbers != null) { - PDFView.this.load(documentSource, password, onLoadCompleteListener, onErrorListener, pageNumbers); + PDFView.this.load(documentSource, password, pageNumbers); } else { - PDFView.this.load(documentSource, password, onLoadCompleteListener, onErrorListener); + PDFView.this.load(documentSource, password); } } } diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PagesLoader.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PagesLoader.java index 5b62fae68..26c30f4b2 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PagesLoader.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PagesLoader.java @@ -1,224 +1,276 @@ +/** + * Copyright 2017 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.github.barteksc.pdfviewer; - import android.graphics.RectF; -import android.util.Pair; import com.github.barteksc.pdfviewer.util.Constants; import com.github.barteksc.pdfviewer.util.MathUtils; +import com.github.barteksc.pdfviewer.util.Util; +import com.shockwave.pdfium.util.SizeF; + +import java.util.LinkedList; +import java.util.List; import static com.github.barteksc.pdfviewer.util.Constants.Cache.CACHE_SIZE; +import static com.github.barteksc.pdfviewer.util.Constants.PRELOAD_OFFSET; class PagesLoader { private PDFView pdfView; - - - // variables set on every call to loadPages() private int cacheOrder; - private float scaledHeight; - private float scaledWidth; - private Pair colsRows; private float xOffset; private float yOffset; - private float rowHeight; - private float colWidth; private float pageRelativePartWidth; private float pageRelativePartHeight; private float partRenderWidth; private float partRenderHeight; - private int thumbnailWidth; - private int thumbnailHeight; - private float scaledSpacingPx; private final RectF thumbnailRect = new RectF(0, 0, 1, 1); + private final int preloadOffset; private class Holder { - int page; int row; int col; - } - PagesLoader(PDFView pdfView) { - this.pdfView = pdfView; + @Override + public String toString() { + return "Holder{" + + "row=" + row + + ", col=" + col + + '}'; + } } - private Pair getPageColsRows() { - float ratioX = 1f / pdfView.getOptimalPageWidth(); - float ratioY = 1f / pdfView.getOptimalPageHeight(); - final float partHeight = (Constants.PART_SIZE * ratioY) / pdfView.getZoom(); - final float partWidth = (Constants.PART_SIZE * ratioX) / pdfView.getZoom(); - final int nbRows = MathUtils.ceil(1f / partHeight); - final int nbCols = MathUtils.ceil(1f / partWidth); - return new Pair<>(nbCols, nbRows); - } + private class RenderRange { + int page; + GridSize gridSize; + Holder leftTop; + Holder rightBottom; - private int documentPage(int userPage) { - int documentPage = userPage; - if (pdfView.getOriginalUserPages() != null) { - if (userPage < 0 || userPage >= pdfView.getOriginalUserPages().length) { - return -1; - } else { - documentPage = pdfView.getOriginalUserPages()[userPage]; - } + RenderRange() { + this.page = 0; + this.gridSize = new GridSize(); + this.leftTop = new Holder(); + this.rightBottom = new Holder(); } - if (documentPage < 0 || userPage >= pdfView.getDocumentPageCount()) { - return -1; + @Override + public String toString() { + return "RenderRange{" + + "page=" + page + + ", gridSize=" + gridSize + + ", leftTop=" + leftTop + + ", rightBottom=" + rightBottom + + '}'; } - - return documentPage; } - /** - * @param offset - * @param endOffset, if true, then rounding up, else rounding down - * @return - */ - private Holder getPageAndCoordsByOffset(float offset, boolean endOffset) { - Holder holder = new Holder(); - float fixOffset = -MathUtils.max(offset, 0); - float row, col; - - if (pdfView.isSwipeVertical()) { - holder.page = MathUtils.floor(fixOffset / (scaledHeight + scaledSpacingPx)); - row = Math.abs(fixOffset - (scaledHeight + scaledSpacingPx) * holder.page) / rowHeight; - col = xOffset / colWidth; - } else { - holder.page = MathUtils.floor(fixOffset / (scaledWidth + scaledSpacingPx)); - col = Math.abs(fixOffset - (scaledWidth + scaledSpacingPx) * holder.page) / colWidth; - row = yOffset / rowHeight; - } + private class GridSize { + int rows; + int cols; - if (endOffset) { - holder.row = MathUtils.ceil(row); - holder.col = MathUtils.ceil(col); - } else { - holder.row = MathUtils.floor(row); - holder.col = MathUtils.floor(col); + @Override + public String toString() { + return "GridSize{" + + "rows=" + rows + + ", cols=" + cols + + '}'; } - return holder; } - private void loadThumbnail(int userPage, int documentPage) { - if (!pdfView.cacheManager.containsThumbnail(userPage, documentPage, - thumbnailWidth, thumbnailHeight, thumbnailRect)) { - pdfView.renderingHandler.addRenderingTask(userPage, documentPage, - thumbnailWidth, thumbnailHeight, thumbnailRect, - true, 0, pdfView.isBestQuality(), pdfView.isAnnotationRendering()); - } + PagesLoader(PDFView pdfView) { + this.pdfView = pdfView; + this.preloadOffset = Util.getDP(pdfView.getContext(), PRELOAD_OFFSET); } + private void getPageColsRows(GridSize grid, int pageIndex) { + SizeF size = pdfView.pdfFile.getPageSize(pageIndex); + float ratioX = 1f / size.getWidth(); + float ratioY = 1f / size.getHeight(); + final float partHeight = (Constants.PART_SIZE * ratioY) / pdfView.getZoom(); + final float partWidth = (Constants.PART_SIZE * ratioX) / pdfView.getZoom(); + grid.rows = MathUtils.ceil(1f / partHeight); + grid.cols = MathUtils.ceil(1f / partWidth); + } + + private void calculatePartSize(GridSize grid) { + pageRelativePartWidth = 1f / (float) grid.cols; + pageRelativePartHeight = 1f / (float) grid.rows; + partRenderWidth = Constants.PART_SIZE / pageRelativePartWidth; + partRenderHeight = Constants.PART_SIZE / pageRelativePartHeight; + } + + /** - * @param number if < 0 then row (column) is above view, else row (column) is visible or below view - * @return + * calculate the render range of each page */ - private int loadRelative(int number, int nbOfPartsLoadable, boolean belowView) { - int loaded = 0; - float newOffset; - if (pdfView.isSwipeVertical()) { - float rowsHeight = rowHeight * number + 1; - newOffset = pdfView.getCurrentYOffset() - (belowView ? pdfView.getHeight() : 0) - rowsHeight; - } else { - float colsWidth = colWidth * number; - newOffset = pdfView.getCurrentXOffset() - (belowView ? pdfView.getWidth() : 0) - colsWidth; - } + private List getRenderRangeList(float firstXOffset, float firstYOffset, float lastXOffset, float lastYOffset) { - Holder holder = getPageAndCoordsByOffset(newOffset, false); - int documentPage = documentPage(holder.page); - if (documentPage < 0) { - return 0; - } - loadThumbnail(holder.page, documentPage); + float fixedFirstXOffset = -MathUtils.max(firstXOffset, 0); + float fixedFirstYOffset = -MathUtils.max(firstYOffset, 0); - if (pdfView.isSwipeVertical()) { - int firstCol = MathUtils.floor(xOffset / colWidth); - firstCol = MathUtils.min(firstCol - 1, 0); - int lastCol = MathUtils.ceil((xOffset + pdfView.getWidth()) / colWidth); - lastCol = MathUtils.max(lastCol + 1, colsRows.first); - for (int col = firstCol; col <= lastCol; col++) { - if (loadCell(holder.page, documentPage, holder.row, col, pageRelativePartWidth, pageRelativePartHeight)) { - loaded++; - } - if (loaded >= nbOfPartsLoadable) { - return loaded; - } - } - } else { - int firstRow = MathUtils.floor(yOffset / rowHeight); - firstRow = MathUtils.min(firstRow - 1, 0); - int lastRow = MathUtils.ceil((yOffset + pdfView.getHeight()) / rowHeight); - lastRow = MathUtils.max(lastRow + 1, colsRows.second); - for (int row = firstRow; row <= lastRow; row++) { - if (loadCell(holder.page, documentPage, row, holder.col, pageRelativePartWidth, pageRelativePartHeight)) { - loaded++; + float fixedLastXOffset = -MathUtils.max(lastXOffset, 0); + float fixedLastYOffset = -MathUtils.max(lastYOffset, 0); + + float offsetFirst = pdfView.isSwipeVertical() ? fixedFirstYOffset : fixedFirstXOffset; + float offsetLast = pdfView.isSwipeVertical() ? fixedLastYOffset : fixedLastXOffset; + + int firstPage = pdfView.pdfFile.getPageAtOffset(offsetFirst, pdfView.getZoom()); + int lastPage = pdfView.pdfFile.getPageAtOffset(offsetLast, pdfView.getZoom()); + int pageCount = lastPage - firstPage + 1; + + List renderRanges = new LinkedList<>(); + + for (int page = firstPage; page <= lastPage; page++) { + RenderRange range = new RenderRange(); + range.page = page; + + float pageFirstXOffset, pageFirstYOffset, pageLastXOffset, pageLastYOffset; + if (page == firstPage) { + pageFirstXOffset = fixedFirstXOffset; + pageFirstYOffset = fixedFirstYOffset; + if (pageCount == 1) { + pageLastXOffset = fixedLastXOffset; + pageLastYOffset = fixedLastYOffset; + } else { + float pageOffset = pdfView.pdfFile.getPageOffset(page, pdfView.getZoom()); + SizeF pageSize = pdfView.pdfFile.getScaledPageSize(page, pdfView.getZoom()); + if (pdfView.isSwipeVertical()) { + pageLastXOffset = fixedLastXOffset; + pageLastYOffset = pageOffset + pageSize.getHeight(); + } else { + pageLastYOffset = fixedLastYOffset; + pageLastXOffset = pageOffset + pageSize.getWidth(); + } } - if (loaded >= nbOfPartsLoadable) { - return loaded; + } else if (page == lastPage) { + float pageOffset = pdfView.pdfFile.getPageOffset(page, pdfView.getZoom()); + + if (pdfView.isSwipeVertical()) { + pageFirstXOffset = fixedFirstXOffset; + pageFirstYOffset = pageOffset; + } else { + pageFirstYOffset = fixedFirstYOffset; + pageFirstXOffset = pageOffset; } - } - } - return loaded; - } + pageLastXOffset = fixedLastXOffset; + pageLastYOffset = fixedLastYOffset; - public int loadVisible() { - int parts = 0; - Holder firstHolder, lastHolder; - if (pdfView.isSwipeVertical()) { - firstHolder = getPageAndCoordsByOffset(pdfView.getCurrentYOffset(), false); - lastHolder = getPageAndCoordsByOffset(pdfView.getCurrentYOffset() - pdfView.getHeight() + 1, true); - int visibleRows = 0; - if (firstHolder.page == lastHolder.page) { - visibleRows = lastHolder.row - firstHolder.row + 1; } else { - visibleRows += colsRows.second - firstHolder.row; - for (int page = firstHolder.page + 1; page < lastHolder.page; page++) { - visibleRows += colsRows.second; + float pageOffset = pdfView.pdfFile.getPageOffset(page, pdfView.getZoom()); + SizeF pageSize = pdfView.pdfFile.getScaledPageSize(page, pdfView.getZoom()); + if (pdfView.isSwipeVertical()) { + pageFirstXOffset = fixedFirstXOffset; + pageFirstYOffset = pageOffset; + + pageLastXOffset = fixedLastXOffset; + pageLastYOffset = pageOffset + pageSize.getHeight(); + } else { + pageFirstXOffset = pageOffset; + pageFirstYOffset = fixedFirstYOffset; + + pageLastXOffset = pageOffset + pageSize.getWidth(); + pageLastYOffset = fixedLastYOffset; } - visibleRows += lastHolder.row + 1; } - for (int i = 0; i < visibleRows && parts < CACHE_SIZE; i++) { - parts += loadRelative(i, CACHE_SIZE - parts, false); - } - } else { - firstHolder = getPageAndCoordsByOffset(pdfView.getCurrentXOffset(), false); - lastHolder = getPageAndCoordsByOffset(pdfView.getCurrentXOffset() - pdfView.getWidth() + 1, true); - int visibleCols = 0; - if (firstHolder.page == lastHolder.page) { - visibleCols = lastHolder.col - firstHolder.col + 1; + getPageColsRows(range.gridSize, range.page); // get the page's grid size that rows and cols + SizeF scaledPageSize = pdfView.pdfFile.getScaledPageSize(range.page, pdfView.getZoom()); + float rowHeight = scaledPageSize.getHeight() / range.gridSize.rows; + float colWidth = scaledPageSize.getWidth() / range.gridSize.cols; + + + // get the page offset int the whole file + // --------------------------------------- + // | | | | + // |<--offset-->| (page) |<--offset-->| + // | | | | + // | | | | + // --------------------------------------- + float secondaryOffset = pdfView.pdfFile.getSecondaryPageOffset(page, pdfView.getZoom()); + + // calculate the row,col of the point in the leftTop and rightBottom + if (pdfView.isSwipeVertical()) { + range.leftTop.row = MathUtils.floor(Math.abs(pageFirstYOffset - pdfView.pdfFile.getPageOffset(range.page, pdfView.getZoom())) / rowHeight); + range.leftTop.col = MathUtils.floor(MathUtils.min(pageFirstXOffset - secondaryOffset, 0) / colWidth); + + range.rightBottom.row = MathUtils.ceil(Math.abs(pageLastYOffset - pdfView.pdfFile.getPageOffset(range.page, pdfView.getZoom())) / rowHeight); + range.rightBottom.col = MathUtils.floor(MathUtils.min(pageLastXOffset - secondaryOffset, 0) / colWidth); } else { - visibleCols += colsRows.first - firstHolder.col; - for (int page = firstHolder.page + 1; page < lastHolder.page; page++) { - visibleCols += colsRows.first; - } - visibleCols += lastHolder.col + 1; - } + range.leftTop.col = MathUtils.floor(Math.abs(pageFirstXOffset - pdfView.pdfFile.getPageOffset(range.page, pdfView.getZoom())) / colWidth); + range.leftTop.row = MathUtils.floor(MathUtils.min(pageFirstYOffset - secondaryOffset, 0) / rowHeight); - for (int i = 0; i < visibleCols && parts < CACHE_SIZE; i++) { - parts += loadRelative(i, CACHE_SIZE - parts, false); + range.rightBottom.col = MathUtils.floor(Math.abs(pageLastXOffset - pdfView.pdfFile.getPageOffset(range.page, pdfView.getZoom())) / colWidth); + range.rightBottom.row = MathUtils.floor(MathUtils.min(pageLastYOffset - secondaryOffset, 0) / rowHeight); } + + renderRanges.add(range); + } + + return renderRanges; + } + + private void loadVisible() { + int parts = 0; + float scaledPreloadOffset = preloadOffset; + float firstXOffset = -xOffset + scaledPreloadOffset; + float lastXOffset = -xOffset - pdfView.getWidth() - scaledPreloadOffset; + float firstYOffset = -yOffset + scaledPreloadOffset; + float lastYOffset = -yOffset - pdfView.getHeight() - scaledPreloadOffset; + + List rangeList = getRenderRangeList(firstXOffset, firstYOffset, lastXOffset, lastYOffset); + + for (RenderRange range : rangeList) { + loadThumbnail(range.page); } - int prevDocPage = documentPage(firstHolder.page - 1); - if (prevDocPage >= 0) { - loadThumbnail(firstHolder.page - 1, prevDocPage); + + for (RenderRange range : rangeList) { + calculatePartSize(range.gridSize); + parts += loadPage(range.page, range.leftTop.row, range.rightBottom.row, range.leftTop.col, range.rightBottom.col, CACHE_SIZE - parts); + if (parts >= CACHE_SIZE) { + break; + } } - int nextDocPage = documentPage(firstHolder.page + 1); - if (nextDocPage >= 0) { - loadThumbnail(firstHolder.page + 1, nextDocPage); + + } + + private int loadPage(int page, int firstRow, int lastRow, int firstCol, int lastCol, + int nbOfPartsLoadable) { + int loaded = 0; + for (int row = firstRow; row <= lastRow; row++) { + for (int col = firstCol; col <= lastCol; col++) { + if (loadCell(page, row, col, pageRelativePartWidth, pageRelativePartHeight)) { + loaded++; + } + if (loaded >= nbOfPartsLoadable) { + return loaded; + } + } } - return parts; + return loaded; } - private boolean loadCell(int userPage, int documentPage, int row, int col, float pageRelativePartWidth, float pageRelativePartHeight) { + private boolean loadCell(int page, int row, int col, float pageRelativePartWidth, float pageRelativePartHeight) { float relX = pageRelativePartWidth * col; float relY = pageRelativePartHeight * row; float relWidth = pageRelativePartWidth; float relHeight = pageRelativePartHeight; - // Adjust width and height to - // avoid being outside the page float renderWidth = partRenderWidth; float renderHeight = partRenderHeight; if (relX + relWidth > 1) { @@ -232,10 +284,10 @@ private boolean loadCell(int userPage, int documentPage, int row, int col, float RectF pageRelativeBounds = new RectF(relX, relY, relX + relWidth, relY + relHeight); if (renderWidth > 0 && renderHeight > 0) { - if (!pdfView.cacheManager.upPartIfContained(userPage, documentPage, renderWidth, renderHeight, pageRelativeBounds, cacheOrder)) { - pdfView.renderingHandler.addRenderingTask(userPage, documentPage, - renderWidth, renderHeight, pageRelativeBounds, false, cacheOrder, - pdfView.isBestQuality(), pdfView.isAnnotationRendering()); + if (!pdfView.cacheManager.upPartIfContained(page, pageRelativeBounds, cacheOrder)) { + pdfView.renderingHandler.addRenderingTask(page, renderWidth, renderHeight, + pageRelativeBounds, false, cacheOrder, pdfView.isBestQuality(), + pdfView.isAnnotationRendering()); } cacheOrder++; @@ -244,32 +296,22 @@ private boolean loadCell(int userPage, int documentPage, int row, int col, float return false; } - public void loadPages() { - scaledHeight = pdfView.toCurrentScale(pdfView.getOptimalPageHeight()); - scaledWidth = pdfView.toCurrentScale(pdfView.getOptimalPageWidth()); - thumbnailWidth = (int) (pdfView.getOptimalPageWidth() * Constants.THUMBNAIL_RATIO); - thumbnailHeight = (int) (pdfView.getOptimalPageHeight() * Constants.THUMBNAIL_RATIO); - colsRows = getPageColsRows(); + private void loadThumbnail(int page) { + SizeF pageSize = pdfView.pdfFile.getPageSize(page); + float thumbnailWidth = pageSize.getWidth() * Constants.THUMBNAIL_RATIO; + float thumbnailHeight = pageSize.getHeight() * Constants.THUMBNAIL_RATIO; + if (!pdfView.cacheManager.containsThumbnail(page, thumbnailRect)) { + pdfView.renderingHandler.addRenderingTask(page, + thumbnailWidth, thumbnailHeight, thumbnailRect, + true, 0, pdfView.isBestQuality(), pdfView.isAnnotationRendering()); + } + } + + void loadPages() { + cacheOrder = 1; xOffset = -MathUtils.max(pdfView.getCurrentXOffset(), 0); yOffset = -MathUtils.max(pdfView.getCurrentYOffset(), 0); - rowHeight = scaledHeight / colsRows.second; - colWidth = scaledWidth / colsRows.first; - pageRelativePartWidth = 1f / (float) colsRows.first; - pageRelativePartHeight = 1f / (float) colsRows.second; - partRenderWidth = Constants.PART_SIZE / pageRelativePartWidth; - partRenderHeight = Constants.PART_SIZE / pageRelativePartHeight; - cacheOrder = 1; - scaledSpacingPx = pdfView.toCurrentScale(pdfView.getSpacingPx()); - scaledSpacingPx -= scaledSpacingPx / pdfView.getPageCount(); - int loaded = loadVisible(); - if (pdfView.getScrollDir().equals(PDFView.ScrollDir.END)) { // if scrolling to end, preload next view - for (int i = 0; i < Constants.PRELOAD_COUNT && loaded < CACHE_SIZE; i++) { - loaded += loadRelative(i, loaded, true); - } - } else { // if scrolling to start, preload previous view - for (int i = 0; i > -Constants.PRELOAD_COUNT && loaded < CACHE_SIZE; i--) { - loaded += loadRelative(i, loaded, false); - } - } + + loadVisible(); } } diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PdfFile.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PdfFile.java new file mode 100644 index 000000000..fdc104f2c --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/PdfFile.java @@ -0,0 +1,373 @@ +/** + * Copyright 2017 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.SparseBooleanArray; + +import com.github.barteksc.pdfviewer.exception.PageRenderingException; +import com.github.barteksc.pdfviewer.util.FitPolicy; +import com.github.barteksc.pdfviewer.util.PageSizeCalculator; +import com.shockwave.pdfium.PdfDocument; +import com.shockwave.pdfium.PdfiumCore; +import com.shockwave.pdfium.util.Size; +import com.shockwave.pdfium.util.SizeF; + +import java.util.ArrayList; +import java.util.List; + +class PdfFile { + + private static final Object lock = new Object(); + private PdfDocument pdfDocument; + private PdfiumCore pdfiumCore; + private int pagesCount = 0; + /** Original page sizes */ + private List originalPageSizes = new ArrayList<>(); + /** Scaled page sizes */ + private List pageSizes = new ArrayList<>(); + /** Opened pages with indicator whether opening was successful */ + private SparseBooleanArray openedPages = new SparseBooleanArray(); + /** Page with maximum width */ + private Size originalMaxWidthPageSize = new Size(0, 0); + /** Page with maximum height */ + private Size originalMaxHeightPageSize = new Size(0, 0); + /** Scaled page with maximum height */ + private SizeF maxHeightPageSize = new SizeF(0, 0); + /** Scaled page with maximum width */ + private SizeF maxWidthPageSize = new SizeF(0, 0); + /** True if scrolling is vertical, else it's horizontal */ + private boolean isVertical; + /** Fixed spacing between pages in pixels */ + private int spacingPx; + /** Calculate spacing automatically so each page fits on it's own in the center of the view */ + private boolean autoSpacing; + /** Calculated offsets for pages */ + private List pageOffsets = new ArrayList<>(); + /** Calculated auto spacing for pages */ + private List pageSpacing = new ArrayList<>(); + /** Calculated document length (width or height, depending on swipe mode) */ + private float documentLength = 0; + private final FitPolicy pageFitPolicy; + /** + * True if every page should fit separately according to the FitPolicy, + * else the largest page fits and other pages scale relatively + */ + private final boolean fitEachPage; + /** + * The pages the user want to display in order + * (ex: 0, 2, 2, 8, 8, 1, 1, 1) + */ + private int[] originalUserPages; + + PdfFile(PdfiumCore pdfiumCore, PdfDocument pdfDocument, FitPolicy pageFitPolicy, Size viewSize, int[] originalUserPages, + boolean isVertical, int spacing, boolean autoSpacing, boolean fitEachPage) { + this.pdfiumCore = pdfiumCore; + this.pdfDocument = pdfDocument; + this.pageFitPolicy = pageFitPolicy; + this.originalUserPages = originalUserPages; + this.isVertical = isVertical; + this.spacingPx = spacing; + this.autoSpacing = autoSpacing; + this.fitEachPage = fitEachPage; + setup(viewSize); + } + + private void setup(Size viewSize) { + if (originalUserPages != null) { + pagesCount = originalUserPages.length; + } else { + pagesCount = pdfiumCore.getPageCount(pdfDocument); + } + + for (int i = 0; i < pagesCount; i++) { + Size pageSize = pdfiumCore.getPageSize(pdfDocument, documentPage(i)); + if (pageSize.getWidth() > originalMaxWidthPageSize.getWidth()) { + originalMaxWidthPageSize = pageSize; + } + if (pageSize.getHeight() > originalMaxHeightPageSize.getHeight()) { + originalMaxHeightPageSize = pageSize; + } + originalPageSizes.add(pageSize); + } + + recalculatePageSizes(viewSize); + } + + /** + * Call after view size change to recalculate page sizes, offsets and document length + * + * @param viewSize new size of changed view + */ + public void recalculatePageSizes(Size viewSize) { + pageSizes.clear(); + PageSizeCalculator calculator = new PageSizeCalculator(pageFitPolicy, originalMaxWidthPageSize, + originalMaxHeightPageSize, viewSize, fitEachPage); + maxWidthPageSize = calculator.getOptimalMaxWidthPageSize(); + maxHeightPageSize = calculator.getOptimalMaxHeightPageSize(); + + for (Size size : originalPageSizes) { + pageSizes.add(calculator.calculate(size)); + } + if (autoSpacing) { + prepareAutoSpacing(viewSize); + } + prepareDocLen(); + preparePagesOffset(); + } + + public int getPagesCount() { + return pagesCount; + } + + public SizeF getPageSize(int pageIndex) { + int docPage = documentPage(pageIndex); + if (docPage < 0) { + return new SizeF(0, 0); + } + return pageSizes.get(pageIndex); + } + + public SizeF getScaledPageSize(int pageIndex, float zoom) { + SizeF size = getPageSize(pageIndex); + return new SizeF(size.getWidth() * zoom, size.getHeight() * zoom); + } + + /** + * get page size with biggest dimension (width in vertical mode and height in horizontal mode) + * + * @return size of page + */ + public SizeF getMaxPageSize() { + return isVertical ? maxWidthPageSize : maxHeightPageSize; + } + + public float getMaxPageWidth() { + return getMaxPageSize().getWidth(); + } + + public float getMaxPageHeight() { + return getMaxPageSize().getHeight(); + } + + private void prepareAutoSpacing(Size viewSize) { + pageSpacing.clear(); + for (int i = 0; i < getPagesCount(); i++) { + SizeF pageSize = pageSizes.get(i); + float spacing = Math.max(0, isVertical ? viewSize.getHeight() - pageSize.getHeight() : + viewSize.getWidth() - pageSize.getWidth()); + if (i < getPagesCount() - 1) { + spacing += spacingPx; + } + pageSpacing.add(spacing); + } + } + + private void prepareDocLen() { + float length = 0; + for (int i = 0; i < getPagesCount(); i++) { + SizeF pageSize = pageSizes.get(i); + length += isVertical ? pageSize.getHeight() : pageSize.getWidth(); + if (autoSpacing) { + length += pageSpacing.get(i); + } else if (i < getPagesCount() - 1) { + length += spacingPx; + } + } + documentLength = length; + } + + private void preparePagesOffset() { + pageOffsets.clear(); + float offset = 0; + for (int i = 0; i < getPagesCount(); i++) { + SizeF pageSize = pageSizes.get(i); + float size = isVertical ? pageSize.getHeight() : pageSize.getWidth(); + if (autoSpacing) { + offset += pageSpacing.get(i) / 2f; + if (i == 0) { + offset -= spacingPx / 2f; + } else if (i == getPagesCount() - 1) { + offset += spacingPx / 2f; + } + pageOffsets.add(offset); + offset += size + pageSpacing.get(i) / 2f; + } else { + pageOffsets.add(offset); + offset += size + spacingPx; + } + } + } + + public float getDocLen(float zoom) { + return documentLength * zoom; + } + + /** + * Get the page's height if swiping vertical, or width if swiping horizontal. + */ + public float getPageLength(int pageIndex, float zoom) { + SizeF size = getPageSize(pageIndex); + return (isVertical ? size.getHeight() : size.getWidth()) * zoom; + } + + public float getPageSpacing(int pageIndex, float zoom) { + float spacing = autoSpacing ? pageSpacing.get(pageIndex) : spacingPx; + return spacing * zoom; + } + + /** Get primary page offset, that is Y for vertical scroll and X for horizontal scroll */ + public float getPageOffset(int pageIndex, float zoom) { + int docPage = documentPage(pageIndex); + if (docPage < 0) { + return 0; + } + return pageOffsets.get(pageIndex) * zoom; + } + + /** Get secondary page offset, that is X for vertical scroll and Y for horizontal scroll */ + public float getSecondaryPageOffset(int pageIndex, float zoom) { + SizeF pageSize = getPageSize(pageIndex); + if (isVertical) { + float maxWidth = getMaxPageWidth(); + return zoom * (maxWidth - pageSize.getWidth()) / 2; //x + } else { + float maxHeight = getMaxPageHeight(); + return zoom * (maxHeight - pageSize.getHeight()) / 2; //y + } + } + + public int getPageAtOffset(float offset, float zoom) { + int currentPage = 0; + for (int i = 0; i < getPagesCount(); i++) { + float off = pageOffsets.get(i) * zoom - getPageSpacing(i, zoom) / 2f; + if (off >= offset) { + break; + } + currentPage++; + } + return --currentPage >= 0 ? currentPage : 0; + } + + public boolean openPage(int pageIndex) throws PageRenderingException { + int docPage = documentPage(pageIndex); + if (docPage < 0) { + return false; + } + + synchronized (lock) { + if (openedPages.indexOfKey(docPage) < 0) { + try { + pdfiumCore.openPage(pdfDocument, docPage); + openedPages.put(docPage, true); + return true; + } catch (Exception e) { + openedPages.put(docPage, false); + throw new PageRenderingException(pageIndex, e); + } + } + return false; + } + } + + public boolean pageHasError(int pageIndex) { + int docPage = documentPage(pageIndex); + return !openedPages.get(docPage, false); + } + + public void renderPageBitmap(Bitmap bitmap, int pageIndex, Rect bounds, boolean annotationRendering) { + int docPage = documentPage(pageIndex); + pdfiumCore.renderPageBitmap(pdfDocument, bitmap, docPage, + bounds.left, bounds.top, bounds.width(), bounds.height(), annotationRendering); + } + + public PdfDocument.Meta getMetaData() { + if (pdfDocument == null) { + return null; + } + return pdfiumCore.getDocumentMeta(pdfDocument); + } + + public List getBookmarks() { + if (pdfDocument == null) { + return new ArrayList<>(); + } + return pdfiumCore.getTableOfContents(pdfDocument); + } + + public List getPageLinks(int pageIndex) { + int docPage = documentPage(pageIndex); + return pdfiumCore.getPageLinks(pdfDocument, docPage); + } + + public RectF mapRectToDevice(int pageIndex, int startX, int startY, int sizeX, int sizeY, + RectF rect) { + int docPage = documentPage(pageIndex); + return pdfiumCore.mapRectToDevice(pdfDocument, docPage, startX, startY, sizeX, sizeY, 0, rect); + } + + public void dispose() { + if (pdfiumCore != null && pdfDocument != null) { + pdfiumCore.closeDocument(pdfDocument); + } + + pdfDocument = null; + originalUserPages = null; + } + + /** + * Given the UserPage number, this method restrict it + * to be sure it's an existing page. It takes care of + * using the user defined pages if any. + * + * @param userPage A page number. + * @return A restricted valid page number (example : -2 => 0) + */ + public int determineValidPageNumberFrom(int userPage) { + if (userPage <= 0) { + return 0; + } + if (originalUserPages != null) { + if (userPage >= originalUserPages.length) { + return originalUserPages.length - 1; + } + } else { + if (userPage >= getPagesCount()) { + return getPagesCount() - 1; + } + } + return userPage; + } + + public int documentPage(int userPage) { + int documentPage = userPage; + if (originalUserPages != null) { + if (userPage < 0 || userPage >= originalUserPages.length) { + return -1; + } else { + documentPage = originalUserPages[userPage]; + } + } + + if (documentPage < 0 || userPage >= getPagesCount()) { + return -1; + } + + return documentPage; + } +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/RenderingHandler.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/RenderingHandler.java index 70e0ec2ae..22f55f4fb 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/RenderingHandler.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/RenderingHandler.java @@ -16,19 +16,17 @@ package com.github.barteksc.pdfviewer; import android.graphics.Bitmap; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.util.Log; +import com.github.barteksc.pdfviewer.exception.PageRenderingException; import com.github.barteksc.pdfviewer.model.PagePart; -import com.shockwave.pdfium.PdfDocument; -import com.shockwave.pdfium.PdfiumCore; - -import java.util.HashSet; -import java.util.Set; /** * A {@link Handler} that will process incoming {@link RenderingTask} messages @@ -41,26 +39,22 @@ class RenderingHandler extends Handler { */ static final int MSG_RENDER_TASK = 1; - private PdfiumCore pdfiumCore; - private PdfDocument pdfDocument; + private static final String TAG = RenderingHandler.class.getName(); private PDFView pdfView; private RectF renderBounds = new RectF(); private Rect roundedRenderBounds = new Rect(); private Matrix renderMatrix = new Matrix(); - private final Set openedPages = new HashSet<>(); private boolean running = false; - RenderingHandler(Looper looper, PDFView pdfView, PdfiumCore pdfiumCore, PdfDocument pdfDocument) { + RenderingHandler(Looper looper, PDFView pdfView) { super(looper); this.pdfView = pdfView; - this.pdfiumCore = pdfiumCore; - this.pdfDocument = pdfDocument; } - void addRenderingTask(int userPage, int page, float width, float height, RectF bounds, boolean thumbnail, int cacheOrder, boolean bestQuality, boolean annotationRendering) { - RenderingTask task = new RenderingTask(width, height, bounds, userPage, page, thumbnail, cacheOrder, bestQuality, annotationRendering); + void addRenderingTask(int page, float width, float height, RectF bounds, boolean thumbnail, int cacheOrder, boolean bestQuality, boolean annotationRendering) { + RenderingTask task = new RenderingTask(width, height, bounds, page, thumbnail, cacheOrder, bestQuality, annotationRendering); Message msg = obtainMessage(MSG_RENDER_TASK, task); sendMessage(msg); } @@ -68,43 +62,53 @@ void addRenderingTask(int userPage, int page, float width, float height, RectF b @Override public void handleMessage(Message message) { RenderingTask task = (RenderingTask) message.obj; - final PagePart part = proceed(task); - if (part != null) { - if (running) { - pdfView.post(new Runnable() { - @Override - public void run() { - pdfView.onBitmapRendered(part); - } - }); - } else { - part.getRenderedBitmap().recycle(); + try { + final PagePart part = proceed(task); + if (part != null) { + if (running) { + pdfView.post(new Runnable() { + @Override + public void run() { + pdfView.onBitmapRendered(part); + } + }); + } else { + part.getRenderedBitmap().recycle(); + } } + } catch (final PageRenderingException ex) { + pdfView.post(new Runnable() { + @Override + public void run() { + pdfView.onPageError(ex); + } + }); } } - private PagePart proceed(RenderingTask renderingTask) { - if (!openedPages.contains(renderingTask.page)) { - openedPages.add(renderingTask.page); - pdfiumCore.openPage(pdfDocument, renderingTask.page); - } + private PagePart proceed(RenderingTask renderingTask) throws PageRenderingException { + PdfFile pdfFile = pdfView.pdfFile; + pdfFile.openPage(renderingTask.page); int w = Math.round(renderingTask.width); int h = Math.round(renderingTask.height); + + if (w == 0 || h == 0 || pdfFile.pageHasError(renderingTask.page)) { + return null; + } + Bitmap render; try { render = Bitmap.createBitmap(w, h, renderingTask.bestQuality ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565); } catch (IllegalArgumentException e) { - e.printStackTrace(); + Log.e(TAG, "Cannot create bitmap", e); return null; } calculateBounds(w, h, renderingTask.bounds); - pdfiumCore.renderPageBitmap(pdfDocument, render, renderingTask.page, - roundedRenderBounds.left, roundedRenderBounds.top, - roundedRenderBounds.width(), roundedRenderBounds.height(), renderingTask.annotationRendering); - return new PagePart(renderingTask.userPage, renderingTask.page, render, - renderingTask.width, renderingTask.height, + pdfFile.renderPageBitmap(render, renderingTask.page, roundedRenderBounds, renderingTask.annotationRendering); + + return new PagePart(renderingTask.page, render, renderingTask.bounds, renderingTask.thumbnail, renderingTask.cacheOrder); } @@ -135,8 +139,6 @@ private class RenderingTask { int page; - int userPage; - boolean thumbnail; int cacheOrder; @@ -145,12 +147,11 @@ private class RenderingTask { boolean annotationRendering; - RenderingTask(float width, float height, RectF bounds, int userPage, int page, boolean thumbnail, int cacheOrder, boolean bestQuality, boolean annotationRendering) { + RenderingTask(float width, float height, RectF bounds, int page, boolean thumbnail, int cacheOrder, boolean bestQuality, boolean annotationRendering) { this.page = page; this.width = width; this.height = height; this.bounds = bounds; - this.userPage = userPage; this.thumbnail = thumbnail; this.cacheOrder = cacheOrder; this.bestQuality = bestQuality; diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/exception/PageRenderingException.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/exception/PageRenderingException.java new file mode 100644 index 000000000..b6ff2b892 --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/exception/PageRenderingException.java @@ -0,0 +1,14 @@ +package com.github.barteksc.pdfviewer.exception; + +public class PageRenderingException extends Exception { + private final int page; + + public PageRenderingException(int page, Throwable cause) { + super(cause); + this.page = page; + } + + public int getPage() { + return page; + } +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/link/DefaultLinkHandler.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/link/DefaultLinkHandler.java new file mode 100644 index 000000000..0849da389 --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/link/DefaultLinkHandler.java @@ -0,0 +1,61 @@ +/** + * Copyright 2017 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer.link; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import com.github.barteksc.pdfviewer.PDFView; +import com.github.barteksc.pdfviewer.model.LinkTapEvent; + +public class DefaultLinkHandler implements LinkHandler { + + private static final String TAG = DefaultLinkHandler.class.getSimpleName(); + + private PDFView pdfView; + + public DefaultLinkHandler(PDFView pdfView) { + this.pdfView = pdfView; + } + + @Override + public void handleLinkEvent(LinkTapEvent event) { + String uri = event.getLink().getUri(); + Integer page = event.getLink().getDestPageIdx(); + if (uri != null && !uri.isEmpty()) { + handleUri(uri); + } else if (page != null) { + handlePage(page); + } + } + + private void handleUri(String uri) { + Uri parsedUri = Uri.parse(uri); + Intent intent = new Intent(Intent.ACTION_VIEW, parsedUri); + Context context = pdfView.getContext(); + if (intent.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(intent); + } else { + Log.w(TAG, "No activity found for URI: " + uri); + } + } + + private void handlePage(int page) { + pdfView.jumpTo(page); + } +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/link/LinkHandler.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/link/LinkHandler.java new file mode 100644 index 000000000..f09dc11e4 --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/link/LinkHandler.java @@ -0,0 +1,28 @@ +/** + * Copyright 2017 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer.link; + +import com.github.barteksc.pdfviewer.model.LinkTapEvent; + +public interface LinkHandler { + + /** + * Called when link was tapped by user + * + * @param event current event + */ + void handleLinkEvent(LinkTapEvent event); +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/Callbacks.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/Callbacks.java new file mode 100644 index 000000000..09becb1c0 --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/Callbacks.java @@ -0,0 +1,180 @@ +/** + * Copyright 2017 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer.listener; + +import android.view.MotionEvent; + +import com.github.barteksc.pdfviewer.link.LinkHandler; +import com.github.barteksc.pdfviewer.model.LinkTapEvent; + +public class Callbacks { + + /** + * Call back object to call when the PDF is loaded + */ + private OnLoadCompleteListener onLoadCompleteListener; + + /** + * Call back object to call when document loading error occurs + */ + private OnErrorListener onErrorListener; + + /** + * Call back object to call when the page load error occurs + */ + private OnPageErrorListener onPageErrorListener; + + /** + * Call back object to call when the document is initially rendered + */ + private OnRenderListener onRenderListener; + + /** + * Call back object to call when the page has changed + */ + private OnPageChangeListener onPageChangeListener; + + /** + * Call back object to call when the page is scrolled + */ + private OnPageScrollListener onPageScrollListener; + + /** + * Call back object to call when the above layer is to drawn + */ + private OnDrawListener onDrawListener; + + private OnDrawListener onDrawAllListener; + + /** + * Call back object to call when the user does a tap gesture + */ + private OnTapListener onTapListener; + + /** + * Call back object to call when the user does a long tap gesture + */ + private OnLongPressListener onLongPressListener; + + /** + * Call back object to call when clicking link + */ + private LinkHandler linkHandler; + + public void setOnLoadComplete(OnLoadCompleteListener onLoadCompleteListener) { + this.onLoadCompleteListener = onLoadCompleteListener; + } + + public void callOnLoadComplete(int pagesCount) { + if (onLoadCompleteListener != null) { + onLoadCompleteListener.loadComplete(pagesCount); + } + } + + public void setOnError(OnErrorListener onErrorListener) { + this.onErrorListener = onErrorListener; + } + + public OnErrorListener getOnError() { + return onErrorListener; + } + + public void setOnPageError(OnPageErrorListener onPageErrorListener) { + this.onPageErrorListener = onPageErrorListener; + } + + public boolean callOnPageError(int page, Throwable error) { + if (onPageErrorListener != null) { + onPageErrorListener.onPageError(page, error); + return true; + } + return false; + } + + public void setOnRender(OnRenderListener onRenderListener) { + this.onRenderListener = onRenderListener; + } + + public void callOnRender(int pagesCount) { + if (onRenderListener != null) { + onRenderListener.onInitiallyRendered(pagesCount); + } + } + + public void setOnPageChange(OnPageChangeListener onPageChangeListener) { + this.onPageChangeListener = onPageChangeListener; + } + + public void callOnPageChange(int page, int pagesCount) { + if (onPageChangeListener != null) { + onPageChangeListener.onPageChanged(page, pagesCount); + } + } + + public void setOnPageScroll(OnPageScrollListener onPageScrollListener) { + this.onPageScrollListener = onPageScrollListener; + } + + public void callOnPageScroll(int currentPage, float offset) { + if (onPageScrollListener != null) { + onPageScrollListener.onPageScrolled(currentPage, offset); + } + } + + public void setOnDraw(OnDrawListener onDrawListener) { + this.onDrawListener = onDrawListener; + } + + public OnDrawListener getOnDraw() { + return onDrawListener; + } + + public void setOnDrawAll(OnDrawListener onDrawAllListener) { + this.onDrawAllListener = onDrawAllListener; + } + + public OnDrawListener getOnDrawAll() { + return onDrawAllListener; + } + + public void setOnTap(OnTapListener onTapListener) { + this.onTapListener = onTapListener; + } + + public boolean callOnTap(MotionEvent event) { + return onTapListener != null && onTapListener.onTap(event); + } + + public void setOnLongPress(OnLongPressListener onLongPressListener) { + this.onLongPressListener = onLongPressListener; + } + + public void callOnLongPress(MotionEvent event) { + if (onLongPressListener != null) { + onLongPressListener.onLongPress(event); + } + } + + public void setLinkHandler(LinkHandler linkHandler) { + this.linkHandler = linkHandler; + } + + public void callLinkHandler(LinkTapEvent event) { + if (linkHandler != null) { + linkHandler.handleLinkEvent(event); + } + } +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/OnLongPressListener.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/OnLongPressListener.java new file mode 100644 index 000000000..fc94c7260 --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/OnLongPressListener.java @@ -0,0 +1,32 @@ +/** + * Copyright 2017 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer.listener; + +import android.view.MotionEvent; + +/** + * Implement this interface to receive events from PDFView + * when view has been long pressed + */ +public interface OnLongPressListener { + + /** + * Called when the user has a long tap gesture, before processing scroll handle toggling + * + * @param e MotionEvent that registered as a confirmed long press + */ + void onLongPress(MotionEvent e); +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/OnPageErrorListener.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/OnPageErrorListener.java new file mode 100644 index 000000000..3fb38a2e4 --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/OnPageErrorListener.java @@ -0,0 +1,25 @@ +/** + * Copyright 2017 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer.listener; + +public interface OnPageErrorListener { + + /** + * Called if error occurred while loading PDF page + * @param t Throwable with error + */ + void onPageError(int page, Throwable t); +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/OnRenderListener.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/OnRenderListener.java index 1213bf8d2..999824979 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/OnRenderListener.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/listener/OnRenderListener.java @@ -20,8 +20,6 @@ public interface OnRenderListener { /** * Called only once, when document is rendered * @param nbPages number of pages - * @param pageWidth width of page - * @param pageHeight height of page */ - void onInitiallyRendered(int nbPages, float pageWidth, float pageHeight); + void onInitiallyRendered(int nbPages); } diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/model/LinkTapEvent.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/model/LinkTapEvent.java new file mode 100644 index 000000000..7e72bdd36 --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/model/LinkTapEvent.java @@ -0,0 +1,62 @@ +/** + * Copyright 2016 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer.model; + +import android.graphics.RectF; + +import com.shockwave.pdfium.PdfDocument; + +public class LinkTapEvent { + private float originalX; + private float originalY; + private float documentX; + private float documentY; + private RectF mappedLinkRect; + private PdfDocument.Link link; + + public LinkTapEvent(float originalX, float originalY, float documentX, float documentY, RectF mappedLinkRect, PdfDocument.Link link) { + this.originalX = originalX; + this.originalY = originalY; + this.documentX = documentX; + this.documentY = documentY; + this.mappedLinkRect = mappedLinkRect; + this.link = link; + } + + public float getOriginalX() { + return originalX; + } + + public float getOriginalY() { + return originalY; + } + + public float getDocumentX() { + return documentX; + } + + public float getDocumentY() { + return documentY; + } + + public RectF getMappedLinkRect() { + return mappedLinkRect; + } + + public PdfDocument.Link getLink() { + return link; + } +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/model/PagePart.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/model/PagePart.java index f7d84f211..376d2ccdf 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/model/PagePart.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/model/PagePart.java @@ -20,23 +20,18 @@ public class PagePart { - private int userPage; - private int page; private Bitmap renderedBitmap; - private float width, height; - private RectF pageRelativeBounds; private boolean thumbnail; private int cacheOrder; - public PagePart(int userPage, int page, Bitmap renderedBitmap, float width, float height, RectF pageRelativeBounds, boolean thumbnail, int cacheOrder) { + public PagePart(int page, Bitmap renderedBitmap, RectF pageRelativeBounds, boolean thumbnail, int cacheOrder) { super(); - this.userPage = userPage; this.page = page; this.renderedBitmap = renderedBitmap; this.pageRelativeBounds = pageRelativeBounds; @@ -52,10 +47,6 @@ public int getPage() { return page; } - public int getUserPage() { - return userPage; - } - public Bitmap getRenderedBitmap() { return renderedBitmap; } @@ -64,14 +55,6 @@ public RectF getPageRelativeBounds() { return pageRelativeBounds; } - public float getWidth() { - return width; - } - - public float getHeight() { - return height; - } - public boolean isThumbnail() { return thumbnail; } @@ -88,9 +71,6 @@ public boolean equals(Object obj) { PagePart part = (PagePart) obj; return part.getPage() == page - && part.getUserPage() == userPage - && part.getWidth() == width - && part.getHeight() == height && part.getPageRelativeBounds().left == pageRelativeBounds.left && part.getPageRelativeBounds().right == pageRelativeBounds.right && part.getPageRelativeBounds().top == pageRelativeBounds.top diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/scroll/DefaultScrollHandle.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/scroll/DefaultScrollHandle.java index c74cf5efc..aa9206b4f 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/scroll/DefaultScrollHandle.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/scroll/DefaultScrollHandle.java @@ -4,7 +4,7 @@ import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Handler; -import android.support.v4.content.ContextCompat; +import androidx.core.content.ContextCompat; import android.util.TypedValue; import android.view.MotionEvent; import android.view.ViewGroup; @@ -110,7 +110,9 @@ public void setScroll(float position) { } else { handler.removeCallbacks(hidePageScrollerRunnable); } - setPosition((pdfView.isSwipeVertical() ? pdfView.getHeight() : pdfView.getWidth()) * position); + if (pdfView != null) { + setPosition((pdfView.isSwipeVertical() ? pdfView.getHeight() : pdfView.getWidth()) * position); + } } private void setPosition(float pos) { @@ -228,6 +230,7 @@ public boolean onTouchEvent(MotionEvent event) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: hideDelayed(); + pdfView.performPageSnap(); return true; } diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/Constants.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/Constants.java index eb0760a10..e03de0c38 100644 --- a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/Constants.java +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/Constants.java @@ -29,15 +29,15 @@ public class Constants { */ public static float PART_SIZE = 256; - /** Number of preloaded rows or columns */ - public static int PRELOAD_COUNT = 7; + /** Part of document above and below screen that should be preloaded, in dp */ + public static int PRELOAD_OFFSET = 20; public static class Cache { /** The size of the cache (number of bitmaps kept) */ public static int CACHE_SIZE = 120; - public static int THUMBNAILS_CACHE_SIZE = 6; + public static int THUMBNAILS_CACHE_SIZE = 8; } public static class Pinch { diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/FitPolicy.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/FitPolicy.java new file mode 100644 index 000000000..6e90dd552 --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/FitPolicy.java @@ -0,0 +1,20 @@ +/** + * Copyright 2017 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer.util; + +public enum FitPolicy { + WIDTH, HEIGHT, BOTH +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/PageSizeCalculator.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/PageSizeCalculator.java new file mode 100644 index 000000000..4d678c98a --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/PageSizeCalculator.java @@ -0,0 +1,119 @@ +/** + * Copyright 2017 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer.util; + +import com.shockwave.pdfium.util.Size; +import com.shockwave.pdfium.util.SizeF; + +public class PageSizeCalculator { + + private FitPolicy fitPolicy; + private final Size originalMaxWidthPageSize; + private final Size originalMaxHeightPageSize; + private final Size viewSize; + private SizeF optimalMaxWidthPageSize; + private SizeF optimalMaxHeightPageSize; + private float widthRatio; + private float heightRatio; + private boolean fitEachPage; + + public PageSizeCalculator(FitPolicy fitPolicy, Size originalMaxWidthPageSize, Size originalMaxHeightPageSize, + Size viewSize, boolean fitEachPage) { + this.fitPolicy = fitPolicy; + this.originalMaxWidthPageSize = originalMaxWidthPageSize; + this.originalMaxHeightPageSize = originalMaxHeightPageSize; + this.viewSize = viewSize; + this.fitEachPage = fitEachPage; + calculateMaxPages(); + } + + public SizeF calculate(Size pageSize) { + if (pageSize.getWidth() <= 0 || pageSize.getHeight() <= 0) { + return new SizeF(0, 0); + } + float maxWidth = fitEachPage ? viewSize.getWidth() : pageSize.getWidth() * widthRatio; + float maxHeight = fitEachPage ? viewSize.getHeight() : pageSize.getHeight() * heightRatio; + switch (fitPolicy) { + case HEIGHT: + return fitHeight(pageSize, maxHeight); + case BOTH: + return fitBoth(pageSize, maxWidth, maxHeight); + default: + return fitWidth(pageSize, maxWidth); + } + } + + public SizeF getOptimalMaxWidthPageSize() { + return optimalMaxWidthPageSize; + } + + public SizeF getOptimalMaxHeightPageSize() { + return optimalMaxHeightPageSize; + } + + private void calculateMaxPages() { + switch (fitPolicy) { + case HEIGHT: + optimalMaxHeightPageSize = fitHeight(originalMaxHeightPageSize, viewSize.getHeight()); + heightRatio = optimalMaxHeightPageSize.getHeight() / originalMaxHeightPageSize.getHeight(); + optimalMaxWidthPageSize = fitHeight(originalMaxWidthPageSize, originalMaxWidthPageSize.getHeight() * heightRatio); + break; + case BOTH: + SizeF localOptimalMaxWidth = fitBoth(originalMaxWidthPageSize, viewSize.getWidth(), viewSize.getHeight()); + float localWidthRatio = localOptimalMaxWidth.getWidth() / originalMaxWidthPageSize.getWidth(); + this.optimalMaxHeightPageSize = fitBoth(originalMaxHeightPageSize, originalMaxHeightPageSize.getWidth() * localWidthRatio, + viewSize.getHeight()); + heightRatio = optimalMaxHeightPageSize.getHeight() / originalMaxHeightPageSize.getHeight(); + optimalMaxWidthPageSize = fitBoth(originalMaxWidthPageSize, viewSize.getWidth(), originalMaxWidthPageSize.getHeight() * heightRatio); + widthRatio = optimalMaxWidthPageSize.getWidth() / originalMaxWidthPageSize.getWidth(); + break; + default: + optimalMaxWidthPageSize = fitWidth(originalMaxWidthPageSize, viewSize.getWidth()); + widthRatio = optimalMaxWidthPageSize.getWidth() / originalMaxWidthPageSize.getWidth(); + optimalMaxHeightPageSize = fitWidth(originalMaxHeightPageSize, originalMaxHeightPageSize.getWidth() * widthRatio); + break; + } + } + + private SizeF fitWidth(Size pageSize, float maxWidth) { + float w = pageSize.getWidth(), h = pageSize.getHeight(); + float ratio = w / h; + w = maxWidth; + h = (float) Math.floor(maxWidth / ratio); + return new SizeF(w, h); + } + + private SizeF fitHeight(Size pageSize, float maxHeight) { + float w = pageSize.getWidth(), h = pageSize.getHeight(); + float ratio = h / w; + h = maxHeight; + w = (float) Math.floor(maxHeight / ratio); + return new SizeF(w, h); + } + + private SizeF fitBoth(Size pageSize, float maxWidth, float maxHeight) { + float w = pageSize.getWidth(), h = pageSize.getHeight(); + float ratio = w / h; + w = maxWidth; + h = (float) Math.floor(maxWidth / ratio); + if (h > maxHeight) { + h = maxHeight; + w = (float) Math.floor(maxHeight * ratio); + } + return new SizeF(w, h); + } + +} diff --git a/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/SnapEdge.java b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/SnapEdge.java new file mode 100644 index 000000000..e2e73ab79 --- /dev/null +++ b/android-pdf-viewer/src/main/java/com/github/barteksc/pdfviewer/util/SnapEdge.java @@ -0,0 +1,20 @@ +/** + * Copyright 2017 Bartosz Schiller + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.barteksc.pdfviewer.util; + +public enum SnapEdge { + START, CENTER, END, NONE +} diff --git a/build.gradle b/build.gradle index 9cb085f44..7db9a51fe 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,27 @@ buildscript { repositories { - jcenter() + google() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.3' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' + classpath 'com.android.tools.build:gradle:8.13.0' } } allprojects { repositories { - jcenter() + google() + mavenCentral() + } +} + +subprojects { + configurations.configureEach { + resolutionStrategy { + force 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22' + force 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22' + force 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22' + } } } diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..db52ba4c3 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.enableJetifier=true + +# 16 KB page size support +android.enableR8.fullMode=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 48650bbec..121992f64 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Mar 15 17:39:08 CET 2017 +#Wed Sep 10 21:17:12 IST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/sample/build.gradle b/sample/build.gradle index d963053dc..780d3b427 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,36 +1,58 @@ buildscript { repositories { - jcenter() - } - dependencies { - // replace with the current version of the android-apt plugin - classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' + google() + mavenCentral() } } repositories { - jcenter() + google() + mavenCentral() } apply plugin: 'com.android.application' -apply plugin: 'android-apt' android { - compileSdkVersion 25 - buildToolsVersion "25.0.3" + namespace 'com.github.barteksc.sample' + compileSdkVersion 36 defaultConfig { - minSdkVersion 11 - targetSdkVersion 25 + minSdkVersion 21 + targetSdkVersion 36 versionCode 3 - versionName "2.0.0" + versionName "3.0.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } + buildFeatures { + buildConfig true + } + + packagingOptions { + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/LICENSE' + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/NOTICE' + exclude 'META-INF/NOTICE.txt' + + // 16 KB page size support configuration + jniLibs { + useLegacyPackaging true // Use compressed shared libraries to avoid 16 KB alignment issues + } + } + + // Enable 16 KB page size support for native libraries + ndkVersion "28.0.12433566" // Use NDK r28+ for 16 KB support + } dependencies { - compile project(':android-pdf-viewer') - compile 'com.android.support:appcompat-v7:25.3.1' - provided 'org.androidannotations:androidannotations:4.0.0' - compile 'org.androidannotations:androidannotations-api:4.0.0' + implementation project(':android-pdf-viewer') + implementation 'androidx.appcompat:appcompat:1.7.1' } + + diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index d6ce646c4..6b6d9502c 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + @@ -8,8 +7,9 @@ android:label="@string/app_name" android:theme="@style/Theme.AppCompat.Light"> + android:name=".PDFViewActivity" + android:label="@string/app_name" + android:exported="true" > diff --git a/sample/src/main/assets/sample.pdf b/sample/src/main/assets/sample.pdf index 376f27729..197cc156a 100644 Binary files a/sample/src/main/assets/sample.pdf and b/sample/src/main/assets/sample.pdf differ diff --git a/sample/src/main/java/com/github/barteksc/sample/PDFViewActivity.java b/sample/src/main/java/com/github/barteksc/sample/PDFViewActivity.java index ed18cea10..47e6d2924 100755 --- a/sample/src/main/java/com/github/barteksc/sample/PDFViewActivity.java +++ b/sample/src/main/java/com/github/barteksc/sample/PDFViewActivity.java @@ -1,18 +1,3 @@ -/** - * Copyright 2016 Bartosz Schiller - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package com.github.barteksc.sample; import android.content.ActivityNotFoundException; @@ -21,144 +6,117 @@ import android.database.Cursor; import android.graphics.Color; import android.net.Uri; +import android.os.Bundle; import android.provider.OpenableColumns; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AppCompatActivity; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import android.util.Log; import android.widget.Toast; import com.github.barteksc.pdfviewer.PDFView; import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener; import com.github.barteksc.pdfviewer.listener.OnPageChangeListener; +import com.github.barteksc.pdfviewer.listener.OnPageErrorListener; import com.github.barteksc.pdfviewer.scroll.DefaultScrollHandle; +import com.github.barteksc.pdfviewer.util.FitPolicy; import com.shockwave.pdfium.PdfDocument; -import org.androidannotations.annotations.AfterViews; -import org.androidannotations.annotations.EActivity; -import org.androidannotations.annotations.NonConfigurationInstance; -import org.androidannotations.annotations.OnActivityResult; -import org.androidannotations.annotations.OptionsItem; -import org.androidannotations.annotations.OptionsMenu; -import org.androidannotations.annotations.ViewById; - import java.util.List; -@EActivity(R.layout.activity_main) -@OptionsMenu(R.menu.options) -public class PDFViewActivity extends AppCompatActivity implements OnPageChangeListener, OnLoadCompleteListener { +public class PDFViewActivity extends AppCompatActivity implements + OnPageChangeListener, OnLoadCompleteListener, OnPageErrorListener { private static final String TAG = PDFViewActivity.class.getSimpleName(); - private final static int REQUEST_CODE = 42; - public static final int PERMISSION_CODE = 42042; - - public static final String SAMPLE_FILE = "sample.pdf"; - public static final String READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE"; + private static final int PERMISSION_CODE = 42042; + private static final String SAMPLE_FILE = "sample.pdf"; + private static final String READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE"; - @ViewById - PDFView pdfView; + private PDFView pdfView; + private Uri uri; + private int pageNumber = 0; + private String pdfFileName; - @NonConfigurationInstance - Uri uri; + private final ActivityResultLauncher filePickerLauncher = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + if (result.getResultCode() == RESULT_OK && result.getData() != null) { + uri = result.getData().getData(); + displayFromUri(uri); + } + }); - @NonConfigurationInstance - Integer pageNumber = 0; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); - String pdfFileName; + pdfView = findViewById(R.id.pdfView); + pdfView.setBackgroundColor(Color.LTGRAY); - @OptionsItem(R.id.pickFile) - void pickFile() { - int permissionCheck = ContextCompat.checkSelfPermission(this, - READ_EXTERNAL_STORAGE); + if (uri != null) { + displayFromUri(uri); + } else { + displayFromAsset(SAMPLE_FILE); + } + } + private void pickFile() { + int permissionCheck = ContextCompat.checkSelfPermission(this, READ_EXTERNAL_STORAGE); if (permissionCheck != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions( - this, - new String[]{READ_EXTERNAL_STORAGE}, - PERMISSION_CODE - ); - - return; + ActivityCompat.requestPermissions(this, new String[]{READ_EXTERNAL_STORAGE}, PERMISSION_CODE); + } else { + launchPicker(); } - - launchPicker(); } - void launchPicker() { + private void launchPicker() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("application/pdf"); try { - startActivityForResult(intent, REQUEST_CODE); + filePickerLauncher.launch(intent); } catch (ActivityNotFoundException e) { - //alert user that file manager not working Toast.makeText(this, R.string.toast_pick_file_error, Toast.LENGTH_SHORT).show(); } } - @AfterViews - void afterViews() { - pdfView.setBackgroundColor(Color.LTGRAY); - if (uri != null) { - displayFromUri(uri); - } else { - displayFromAsset(SAMPLE_FILE); - } - setTitle(pdfFileName); - } - private void displayFromAsset(String assetFileName) { pdfFileName = assetFileName; - - pdfView.fromAsset(SAMPLE_FILE) + pdfView.fromAsset(assetFileName) .defaultPage(pageNumber) .onPageChange(this) .enableAnnotationRendering(true) .onLoad(this) .scrollHandle(new DefaultScrollHandle(this)) - .spacing(10) // in dp + .spacing(10) + .onPageError(this) + .pageFitPolicy(FitPolicy.BOTH) .load(); } private void displayFromUri(Uri uri) { pdfFileName = getFileName(uri); - pdfView.fromUri(uri) .defaultPage(pageNumber) .onPageChange(this) .enableAnnotationRendering(true) .onLoad(this) .scrollHandle(new DefaultScrollHandle(this)) - .spacing(10) // in dp + .spacing(10) + .onPageError(this) .load(); } - @OnActivityResult(REQUEST_CODE) - public void onResult(int resultCode, Intent intent) { - if (resultCode == RESULT_OK) { - uri = intent.getData(); - displayFromUri(uri); - } - } - - @Override - public void onPageChanged(int page, int pageCount) { - pageNumber = page; - setTitle(String.format("%s %s / %s", pdfFileName, page + 1, pageCount)); - } - - public String getFileName(Uri uri) { + private String getFileName(Uri uri) { String result = null; - if (uri.getScheme().equals("content")) { - Cursor cursor = getContentResolver().query(uri, null, null, null, null); - try { + if ("content".equals(uri.getScheme())) { + try (Cursor cursor = getContentResolver().query(uri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { - result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); - } - } finally { - if (cursor != null) { - cursor.close(); + int nameIndex = cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME); + result = cursor.getString(nameIndex); } } } @@ -168,6 +126,12 @@ public String getFileName(Uri uri) { return result; } + @Override + public void onPageChanged(int page, int pageCount) { + pageNumber = page; + setTitle(String.format("%s %s / %s", pdfFileName, page + 1, pageCount)); + } + @Override public void loadComplete(int nbPages) { PdfDocument.Meta meta = pdfView.getDocumentMeta(); @@ -181,36 +145,29 @@ public void loadComplete(int nbPages) { Log.e(TAG, "modDate = " + meta.getModDate()); printBookmarksTree(pdfView.getTableOfContents(), "-"); - } - public void printBookmarksTree(List tree, String sep) { + private void printBookmarksTree(List tree, String sep) { for (PdfDocument.Bookmark b : tree) { - Log.e(TAG, String.format("%s %s, p %d", sep, b.getTitle(), b.getPageIdx())); - if (b.hasChildren()) { printBookmarksTree(b.getChildren(), sep + "-"); } } } - /** - * Listener for response to user permission request - * - * @param requestCode Check that permission request code matches - * @param permissions Permissions that requested - * @param grantResults Whether permissions granted - */ @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == PERMISSION_CODE) { - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - launchPicker(); - } + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == PERMISSION_CODE && grantResults.length > 0 && + grantResults[0] == PackageManager.PERMISSION_GRANTED) { + launchPicker(); } } + @Override + public void onPageError(int page, Throwable t) { + Log.e(TAG, "Cannot load page " + page, t); + } }