Android - custom button with shape drawables and a gradient programmatically

I want to make a custom button like this program does with perhaps a radial gradient.

I subclassed view, and draw three shape drawables and then draw the text. the text seems off center, so I tried to draw a bounding rectangle for the text, but no luck there. and was planning on adding a click listener to get button like behaviour.

perhaps I should subclass button, but where to draw my drawables so they don't get messed up by the button's text being drawn.

Any pointers will be appreciated.

thanks

Edit2: see second attempt below.

Edit3: the reason for the bounty is to figure out why subclassing drawable does not work. the gradient is not so important.

edit4: discovered drawRect before getTextBounds() in DrawableView::OnDraw().

package acme.drawables;
import android.content.*;
import android.content.pm.*;
import android.graphics.*;
import android.graphics.drawable.*;
import android.graphics.drawable.shapes.*;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.*;
import android.view.Menu;
import android.view.View;
import android.widget.*;
import static java.lang.Math.*;
public class MainActivity extends AppCompatActivity {
    DrawableView drawableView;
    LinearLayout row(boolean isRow1) {
        LinearLayout layout=new LinearLayout(this);
        layout.setOrientation(LinearLayout.HORIZONTAL);
        LinearLayout.LayoutParams layoutParams=new LinearLayout.LayoutParams(w,d);
        int m=(int)round(w*margin);
        layoutParams.setMargins(m,m,m,m);
        for(int i=0;i<n;i++)
            layout.addView(drawableView=new DrawableView(this,i,isRow1),layoutParams);
        return layout;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        DisplayMetrics metrics=new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        w=d=(int)round(metrics.densityDpi);
        LinearLayout row1=row(true);
        LinearLayout row2=row(false);
        LinearLayout layout=new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);
        layout.addView(row1);
        layout.addView(row2);
        LinearLayout l=new LinearLayout(this);
        setContentView(layout);
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        return true;
    }
    public class DrawableView extends View {
        public DrawableView(Context context,int column,boolean isRow1) {
            super(context);
            setBackgroundColor(Color.TRANSPARENT);
            this.column=column;
            text=""+(char)('0'+column);
            int r=(int)round(w*radius);
            d0=new ShapeDrawable(new RoundRectShape(new float[]{r,r,r,r,r,r,r,r},null,null));
            d0.getPaint().setColor(0xff000000);
            d0.setBounds(0,0,w,d);
            d1=new ShapeDrawable(new RoundRectShape(new float[]{r,r,r,r,r,r,r,r},null,null));
            d1.getPaint().setColor(on[column]);
            d1.setBounds(edge,edge,w-edge,d-edge);
            d2=new ShapeDrawable(new RoundRectShape(new float[]{r,r,r,r,r,r,r,r},null,null));
            int b=(int)round(w*border);
            d2.setBounds(b/2,b/2,w-b/2,d-b/2);
            d2.getPaint().setColor(isRow1?on[column]:off[column]);
        }
        protected void onDraw(Canvas canvas) {
            d0.draw(canvas);
            d1.draw(canvas);
            d2.draw(canvas);
            Paint paint = new Paint();
            //paint.setColor(Color.WHITE);
            paint.setStyle(Paint.Style.FILL);
            //canvas.drawPaint(paint);
            paint.setColor(Color.CYAN);
            paint.setTextSize(w*95/100);
            Rect r=new Rect();
            paint.getTextBounds(text,0,1,r); // were switched
            canvas.drawRect(r,paint);  // were switched
            int x=(w-r.width())/2,y=(d-r.height())/2;
            paint.setColor(Color.BLACK);
            canvas.drawText(text,x,d-y,paint);
        }
        final int column;
        final String text;
        ShapeDrawable d0, d1, d2;
    }
    final int n=5, edge=1;
    double margin=.10, radius=.05, border=.15;
    int w, d;
    final int[] on=new int[]{0xffff0000,0xffffff00,0xff00ff00,0xff0000ff,0xffff8000};
    final int[] off=new int[]{0xff800000,0xff808000,0xff008000,0xff000080,0xff804000};
}

This version tries to subclass drawable and use a button. but the button's drawing seems to interfere with drawing my drawable shapes. it looks like the bounds are being ignored.

package acme.drawables;
import android.content.*;
import android.content.pm.*;
import android.graphics.*;
import android.graphics.drawable.*;
import android.graphics.drawable.shapes.*;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.*;
import android.view.Menu;
import android.view.View;
import android.widget.*;

import static java.lang.Math.*;
public class MainActivity extends AppCompatActivity {
    LinearLayout row(boolean isRow1) {
        LinearLayout layout=new LinearLayout(this);
        layout.setOrientation(LinearLayout.HORIZONTAL);
        LinearLayout.LayoutParams layoutParams=new LinearLayout.LayoutParams(w,d);
        int m=(int)round(w*margin);
        layoutParams.setMargins(m,m,m,m);
        if(true)
            for(int i=0;i<n;i++) { // subclass drawable
                Button b=new Button(this);
                b.setText(""+(char)('0'+i));
                b.setBackground(new MyDrawable(i,i/n%2==0));
                layout.addView(b,layoutParams);
            }
        else
            for(int i=0;i<n;i++) // use drawable view with canvas draw text
                layout.addView(drawableView=new DrawableView(i,isRow1),layoutParams);
        return layout;
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        DisplayMetrics metrics=new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        w=d=(int)round(metrics.densityDpi);
        LinearLayout row1=row(true);
        LinearLayout row2=row(false);
        LinearLayout layout=new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);
        layout.addView(row1);
        layout.addView(row2);
        LinearLayout l=new LinearLayout(this);
        setContentView(layout);
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        return true;
    }
    public class MyDrawable extends Drawable {
        public MyDrawable(int column,boolean isRow1) {
            drawableView=new DrawableView(column,isRow1);
        }
        public void setAlpha(int alpha) {
            System.out.println("ignoring set alpha to: "+alpha);
        }
        public void setColorFilter(ColorFilter colorFilter) {
            System.out.println("ignoring set color filter to: "+colorFilter);
        }
        public int getOpacity() {
            return PixelFormat.OPAQUE;
        }
        public void draw(Canvas canvas) {
            System.out.println(this+" is drawing.");
            drawableView.d0.draw(canvas);
            System.out.println("d0 bounds: "+drawableView.d0.getBounds());
            drawableView.d1.draw(canvas);
            System.out.println("d1 bounds: "+drawableView.d1.getBounds());
            drawableView.d2.draw(canvas);
            System.out.println("d2 bounds: "+drawableView.d2.getBounds());
        }
        final DrawableView drawableView; // cheat by delegating
    }
    public class DrawableView extends View {
        public DrawableView(int column,boolean isRow1) {
            super(MainActivity.this);
            setBackgroundColor(Color.TRANSPARENT);
            this.column=column;
            text=""+(char)('0'+column);
            int r=(int)round(w*radius);
            d0=new ShapeDrawable(new RoundRectShape(new float[]{r,r,r,r,r,r,r,r},null,null));
            d0.getPaint().setColor(0xff000000);
            d0.setBounds(0,0,w,d);
            d1=new ShapeDrawable(new RoundRectShape(new float[]{r,r,r,r,r,r,r,r},null,null));
            d1.getPaint().setColor(on[column]);
            d1.setBounds(edge,edge,w-edge,d-edge);
            d2=new ShapeDrawable(new RoundRectShape(new float[]{r,r,r,r,r,r,r,r},null,null));
            int b=(int)round(w*border);
            d2.setBounds(b/2,b/2,w-b/2,d-b/2);
            d2.getPaint().setColor(isRow1?on[column]:off[column]);
        }
        protected void onDraw(Canvas canvas) {
            d0.draw(canvas);
            d1.draw(canvas);
            d2.draw(canvas);
            Paint paint=new Paint();
            //paint.setColor(Color.WHITE);
            paint.setStyle(Paint.Style.FILL);
            //canvas.drawPaint(paint);
            paint.setColor(Color.CYAN);
            paint.setTextSize(w*95/100);
            Rect r=new Rect();
            canvas.drawRect(r,paint);
            paint.getTextBounds(text,0,1,r);
            int x=(w-r.width())/2, y=(d-r.height())/2;
            paint.setColor(Color.BLACK);
            canvas.drawText(text,x,d-y,paint);
        }
        final int column;
        final String text;
        ShapeDrawable d0, d1, d2;
    }
    DrawableView drawableView;
    final int n=5, edge=1;
    double margin=.10, radius=.05, border=.15;
    int w, d;
    final int[] on=new int[]{0xffff0000,0xffffff00,0xff00ff00,0xff0000ff,0xffff8000};
    final int[] off=new int[]{0xff800000,0xff808000,0xff008000,0xff000080,0xff804000};
}

Answers


1) Use alignment to draw text at the center in your DrawableView (should help with the text seems off center):

paint.setTextAlign(Align.CENTER); // <- should help you with centering
paint.getTextBounds(text, 0, 1, r);
int x = w / 2, y = (d - r.height()) / 2; // <- was updated too

2) To answer your question the reason for the bounty is to figure out why subclassing drawable does not work:

I suppose it's because you create DrawableView in MyDrawable and don't add it to any container which means you don't measure and layout it. So, it's probably zero height and width.

3) I would suggest you to use Button instead of custom views and drawables. You extend from Button and do additional drawings in the end of onDraw method, something like this:

public void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // your custom drawing over button
}

Original Incorrect Answer

the reason for the bounty is to figure out why subclassing drawable does not work

Try to check if you need to call:

  • super.onDraw(canvas) in DrawableView.onDraw
  • super.draw(canvas) in MyDrawable.draw

use this code to make gradient button

Button your_button= findViewById(R.id.button);

    GradientDrawable gd = new GradientDrawable(
            GradientDrawable.Orientation.TOP_BOTTOM,
            new int[] {0xFF616261,0xFF131313});
    gd.setCornerRadius(0f);

    your_button.setBackgroundDrawable(gd);

It is not a good idea to create Drawable depended on a View. As Eugen Pechanec suggested, make MyDrawable and DrawableView static.

You are using ShapeDrawables only in MyDrawable, so you can move it from DrawableView.

It can be something like this:

public static class MyDrawable extends Drawable {
    private ShapeDrawable d0, d1, d2;
    private int edge;
    private int border;

    public MyDrawable(int color1, int color2, int radius, int edge, int border) {
        this.edge = edge;
        this.border = border;

        float[] outerRadii = new float[] {
                radius, radius,
                radius, radius,
                radius, radius,
                radius, radius
        };

        d0 = new ShapeDrawable(new RoundRectShape(outerRadii, null, null));
        d0.getPaint().setColor(0xff000000);
        d1 = new ShapeDrawable(new RoundRectShape(outerRadii, null, null));
        d1.getPaint().setColor(color1);
        d2 = new ShapeDrawable(new RoundRectShape(outerRadii, null, null));
        d2.getPaint().setColor(color2);
    }

    public void setAlpha(int alpha) {
        System.out.println("ignoring set alpha to: " + alpha);
    }

    public void setColorFilter(ColorFilter colorFilter) {
        System.out.println("ignoring set color filter to: " + colorFilter);
    }

    public int getOpacity() {
        return PixelFormat.OPAQUE;
    }

    public void draw(Canvas canvas) {
        System.out.println(this + " is drawing.");
        d0.draw(canvas);
        System.out.println("d0 bounds: " + d0.getBounds());
        d1.draw(canvas);
        System.out.println("d1 bounds: " + d1.getBounds());
        d2.draw(canvas);
        System.out.println("d2 bounds: " + d2.getBounds());
    }

    @Override
    public void setBounds(int left, int top, int right, int bottom) {
        super.setBounds(left, top, right, bottom);
        d0.setBounds(left, top, right, bottom);
        d1.setBounds(left + edge, top + edge, right - edge, bottom - edge);
        d2.setBounds(left + border / 2, top + border / 2,
                right - border / 2, bottom - border / 2);
    }
}

You can consider to not use ShapeDrawable and draw the shapes by yourself:

public static class MyDrawable extends Drawable {
    private int radius;
    private int edge;
    private int border;

    private RectF bounds1 = new RectF();
    private RectF bounds2 = new RectF();
    private RectF bounds3 = new RectF();

    private Paint paint1 = new Paint();
    private Paint paint2 = new Paint();
    private Paint paint3 = new Paint();

    public MyDrawable(int color1, int color2, int radius, int edge, int border) {
        this.radius = radius;
        this.edge = edge;
        this.border = border;

        float[] outerRadii = new float[] {
                radius, radius,
                radius, radius,
                radius, radius,
                radius, radius
        };

        paint1.setColor(0xff000000);
        paint2.setColor(color1);
        paint3.setColor(color2);
    }

    public void setAlpha(int alpha) {
        System.out.println("ignoring set alpha to: " + alpha);
    }

    public void setColorFilter(ColorFilter colorFilter) {
        System.out.println("ignoring set color filter to: " + colorFilter);
    }

    public int getOpacity() {
        return PixelFormat.OPAQUE;
    }

    public void draw(Canvas canvas) {
        canvas.drawRoundRect(bounds1, radius, radius, paint1);
        canvas.drawRoundRect(bounds2, radius, radius, paint2);
        canvas.drawRoundRect(bounds3, radius, radius, paint3);
    }

    @Override
    public void setBounds(int left, int top, int right, int bottom) {
        super.setBounds(left, top, right, bottom);

        bounds1.set(left, top, right, bottom);

        bounds2.set(bounds1);
        bounds2.inset(edge, edge);

        bounds3.set(bounds1);
        bounds3.inset(border / 2, border / 2);
    }
}

By the way, it is good to use StateListDrawable for a Button. So you can use MyDrawable like this:

MyDrawable drawable = new MyDrawable(...);
MyDrawable drawablePressed = new MyDrawable(...);
MyDrawable drawableFocused = new MyDrawable(...);

StateListDrawable stateDrawable = new StateListDrawable();
stateDrawable.addState(new int[]{android.R.attr.state_pressed}, drawablePressed);
stateDrawable.addState(new int[]{android.R.attr.state_focused}, drawableFocused);
stateDrawable.addState(new int[]{}, drawable);

Button button = (Button) findViewById(R.id.button);
button.setBackground(stateDrawable);

Need Your Help

URL/ Path in address bar

url path address-bar

I have seen my websites only shows path as below when you navigate to different webpage or items in that website while when i created the website i have to create a webpage for everything &amp; it do

Implementation specific methods on an interface - how can I avoid them?

.net interface idisposable

We have an interface that is passed to the constructor using an IoC container