Question

I have the following shape XML:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:a="http://schemas.android.com/apk/res/android"
       a:shape="ring"
       a:innerRadiusRatio="3"
       a:thicknessRatio="8"
       a:useLevel="false">

    <!-- some other stuff goes here -->

    </gradient>
</shape>

I would like to use code instead to create this shape, since some things need to be calculated on the fly before I do it, so static pre-defined layout doesn't cut it.

I'm new to Android and can't quite figure out how XML translates to code, and there's no RingShape class inheriting from Shape.

In addition to answering just this question, if there's a guide somewhere that details relation between XML and Java code and how XML gets processed in order to end up on the screen I would appreciate a link too. Thanks.

Was it helpful?

Solution

Ring and other shapes are GradientDrawables.

If you look at the source code for GradientDrawable, you'll see it looks like certain properties (like innerRadius) can only be defined through XML... they are not exposed through accessor methods. The relevant state is also unhelpfully private to the class, so subclassing is no help either.

OTHER TIPS

Reuben already pointed out most the most useful observations, so I'll just focus on the implementation side of the story. There's multiple approaches using reflection that'll probably give you what you're looking for.

First one is to (ab)use the private GradientDrawable constructor that takes a GradientState reference. Unfortunately the latter is a final subclass with package visibility, so you can't easily get access to it. In order to use it, you would need to dive further in using reflection or mimic its functionality into your own code.

Second approach is to use reflection to get the private member variable mGradientState, which fortunately has a getter in the form of getConstantState(). This'll give you the ConstantState, which at runtime is really a GradientState and hence we can use reflection to access its members and change them at runtime.

In order to support above statements, here's a somewhat basic implementation to create a ring-shaped drawable from code:

RingDrawable.java

public class RingDrawable extends GradientDrawable {

    private Class<?> mGradientState;

    public RingDrawable() {
        this(Orientation.TOP_BOTTOM, null);
    }

    public RingDrawable(int innerRadius, int thickness, float innerRadiusRatio, float thicknessRatio) {
        this(Orientation.TOP_BOTTOM, null, innerRadius, thickness, innerRadiusRatio, thicknessRatio);
    }

    public RingDrawable(GradientDrawable.Orientation orientation, int[] colors) {
        super(orientation, colors);
        setShape(RING);
    }

    public RingDrawable(GradientDrawable.Orientation orientation, int[] colors, int innerRadius, int thickness, float innerRadiusRatio, float thicknessRatio) {
        this(orientation, colors);
        try {
            setInnerRadius(innerRadius);
            setThickness(thickness);
            setInnerRadiusRatio(innerRadiusRatio);
            setThicknessRatio(thicknessRatio);
        } catch (Exception e) {
            // fail silently - change to your own liking
            e.printStackTrace();
        }
    }

    public void setInnerRadius(int radius) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        if (mGradientState == null) mGradientState = resolveGradientState();
        Field innerRadius = resolveField(mGradientState, "mInnerRadius");
        innerRadius.setInt(getConstantState(), radius);
    }       

    public void setThickness(int thicknessValue) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        if (mGradientState == null) mGradientState = resolveGradientState();
        Field thickness = resolveField(mGradientState, "mThickness");
        thickness.setInt(getConstantState(), thicknessValue);
    }

    public void setInnerRadiusRatio(float ratio) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        if (mGradientState == null) mGradientState = resolveGradientState();
        Field innerRadiusRatio = resolveField(mGradientState, "mInnerRadiusRatio");
        innerRadiusRatio.setFloat(getConstantState(), ratio);
    }

    public void setThicknessRatio(float ratio) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        if (mGradientState == null) mGradientState = resolveGradientState();
        Field thicknessRatio = resolveField(mGradientState, "mThicknessRatio");
        thicknessRatio.setFloat(getConstantState(), ratio);
    }

    private Class<?> resolveGradientState() {
        Class<?>[] classes = GradientDrawable.class.getDeclaredClasses();
        for (Class<?> singleClass : classes) {
            if (singleClass.getSimpleName().equals("GradientState")) return singleClass;
        }
        throw new RuntimeException("GradientState could not be found in current GradientDrawable implementation");
    }

    private Field resolveField(Class<?> source, String fieldName) throws SecurityException, NoSuchFieldException {
        Field field = source.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field;
    }

}

Above can be used as follows to create a RingDrawable from code and display it in a standard ImageView.

ImageView target = (ImageView) findViewById(R.id.imageview);
RingDrawable ring = new RingDrawable(10, 20, 0, 0);
ring.setColor(Color.BLUE);
target.setImageDrawable(ring);

This will show a simple, opaque blue ring in the ImageView (10 units inner radius, 20 units thick). You'll need to make sure to not set the ImageView's width and height to wrap_content, unless you add ring.setSize(width, height) to above code in order for it to show up.

Hope this helps you out in any way.

You can do something like this:

private ShapeDrawable newRingShapeDrawable(int color) {
        ShapeDrawable drawable = new ShapeDrawable(new OvalShape());
        drawable.getPaint().setColor(color);
        drawable.getPaint().setStrokeWidth(2);
        drawable.getPaint().setStyle(Paint.Style.STROKE);
        return drawable;
}

It is possible to do it from code:

int r = dipToPixels(DEFAULT_CORNER_RADIUS_DIP); // this can be used to make it circle
float[] outerR = new float[]{r, r, r, r, r, r, r, r};
int border = dipToPixels(2); // border of circle
RectF rect = new RectF(border, border, border, border);
RoundRectShape rr = new RoundRectShape(outerR, rect, outerR);// must checkout this constructor
ShapeDrawable drawable = new ShapeDrawable(rr);
drawable.getPaint().setColor(badgeColor);// change color of border
// use drawble now

For me it works as follow: (also for Android version > lollipop)

    ImageView target = (ImageView) findViewById(R.id.imageview);

    GradientDrawable shapeRing = new GradientDrawable();
    shapeRing.setShape(GradientDrawable.OVAL);
    shapeRing.setColor(centerColor); // transparent
    shapeRing.setStroke(stroke, strokeColor);
    shapeRing.setSize(width, width);

    target.setImageDrawable(ring);
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top