异步调用WebView#getDrawingCache()而不冻结UI线程

nub*_*ela 9 android android-webview

我正在尝试抓取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);
}
Run Code Online (Sandbox Code Playgroud)

这是运行代码:

new TouchTask().execute(id, callback);
Run Code Online (Sandbox Code Playgroud)

这是错误

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)
Run Code Online (Sandbox Code Playgroud)

Vik*_*ram 5

要从后台线程获取屏幕截图,您必须使用其他方法.

我们将创建一个Canvas由a 支持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();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在测试时,我还发现这种方法比使用时间少getDrawingCache().我还没有试过找到原因.

...or the container holding the WebView(will discuss why you would need this later)...

考虑WebView滚动时的场景.如果您使用上述方法,获得的屏幕截图将不准确 - 屏幕截图将显示WebView滚动为0- 顶部和左侧对齐.要WebView在滚动状态下捕获偶数,请将其放在容器中 - 比方说Framelayout.除了以下更改之外,该过程仍然类似:我们将FrameLayout's在创建位图时使用宽度和高度:

Bitmap b = Bitmap.createBitmap(frameLayout.getWidth(), 
                            frameLayout.getHeight(), Bitmap.Config.ARGB_8888);
Run Code Online (Sandbox Code Playgroud)

WebView我们不会问问,而是要求FrameLayout自己画画:

frameLayout.draw(c);
Run Code Online (Sandbox Code Playgroud)

这应该做到这一点.

  • 这实际上仍然不起作用(在Android L上).这是异常:引起:android.view.ViewRootImpl $ CalledFromWrongThreadException:只有创建视图层次结构的原始线程才能触及其视图. (3认同)
  • 还没有主线程,但我会给这个卷.我觉得这个webview主线程的限制非常愚蠢.每次我想用抽奖做任何事情都要冻结UI. (2认同)