Color banding and artifacts with gradients despite using RGBA_8888 everywhere
Asked Answered
B

4

27

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.

Bwana answered 16/8, 2012 at 19:28 Comment(3)
could be related to #9098387Goncourt
I was struggling a lot with the same issue, and the solution to your other question solved it for me. I'm using Android 4.0 on the Samsung Galaxy S2Earthshine
Trevor I have a Nexus7 (2013) when I applied getHolder().setFormat(PixelFormat.RGBA_8888); as mentioned in questioned you mentioned, it fixed the issue, and the banding was gone.Nashner
B
3

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.

Bwana answered 10/5, 2013 at 14:53 Comment(3)
Your comprehensive analysis saved me a lot of work. Thanks! Is this problem present on the new Nexus 7 (2nd generation) too?Thorathoracic
I'm not sure about the new Nexus 7 and I'm not planning to obtain one soon, but it would be interesting to know. One interesting thing I should mention is that there's an app called Display Tester (on Play) which, surprisingly, shows that the original Nexus 7 can render colour gradients nicely. Try that app out and see what you think. I was just thinking about this only this morning, and I thought of emailing the developer to find out about how those gradients are being rendered. I am wondering if his gradients work because he is rendering them 'manually', as opposed to using API gradients.Bwana
HTC One would appear to have a similar problem. I've got a gradient that appears perfect to near perfect on 4 other test devices, including a 4+ year old 800x480 HTC Desire and a 2560x1600 Samsung tablet... but on the HTC One it's 'pants'.Insecurity
A
2

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

    final SurfaceHolder holder = getHolder();
    holder.setFormat(PixelFormat.RGBA_8888);
Aphorize answered 12/5, 2015 at 9:47 Comment(1)
omg, finally a solution that works, been pulling my hair for 2 daysDisingenuous
J
1

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.

Jonathanjonathon answered 17/8, 2012 at 7:7 Comment(5)
The Window is being switched to RGBA_8888 as can be seen near the start of the test Activity code. The hardware acceleration was enabled and disabled using android:hardwareAccelerated= "true" and "false" respectively in the ManifestBwana
Just to add: In the test code above I've set the ARGB_8888 in the SurfaceView, too. The API docs state that the setFormat() call on the Holder must be done within the Thread that operates on the surface, so that I tried; in fact, I've tried the call at several places. The situation is that despite the device(s) tested on having 32-bit displays, and though both Window and surface are set to ARGB_8888, I get similar artifacts both with and without hardware acceleration explicity set. If you have a 4.0 / 4.1 device, I'd be interested to know what results you see from my test code.Bwana
Sorry, I only skimmed over your code, if I have some time I'll try the code on my Galaxy Nexus.Jonathanjonathon
on't know about the HTC One X, but the Nexus 7 is known for it's bad display. The gray scale is seriously off the standard 2.2 gamma. And a firmware bug degraded the display quality even more on "some" devices. (Though I don't know if that has been fixed.) I'd strongly advise against using the Nexus 7 for optimizing you code for display quality.Myalgia
Displaymate has some details: displaymate.com/Tablet_7inch_ShootOut_1.htmMyalgia
P
1

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);
Parturient answered 22/10, 2012 at 17:49 Comment(3)
Though I have no actual experience with this, this sounds like a plausible explanation. Windows Phone does the same thing with gradients, as the display on WP7 (usually) only supports RGB656, but you can render in 8888.Gare
Thanks for your answer. I'm 100% certain I already tried dither both disabled and enabled, as it was one of the first most obvious things to try, but I admit I neglected to put that in the question. Furthermore, the native resolution of the display is apparently 32-bit. I'll check this again later just to be absolutely sure and update accordingly. At this stage to be honest I am beginning to suspect a firmware issue with the Asus Nexus 7, given the bizarre randomness of the banding on each draw, which causes a horrible moving noise effect on a SurfaceView.Bwana
Just to confirm, sadly dithering definitely doesn't make any apparent visual difference. On my Nexus 7 right now (even with the latest firmware), if I erase the Canvas each time I draw in my SurfaceView's thread using a gradient, then on each draw the banding seems to be random.Bwana

© 2022 - 2024 — McMap. All rights reserved.