Layout enhancements + discrete transitions
This PR builds on the layout enhancements PR (https://github.com/espresso3389/pdfrx/pull/504) and adds a new discrete page transitions feature where pages (or spreads of pages) transition one at a time.
Architecturally this is achieved by leveraging existing boundary behaviour of InteractiveViewer's ScrollPhysics enhancements - pdfrx provides the relevant boundaries for the current pages/spreads so that InteractiveViewer can restrict flings to these boundaries. This allows us to continue using the single InteractiveViewer instance and document layout.
New Features
Discrete Page Transition Mode
A new PageTransition.discrete mode that enables page-by-page navigation:
PdfViewer.asset(
'assets/document.pdf',
params: PdfViewerParams(
pageTransition: PageTransition.discrete,
),
)
Key behaviors:
- Pages transition one at a time (or one spread at a time for spread based layouts such as facing pages layouts)
- Swipe gestures advance to next/previous page based on velocity and boundary checks
- Drag gestures snap to nearest page based on visible page area
- Works seamlessly with all layout types (single pages, facing pages, etc)
- Only active at fit zoom level - free panning when zoomed in beyond fit scale, unless at a boundary
- Smooth animations with configurable duration and curves
Automatic ScrollPhysics Configuration
Discrete mode automatically uses ClampingScrollPhysics by default to prevent overscroll into neighbouring pages if no scrollPhysics parameter is defined.
Implementation Details
New Components
PageTransition enum (pdf_viewer_params.dart):
enum PageTransition {
/// Continuous scrolling (default)
continuous,
/// Discrete page-by-page transitions
discrete,
}
Core transition logic (pdf_viewer.dart):
_handleDiscretePageTransition()- Main transition coordinator_isAtBoundary()- Boundary detection with tolerance_getTargetPageBasedOnThreshold()- Visible area calculation_calcPageIntersectionArea()- Page visibility metrics_getAdjacentPage()- Decide which page to transition to_getSnapAnchor()- Page anchor selection based on page overflow_snapToPage()- Animated page transition
Boundary management:
_getDiscreteBoundaryRect()- Dynamic boundary calculation per page_addDiscreteSpacing()- Viewport-sized gaps between pages to prevent neighbor visibility- Boundary extension during active gestures (50% viewport) to enable panning to next page
https://github.com/user-attachments/assets/908520cf-a90d-4cf2-977b-c0fd0a42b9f9
https://github.com/user-attachments/assets/c39e8fc8-64f8-4843-a85e-a52ed5accd53
So, there are confusions on _alternativeFitScale; it's not corresponding to FitMode.cover. It's "relatively near meaing" to FitMode.fill as it is intended to smoothly scrolling on the scroll axis.
But it is useful especially for PDF files that contains pages of different dimensions (in some scanning PDF files, each page has its own irregular page dimensions... But anyway, I can give up supporting of such kind of files because no one is actually understanding the meaning of this. It's a really long story and you don't have to understand the background :(
So my question is, should we need FitMode.cover? If it's not needed, I want to remove it.
At least, the meaning of the word "cover" here seems different from what I expected from the word itself. My feeling is very similar to BoxFit.cover.
UPDATE: it seems that my _alternativeFitScale's meaning is really identical to the one of fit on PageTransition.discrete mode.
And, FitMode.fill sounds a little strange to me although I understood it as it literally "fills" the view by pages as if they were tiles; in my words, it is the meaning of "cover".
And, for facing pages, it does not maintain the existing facing pages requirements,
- The cover page may be placed on left-side or right-side according to user's demand (centering can be one of the option)
- goToPage behavior difference (but it may be accepted)
Other issues:
- The default for page-gap and margin seems different from the existing pdfrx's ones
- The page margins on left and right may be not not correct (at least, smaller and not compatible to the existing ones)
Of course, the existing facing pages layout can be implemented using PdfPageLayout but if the new class is named as FacingPagesLayout, users expects that it supports the existing logic with some options.
SinglePagesLayoutis not explain the actual behavior; it may be something likeSequentialPagesLayoutor such. (I usesequential layoutthat was borrowed from ancient Acrobat Reader); So the facing-pages was called asfacing sequential layoutthere.
"Single" page mode
The "single" page mode actually means the viewer only shows one page in the view. It can be realized by snapping logic. But even with it, we can selectively choose some of the pages from the all the PDF pages.
So, to realize this, we should change the LayoutResult.pageLayouts to Map<int, Rect> or List<Rect?> to support removing pages. Or SplayTreeMap<int, Rect> to keep the page order (of course, we may be able to sort the pages by Rect order).
I'm very sorry to write so many things once. But they're very important before doing such huge update.
So my question is, should we need
FitMode.cover? If it's not needed, I want to remove it. At least, the meaning of the word "cover" here seems different from what I expected from the word itself. My feeling is very similar to BoxFit.cover.UPDATE: it seems that my
_alternativeFitScale's meaning is really identical to the one of fit onPageTransition.discretemode.And,
FitMode.fillsounds a little strange to me although I understood it as it literally "fills" the view by pages as if they were tiles; in my words, it is the meaning of "cover".
Looking at BoxFit, you're right that the FitMode.fill looks most similar to BoxFit.cover (and FitMode.fit represents BoxFit.contain). Maybe we replace the term FitMode.fill with FitMode.cover but keep the functionality as I implemented for .fill as I think this provides mostly similar behaviour as _coverScale provided (except for a single page document).
And, for facing pages, it does not maintain the existing facing pages requirements,
- The cover page may be placed on left-side or right-side according to user's demand (centering can be one of the option)
- goToPage behavior difference (but it may be accepted)
Other issues:
- The default for page-gap and margin seems different from the existing pdfrx's ones
- The page margins on left and right may be not not correct (at least, smaller and not compatible to the existing ones)
I have made some changes to FacingPagesLayout to address these #dbc6046.
- The gutter between pages uses params.margin by default
- added two new arguments:
bool singlePagesFillAvailableWidth = true, bool independentPageScaling = true,
with singlePagesFillAvailableWidth == false and independentPageScaling == false, a similar behaviour to the existing facing pages layout is achieved.
singlePagesFillAvailableWidth = true
independentPageScaling = true
singlePagesFillAvailableWidth = false
independentPageScaling = true
singlePagesFillAvailableWidth = false
independentPageScaling = false
(replicates legacy facing pages layout)
Note that margins look different between layouts with/without independentPageScaling because with page scaling, different pages sizes result in different margins when scaling, and I've taken an approach of normalising these in _normalizePageSizes so that there is a consistent margin across page of differencing size/scaling.
SinglePagesLayoutis not explain the actual behavior; it may be something likeSequentialPagesLayoutor such. (I usesequential layoutthat was borrowed from ancient Acrobat Reader); So the facing-pages was called asfacing sequential layoutthere.
I've renamed it to SequentialPagesLayout - I agree this explains the behaviour better.
"Single" page mode
The "single" page mode actually means the viewer only shows one page in the view. It can be realized by snapping logic. But even with it, we can selectively choose some of the pages from the all the PDF pages.
So, to realize this, we should change the
LayoutResult.pageLayoutstoMap<int, Rect>orList<Rect?>to support removing pages. OrSplayTreeMap<int, Rect>to keep the page order (of course, we may be able to sort the pages by Rect order).
By the "single" page mode, I think you're referring the PageTransition.discrete in the commit? This shows either single pages (from a PdfPageLayout) or single spreads from a PdfSpreadLayout.
The idea with PdfSpreadLayout was to address the requirement of multiple pages presented on the screen in a discrete mode, and to enable scaling of a 'spread' as a single unit like a page.
PdfSpreadLayout is based on PdfPageLayout and keeps the same pageLayouts List<Rect> but adds:
/// List of spread bounds, indexed by spread index /// /// Each Rect represents the bounds of one spread in document coordinates. /// Use [getSpreadBounds] to get the spread for a specific page number. final List<Rect> spreadLayouts; /// Maps page number to spread index. /// /// - Example: `pageToSpreadIndex[0]` = spread index for page 1 final List<int> pageToSpreadIndex;
Does this address what you mean by 'selectively choose some of the pages from all of the PDF pages'?
@enhancient
When _goToXXX is initiated by non-user action, such as _goToPage, _calcOverscroll returns value that actually cancels the move. For example, to go to the last page, I should press [End] key twice.
@enhancient When
_goToXXXis initiated by non-user action, such as_goToPage,_calcOverscrollreturns value that actually cancels the move. For example, to go to the last page, I should press [End] key twice.
Oh, I see the issue - in discrete pageTransition mode _calcOverscroll uses _getDiscreteBoundaryRect which looks at the current page (_pageNumber) not the target page set in the goTo - Fixed in #2c4ca34
@espresso3389 I've had a go at merging in upstream/master and fixed a few small things in the process.
When can this PR be merged? The features this PR adds are very useful to me.