Android WebView renders blank/white, view doesn't update on css changes or HTML changes, animations are choppy

45,086

Solution 1

I implemented kyle's solution and it solved the problem. Howewer I noticed a huge battery drain on android 4.0.4 when the app was open. Also after the change I had users complaining that the swiftKey keyboard was not working with my app anymore.

Every change in my app are triggered by a user action so I came up with a modified version that only trigger invalidate() after a touchEvent:

    Handler handler = new Handler();
    public boolean onTouchEvent (MotionEvent event){
        super.onTouchEvent(event);
        handler.postDelayed(triggerInvalidate, 60);
            handler.postDelayed(triggerInvalidate, 300);
        return true;
    }

    private Runnable triggerInvalidate=new Runnable(){
        public void run(){
            invalidate();
        }
    };

Never did any programming with Java so there might be a better solution to do this.

Solution 2

Note:
There's a better solution as of Android 4.4+. It's a drop-in WebView replacement called CrossWalk. It uses the latest Chromium-kit and it's fantastic. You can read up about it here: crosswalk-project.org

Also, it appears that since Android 4.4, the invalidate() solution is no longer necessary and you can get away with using some of the other safer answer. I would only use this invalidate() approach as a last-ditch effort.


I'm answering my own question to hopefully help people out with the same issues as me.

I've tried several methods to making things better, even the all notorious -webkit-transform: translate3d(0,0,0); Even that didn't work all too well.

Let me share with you what did work.

First, use the most recent API. I'm using API 15. In your AndroidManifest.xml, make sure to enable hardware acceleration. If your version of API does not support this, then move on to the next bit.

If your version does support it, you can enable it by modifying your manifest:

<application
   ...
   android:hardwareAccelerated="true">

Also, make sure that your manifest has the minimum supported API to the one that you are using. Since I'm using API 15, this is what my manifest has:

<uses-sdk
    android:minSdkVersion="15"
    android:targetSdkVersion="15" />

(Update: you should now modify that values in your build.gradle)

Now, in your primary CSS file for what will be presented in a WebView, add something like this:

body div {
    -webkit-transform: translate3d(0,0,0);
}

Add onto the body div bit with any other element types you have; you can probably exclude images, ul, li, etc. The reason for applying this CSS style to everything is just by trial and error, and I found that brute-applying it to everything appears to work the best. With a larger DOM tree, you may need to be more-specific. I'm not sure what the specification would be, however.

When you instantiate your WebView, there are some settings you'll want to set:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    super.loadUrl("file:///android_asset/www/index.html");
    appView.getSettings().setRenderPriority(RenderPriority.HIGH);
    appView.getSettings()
            .setPluginState(WebSettings.PluginState.ON_DEMAND);
}

Second to last, but crucial bit: I was reading through the source code for the WebView class and found this little tiny comment about force redrawing. There is a static final boolean, that when set to true will force the view to always redraw. I'm not huge on Java syntax, but I don't think you can directly change a static final attribute of a class. So what I ended up doing was I extended the class like so:

import org.apache.cordova.CordovaWebView;

import android.content.Context;
import android.graphics.Canvas;

public class MyWebView extends CordovaWebView {
    public static final String TAG = "MyWebView";

    public MyWebView(Context context) {
        super(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Warning! This will cause the WebView to continuously be redrawn
        // and will drain the devices battery while the view is displayed!
        invalidate();
    }

}

Keep in mind, I'm using Cordova/PhoneGap, so I had to extend from the CordovaWebView. If you see in the onDraw method, there is a call to invalidate. This will cause the view to redraw constantly. I highly recommend adding in logic to only redraw when you need it, however.

There is one final step, if you are using Cordova. You have to tell PhoneGap to use your new WebView class instead of their own WebView class. In your MainActivity class, add this:

public void init(){
    CordovaWebView webView = new MyWebView(MainActivity.this);
    super.init(webView, new CordovaWebViewClient(this, webView), new CordovaChromeClient(this, webView));
}

That's it! Try and run your app and see if everything is much smoother. Before doing all of these changes, the pages would appear white, no CSS changes would be applied until after tapping on the screen again, animations were super choppy or not even noticeable. I should mention that the animations are still choppy, but far much less choppy than before.

If anyone has anything to add to this, just comment under. I'm always open for optimizations; and I'm fully aware there may be better ways to do what I have just recommended.

If my above solution did not work for you, could you describe your specific situation and what results you are seeing with Androids WebView?

Lastly, I have enabled this answer as a "community wiki", so anyone and everyone, feel free to make adjustments.

Thanks!


Edit:

With the most latest PhoneGap, you'll need to have your init() method look more like this:

public void init(){
    CordovaWebView webView = new MyWebView(MainActivity.this);
    super.init(webView, new IceCreamCordovaWebViewClient(this, webView), new CordovaChromeClient(this, webView));
}

Solution 3

For me this issue was only happening on Samsung devices. I was able to fix it by disabling Hardware Acceleration for WebViews:

webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

Hope it helps.

Solution 4

re: the redraw problem, you can force a redraw by reading a property from the element

so say you do this:

$('#myElement').addClass('foo'); // youre not seeing this take effect

if you do this afterwards:

$('#myElement').width();

it will force a redraw.

This way you can be selective instead of redrawing the whole page all the time, which is expensive

Solution 5

As pointed out above and elsewhere - overriding View.onDraw() and calling View.invalidate will make for an unhappy battery / app performance will drop. You can also do a manual invalidate call ever x ms like so

/**
 * Due to bug in 4.2.2 sometimes the webView will not draw its contents after the data has loaded. 
 * Triggers redraw. Does not work in webView's onPageFinished callback for some reason
 */
private void forceWebViewRedraw()
{
    mWebView.post(new Runnable() {
        @Override
        public void run()
        {
            mWebView.invalidate();
            if(!isFinishing())
                mWebView.postDelayed(this, 1000);
        }
    });
}

I tried putting an invalidate call in WebViewClient.onPageLoaded() but this does not seem to work. While my solution could be better its simple and it works for me (im just showing a twitter login)

Share:
45,086
Kyle
Author by

Kyle

Updated on July 09, 2022

Comments

  • Kyle
    Kyle almost 2 years

    When ever I make a css class change, the changes don't always appear. This appears to happen when I have a touch event that adds something like a down class name to a button. The button doesn't update, and neither does anything else on the page. It's very inconstant in when it works. I've also noticed that sometimes my elements appear white with no content or anything. This is very frustrating!

  • Simon MacDonald
    Simon MacDonald over 11 years
    I'm going to ask Joe to look at your answer and maybe this would be a good idea to be included in the CordovaWebView code.
  • rupps
    rupps about 11 years
    By the way... all this is about to change yet another more time, Google is about to change the WebView component to use Chromium, so ... I'm really expectant to see what will be broken again... For me it was an absolute disaster the change from HoneyComb to ICS. I'm scared :S :S :S
  • rupps
    rupps about 11 years
    very nice approach !!! I think it really hits the spot and it's clean! The only better solution I can think of is Google releasing a properly working WebView and NEVER touch it again. It's really discouraging for indy developers like me to see the apps broken every other sunday just because. And, you know, get ready for the HUGE change in next Android version, they threaten us with a TOTAL webview change based on Chromium ... And in my experience Chrome on Android is slower than the stock webview...
  • VicVu
    VicVu almost 11 years
    Just a note, calling invalidate on onDraw means that your webview will be continuously redrawing forever. Not the best idea.
  • Kyle
    Kyle almost 11 years
    @Vee, At the time, I did not know what invalidate actually did. Do you have a solution that you think would work better? Please share.
  • VicVu
    VicVu almost 11 years
    Yes, Olivier's response below this is much better. It only invalidates when it needs to.
  • VicVu
    VicVu almost 11 years
    The battery drain is due to the view being continuously redrawn. This solution completely mitigates that. I would strongly suggest NOT using the selected answer because of this.
  • DarkLeafyGreen
    DarkLeafyGreen almost 11 years
    +1 Its definitely smoother now. BTW why is hardware acceleration needed for a simple webapp? It will load up the open gl drivers and consume more memory.
  • Bahadır Yıldırım
    Bahadır Yıldırım over 10 years
    @artworkadシ Using graphical acceleration will almost always use less power. Only disable hardware acceleration if you have a compelling reason to do so (for instance, for a legacy app that relies on unsupported techniques, such as some Porter Duff operations).
  • Codeversed
    Codeversed over 10 years
    -webkit-transform: translate3d(0,0,0); saves the day ..even on Chromium. This corrects the rendering issues for devices that go blank after turning on hardware acceleration. I use setLayerType(View.LAYER_TYPE_NONE, null); so this does not happen on certain devices.
  • Dekra
    Dekra about 10 years
    Where did you call the method forceWebViewRedraw() exaclty ?
  • Dori
    Dori about 10 years
    you could call it in the onPageFinished callback. The method comment refers to not being able to call mWebView.invalidate(); in onPageFinished
  • ydnas
    ydnas about 10 years
    What is the role of "isFinishing()" here? @Dori
  • Dori
    Dori about 10 years
    so the view keeps drawing until the activity is finishing, in this case i think finish() was being called manually. You wouldnt want to start a runnable inside an activity that runs forever else you will get an exception when the view is eventually detached...
  • Thomas Lee
    Thomas Lee over 9 years
    invalidate() doesn't work for all the cases but -webkit-transform: translate3d(0,0,0); works great.
  • Veaceslav Gaidarji
    Veaceslav Gaidarji almost 9 years
    worked for me, thanks. I'm calling redraw just once, no need in delayed redraws, it's enough.
  • Samuel Willems
    Samuel Willems over 8 years
    Wow I am so glad I ran in to this, the other solutions drained my battery for no reason, even when limiting the redraws per second. This answer should be wayyy up there!
  • Md Mohsin
    Md Mohsin almost 8 years
    Thanks, it works. I am using canvas in cordova webview.
  • Jan Heinrich Reimer
    Jan Heinrich Reimer almost 8 years
    Invalidating a View in its onDraw() is a thing you should NOT do in Android! As @VicVu said, it ends up in an infinite loop redrawing the view again and again. It'll consume enormous amounts of memory and will force the GPU to work constantly. It's pretty much the worst case that can happen to an app, so never ever do that. I'm pretty amazed this answer got so many upvotes. @Kyle you should definitely remove that "invalidate-trick" from your answer!
  • Kyle
    Kyle almost 8 years
    @Heinrich I've added a pretty obvious disclaimer at the top :)
  • Jan Heinrich Reimer
    Jan Heinrich Reimer almost 8 years
    @Kyle I don't think that disclaimer is the way to go. Your answer lists some different approaches. Some of them are really good so what speaks against simply removing the invalidate()-hack and just keep the other approaches. This would be the obvious solution as the invalidate()-hack is clearly an anti-pattern, a thing you should never do or recommend to other devs. Just keep the hardware acelleration trick and the one using CSS 3D transforms. Don't get me wrong, but that invalidate()-hacky-bit is not the kind of solution we want to have on StackOverflow.
  • Kyle
    Kyle almost 8 years
    @Heinrich You fail to understand that the Android WebView doesn't update the view with just CSS 3D Transforms. Everything that I listed is required to get it to work. I don't like the invalidate either, but its the only thing that actually worked. We have all tried the CSS 3D transforms approach and they don't always work. I'm guessing you've never experienced this issue in the severity that we're solving.
  • Kyle
    Kyle almost 8 years
    @VicVu I have changed this to the selected answer instead of my own.
  • Kyle
    Kyle almost 8 years
    @Heinrich I also think that the issue that I was seeing and trying to prevent is no longer an issue since it really only existed on Android <= 4.3, which I've pointed out in my answer.