我试图抓住WebView的屏幕截图,但是,WebView#getDrawingCache(true)是一个昂贵的调用并冻结了UI线程。
然而,任何将此移动到AsyncTask或单独线程的尝试都将导致"必须在UI线程上调用所有WebView方法"错误。
是否有解决方法,以便我可以在后台线程中使用WebView#getDrawingCache(true)而不冻结UI线程?
这是一个崩溃的代码示例
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);
}
这是正在运行的代码:
new TouchTask().execute(id, callback);
这是错误
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)
答案 0 :(得分:5)
要从后台线程获取屏幕截图,您必须使用其他方法。
我们将创建一个由Canvas
支持的Bitmap
,然后询问WebView
或持有WebView
的容器(稍后将讨论为什么需要此功能) ),在这个画布上绘制本身。最后,Bitmap
使用的Canvas
将保留所需的屏幕截图。然后可以将其刷新到存储区。
我修改了您的AsyncTask
来代表裸机实现。目前,它只显示在Bitmap
方法中生成doInBackground(...)
- 在主线程之外。
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();
}
}
}
}
在测试时,我还发现此方法比使用getDrawingCache()
所花费的时间更少。我还没有试过找到原因。
<强> ...or the container holding the WebView(will discuss why you would need this later)...
强>
考虑滚动WebView
的情况。如果您使用上述方法,则获得的屏幕截图将不会准确 - 屏幕截图将显示WebView
,就好像滚动为0
- 对齐顶部&amp;剩下。要在滚动状态下捕获WebView
,请将其放在容器中 - 比如说Framelayout
。除了以下更改之外,该过程仍然类似:我们将在创建位图时使用FrameLayout's
宽度和高度:
Bitmap b = Bitmap.createBitmap(frameLayout.getWidth(),
frameLayout.getHeight(), Bitmap.Config.ARGB_8888);
我们不会询问WebView
,而是要求FrameLayout
自我画出:
frameLayout.draw(c);
那应该这样做。
答案 1 :(得分:1)
这取决于您是尝试进行单次捕获还是连续捕获流。 在这两种情况下,屏幕捕获和位图存储都将在后台线程中完成,以避免冻结Android主线程。
第一个AsyncTask可用于获取并保存单个屏幕截图:
public class SingleScreenshotTask extends AsyncTask<Void, Void, Bitmap> {
View view;
Context context;
public SingleScreenshotTask(View view) {
this.view = view;
this.context = this.view.getContext().getApplicationContext();
}
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
saveBitmap(context, bitmap);
return bitmap;
}
@Override
protected void onPostExecute(Bitmap result) {
super.onPostExecute(result);
// do something more on the UI thread?
}
}
第二个可用于连续保存屏幕截图:(查看Bitmap和Canvas如何重复使用以提高内存效率)
public class ContinuousScreenshotTask extends AsyncTask<Void, Bitmap, Void> {
View view;
Context context;
Bitmap bitmap;
Canvas canvas;
public ContinuousScreenshotTask(View view) {
this.view = view;
this.context = this.view.getContext().getApplicationContext();
bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
canvas = new Canvas(bitmap);
}
@Override
protected Void doInBackground(Void... params) {
while (!isCancelled()) {
try {
// some timeout to avoid cpu overload
Thread.sleep(500);
} catch (InterruptedException e) {
return null;
}
view.draw(canvas);
saveBitmap(context, bitmap);
}
return null;
}
@Override
protected void onProgressUpdate(Bitmap... values) {
super.onProgressUpdate(values);
// do something more on the UI thread?
}
}
AsyncTask都使用此方法将Bitmap保存到内部存储:
public static void saveBitmap(Context context, Bitmap bitmap) {
FileOutputStream out;
try {
out = context.openFileOutput(System.currentTimeMillis() + ".png", Context.MODE_PRIVATE);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
要执行这些任务,您可以执行以下操作:
View yourViewToCapture;
new ScreenShotTask(yourViewToCapture).execute();
或
View yourViewToCapture;
ContinuousScreenshotTask continuousScreenShotTask;
public void toggle() {
if (continuousScreenShotTask == null) {
continuousScreenShotTask = new ContinuousScreenshotTask(webview);
continuousScreenShotTask.execute();
} else {
continuousScreenShotTask.cancel(true);
continuousScreenShotTask = null;
}
}