Color banding and artifacts with gradients despite using RGBA_8888 everywhere

9.1k views Asked by At

I'm aware that colour banding is an old chestnut of a problem that has been discussed many times before with various solutions provided (which essentially boil down to use 32-bit throughout, or use dithering). In fact not so long ago I asked and subsequently answered my own SO question regarding this. Back then, I thought that the solution I put in the answer to that question (which is to apply setFormat(PixelFormat.RGBA_8888) to the Window and also to the Holder in the case of a SurfaceView) had solved the problem in my application for good. At least the solution made the gradient look very nice on the devices I was developing on back then (most probably Android 2.2).

I'm now developing with a HTC One X (Android 4.0) and an Asus Nexus 7 (Android 4.1). What I tried to do was apply a grey gradient to the entire area of a SurfaceView. Even though I supposedly ensured that the containing Window and the Holder are configured for 32-bit colour, I get horrible banding artifacts. In fact, on the Nexus 7, I even see the artifacts move about. This occurs not only on the SurfaceView which is of course continuously drawing, but also in a normal View I added alongside to draw exactly the same gradient for test purposes, which would have drawn once. The way that these artifacts are there and also appear to move around of course looks absolutely awful, and it's actually like viewing an analogue TV with a poor signal. Both the View and SurfaceView exhibit exactly the same artifacts, which move around together.

My intention is to use 32-bit throughout, and not use dithering. I am under the impression that the Window was 32-bit by default long before Android 4.0. By applying RGBA_8888 in the SurfaceView I would have expected everything to have been 32-bit throughout, thus avoiding any artifacts.

I do note that there are some other questions on SO where people have observed that the RGBA_8888 no longer seems to be effective on the 4.0 / 4.1 platforms.

This is a screenshot from my Nexus 7, with a normal View at the top and a SurfaceView below, both applying the same gradient to the Canvas. Of course, it does not show the artifacts as well as they do when looking at the display, and so it is probably fairly pointless showing this screen grab. I want to emphasise though that the banding really does look terrible on the screen of the Nexus. Edit: In fact, the screenshot really doesn't show the artifacts at all. The artifacts I'm seeing on the Nexus 7 aren't uniform banding; it looks random in nature.

enter image description here

The test Activity used to create the above:

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Shader;
import android.os.Bundle;
import android.os.Handler;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
import android.view.SurfaceHolder.Callback;
import android.widget.LinearLayout;

public class GradientTest extends Activity {

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        getWindow().setFormat(PixelFormat.RGBA_8888);
    }


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);      
        WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
        lp.copyFrom(getWindow().getAttributes());
        lp.format = PixelFormat.RGBA_8888;
        getWindow().setAttributes(lp);     
        LinearLayout ll = new LinearLayout(this);
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(500,500);
        params.setMargins(20, 0, 0, 0);
        ll.addView(new GradientView(this), params);    
        ll.addView(new GradientSurfaceView(this), params);
        ll.setOrientation(LinearLayout.VERTICAL);
        setContentView(ll);
    }


    public class GradientView extends View {

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

        @Override
        protected void onDraw(Canvas canvas) {

            Paint paint = new Paint();
            paint.setStyle(Paint.Style.FILL);
            paint.setAntiAlias(false);
            paint.setFilterBitmap(false);
            paint.setDither(false); 
            Shader shader = new LinearGradient(
                    0,
                    0,
                    0,
                    500,
                    //new int[]{0xffafafaf, 0xff414141},
                    new int[]{0xff333333, 0xff555555},
                    null,

                    Shader.TileMode.CLAMP
                    );

            paint.setShader(shader);    
            canvas.drawRect(0,0,500,500, paint);
        }       
    }




    public class GradientSurfaceView extends SurfaceView implements Callback {

        public GradientSurfaceView(Context context) {
            super(context);

            getHolder().setFormat(PixelFormat.RGBA_8888); // Ensure no banding on gradients 
            SurfaceHolder holder = getHolder();
            holder.addCallback(this);    
        }


        Paint paint;
        private GraphThread thread;

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            holder.setFormat(PixelFormat.RGBA_8888); // Ensure no banding on gradients    

            paint = new Paint();
            paint.setStyle(Paint.Style.FILL);
            paint.setAntiAlias(false);
            paint.setFilterBitmap(false);
            paint.setDither(false); 

            Shader shader = new LinearGradient(
                    0,
                    0,
                    0,
                    500,
                    //new int[]{0xffafafaf, 0xff414141},
                    new int[]{0xff333333, 0xff555555},
                    null,
                    Shader.TileMode.CLAMP
                    );


            paint.setShader(shader);  



            thread = new GraphThread(holder, new Handler() );
            thread.setName("GradientSurfaceView_thread");
            thread.start();
        }

        class GraphThread extends Thread {

            /** Handle to the surface manager object we interact with */
            private SurfaceHolder mSurfaceHolder;   

            public GraphThread(SurfaceHolder holder, Handler handler) {
                mSurfaceHolder = holder;
                holder.setFormat(PixelFormat.RGBA_8888); // Ensure no banding on gradients  
            }

            @Override
            public void run() {

                Canvas c = null;

                while (true) {

                    try {
                        c = mSurfaceHolder.lockCanvas();

                        synchronized (mSurfaceHolder) {

                            if (c != null){


                                c.drawRect(0,0,500,500, paint);

                            }
                        }                                 
                    } 

                    finally {
                        if (c != null) {
                            mSurfaceHolder.unlockCanvasAndPost(c);
                        }
                    }
                }
            }    

        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width,
                int height) {

        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {

        }
    }


}

I have installed an application called Display Tester from Google Play. This application can be used to create test gradients on the screen. Although its gradients do not look perfect, they seem a bit better than what I have been able to achieve, which makes me wonder if there is a further measure I can do to prevent banding.

The other thing I note is that the Display Tester application reports that my Nexus' screen is 32-bit.

For information, I am explicitly enabling hardware acceleration. My SDK levels are:

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

Another thing I notice is that the default gradient background for the Activity, which I understand to be a feature of Holo, is also very banded. This also doesn't show at all in the screenshot. And, I also just noticed the banding of the background moves about briefly on my Nexus 7, in sympathy with the banding movement in my two Views. If I create a completely new Android project with the default 'empty' Activity, the Activity shows a nasty banded gradient background on both my Nexus and HTC One X. Is this normal? I understand that this black / purple gradient default background is what an Activity shall have if hardware acceleration is enabled. Well, regardless of whether hardware acceleration is enabled or not, I see the same nasty banded Activity background gradient. This even happens in my empty test project, whose target SDK is 15. To clarify, the way I am enabling or disabling hardware acceleration is explicitly using android:hardwareAccelerated="true" and android:hardwareAccelerated="false".

I'm not sure if my observation about the Holo black / purple Activity gradient background has anything to do with my primary question, but it does seem oddly related. It's also odd that it looks such poor quality (i.e. banded) and looks the same regardless of whether hardware acceleration is turned on. So, a secondary question would be: When you have an Activity with the default Holo gradient background, and for each case of hardware acceleration being explicity enabled and then disabled, should this gradient background (a) be present and (b) look perfectly smooth? I would ask this in a separate question, but again, it seems related.

So, in summary: The basic problem I have is that applying a gradient background to a SurfaceView simply cannot be done, it seems, on my Nexus 7. It's not just banding that's the problem (which I could happily put up with if it were just that); it's actually the fact that the banding is random in nature on each draw. This means that a SurfaceView that constantly redraws ends up having a moving, fuzzy background.

4

There are 4 answers

3
Trevor On BEST ANSWER

Just to wrap this up with an answer, the conclusion I have reached is that the Nexus 7 just has some hardware / firmware issue which means that it is utterly pants at rendering gradients.

5
botteaap On

If you see no effect of setting the pixel format in ICS and up, it is most likely due to hardware acceleration which will always render to the native pixel format. Most of the time that should be just ARGB_8888 though. Make sure you also set the window pixel format of your activity, not just the pixel format on the SurfaceView.

You can easily verify if that is the case by turning acceleration off. You mention that you tested that, but you don't mention how you did that. Setting the target sdk level is not the most explicit way to do that.

From the top of my head the software rendering switched to ARGB_8888 by default in HoneyComb (3.0), but again you will need to set it explicitly to correctly support older devices where this is not the default.

3
Damien Praca On

Why have you set to false the dithering on your paint. I would suggest to activate dithering

paint.setDither(true);

Android doc clearly says that it will downgrade your rendering:

setting or clearing the DITHER_FLAG bit Dithering affects how colors that are higher precision than the device are down-sampled. No dithering is generally faster, but higher precision colors are just truncated down (e.g. 8888 -> 565). Dithering tries to distribute the error inherent in this process, to reduce the visual artifacts.

You can also try to add the FLAG_DITHER to the window:

window.setFormat(PixelFormat.RGBA_8888);
window.setFlags(WindowManager.LayoutParams.FLAG_DITHER, WindowManager.LayoutParams.FLAG_DITHER);
1
slott On

Have you tried setting the pixelformat for the surface view itself ?

    final SurfaceHolder holder = getHolder();
    holder.setFormat(PixelFormat.RGBA_8888);