I'm trying to grab the screenshot of a WebView, however, WebView#getDrawingCache(true) is an expensive call and freezes the UI thread.
Nevertheless, any attempt to move this to an AsyncTask or a separate thread, will result in a "All WebView methods must be called on the UI thread" error.
Is there a workaround for this, such that I can work on WebView#getDrawingCache(true) in a background thread without freezing the UI thread?
Here's a some code example that crashes
public class TouchTask extends AsyncTask<Object, Void, Void> {
Runnable completionCallback;
@Override
protected Void doInBackground(Object... objects) {
Integer id = (Integer) objects[0];
completionCallback = (Runnable) objects[1];
touch(id);
return null;
}
@Override
protected void onPostExecute(Void res) {
super.onPostExecute(res);
completionCallback.run();
}
}
/*
Updates the webview's bitmap in cache
*/
public Pair<String, Bitmap> touch(Integer id) {
CustomWebView webView = getWebView(id);
String title = webView.getTitle();
Bitmap screenie = getScreenshotFromWebView(webView);
Pair<String, Bitmap> res = new Pair<String, Bitmap>(title, screenie);
bitmapCache.put(id, res);
return res;
}
public void touchAsync(Integer id, Runnable callback) {
new TouchTask().execute(id, callback);
}
public Bitmap getScreenshotFromWebView(WebView webView) {
webView.setDrawingCacheEnabled(true);
webView.buildDrawingCache(true);
Bitmap screenieBitmap = null;
if ((webView.getDrawingCache(true) != null) && (!webView.getDrawingCache(true).isRecycled())) {
screenieBitmap = webView.getDrawingCache(false);
screenieBitmap = compressScreenshot(webView.getDrawingCache(false));
}
return screenieBitmap;
}
public Bitmap compressScreenshot(Bitmap screenie) {
Display display = activity.getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int width = size.x;
int height = size.y;
return Bitmap.createScaledBitmap(screenie, width / 3, height / 3, true);
}
And here's the running code:
new TouchTask().execute(id, callback);
And here's the error
java.lang.RuntimeException: An error occured while executing doInBackground()
at android.os.AsyncTask$3.done(AsyncTask.java:300)
at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
at java.util.concurrent.FutureTask.run(FutureTask.java:242)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.RuntimeException: java.lang.Throwable: A WebView method was called on thread 'AsyncTask #3'. All WebView methods must be called on the same thread. (Expected Looper Looper (main, tid 1) {16dba051} called on null, FYI main Looper is Looper (main, tid 1) {16dba051})
at android.webkit.WebView.checkThread(WebView.java:2185)
at android.webkit.WebView.getTitle(WebView.java:1321)
at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager.touch(TabManager.java:53)
at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:145)
at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:138)
at android.os.AsyncTask$2.call(AsyncTask.java:288)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:818)
Caused by: java.lang.Throwable: A WebView method was called on thread 'AsyncTask #3'. All WebView methods must be called on the same thread. (Expected Looper Looper (main, tid 1) {16dba051} called on null, FYI main Looper is Looper (main, tid 1) {16dba051})
at android.webkit.WebView.checkThread(WebView.java:2175)
at android.webkit.WebView.getTitle(WebView.java:1321)
at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager.touch(TabManager.java:53)
at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:145)
at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:138)
at android.os.AsyncTask$2.call(AsyncTask.java:288)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:818)
Android System WebView lets applications display browser windows in an app instead of transporting the user to another browser. Android developers use WebView when they want to display webpages in a Google app or other application.
In Android, WebView is a view used to display the web pages in application. This class is the basis upon which you can roll your own web browser or simply use it to display some online content within your Activity. We can also specify HTML string and can show it inside our application using a WebView.
WebView is a view that display web pages inside your application. You can also specify HTML string and can show it inside your application using WebView. WebView makes turns your application to a web application. In order to add WebView to your application, you have to add <WebView> element to your xml layout file.
To obtain a screenshot off a background thread, you'll have to use a different method.
We'll create a Canvas
backed by a Bitmap
and then ask the WebView
, or the container holding the WebView
(will discuss why you would need this later), to draw itself on this canvas. At the end, the Bitmap
used by the Canvas
will hold the required screenshot. It can then be flushed to storage.
I have modified your AsyncTask
to represent the bare-bone implementation. Currently, it only shows that the Bitmap
is being generated inside the doInBackground(...)
method - off the main thread.
public class TouchTask extends AsyncTask<Object, Void, Bitmap> {
@Override
protected Bitmap doInBackground(Object... objects) {
// Create a new Bitmap with dimensions of the WebView
Bitmap b = Bitmap.createBitmap(webView.getWidth(),
webView.getHeight(), Bitmap.Config.ARGB_8888);
// Canvas that is backed by the `Bitmap` and used by webView to draw on
Canvas c = new Canvas(b);
// WebView draws itself
webView.draw(c);
// Return bitmap
// OR: Compress the bitmap here itself
// No need to do that in `onPostExecute(...)`
return b;
}
// There's no need to actually do anything inside `onPostExecute(..)`
// Compressing and saving the bitmap to sdcard can be handled
// in `doInBackground(...)` itself
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
if (bitmap != null) {
FileOutputStream fos;
try {
fos = new FileOutputStream(new File("/sdcard/webview_screenshot.png"));
bitmap.compress(CompressFormat.PNG, 100, fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
}
While testing, I also found that this method takes less time than using getDrawingCache()
. I haven't tried to find the reason yet.
...or the container holding the WebView(will discuss why you would need this later)...
Consider the scenario when the WebView
is scrolled. If you use the method described above, the screenshot obtained will not be accurate - the screenshot will show the WebView
as if the scroll was 0
- aligned top & left. To capture the WebView
even in the scrolled state, place it inside a container - say Framelayout
. The process remains similar, except for the following changes: we will use the FrameLayout's
width and height when creating the Bitmap:
Bitmap b = Bitmap.createBitmap(frameLayout.getWidth(),
frameLayout.getHeight(), Bitmap.Config.ARGB_8888);
Instead of asking the WebView
, we'll ask the FrameLayout
to draw itself:
frameLayout.draw(c);
And that should do it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With