Simbul / baker

The HTML5 ebook framework to publish interactive books & magazines on iPad & iPhone using simply open web standards
http://bakerframework.com
1.53k stars 378 forks source link

Vertical page snap on orientation change #494

Open anjimi opened 12 years ago

anjimi commented 12 years ago

I couldn't find any other mention of this problem, so I thought I'd submit an issue to see if others have experienced the same thing.

I have my magazine app which has multiple 'pages' within each article which you swipe vertically to read. I have "-baker-vertical-pagination" set to true to allow tap for page up/down.

On orientation change on a page which is not the first or last page when the iPad is rotated the display doesn't snap to the current page which you are viewing. It will sometimes show a portion of the current page, sometimes it jumps to a couple of pages up/down so you have to swipe back to the page you were reading.

The first page always snaps in place on orientation change, the last page snaps in place on change portrait>landscape but not on landscape>portrait.

Is this a known issue?

folletto commented 12 years ago

Maybe you're referring to another meaning for "page" (for us it's a single HTML file). Could you explain better what you mean by "doesn't snap to the current page which you are viewing"?

anjimi commented 12 years ago

Hi Davide, yes sorry, I thought I had explained in the second paragraph, each HTML file or 'page' has content which scrolls vertically. As mentioned I have "-baker-vertical-pagination" in book.json set to true so each page behaves like it has multiple vertical 'pages'. I have css rules to adapt the layout so it works for both portrait and landscape orientation.

On orientation change the display view does not always show the section of the page which was visible before re-orientation, as I described.

folletto commented 12 years ago

Thanks, now it's clear. :)

However, you are saying that it doesn't happen every time, but in some situations it behaves properly (first and last page). Did I get this correctly? And, is there any difference in these pages?

Also, following the tutorial here: https://github.com/Simbul/baker/wiki/It%27s-crashing%2C-exploding-or-any-other-issue

  1. What version of Baker are you using?
  2. Is there any difference in the Debug log between when it works and when it doesn't?
  3. How does Safari Mobile perform on the same pages?
  4. Did you try moving a page that works (the first I guess) to a different position in the book (using book.json) and see if it still works?
anjimi commented 12 years ago

Hi, thanks. Sorry I think I still need to explain, when I say first and last page I mean first (top) and last (bottom) vertical page section within one html page. It behaves the same all the time, on all html pages regardless of content.

I'll refer to an html page as 'file' and a vertical page section within a 'page' as 'vertical-page'.

So, for example, if I am viewing a file on portrait view, I'm at the top of the file, flip to landscape orientation and I can still see the top vertical-page. Flip back and once again I can still see the same vertical-page. All good.

If I swipe or tap down to another vertical-page, flip to landscape and I see a cropped part of a vertical-page, not necessarily the vertical-page I was on before turning the iPad. Same for all vertical-pages within one file.

I just figured out the cause of this: On orientation change the vertical position is set to the same vertical position as it was before rotating. In my case because the responsive css rules have already re-sized the content height, it means the vertical-page top positions are now different.

I just tried adding some js which works out how your vertical-page position and changes the scrollTop, but it's very slow to catch up after orientation change. The adaptive css rules update the layout before you have even finished rotating.

Not sure where to go from here...

folletto commented 12 years ago

That's correct.

It's likely a combination of the CSS changes and the orientation and I think there isn't much you can do. To rule completely out or in the possibility of that, did you try to test the page in Mobile Safari and rotate?

Otherwise yes, it's likely you'll have to use JavaScript.

Simbul commented 12 years ago

Hey @anjimi, have you had any luck in solving this?

anjimi commented 12 years ago

No, not yet. I have been distracted with getting content into my app.

I am planning to try a different fix using JS to change the CSS for the content height of 'vertical page sections' and the scrollTop after reorientation. Bit hacky but I think it may be a better solution than just using JS to fix the scrollTop, as the JS does't run until after reorientation, whereas the CSS rules update much quicker.

thejtate commented 11 years ago

I wonder since the CSS kicks in so quickly if there is some way to set a slight delay to them.

I am having the similar issue when I am having a long vertical single HTML page with full screen images. Seems a bit confusing when you change the orientation and it's displaying half of the image (like it's half-scrolled a bit) or sometimes it's display the whole image above/below where you were at.

Been looking around for some sort of Javascript that might push the page to a specific area after rotation.

anjimi commented 11 years ago

I did try using jquery to adjust the scrollTop after rotating, but the js is really slow to fire, whereas the css updates very quickly. I think there are two possible solutions, either to use the js to change both the height of the elements on the page (not css), AND the scrollTop. Seems clunky and inelegant though.

My other thought was to dig around in Xcode and see if it's possible to change something so when it rotates, instead of keeping the vertical position to the same setting as for the previous orientation, do a calculation based on the height to work out a new vertical position for the new orientation.

This would only benefit publications with a magazine type layout, with content laid out in distinct vertical 'pages' within each page, a more book-like publication works fine as it is.

If this could be implemented, it could possibly be built into Baker and maybe tied to the '-baker-vertical-pagination' property? I don't know.

I had a little look and I think this stuff happens in BakerViewController.m but as I'm only novice with Xcode and haven't had the time I haven't managed to figure out if this is possible yet, when I have the time it's on my to-do list!

benoitchantre commented 11 years ago

I haven't tested this issue before, but with the actual code from the master and without any javascript, there's something interesting to observe

  1. open a page with a vertical pagination
  2. scroll to a section -> page snaps
  3. rotate the device -> page is rotated
  4. click somewhere on the page (with iOS Simulator) -> the page scrolls and snap to a section

Sometimes, the page scrolls to the right section, sometimes not. I think it's a simple math problem to find the right value from the top of the page and the to convert it after an orientation change.

Landscape to portrait conversion pageOffset = previousPageOffset * 768 / 1024

Portrait to landscape conversion pageOffset = previousPageOffset * 1024 / 768

Hard coded values should be replaced with variables to be compatible with other devices.

After the rotation, the page must scroll automatically to the pageOffset value.

This logic can be applied to pages without vertical pagination too.

Update: Here's a universal solution to adjust the page offset after the rotation. pageOffset = previousPageOffset * screenWidth / screenHeight

benoitchantre commented 11 years ago

In JavaScript, you can apply the code below on pages which use vertical pagination.

var pageYOffset;

handleScroll: function () {
    pageYOffset = window.pageYOffset;
}

adjustScrollAfterRotation: function () {
    var newPageYOffset = Math.round(pageYOffset * window.innerHeight / window.innerWidth);
    window.scrollTo(0, newPageYOffset);
}

window.addEventListener('orientationchange', adjustScrollAfterRotation, true);
window.addEventListener('scroll', handleScroll, true);

It's a simple solution, but I think performances would better if Baker could handle that natively.

Update: This solution works only if you have a page divided in parts that match the size of the screen. If the height of your document doesn't change after a rotation, this is useless, the page stays to the right place after the rotation. This script needs to be improved to check if the page height has changed after the rotation. If this is the case, it has to scroll.

benoitchantre commented 11 years ago

UIWebView in iOS7 can be paginated directly. Maybe there's a better support to handle layout changes after a rotation.

UIWebPaginationBreakingMode The manner in which column- or page-breaking occurs. typedef NS_ENUM(NSInteger, UIWebPaginationBreakingMode) { UIWebPaginationBreakingModePage, UIWebPaginationBreakingModeColumn }; Constants UIWebPaginationBreakingModePage Content respects CSS properties related to page-breaking. Available in iOS 7.0 and later. Declared in UIWebView.h. UIWebPaginationBreakingModeColumn Content respects CSS properties related to column-breaking. Available in iOS 7.0 and later. Declared in UIWebView.h. UIWebPaginationMode The layout of content in the web view, which determines the direction that the pages flow. typedef NS_ENUM(NSInteger, UIWebPaginationMode) { UIWebPaginationModeUnpaginated, UIWebPaginationModeLeftToRight, UIWebPaginationModeTopToBottom, UIWebPaginationModeBottomToTop, UIWebPaginationModeRightToLeft };

folletto commented 11 years ago

Interesting. Well discovered. :)

benoitchantre commented 11 years ago

There's a demo in the session 600 of WWDC 2013.

benoitchantre commented 11 years ago

@folletto do you think it could be a feature for the v4.2 (pagination handled directly by UIWebView)?

folletto commented 11 years ago

I think it won't, but I'm not sure. What's the direct benefit? Did you test it? :)

benoitchantre commented 11 years ago

I would like to test it, but I don't understand how to implement it. We should be able to handle the pagination of UIWebView with CSS ; I think the first benefit would be to reposition the view after a rotation.

Constants UIWebPaginationBreakingModePage Content respects CSS properties related to page-breaking. Available in iOS 7.0 and later.

folletto commented 11 years ago

From the documentation, it appears something like:

webview.paginationMode = UIWebPaginationModeTopToBottom;
webview.paginationBreakingMode = UIWebPaginationBreakingModePage;
webview.gapBetweenPages = 50;

What I'm not sure is what does this mean. I can guess it makes use of CSS properties like page-break-after :D

benoitchantre commented 11 years ago

webview.pageLength = screenHeight should solve this issue (not sure).

When paginationMode is top to bottom or bottom to top, this property represents the height of each page.

The default value is 0, which means the layout uses the size of the viewport to determine the dimensions of the page. Adjusting the value of this property causes a relayout.

folletto commented 11 years ago

Hehe yes we need to test. Even right now, it's not us handling that. We just set a different property inside the WebView ScrollView, and Apple's code does the rest. We can just hope in iOS7 that triggers a different code block.

...even if, did anyone test if the problem is still present in iOS7? Because it might have been fixed by Apple already... :)

benoitchantre commented 11 years ago

Unfortunately, the behavior doesn't change: after a rotation, the page is not positioned properly. Tested with the latest code from the master branch and iOS7 (without the code to use the new pagination API in iOS7).

benoitchantre commented 11 years ago

Here's a start with the new API: in BakerViewController.m, I have added these lines at the end of `webViewDidFinishLoad

if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")) {
    webView.paginationMode = UIWebPaginationModeTopToBottom;
    webView.paginationBreakingMode = UIWebPaginationBreakingModePage; // not required (default value)
    webView.gapBetweenPages = 0;
    webView.pageLength = pageHeight;
}

webView.pageLength value needs then to be adjusted after a rotation (I don't know how to do that properly).

In CSS, you need to add page-break-after:always; where you need a pagination.

folletto commented 11 years ago

Nice. :)

Two things that you might want to check. :)

I would probably put that code still inside a conditional triggered by the book.json, where we use bakerVerticalPagination here: https://github.com/Simbul/baker/blob/master/BakerView/BakerViewController.m#L937

Also, check setCurrentPageHeight, that should manage the part you mention for pageLength. :)

benoitchantre commented 11 years ago

Thanks for the help. I have moved the code in BakerViewController.m as suggested

if (book.bakerVerticalPagination && SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")) {
    webView.paginationMode = UIWebPaginationModeTopToBottom;
    webView.gapBetweenPages = 0;
    webView.pageLength = pageHeight;
}

Now I need to adjust the pageLength value. The pageLength value needs to be equal to the height of the viewport, not the total height of the page.

folletto commented 11 years ago

Clearly. But that function is where the evaluation could be done. If you check where it gets called, it should be already in all the points where it will adapt (creation, rotation). I'm not sure of course, but might be worth checking. ;)

benoitchantre commented 11 years ago

My knowledge in objective-c is limited. I don't know how to the retrieve the webView in that function or at the end of didRotateFromInterfaceOrientation to set the value of pageLength.

Here's a recap of what we have now: When you scroll and then rotate the device, the page is positioned properly. When you scroll again in the new orientation, the value of pageLength is wrong (because not adjusted) and the page the stops scrolling at an unexpected position. The adjustment of pageLength after the rotation should solve that. If you rotate again the device, the page is properly positioned.

benoitchantre commented 11 years ago

I have added this line at the end of the setCurrentPageHeight method

currPage.pageLength = pageHeight;

Unfortunately the page is not positioned properly, but I think it's because the adjustment is not done at the right moment. The adjustment should be done only after a rotation.

benoitchantre commented 11 years ago

Conclusion of my test: the new pagination API doesn't reposition the view after a rotation. We need to manually reposition the page. A test should be done to determine if the size of the page changes after a rotation. If true, we have to reposition the page, but with the assumption that each section match the size of the viewport. If false, we don't need to reposition the page.

folletto commented 11 years ago

Ach, unfortunate :/

benoitchantre commented 11 years ago

@folletto what do you think of this solution on stackoverrflow, or this one?

folletto commented 11 years ago

Yes that's a workaround. Still, it's a pretty bad hack considering that we'd like to avoid "injecting" stuff from Baker as much as possible into the HTML page. :/

benoitchantre commented 11 years ago

I think we have all the ingredients to solve that properly (without Javascript)

What we need:

The process:

@folletto can you confirm that?

folletto commented 11 years ago

I guess that "scroll" will require javascript...

However, I'm not sure that is what work, because the point where the scrolling is might not match where the current page is (is it the top of the page? the middle of it? the bottom?). But I don't have time right now to work on this... not sure.

benoitchantre commented 11 years ago

Ok, thanks for your feedbacks.

anjimi commented 10 years ago

I have done some work on this... I can't find any discussion relating to this issue on the new Baker github so as this issue is still open I will post here, hopefully this will help others.

I have been working this through and testing with a standalone (not newsstand) Baker version 4 so I don't know if there could be differences to the latest Baker. I am also not very Xcode-savvy so there has been a lot of trial-and-error and head-scratching on my part, and I don't know if I'm really doing this the right way. But it works for me. Lol

I made code additions to BakerViewController.h and BakerViewController.m

BakerViewController.h: inside @interface BakerViewController : UIViewController <UIWebViewDelegate, UIScrollViewDelegate, MFMailComposeViewControllerDelegate, modalWebViewDelegate> { I declared a new variable: int currentVerticalPage; after int currentPageHeight;

BakerViewController.m: my code additions below are commented // Ak under #pragma mark - PAGE SCROLLING

- (void)setCurrentPageHeight {
    for (UIView *subview in currPage.subviews) {
        if ([subview isKindOfClass:[UIScrollView class]]) {
            CGSize size = ((UIScrollView *)subview).contentSize;
            NSLog(@"• Setting current page height from %d to %f", currentPageHeight, size.height);
            currentPageHeight = size.height;
            // Ak set vertical page before rotate
            currentVerticalPage = [self getCurrentPageOffset] / pageHeight;
            NSLog(@"• Setting currentVerticalPage %d", currentVerticalPage);
        }
    }
}

under #pragma mark - ORIENTATION

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    // Ak set vertical page before rotate
    [self setCurrentPageHeight];
    // Notify the index view
    [indexViewController willRotate];

    // Notify the current loaded views
    [self webView:currPage setCorrectOrientation:toInterfaceOrientation];
    if (nextPage) [self webView:nextPage setCorrectOrientation:toInterfaceOrientation];
    if (prevPage) [self webView:prevPage setCorrectOrientation:toInterfaceOrientation];

    [self setPageSize:[self getCurrentInterfaceOrientation:toInterfaceOrientation]];
    [self updateBookLayout];
    // Ak handle vertical page offset
    [self handlePageOffsetOnRotate:NO];
}
- (void)handlePageOffsetOnRotate:(BOOL)animating {
    // Ak handle vertical page offset
    int currentPageOffset = [self getCurrentPageOffset];
    if (currentPageOffset > 0)
    {
        int offset = currentVerticalPage * pageHeight;
        NSLog(@"• Scrolling pageOffset from %d to %d", currentPageOffset, offset);
        [self scrollPage:currPage to:[NSString stringWithFormat:@"%d", offset] animating:animating];
    }
}

I found I had to store the value currentVerticalPage before rotating and use that to work out the new offset, as there seems to be some auto correction of the page offset when rotating on the bottom-most vertical page and I couldn't figure out where that was happening. By saving the currentVerticalPage before rotating (via setCurrentPageHeight) it was possible to override that.

With this code there is no delay on the page position adjustment on rotating (as there is with js), it updates as you are rotating - looks really neat.

I'm sure this code could be improved, and I'm not sure how to go about tying it to the vertical-pagination property in book.json - for our app this always needs to apply so this isn't an issue for me. I look forward to any feedback on whether this is correct way to do this, or any improvements.

folletto commented 10 years ago

It might still be worth posting on the other tracker, just with a line of introduction, so others could check that. :)