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):
Correct rendering (running on Motorola Bionic):
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
Thanks!
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 theAndroidManifest.xml
file. Recreating theWebView
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 theandroid:configChanges
, there is a solution. You can create your own customView
which inherits from aWebView
. Simply overrideonConfigurationChanged
and don't forward it on to the super class.. in this case that's theWebView
. 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/
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!
Make sure hardware-accelerated turned on within your Manifest XML.
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...
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