6

I created an Android gradient drawable where the top and bottom are black and the center is transparent:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >

    <gradient
        android:startColor="@android:color/black"
        android:centerColor="@android:color/transparent"
        android:endColor="@android:color/black"
        android:angle="90"/>
</shape>

The rendered gradient looks like this:

gradient image

As you can see, the black parts spread to most of the screen. I want the black to show only on a small portion of the top and bottom. Is there a way I can make the transparent center larger, or make the top and bottom black stripes smaller?

I tried playing around with some of the other XML attributes mentioned in the linked GradientDrawable documentation, yet none of them seem to make and difference.

Tot Zam
  • 8,406
  • 10
  • 51
  • 76
  • if there is no way to set more than one color with xml attributes you could try to distort the drawable, if it's possible somehow. otherwise go programmatically – lelloman Mar 16 '17 at 12:09
  • @lelloman I'm open to programmatic suggestions as well, although sticking with just xml would probably be easier. Do you have any ideas? – Tot Zam Mar 16 '17 at 12:16
  • I think that you need a LinearGradient with black - white - black but just not at the same distance, I'm afraid that GradientDrawable doesn't allow you to tweak the position of the colors, would you use a custom Drawable programmatically? – lelloman Mar 16 '17 at 12:37
  • @lelloman Thanks for responding. I will need to try out your solution. Just pointing out to prevent confusion, the middle color is transparent, not white. – Tot Zam Mar 16 '17 at 13:37
  • it shouldn't be an issue, I think you can just replace it with 0, also you can try with different amounts of steps and colors – lelloman Mar 16 '17 at 13:40
  • 1
    You can make the black parts be a percentage of the view's height by layering two gradient drawables. Check this gist https://gist.github.com/luksprog/4ac6e7550564b3823840d4a0943c8dd6 for an example(in the example above the black parts will each occupy 10% of the view height). – user Mar 16 '17 at 14:06
  • @Luksprog After seeing thenaoh's first suggestion, I was actually thinking of trying something like that. In this case, does the centerColor actually do anything, or can I leave it out? – Tot Zam Mar 16 '17 at 14:13
  • The idea was to use the centerColor and centerY as a pseudo startColor. Otherwise, without the centerColor, I think the gradient will simply spread the entire height(between transparent and black). – user Mar 16 '17 at 14:17

3 Answers3

7

For an XML only solution, you can create a layer-list with two separate gradient objects.

The following code creates two overlapping gradient objects and uses centerY with centerColor to offset the black section. Here, the centerY attributes are set to 0.9 and 0.1, so the black color is restricted to the top and bottom 10% of the view height.

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape>
            <gradient
                android:angle="90"
                android:centerColor="@android:color/transparent"
                android:centerY="0.9"
                android:endColor="@android:color/black"
                android:startColor="@android:color/transparent" />
        </shape>
    </item>

    <item>
        <shape>
            <gradient
                android:angle="90"
                android:centerColor="@android:color/transparent"
                android:centerY="0.1"
                android:endColor="@android:color/transparent"
                android:startColor="@android:color/black" />
        </shape>
    </item>
</layer-list>

For API level 23 or higher, the following solution will also work, using android:height. This solution can work even if you don't know the total height of your view, as long as you know how large you want the gradient to be.

This code creates two separate gradients, each with a height of 60sp, and then uses android:gravity to float the gradients to the top and bottom of the view.

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:height="60sp"
        android:gravity="top">
        <shape android:shape="rectangle">
            <gradient
                android:angle="90"
                android:endColor="@android:color/black"
                android:startColor="@android:color/transparent" />
        </shape>
    </item>
    <item
        android:height="65sp"
        android:gravity="bottom">
        <shape android:shape="rectangle">
            <gradient
                android:angle="90"
                android:endColor="@android:color/transparent"
                android:startColor="@android:color/black" />
        </shape>
    </item>
</layer-list>

Thank you @Luksprog for the code help, and @thenaoh for the start of the idea.

The above solutions work and it is nice that they are pure XML. If your gradient is showing with stripes, you may want to try a programmatic solution, like shown in @lelloman's answer, to create a smoother gradient.

Tot Zam
  • 8,406
  • 10
  • 51
  • 76
3

Here is how it could be done with a custom Drawable. You can tune the LinearGradient as you want, and then set it as the view's background with view.setBackground(new CustomDrawable());.

public class CustomDrawable extends Drawable {

    private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private int[] colors;
    private float[] positions;

    public CustomDrawable() {
        paint.setStyle(Paint.Style.FILL);
        this.colors = new int[]{0xff000000, 0xffaaaaaa, 0xffffffff, 0xffaaaaaa, 0xff000000};
        this.positions = new float[]{.0f, .2f, .5f, .8f, 1.f};
    }

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

        LinearGradient linearGradient = new LinearGradient(left, top,left, bottom, colors, positions, Shader.TileMode.CLAMP);
        paint.setShader(linearGradient);
    }

    @Override
    public void draw(@NonNull Canvas canvas) {    
        canvas.drawRect(getBounds(), paint);
    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
        paint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        paint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSPARENT;
    }
}
Tot Zam
  • 8,406
  • 10
  • 51
  • 76
lelloman
  • 13,883
  • 5
  • 63
  • 85
0

There is a solution, assuming that you know in advance the height of your view (let's say here 60dp):

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
    android:bottom="40dp">
    <shape android:shape="rectangle">
        <gradient
            android:type="linear"
            android:angle="90"
            android:startColor="#FFFFFF"
            android:endColor="#000000"/>
    </shape>
</item>
<item
    android:top="20dp"
    android:bottom="20dp"
    android:gravity="center_vertical">
    <shape android:shape="rectangle">
        <solid android:color="#FFFFFF"/>
    </shape>
</item>
<item
    android:top="40dp"
    android:gravity="bottom">
    <shape android:shape="rectangle">
        <gradient
            android:type="linear"
            android:angle="90"
            android:startColor="#000000"
            android:endColor="#FFFFFF"/>
    </shape>
</item>
</layer-list>

But if you don't know the height in advance, another solution would be to make your own custom view, like this:

public class MyView extends ImageView
{
    private Paint paint = null;

    public MyView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        super.onDraw(canvas);
        paint.setShader(getLinearGradient(0, getHeight()));
        canvas.drawPaint(paint);
    }

    private LinearGradient getLinearGradient(float y0, float y1)
    {
        // colors :
        int[] listeColors = new int[3];
        listeColors[0] = 0xFF000000;
        listeColors[1] = 0xFFFFFFFF;
        listeColors[2] = 0xFFFFFFFF;

        // positions :
        float[] listPositions = new float[3];
        listPositions[0] = 0;
        listPositions[1] = 0.25F;
        listPositions[2] = 1;

        // gradient :
        return new LinearGradient(0, y0, 0, y0 + (y1 - y0) / 2, listeColors, listPositions, Shader.TileMode.MIRROR);
    }
}

Hope it helps.

matteoh
  • 2,810
  • 2
  • 29
  • 54
  • The height of my view is not a constant size, so the first solution will not work. Is there a way to use that solution with percentages? – Tot Zam Mar 16 '17 at 12:23
  • 1
    I don't think so, as you can see here: http://stackoverflow.com/a/6061494/3527024 . So that's why I suggest my second option. – matteoh Mar 16 '17 at 12:28
  • the second options looks good, however instead of subclassing ImageView you can do the same thing subclassing Drawable and setting it as background to whatever view you want – lelloman Mar 16 '17 at 12:38
  • I just tried your first solution, and it does not render the correct gradient. The black spreads over the entire area. – Tot Zam Mar 17 '17 at 02:56