18

I created a circle button that can change his color when I call a function. What I want is to create another one, that creates the same circle button but with a radial gradient that starts in the middle with the color selected and that goes to transparent when you go out of the circle.

I created a similar code using the one posted at How to set gradient style to paint object? but don't worked.

The code that I tried is to this porpuse is:

mPaint.setShader(new RadialGradient(0, 0, height/3, Color.BLACK, Color.TRANSPARENT, Shader.TileMode.MIRROR));

The following class is the one that I created for a Circle Button.

public class ColorGradientCircleButton extends View{

private Paint mPaint;
private Paint   mBitmapPaint;
private Bitmap  mBitmap;
private Canvas  mCanvas;
private int width, height;

public ColorGradientCircleButton(Context context) {
    super(context);
    init();
}
public ColorGradientCircleButton(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
}
public ColorGradientCircleButton(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init();
}
private void init() {
    mPaint = new Paint();
    mPaint.setColor(Color.BLACK);
    mPaint.setStrokeWidth(1);
    mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    mBitmapPaint = new Paint(Paint.DITHER_FLAG);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    width = w;
    height = h;
    mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    mCanvas = new Canvas(mBitmap);
    mCanvas.drawCircle(w/2, h/2, h/3, mPaint);
}
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
}
public void changeColor(int color){
    mPaint.setColor(color);
    mCanvas.drawCircle(width/2, height/2, height/3, mPaint);
    invalidate();
}
}
Community
  • 1
  • 1
Gabriel Esteban
  • 752
  • 2
  • 9
  • 34
  • 1
    Hey Grabriel. This code looks technically very promising. I am positive we can correct it. Firstly, why are you creating a bitmap in `onSizeChanged`? When creating a custom view with `andorid.graphics`, there should only be a need to draw straight onto the canvas of that view. What you do in that method is incredibly expensive and not necessary. You should draw the circle using the canvas given `onDraw`. If this isn't enough to solve your problem, I'll post something more in detail. – Tom Oct 17 '13 at 14:21
  • Hi Tom, I created the bitmap because I think that with that I can control the width and height of the view. So, I modified the View to add your improvements, deleting the bitmap but something don't work well. Her is my code https://gist.github.com/galaxyfeeder/7026090 – Gabriel Esteban Oct 17 '13 at 14:37
  • Hi again, I get it, I was requesting the width and height in the constructor, but the onDraw is executed first, so what I do is to request the height and width of the view in the 'onDraw', here is my actual code that works well: https://gist.github.com/galaxyfeeder/7026165 – Gabriel Esteban Oct 17 '13 at 14:40
  • 1
    Sorry for disappearing. Meeting. Your latter one Gabriel, is exactly what I would've done, and you are *exactly* right about the width and height. I'm pleased. If you wanted the gradient you'd put your shader in when you construct the paint. Next stop- hardware acceleration ;) . *Is this now OK, or are there any other annoyances that aren't quite right?* – Tom Oct 17 '13 at 15:03
  • That RadialGradient don't work because it says that the radius should be more than 0. And when I put a value like 1, 2, 3, 4.. creates something a bit extrange, don't create what I think that it should create. What I want is a cercle that in the middle have the color alpha at 100%, and int the borders have the aplha at 0%. – Gabriel Esteban Oct 17 '13 at 15:12
  • 1
    Right let's sort that out then. – Tom Oct 17 '13 at 15:26

1 Answers1

21

We should migrate this to the answer boxes.

OP has basically got it here- and in fact the OP's revised gist is brilliant.

Some general tips regarding the first attempt in the question:

1) In protected void onSizeChanged(int w, int h, int oldw, int oldh):

  • width = w; there is no reason why you can't call getWidth() when you require this. The reason it's advisable is because the View's internal width is set quite late after onMeasure. Consequently, onDraw may be the next time you want a most up to date version, so use the getter there.
  • mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);. Creating a bitmap is an expensive and memory intensive operation. Unless you want to write a bitmap to a file, or send it to a BitmapDrawable for an ImageView or something, you don't need to do this. Especially with effects drawn onto the UI with android's graphics library.
  • mCanvas = new Canvas(mBitmap); followed by a draw operation onto the new canvas. This is never needed. And yet I've seen it (not work) in many code bases and attempts. I think it's the fault of an old stack overflow post that got people doing this so that they could transform a canvas on a custom view without effecting the drawing onto the rest of the canvas. Incidentally, if you need this, use .restore() and .save() instead. If you see new Canvas, be suspicious.

2) onDraw(...):

  • Yes, you need to avoid doing things in onDraw, like, creating objects, or any heavy processing. But you still need to do the things in onDraw you need to do in onDraw!
  • So here you simply need to call : canvas.drawCircle(float cx, float cy, float radius, Paint paint) with arguments as per the docs.
  • This really isn't that sinful for onDraw. If you're worried about calling this too much, as might be the case if your entire button is animating across the screen, you need to use hardware acceleration available in later API versions, as will be detailed in an article called Optimizing the View; very helpful reading if you're using lots of custom drawn views.

3) That pesky radial gradient. The next issue you had is that you quite rightly created your paint in an initmethod so that the object creation was off the draw. But then quite rightly it will have IllegalArgumentExceptioned (I think) on you because at that stage the getHeight() of the view was 0. You tried passing in small pixel values- that won't work unless you know some magic about screen sizes.

This isn't your issue as much as the annoying view cycle at the heart of Android's design patterns. The fix though is easy enough: simply use a later part of the view's drawing process after the onMeasure call to set the paint filter.

But there are some issues with getting this right, namely that sometimes, annoyingly, onDraw gets called before the point at which you'd expect it. The result would be your paint is null and you wouldn't get the desired behavior.

I have found a more robust solution is simply to do a cheeky and naughty little null check in the onDraw and then once only construct the paint object there. It's not strictly speaking optimal, but given the complex way in which the Paint objects hook up with Android's graphics native layer better than trying to straddle the paint configuration and construction in many frequently called places. And it makes for darn clearer code.

This would look like (amending your gist):

        @Override
        protected void onDraw(final Canvas canvas) {
            super.onDraw(canvas);
            if (mPaint == null) {
                mPaint = new Paint();
                mPaint.setColor(Color.BLACK);
                mPaint.setStrokeWidth(1);
                mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
                mPaint.setShader(new RadialGradient(getWidth() / 2, getHeight() / 2,
                        getHeight() / 3, Color.TRANSPARENT, Color.BLACK, TileMode.MIRROR));
            }
            width = getWidth();
            height = getHeight();
            canvas.drawCircle(width / 2, height / 2, height / 3, mPaint);
        }

So note a few changes- I think from your description you want the two colours swapped round in the arguments, also don't forget to center the center of your gradient in your view: width/2 and height/2 arguments.

Best of luck!

Tom
  • 1,773
  • 15
  • 23
  • Thanks for all, I think that I learned more with this question and you that with all the tutorials that I read or watch about drawing and canvas. – Gabriel Esteban Oct 17 '13 at 17:37
  • 1
    Oh I don't know. I have made absolutely every mistake there is to make, I only think at least everybody else shouldn't have to – Tom Oct 17 '13 at 18:38
  • 1
    Very nice, saved me from having to create a bunch of different res drawables. And while the lazy instantiation of the paint might cause a first draw to be slightly slower, it's really a small price to pay to onDraw self-contained. Thanks Tom and Gabriel! – William T. Mallard Nov 27 '13 at 19:23
  • Yes but if i want to update the Color of the Shader later while the application is running? – Cliff Feb 28 '20 at 14:00