Android WebView Hardware Rendering Weird Artifact

2020-02-08 16:05发布

On some devices like the Nexus 7 or Samsung Galaxy Nexus (possibly others), I noticed this issue (See picture). This is running on Hardware Acceleration Mode in a WebView. However, when I turn it to Software Rendering Mode, it displays fine but slows down performance a little. I would love to learn how to fix this issue and only use Hardware Acceleration on the WebViews and not Software Mode.

Bad rendering (running on Samsung Galaxy Nexus): Image showing weird rending issue.

Correct rendering (running on Motorola Bionic): How it should render.

Android Manifest:

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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <supports-screens
       android:resizeable="true"
       android:smallScreens="true" 
       android:normalScreens="true" 
       android:largeScreens="true"
       android:xlargeScreens="true"
       android:anyDensity="true" />

    <application
        android:allowBackup="true"
        android:hardwareAccelerated="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        ....
    </application>
</manifest>

Java code:

....
@TargetApi(VERSION_CODES.HONEYCOMB)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        try{

            db = new DatabaseHandler(this);
            tts = new TextToSpeech(this, this);
            handler = new Handler();

            frame = new FrameLayout(this);
            frame.setBackgroundColor(Color.GRAY);

            wv = new WebView(this);
            wv.setScrollBarStyle(WebView.SCROLLBARS_INSIDE_OVERLAY);

            frame.addView(wv);

            wv.setVisibility(WebView.INVISIBLE);

            iv = new ImageView(this);
            Drawable d = Drawable.createFromStream(getAssets().open("www/img/loading.jpg"), null);
            iv.setImageDrawable(d);

            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                    FrameLayout.LayoutParams.MATCH_PARENT);
            iv.setLayoutParams(params);

            frame.addView(iv);

            WebSettings ws = wv.getSettings();
            ws.setRenderPriority(RenderPriority.HIGH);
            ws.setJavaScriptEnabled(true);
            ws.setAllowFileAccess(true);
            if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) 
                  ws.setAllowUniversalAccessFromFileURLs(true);
            ws.setCacheMode(WebSettings.LOAD_NO_CACHE);
            SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
            if(Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB && !pref.getBoolean("prefHardware", true)){
                wv.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
            }
            wv.setWebChromeClient(new WebChromeClient(){
                //int id = 0;
                @Override
                public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
                    // TODO Auto-generated method stub
//                  Log.d("JS Console", consoleMessage.message() + " at " + consoleMessage.sourceId()  + ":" + consoleMessage.lineNumber());
                    //Toast.makeText(TaskRunner.this, consoleMessage.message() + " at " + consoleMessage.sourceId()  + ":" + consoleMessage.lineNumber(), Toast.LENGTH_LONG).show();
//                  NotificationCompat.Builder mBuilder =
//                          new NotificationCompat.Builder(TaskRunner.this)
//                          .setSmallIcon(R.drawable.ic_launcher)
//                          .setContentTitle("Console Message")
//                          .setContentText(consoleMessage.message() + " at " + consoleMessage.sourceId()  + ":" + consoleMessage.lineNumber());
////                    mBuilder.setContentIntent(null);
//                  NotificationManager mNotificationManager =
//                      (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
//                  // mId allows you to update the notification later on.
//                  mNotificationManager.notify(id++, mBuilder.build());
                    return super.onConsoleMessage(consoleMessage);
                }
            });

            wv.setWebViewClient(new WebViewClient(){
                @Override
                public void onPageFinished(WebView view, String url) {
                    // TODO Auto-generated method stub
                    //Log.i("TEST", "TEST");
                    //hideLoading();
                    super.onPageFinished(view, url);
                }

                @Override
                public void onPageStarted(WebView view, String url,
                        Bitmap favicon) {
                    // TODO Auto-generated method stub
                    if(frame.getChildAt(frame.getChildCount()-1) != iv){
                        frame.addView(iv);
                        wv.setVisibility(WebView.INVISIBLE);
                    }
                    super.onPageStarted(view, url, favicon);
                }

                @Override
                public void onReceivedError(WebView view, int errorCode,
                        String description, String failingUrl) {
                    // TODO Auto-generated method stub
                    Log.e("ERROR", description);
                    super.onReceivedError(view, errorCode, description, failingUrl);
                }
            });
            MyJavascriptInterface ji = new MyJavascriptInterface();
            wv.addJavascriptInterface(ji, "ji");

            setOrientation();

            wv.loadUrl("file:///android_asset/www/index.html");

            setContentView(frame);

        }
        catch(Exception e){

        }

    }
    ....

HTML file:

<!DOCTYPE html>
<html>
<head>
    <title>Task Player</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="format-detection" content="telephone=no" />
    ......
</head>
<body>
    <div id="gamespacer">
        <div id="instructions-block">
            <div id="explaination">
            </div>
            <div id="instructions-close">
                <br /><br />
                <div class="btn btn-large btn-info" id="speak-tts">Speak</div>
                <br /><br />
                <input type='checkbox' id='auto-speak' value="yes" checked="checked" />Auto speak
            </div>

            <div id="drag-container">
                <div id='drag'>
                    <span id='au'><img src="img/arrow-up.png" alt="" width="45" /></span>
                    <span id='ad'><img src="img/arrow-down.png" alt="" width="45"/></span>
                </div>
            </div>
        </div>
    </div>
    <div id="play-container">
            <canvas>
            </canvas>
    </div>
    <div class="centerMe" id="rotate" style="display: none;"><p style="font-size: 24px; margin-top: 20px">Rotate the device to play the game.</p></div>
    <div id="winScreen">
        <div id="points">
            You Got<br/> 
            <div id="mypoints">0</div> 
            Points
        </div>
        <a href="#" id='done' class="btn btn-large btn-inverse">Done</a>
    </div>
....
</body>
</html>

css:

#winScreen{
  display: none;
  width: 100%;
  height: 100%;
  text-align: center;
  background-color: green;
  font-size: 24px;
  margin: 0 auto;
  position: absolute;
  left: 0px;
  top: 0px;
}
  #points {
    padding-top: 110px;
    color: yellow;
    margin-bottom: 10px;
  }

  #mypoints{
    margin: 10px;
  }

  .button{
    margin: 0 auto;
    margin-top: 50px;
  }

#play-container{  
  width: 100%;
  height: 100%;
  margin: 0 auto;
  text-align: center;
}

canvas {
    background-color: white;
    position: absolute;
    top: 0px;
    left: 0px;
}

#gamespacer {
}

      #instructions-block {
            position: absolute;
            left: 0px;
            top: -144px;
            width: 100%;
            background-color: white;
            z-index: 1;
        }

        #instructions-content {
          margin-top: 5px;
        }

        #instructions-close {
          text-align: center;
          margin-bottom: 10px;
        }

        #explaination {
          margin: 5px;
          font-size: 24px;
          line-height: 1;
          position: relative;
          width: 100%
          height: 100%
        }


        #drag-container{
            width: 100%;
        }

        #drag {
            position: absolute;
            left: 92%;
            margin-top: 5px;
        }

#au{
    display: none;
    pointer-events: none;
}    

#ad {
    pointer-events: none;
}

body{

   -webkit-user-select: none;
  user-select: none;


  -webkit-touch-callout: none;
  touch-callout: none;

  -webkit-tap-highlight-color: rgba(0,0,0,0);
  tap-highlight-color: rgba(0,0,0,0); 

}

UPDATE 1

So I tried the following code and that did not work as well.

wv.setLayerType(View.LAYER_TYPE_HARDWARE, null);

UPDATE 2

It looks like so far to my knowledge that this only happens on Google made devices (like the Nexus). So, could there be a bug in their kernel or video driver? Or, it could be something wrong with Android 4.2.2.

If you have any ideas on how to fix this, please let me know.

UPDATE 3

I have made a test project for you to try. Play around with the zoom level and moving the window around. Let me know if it does anything weird. It so far does this with Android 4.2.2

http://jsbin.com/equtoq/2

Thanks!

4条回答
Root(大扎)
2楼-- · 2020-02-08 16:45

This is a race-condition bug in WebView. Where the rendering of the page becomes out of sync with the actual view of the page.

The normal cause is because you are handling rotation with android:configChanges in your <activity> tag in the AndroidManifest.xml file. Recreating the WebView on rotation will resolve this issue, but this itself has other problems attached to it.

Their are two solutions I know of. I would not recommend you keep using android:configChanges as its usually detrimental if you don't really know what this change means to your application. If you absolutely need to keep the android:configChanges, there is a solution. You can create your own custom View which inherits from a WebView. Simply override onConfigurationChanged and don't forward it on to the super class.. in this case that's the WebView. I have included code below as an example of this.

But the more truer and correct approach is to correct handle the saving and restoring of the WebView's state. If you hunt around online you will find a multitude of solutions for this problem. Here is a particually good one. http://www.devahead.com/blog/2012/01/preserving-the-state-of-an-android-webview-on-screen-orientation-change/

import android.content.Context;
import android.content.res.Configuration;
import android.util.AttributeSet;
import android.webkit.WebView;


public class MyWebView extends WebView
{
    public MyWebView(Context context)
    {
        super(context);
    }

    public MyWebView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }
    public MyWebView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig)
    {
        // NOTE: We don't want to call this
        // super.onConfigurationChanged(newConfig);
    }
}
查看更多
可以哭但决不认输i
3楼-- · 2020-02-08 16:48

Thanks to Delyan for solving the issue. How he told me to solve it was to add -webkit-transform: translate3d(0,0,0) to all parts that move.

KUDOS DELYAN!

查看更多
可以哭但决不认输i
4楼-- · 2020-02-08 16:51

Make sure hardware-accelerated turned on within your Manifest XML.

<application android:hardwareAccelerated="true" ... />
查看更多
三岁会撩人
5楼-- · 2020-02-08 16:54

After a number of false starts, I could only find the hackish solution of switching between hardware rendering and software rendering depending on the current scaling.

Basically the idea is that rendering artifacts would only show up after a certain scaling, so we can safely use hardware rendering before this threshold is crossed and switch to slower-but-correct software rendering when larger scaling is used. The scaling threshold is > 1.67 on N7 though you may have to check on Galaxy Nexus to see if this value is universal. In my experience there are quite a number of issues with WebView across every version of Android, so this may be the best solution currently until everyone updated to 4.3...

 webView.setWebViewClient(new WebViewClient() {

                @Override
                public void onScaleChanged(WebView view, float oldScale,
                        float newScale) {
                //4.3 SDK required, or change Build.VERSION_CODES.JELLY_BEAN_MR2 to 18
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
                    if (newScale > 1.7f) {
                        if (webView.getLayerType() != View.LAYER_TYPE_SOFTWARE) {
                            webView.setLayerType(View.LAYER_TYPE_SOFTWARE,
                                    null);
                        }
                    } else {
                        if (webView.getLayerType() != View.LAYER_TYPE_HARDWARE) {
                            webView.setLayerType(View.LAYER_TYPE_HARDWARE,
                                    null);
                        }
                    }
                }
                }
});

Note that onScaleChanged() would be called multiple times, sometimes crossing the threshold with a single zoom (using zoom in/out button), so you should add some logic to reduce the number of webView.setLayerType() calls to one-per-zoom event to reduce hardware/software rendering artifacts that would be displayed for a brief interval

查看更多
登录 后发表回答