I have a view controller with a UIWebView controller. I'm trying to get the javascript inside the html content of the web view to pass some information to my objective c code. I came across a number of examples online that set window.location in a javascript function and then catch the event generated by setting the view controller to be the web view's delegate and catching it in the shouldStartLoadWithRequest function. Unfortunately I can't get it to work because for me shouldStartLoadWithRequest is never called even though I'm setting the web view's delegate.
My code is as follows:
The interface:
#interface StoryTextViewController : UIViewController <UIWebViewDelegate>
{
IBOutlet UIWebView *storyWebView;
NSString *information;
}
#property (nonatomic, retain) IBOutlet UIWebView *storyWebView;
#property (nonatomic, retain) NSString *information;
#end
In the interface, the *information variable is set by the previous view that calls StoryTextViewController.
The implementation:
#import "StoryTextViewController.h"
#implementation StoryTextViewController
#synthesize storyWebView;
#synthesize information;
- (NSString *) contructHTMLText
{
NSString *HTMLText = #"";
NSArray *words = [information componentsSeparatedByString:#" "];
NSString *header =
#"<html>\n"
"<head>\n"
"<script type=\"text/javascript\">\n"
"function marktext(theSpanID)\n"
"{\n"
" var theSpan=document.getElementById(theSpanID);\n"
" if (theSpan.className == \"noHilite\")\n"
" {\n"
" theSpan.className = \"hilite\";\n"
" window.location = \"true\";\n"
" }\n"
" else\n"
" {\n"
" theSpan.className = \"noHilite\";\n"
" window.location = \"false\";\n"
" }\n"
"}\n"
"</script>\n"
"<style type=\"text/css\">\n"
".hilite\n"
"{\n"
" background-color:red;\n"
" color:white;\n"
" font: bold 60px arial,sans-serif\n"
"}\n"
".noHilite\n"
"{\n"
" background-color:white;\n"
" color:black;\n"
" font: 60px arial,sans-serif\n"
"}\n"
"</style>\n"
"</head>\n"
"<body>\n"
"<div class=\"storyText\">\n";
NSString *tailer =
#"</div>\n"
"</body>\n"
"</html>\n";
HTMLText = [HTMLText stringByAppendingString:header];
for (NSInteger i = 0; i < [words count]; i ++)
{
NSString *tag = [NSString stringWithFormat:#" <span id=\"mytext%d\" class=\"noHilite\" onclick=\"marktext(\'mytext%d\')\">%#</span> \n", i, i, (NSString *)[words objectAtIndex:i]];
HTMLText = [HTMLText stringByAppendingString:tag];
}
HTMLText = [HTMLText stringByAppendingString:tailer];
NSLog(HTMLText);
return HTMLText;
}
// The designated initializer. Override to perform setup that is required before the view is loaded.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
storyWebView.delegate = self;
}
return self;
}
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *HTMLData = [self contructHTMLText];
[storyWebView loadHTMLString:HTMLData baseURL:[NSURL URLWithString: #""]];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
// Release anything that's not essential, such as cached data
}
- (void)dealloc {
[super dealloc];
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
// handle event here
return true;
}
#end
Whats happening is that basically, the constructHTML function takes the text inside the *information variable and wraps every word in the text with some html and javascript such that whenever a word is clicked, a css high-lighting is applied on it. What I want to do is to count the number of high-lighted words. I do this by trying to pass something in the function thats called whenever a word is clicked but like i said, the shouldStartLoadWithRequest method thats supposed to be fired is never executed.
I've seen a lot of people do this sort of thing but I cant seem to figure out why its not running for me
You can put breakpoint on setting delegate
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
storyWebView.delegate = self;
}
I'm pretty sure storyWebView is nil on that moment.
You can fix it in this manner:
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *HTMLData = [self contructHTMLText];
// empty statement
// the trick is that if the view is not loaded from the .nib yet
// it will be loaded and all connections established
if (self.view);
// here is setting delegate
storyWebView.delegate = self;
[storyWebView loadHTMLString:HTMLData baseURL:[NSURL URLWithString: #""]];
}
You should set the delegate in viewDidLoad, not in initWithNibName:bundle:. Alternatively, open up your nib file and control-drag from the web view to your File's Owner, and choose "delegate" from the drop down.
Shouldn't window.location be the value of the target URL you are going to? In your marktext function you're setting it to true or false. shouldStartLoadWithRequest gets called only when a URL request has been received and is about to go somewhere. Not sure setting the location to a boolean is enough to trigger a navigation event. You can set it to a magic URL then return FALSE from the shouldStartLoadWithRequest routine if it's asking to navigate to that specific magic URL and prevent it from actually going somewhere. Valid URLs can then be allowed to go through.
Also might want to take a look at the response to this SO question. There may be some timing issues with window.location.
I also see this problem, because I have set the
articleWeb.userInteractionEnabled = NO;`
you'd better check your code, set
articleWeb.userInteractionEnabled = YES;`
Related
I work on a native Objective-C library and I'm exposing some of the API into JavaScript. The purpose is to make it accessible on both Android and iOS
On iOS, I use JSContext and JSExport as described here
Objective-C
#import JavaScriptCore;
#protocol PersonExports <JSExport>
+ (void)sayHello:(NSString *)name;
#end
#interface Person : NSObject
+ (void)sayHello:(NSString *)name;
#end
Then I add the exposed object to the JSContext of a UIWebView when it finishes loading:
JSContext *context = [self.webView valueForKeyPath:#"documentView.webView.mainFrame.javaScriptContext"];
if (nil != context) {
context[#"Person"] = [Person class];
}
After this, I can access the object from the JavaScript in the loaded page and call the method on it:
JavaScript
<script type="text/javascript">
Person.sayHello(#"Mickey");
</script>
The question is how can I achieve this in a WKWebView?
// Create a WKWebView instance
self.webView = [[WKWebView alloc]initWithFrame:self.view.frame configuration:self.webConfig];
// Delegate to handle navigation of web content
self.webView.navigationDelegate = self;
[self.view addSubview:self.webView];
setup accessor for self.webConfig
#pragma mark - accessors
-(WKWebViewConfiguration*) webConfig {
if (!_webConfig) {
// Create WKWebViewConfiguration instance
_webConfig = [[WKWebViewConfiguration alloc]init];
// Setup WKUserContentController instance for injecting user script
WKUserContentController* userController = [[WKUserContentController alloc]init];
// Add a script message handler for receiving "buttonClicked" event notifications posted from the JS document using window.webkit.messageHandlers.buttonClicked.postMessage script message
[userController addScriptMessageHandler:self name:#"buttonClicked"];
// Get script that's to be injected into the document
NSString* js = [self buttonClickEventTriggeredScriptToAddToDocument];
// Specify when and where and what user script needs to be injected into the web document
WKUserScript* userScript = [[WKUserScript alloc]initWithSource:js injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
// Add the user script to the WKUserContentController instance
[userController addUserScript:userScript];
// Configure the WKWebViewConfiguration instance with the WKUserContentController
_webConfig.userContentController = userController;
}
return _webConfig;
}
implement script message handler method from protocol
#pragma mark -WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:#"buttonClicked"]) {
self.buttonClicked ++;
}
// JS objects are automatically mapped to ObjC objects
id messageBody = message.body;
if ([messageBody isKindOfClass:[NSDictionary class]]) {
NSString* idOfTappedButton = messageBody[#"ButtonId"];
[self updateColorOfButtonWithId:idOfTappedButton];
}
}
and post the message form js like this
var button = document.getElementById("clickMeButton");
button.addEventListener("click", function() {
var messgeToPost = {'ButtonId':'clickMeButton'};
window.webkit.messageHandlers.buttonClicked.postMessage(messgeToPost);
},false);
this will help you because i also faced this issue while setting context nad calling method form JSExport
I want to save the state where the user left the webView (which is loading a local html file) so that the next time when he enters the same view, he is taken to the same line of text that he was reading the previous time.
I think that could be achieved by saving the scroll value in an integer every time the user swipe the document, and when he enters the view the next time we use the value that was saved previously and scroll the view using a javascript command or "CG" things.
Any idea?
and an objective c solution:
#interface ViewController () <UIWebViewDelegate>
#property (weak, nonatomic) IBOutlet UIWebView *webView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://yourwebsite.com"]]];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if ([userDefaults objectForKey:#"contentOffset"]) {
webView.scrollView.contentOffset = CGPointFromString([userDefaults objectForKey:#"contentOffset"]);
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:NSStringFromCGPoint(self.webView.scrollView.contentOffset) forKey:#"contentOffset"];
[userDefaults synchronize];
}
From Lyndsey Scott's Answer:
To save and retrieve your scrollview offset from NSUserDefaults, set your UIWebView's delegate and try this:
var viewLaidoutSubviews = false // <-- variable to prevent the viewDidLayoutSubviews code from happening more than once
// Save your content offset when the view disappears
override func viewWillDisappear(animated: Bool) {
var userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setValue(NSStringFromCGPoint(myBrowser.scrollView.contentOffset), forKey: "scroll_offset")
userDefaults.synchronize()
viewLaidoutSubviews = false
}
// Retrieve and set your content offset when the view re-appears
// and its subviews are first laid out
override func viewDidLayoutSubviews() {
if (!viewLaidoutSubviews) {
// If a scroll_offset is store in NSUserDefaults, use it
var userDefaults = NSUserDefaults.standardUserDefaults()
var scrollOffset:CGPoint? = CGPointFromString(userDefaults.valueForKey("scroll_offset") as? NSString)
if let position:CGPoint = scrollOffset {
myBrowser.scrollView.delegate = self
myBrowser.scrollView.setContentOffset(position, animated: false)
}
viewLaidoutSubviews = true
}
}
And utilize your UIWebView's webViewDidFinishLoad delegate method to update the scroll offset in the case that the web view didn't finish rendering before the view laid out its subviews:
// Retrieve and set your content offset once the web view has
// finished rendering (in case it hasn't finished rendering
// by the time the view's subviews were laid out)
func webViewDidFinishLoad(webView: UIWebView) {
// If a scroll_offset is store in NSUserDefaults, use it
var userDefaults = NSUserDefaults.standardUserDefaults()
var scrollOffset:CGPoint? = CGPointFromString(userDefaults.valueForKey("scroll_offset") as? NSString)
if let position:CGPoint = scrollOffset {
myBrowser.scrollView.delegate = self
myBrowser.scrollView.setContentOffset(position, animated: true)
}
}
But note, your NSUserDefaults value will also persist between app launches unless you delete it.
Is it possible to change the JavaScript alert title in a UIWebview in iPhone?
Basically you cannot change the title, but recently I have found a way to show an alert dialog with no title.
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", 'data:text/plain,');
document.documentElement.appendChild(iframe);
window.frames[0].window.alert('hello');
iframe.parentNode.removeChild(iframe);
I present three separate solutions to this problem. The best solution for production code is the one described by #Martin H. My only addition to this is a concrete example showing how to implement it.
My other solutions depend on undocumented behaviors of UIWebView, but don't require any changes to the content/JS.
Solution 1: Here is a concrete example demonstrating the technique put forth by #Martin H. This is likely the best solution as it does not rely on UIWebView / UIAlertView undocumented behaviors.
#interface TSViewController () <UIWebViewDelegate>
#end
#implementation TSViewController
- (UIWebView*) webView
{
return (UIWebView*) self.view;
}
- (void) loadView
{
UIWebView* wv = [[UIWebView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)];
wv.delegate = self;
wv.scalesPageToFit = YES;
self.view = wv;
}
- (void) viewDidLoad
{
[super viewDidLoad];
[self.webView loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString: #"http://www.craigslist.org"]]];
}
- (void) webViewDidFinishLoad: (UIWebView *) webView
{
// inject some js to re-map window.alert()
// ideally just do this in the html/js itself if you have control of that.
NSString* js = #"window.alert = function(message) { window.location = \"http://action/alert?message=\" + message; }";
[webView stringByEvaluatingJavaScriptFromString: js];
// trigger an alert. for demonstration only:
[webView stringByEvaluatingJavaScriptFromString: #"alert('hello, world');" ];
}
- (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL* url = request.URL;
// look for our custom action to come through:
if ( [url.host isEqualToString: #"action"] && [url.path isEqualToString: #"/alert"] )
{
// parse out the message
NSString* message = [[[[url query] componentsSeparatedByString: #"="] lastObject] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
// show our alert
UIAlertView* av = [[UIAlertView alloc] initWithTitle: #"My Custom Title"
message: message
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[av show];
return NO;
}
return YES;
}
Solution 2: First, let me say that I would never personally use the following solution in production code. The best way to achieve this functionality is to do what #Martin H suggests in his answer, or possibly what #twk suggests if an empty title is actually what is desired. If you don't have control over the web content itself, perhaps you could inject #Martin H's code after the content has been loaded.
That said, this is otherwise achievable if we make some assumptions. First, that the Javascript alert() method indeed maps to a real UIAlertView. (In fact it does!) Second, that we can come up with some mechanism to discern an alert()-sourced UIAlertView from other application-sourced UIAlertViews. (We can!)
My approach is to swizzle the UIAlertView. I know - swizzling sucks and has all sorts of drawbacks. I don't swizzle in production apps if I can help it. But for this science project, we're going to swizzle the UIAlertView setDelegate: method.
What I found is that 1) the UIWebView will construct an UIAlertView to display the javascript alert, and 2) the UIAlertView title is set before the UIAlertView delegate is set. By swizzling UIAlertView setDelegate: with my own method I can accomplish two things: 1) I can determine that the UIAlertView is being commissioned on behalf of a UIWebView (just inspect the delegate...) and 2) I have opportunity to re-set the title before the alertview is shown.
You might ask "Why not just swizzle UIAlertview -show method?" That would be great, but in my experimenting I found that the UIWebView never invoked show. It must be calling some other internal method, and I didn't investigate further.
My solution implements a category on UIAlertView, which adds a couple of class methods. Basically you register a UIWebView with these methods, and provide a replacement title to be used. Alternatively you can provide a callback block that returns a title when invoked.
I used the iOS6 NSMapTable class to keep a set of UIWebView-to-Title mappings, where the UIWebView is a weak key. This way I don't ever have to unregister my UIWebView and everything gets cleaned up nicely. Thus, this current implementation is iOS6-only.
Here's the code, shown in-use in a basic view controller:
#import <objc/runtime.h>
typedef NSString*(^TSAlertTitleBlock)(UIWebView* webView, NSString* defaultTitle );
#interface UIAlertView (TS)
#end
#implementation UIAlertView (TS)
NSMapTable* g_webViewMapTable;
+ (void) registerWebView: (UIWebView*) webView alertTitleStringOrBlock: (id) stringOrBlock
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
g_webViewMapTable = [NSMapTable weakToStrongObjectsMapTable];
// $wizzle
Method originalMethod = class_getInstanceMethod(self, #selector(setDelegate:));
Method overrideMethod = class_getInstanceMethod(self, #selector(setDelegate_ts:));
method_exchangeImplementations(originalMethod, overrideMethod);
});
[g_webViewMapTable setObject: [stringOrBlock copy] forKey: webView];
}
+ (void) registerWebView: (UIWebView*) webView alertTitle: (NSString*) title
{
[self registerWebView: webView alertTitleStringOrBlock: title];
}
+ (void) registerWebView: (UIWebView*) webView alertTitleBlock: (TSAlertTitleBlock) alertTitleBlock
{
[self registerWebView: webView alertTitleStringOrBlock: alertTitleBlock];
}
- (void) setDelegate_ts: (id) delegate
{
// call the original implementation
[self setDelegate_ts: delegate];
// oooh - is a UIWebView asking for this UIAlertView????
if ( [delegate isKindOfClass: [UIWebView class]] )
{
// see if we've registered a title/title-block
for ( UIWebView* wv in g_webViewMapTable.keyEnumerator )
{
if ( wv != delegate)
continue;
id stringOrBlock = [g_webViewMapTable objectForKey: wv];
if ( [stringOrBlock isKindOfClass: NSClassFromString( #"NSBlock" )] )
{
stringOrBlock = ((TSAlertTitleBlock)stringOrBlock)( wv, self.title );
}
NSParameterAssert( [stringOrBlock isKindOfClass: [NSString class]] );
[self setTitle: stringOrBlock];
break;
}
}
}
#end
#interface TSViewController () <UIWebViewDelegate>
#end
#implementation TSViewController
- (UIWebView*) webView
{
return (UIWebView*) self.view;
}
- (void) loadView
{
UIWebView* wv = [[UIWebView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)];
wv.delegate = self;
wv.scalesPageToFit = YES;
self.view = wv;
}
- (void) viewDidLoad
{
[super viewDidLoad];
// method 1: bind a title to a webview:
[UIAlertView registerWebView: self.webView alertTitle: nil];
/*
// method 2: return a title each time it's needed:
[UIAlertView registerWebView: self.webView
alertTitleBlock: ^NSString *(UIWebView *webView, NSString* defaultTitle) {
return #"Custom Title";
}];
*/
[self.webView loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString: #"http://www.craigslist.org"]]];
}
- (void) webViewDidFinishLoad: (UIWebView *) webView
{
// trigger an alert
[webView stringByEvaluatingJavaScriptFromString: #"alert('hello, world');" ];
}
#end
Solution 3: Upon reflection, here's a simpler technique than I initially described in Solution 2. It still depends on the undocumented fact that the javascript alert is implemented using a UIAlertView that has its delegate set to the sourcing UIWebView. For this solution, simply subclass UIWebView and implement your own delegate method for UIAlertView willPresentAlertView:, and when it is called, reset the title to whatever you wish.
#interface TSWebView : UIWebView
#end
#implementation TSWebView
- (void) willPresentAlertView:(UIAlertView *)alertView
{
if ( [self.superclass instancesRespondToSelector: #selector( willPresentAlertView:) ])
{
[super performSelector: #selector( willPresentAlertView:) withObject: alertView];
}
alertView.title = #"My Custom Title";
}
#end
#interface TSViewController () <UIWebViewDelegate>
#end
#implementation TSViewController
- (UIWebView*) webView
{
return (UIWebView*) self.view;
}
- (void) loadView
{
UIWebView* wv = [[TSWebView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)];
wv.delegate = self;
wv.scalesPageToFit = YES;
self.view = wv;
}
- (void) viewDidLoad
{
[super viewDidLoad];
[self.webView loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString: #"http://www.craigslist.org"]]];
}
- (void) webViewDidFinishLoad: (UIWebView *) webView
{
// trigger an alert
[webView stringByEvaluatingJavaScriptFromString: #"alert('hello, world');" ];
}
#end
No you can't do this in a Javascript alert.
But if the Javascript is yours then instead of calling the alert you could instead call a function that calls Objective C and invoke an iOS native alert.
If the Javascript isn't yours then using UIWebView you can inject some Javascript to override the default behaviour and change it to call an iOS native alert i.e. something like this
window.alert = function(message) {
window.location = "myScheme://" + message"
};
Then look for myScheme and extract message in UIWebView's shouldStartLoadWithRequest
Here's the standard method for invoking Objective-C from Javascript
How to call Objective-C from Javascript?
For what it's worth, Phonegap/Cordova offers a navigator.notification.alert function. It allows specifying a custom title.
Just make a class for your UIWebView and make willPresentAlertView (alertView: UIAlertView) function in it. After that set alertView.title = "your fav title" and it is done.
import UIKit
class webviewClass: UIWebView {
func willPresentAlertView(alertView: UIAlertView) {
alertView.title = "my custum title"
}
}
After that when ever an alert or confirm and... shown, your custom title will shown.
Going off of what #twk wrote, I simply overwrite the alert function. Works great on ios7.
function alert(words){
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", 'data:text/plain,');
document.documentElement.appendChild(iframe);
window.frames[0].window.alert(words);
iframe.parentNode.removeChild(iframe);
}
Use cordova-plugin-dialogs. It allows to create native dialogs.
Below code will create native alert:
navigator.notification.alert(message, alertCallback, [title], [buttonName])
How can I split the content of a HTML file in screen-sized chunks to "paginate" it in a WebKit browser?
Each "page" should show a complete amount of text. This means that a line of text must not be cut in half in the top or bottom border of the screen.
Edit
This question was originally tagged "Android" as my intent is to build an Android ePub reader. However, it appears that the solution can be implemented just with JavaScript and CSS so I broadened the scope of the question to make it platform-independent.
Building on Dan's answer here is my solution for this problem, with which I was struggling myself until just now. (this JS works on iOS Webkit, no guarantees for android, but please let me know the results)
var desiredHeight;
var desiredWidth;
var bodyID = document.getElementsByTagName('body')[0];
totalHeight = bodyID.offsetHeight;
pageCount = Math.floor(totalHeight/desiredHeight) + 1;
bodyID.style.padding = 10; //(optional) prevents clipped letters around the edges
bodyID.style.width = desiredWidth * pageCount;
bodyID.style.height = desiredHeight;
bodyID.style.WebkitColumnCount = pageCount;
Hope this helps...
Speaking from experience, expect to put a lot of time into this, even for a barebones viewer. An ePub reader was actually first big project I took on when I started learning C#, but the ePub standard is definitely pretty complex.
You can find the latest version of the spec for ePub here:
http://www.idpf.org/specs.htm
which includes the OPS (Open Publication Structure), OPF (Open Packaging Format), and OCF (OEBPS Container Format).
Also, if it helps you at all, here is a link to the C# source code of the project I started on:
https://www.dropbox.com/sh/50kxcr29831t854/MDITIklW3I/ePub%20Test.zip
It's not fleshed out at all; I haven't played with this for months, but if I remember correctly, just stick an ePub in the debug directory, and when you run the program just type some part of the name (e.g. Under the Dome, just type "dome") and it will display the details of the book.
I had it working correctly for a few books, but any eBooks from Google Books broke it completely. They have a completely bizarre implementation of ePub (to me, at least) compared to books from other sources.
Anyway, hopefully some of the structural code in there might help you out!
I've had to code something like this too, and my (working) solution is this:
You have to apply these lines to the webview...
webView_.getSettings().setUseWideViewPort(true);
webView_.getSettings().setLayoutAlgorithm(LayoutAlgorithm.NARROW_COLUMNS);
Also, you have to inject some javascript. I've had tons of problems with the differents scales of my activity and the content rendered in the webview, so my solution doesn't take any kind of value from "outside".
webView_.setWebViewClient(new WebViewClient(){
public void onPageFinished(WebView view, String url) {
injectJavascript();
}
});
[...]
public void injectJavascript() {
String js = "javascript:function initialize() { " +
"var d = document.getElementsByTagName('body')[0];" +
"var ourH = window.innerHeight; " +
"var ourW = window.innerWidth; " +
"var fullH = d.offsetHeight; " +
"var pageCount = Math.floor(fullH/ourH)+1;" +
"var currentPage = 0; " +
"var newW = pageCount*ourW; " +
"d.style.height = ourH+'px';" +
"d.style.width = newW+'px';" +
"d.style.webkitColumnGap = '2px'; " +
"d.style.margin = 0; " +
"d.style.webkitColumnCount = pageCount;" +
"}";
webView_.loadUrl(js);
webView_.loadUrl("javascript:initialize()");
}
Enjoy :)
I recently attempted something similar to this and added some CSS styling to change the layout to horizontal instead of vertical. This gave me the desired effect without having to modify the content of the Epub in any way.
This code should work.
mWebView.setWebViewClient(new WebViewClient() {
public void onPageFinished(WebView view, String url) {
// Column Count is just the number of 'screens' of text. Add one for partial 'screens'
int columnCount = Math.floor(view.getHeight() / view.getWidth())+1;
// Must be expressed as a percentage. If not set then the WebView will not stretch to give the desired effect.
int columnWidth = columnCount * 100;
String js = "var d = document.getElementsByTagName('body')[0];" +
"d.style.WebkitColumnCount=" + columnCount + ";" +
"d.style.WebkitColumnWidth='" + columnWidth + "%';";
mWebView.loadUrl("javascript:(function(){" + js + "})()");
}
});
mWebView.loadUrl("file:///android_asset/chapter.xml");
So, basically you're injecting JavaScript to change the styling of the body element after the chapter has been loaded (very important). The only downfall to this approach is when you have images in the content the calculated column count goes askew. It shouldn't be too hard to fix though. My attempt was going to be injecting some JavaScript to add width and height attributes to all images in the DOM that don't have any.
Hope it helps.
-Dan
I was able to improve Nacho's solution to get horizontal swipe paging effect with WebView. You can find solution here and example code.
Edit:
Solution code.
MainActivity.java
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
wv = (HorizontalWebView) findViewById(R.id.web_view);
wv.getSettings().setJavaScriptEnabled(true);
wv.setWebViewClient(new WebViewClient() {
public void onPageFinished(WebView view, String url) {
injectJavascript();
}
});
wv.setWebChromeClient(new WebChromeClient() {
#Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
int pageCount = Integer.parseInt(message);
wv.setPageCount(pageCount);
result.confirm();
return true;
}
});
wv.loadUrl("file:///android_asset/ch03.html"); // now it will not fail here
}
private void injectJavascript() {
String js = "function initialize(){\n" +
" var d = document.getElementsByTagName('body')[0];\n" +
" var ourH = window.innerHeight;\n" +
" var ourW = window.innerWidth;\n" +
" var fullH = d.offsetHeight;\n" +
" var pageCount = Math.floor(fullH/ourH)+1;\n" +
" var currentPage = 0;\n" +
" var newW = pageCount*ourW;\n" +
" d.style.height = ourH+'px';\n" +
" d.style.width = newW+'px';\n" +
" d.style.margin = 0;\n" +
" d.style.webkitColumnCount = pageCount;\n" +
" return pageCount;\n" +
"}";
wv.loadUrl("javascript:" + js);
wv.loadUrl("javascript:alert(initialize())");
}
In my WebChromeClient's onJsAlert get the number of horizontal pages which i pass to the custom HorizontalWebView to implement paging effect
HorizontalWebView.java
public class HorizontalWebView extends WebView {
private float x1 = -1;
private int pageCount = 0;
public HorizontalWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
break;
case MotionEvent.ACTION_UP:
float x2 = event.getX();
float deltaX = x2 - x1;
if (Math.abs(deltaX) > 100) {
// Left to Right swipe action
if (x2 > x1) {
turnPageLeft();
return true;
}
// Right to left swipe action
else {
turnPageRight();
return true;
}
}
break;
}
return true;
}
private int current_x = 0;
private void turnPageLeft() {
if (getCurrentPage() > 0) {
int scrollX = getPrevPagePosition();
loadAnimation(scrollX);
current_x = scrollX;
scrollTo(scrollX, 0);
}
}
private int getPrevPagePosition() {
int prevPage = getCurrentPage() - 1;
return (int) Math.ceil(prevPage * this.getMeasuredWidth());
}
private void turnPageRight() {
if (getCurrentPage() < pageCount - 1) {
int scrollX = getNextPagePosition();
loadAnimation(scrollX);
current_x = scrollX;
scrollTo(scrollX, 0);
}
}
private void loadAnimation(int scrollX) {
ObjectAnimator anim = ObjectAnimator.ofInt(this, "scrollX",
current_x, scrollX);
anim.setDuration(500);
anim.setInterpolator(new LinearInterpolator());
anim.start();
}
private int getNextPagePosition() {
int nextPage = getCurrentPage() + 1;
return (int) Math.ceil(nextPage * this.getMeasuredWidth());
}
public int getCurrentPage() {
return (int) (Math.ceil((double) getScrollX() / this.getMeasuredWidth()));
}
public void setPageCount(int pageCount) {
this.pageCount = pageCount;
}
}
Maybe it would work to use XSL-FO. This seems heavy for a mobile device, and maybe it's overkill, but it should work, and you wouldn't have to implement the complexities of good pagination (e.g. how do you make sure that each screen doesn't cut text in half) yourself.
The basic idea would be:
transform the XHTML (and other EPUB stuff) to XSL-FO using XSLT.
use an XSL-FO processor to render the XSL-FO into a paged format that you can display on the mobile device, such as PDF (can you display that?)
I don't know whether there is an XSL-FO processor available for Android. You could try Apache FOP. RenderX (XSL-FO processor) has the advantage of having a paged-HTML output option, but again I don't know if it could run on Android.
There is several ways this could be done. If every line is in its own element all you have to do is to check if one of it's edges goes outside of the view (either the browsers, or the "book page").
If you want to know how many "pages" there is going to be in advance, just temporary move them into the view and get what line a page ends. This could potentially be slow because of that page reflow is needed for the browser to know where anything is.
Otherwise I think that you could use the HTML5 canvas element to measure text and / or draw text.
Some info on that here:
https://developer.mozilla.org/en/Drawing_text_using_a_canvas
http://uupaa-js-spinoff.googlecode.com/svn/trunk/uupaa-excanvas.js/demo/8_2_canvas_measureText.html
Had this same problem recently and inspired by the answers found a plain CSS solution using CSS3's column-* attributes:
/* CSS */
.chapter {
width: 600px;
padding: 60px 10px;
-webkit-column-gap: 40px;
-webkit-column-width: 150px;
-webkit-column-count: 2;
height:400px;
}
/* HTML */
<div class="chapter">
your long lorem ipsum arbitrary HTML
</div>
The example above gives great results on a retina iPhone. Playing around with the different attributes yields in different spacing between the pages and such.
If you need to support multiple chapters for instance which need to start on new pages, there's an XCode 5 example on github: https://github.com/dmrschmidt/ios_uiwebview_pagination
You could split the pages in separate XHTML files and store them in a folder. Eg: page01, page02. You can then render those pages one by one underneath each other.
You can look at http://www.litres.ru/static/OR/or.html?data=/static/trials/00/42/47/00424722.gur.html&art=424722&user=0&trial=1 but the code may be heavily obfuscated, so just use Firebug to inspect DOM.
If the link isn't working, comment - would give you fixed.
I have this code-
/**
Save the web view as a screenshot. Currently only supports saving to
the photo library.
/
- (void)saveScreenshot:(NSArray)arguments withDict:(NSDictionary*)options
{
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGRect imageRect = CGRectMake(0, 0, CGRectGetWidth(screenRect),
CGRectGetHeight(screenRect));
UIGraphicsBeginImageContext(imageRect.size);
[webView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageWriteToSavedPhotosAlbum(viewImage, self, nil, nil);
UIAlertView *alert= [[UIAlertView alloc] initWithTitle:nil
message:#"Image Saved" delegate:self cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}
This is for saving whatever you drew in my app. How would I add the button for this in the HTML code. How do i call from it?
So you are trying to send an event from html to a UIWebView?
The easiest way is to use a private scheme. In the html:
Take Picture
In the UIWebView delegate:
-(BOOL) webView:(UIWebView *)inWeb shouldStartLoadWithRequest:(NSURLRequest *)inRequest navigationType:(UIWebViewNavigationType)inType {
NSString *string = [[[inRequest URL] absoluteString] lowercaseString];
if ( [string isEqualToString:#"myscheme:save_picture"] ) {
[self savePicture];
return NO;
}
return YES;
}
You can look at PhoneGap for a more robust usage of this mechanism.