8

REQUIREMENT

How do I create a view looking like this.

enter image description here

I would like to draw a view on the screen which is a line broken into segments showing values percentage of the whole view. My requirements are

  • view has different sections which are different colors
  • the view might not have all sections rendered, it might only have first 2 or first and last or just a single color etc - this is only known at runtime
  • the size of the different sections are only known at runtime therefore need to be specified programmatically
  • left and right corners of the whole view are rounded

IDEAS/THINGS I HAVE TRIED

(1) Custom view rendering 3 rectangles side by side

I have tried a custom view which renders 3 rectangles side by side. But these obviously have square corners.

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int viewHeight = 50;

    canvas.drawrect(0,  0, 60,  viewHeight, paint); // A
    canvas.drawrect(60, 0, 120, viewHeight, paint); // B
    canvas.drawrect(120,0, 180, viewHeight, paint); // C
}

enter image description here

(2) Shape with rounded corners

I know I can use a Shape to define a rectangle with rounded corners using the following, but this is a single color.

<shape xmlns:android="http://schemas.android.com/apk/res/android">
     ...        
    <corners
        android:radius="4dp" />
    ....
</shape>

enter image description here

(3) Layer-list

From Android rectangle shape with two different color, I see I can use a layer-list to specify each item in the shape to have different colors.

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">       

    <item>
        <shape android:shape="rectangle">
            <size
                android:width="40dp"
                android:height="40dp" />
            <solid android:color="#F86F05" />
        </shape>
    </item>

    <item android:top="10dp">
        <shape android:shape="rectangle">
            <size
                android:width="30dp"
                android:height="30dp" />
            <solid android:color="#B31F19" />
        </shape>
    </item>

</layer-list>

enter image description here

(4) Layer-list with corners??

Can I add the "corners" tag to the whole layer-list to get the rounded main corners? I assume not, and that the corners part has to be in the "Item"s shape tags.

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <corners
        android:radius="4dp" />

    <item>
        <shape android:shape="rectangle">
            <size
                android:width="40dp"
                android:height="40dp" />
            <solid android:color="#F86F05" />
        </shape>
    </item>

    <item android:top="10dp">
        <shape android:shape="rectangle">
            <size
                android:width="30dp"
                android:height="30dp" />
            <solid android:color="#B31F19" />
        </shape>
    </item>

</layer-list>

enter image description here

SUMMARY

This last one is getting closer to my requirement however

  • how do i specify each "item"s width programmatically
  • how do i show/hide "item"s programmatically
  • how would i round just the top most visible "item"s top corners and the bottom most "item"s bottom corners

UPDATE: HOW DO I ADD ELEVATION/GREY BORDER

Thank you to "@0X0nosugar" for your solution. I am now wanting to add an elevation or a slight grey border as one of the colors is faily light and close to the background color. When I add the following I get a rectangular shadow which looks terrible with the curved corners.

android:elevation="2dp"
android:outlineProvider="bounds"

enter image description here

I would like it to appear like below

enter image description here

se22as
  • 2,282
  • 5
  • 32
  • 54

4 Answers4

9

You can create a custom View which will draw rectangles on a clipped part of the Canvas:

enter image description here

public class RoundedCornersSegmentedView extends View {

    private Paint paintA, paintB, paintC;
    private float cornerRadius;
    private float measuredWidth, measuredHeight;
    private RectF rect = new RectF(0, 0, 0,0);
    private Path rectPath = new Path();

    public RoundedCornersSegmentedView(Context context) {
        super(context);
        init();
    }

    public RoundedCornersSegmentedView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RoundedCornersSegmentedView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        setWillNotDraw(false);

        // add this so Canvas.clipPath() will give the desired result also for devices running Api level lower than 17,
        // see https://stackoverflow.com/a/30354461/5015207
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            setLayerType(LAYER_TYPE_SOFTWARE, null);
        }
        paintA = new Paint(Paint.ANTI_ALIAS_FLAG);
        paintA.setColor(Color.GREEN);
        paintA.setStyle(Paint.Style.FILL);
        paintB = new Paint(Paint.ANTI_ALIAS_FLAG);
        paintB.setColor(Color.YELLOW);
        paintB.setStyle(Paint.Style.FILL);
        paintC = new Paint(Paint.ANTI_ALIAS_FLAG);
        paintC.setColor(Color.MAGENTA);
        paintC.setStyle(Paint.Style.FILL);

        // with  <dimen name="corner_radius">60dp</dimen> in res/values/dimens.xml
        cornerRadius = getResources().getDimensionPixelSize(R.dimen.corner_radius);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        measuredWidth = right - left;
        measuredHeight = bottom - top;
        rect.set(0, 0, measuredWidth, measuredHeight);
        rectPath.reset();
        rectPath.addRoundRect(rect, cornerRadius, cornerRadius, Path.Direction.CW);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.clipPath(rectPath);
        canvas.drawRect(0,0,measuredWidth/3f, measuredHeight, paintA);
        canvas.drawRect(measuredWidth/3f,0,2 * measuredWidth/3f, measuredHeight, paintB);
        canvas.drawRect(2 * measuredWidth/3f,0,measuredWidth, measuredHeight, paintC);
    }
}

If you want to add some kind of semi-transparent edge, you can use a Paint with a transparent color and fill type Paint.Style.STROKE and draw a rounded rectangle.

Paint shadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// material color "Blue Gray 400",
// see https://material.io/design/color/the-color-system.html
shadowPaint.setColor(Color.argb(30, 120, 144, 156));
shadowPaint.setStyle(Paint.Style.STROKE);
shadowPaint.setStrokeWidth(30);

The rectangle (instantiate outside of onLayout() for better performance):

private RectF shadowRect = new RectF(0,0,0,0);

In onLayout():

int inset = 20;
shadowRect.set(inset, inset, measuredWidth - inset, measuredHeight - inset);

You should toggle the color/ the alpha value for the shadow Paint as well as the values for stroke width and inset until you think it looks good.

Apply in onDraw() after you've drawn the colored segments:

canvas.drawRoundRect(shadowRect, cornerRadius, cornerRadius, shadowPaint);

enter image description here

It can also look nice (more 3D) if you stack semi-transparent Paints with decreasing stroke width and increasing inset, like building your own color gradient.

Thanks to @wblaschko for sharing the code snippet on ViewOutlineProvider! I added it to my example and got the following effect:

enter image description here

Changes to my code (note: only possible for Api level 21+)

An inner class of the custom View:

@TargetApi(21)
static class ScalingOutlineProvider extends ViewOutlineProvider {
    private int cornerRadius;
    ScalingOutlineProvider(int cornerRadius){
        this.cornerRadius = cornerRadius;
    }
    @Override
    public void getOutline(View view, Outline outline) {
        outline.setRoundRect(0, 0, view.getWidth(), view.getHeight (), cornerRadius);
    }
}

And at the end of init():

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
    // elevation of 4dp (cornerRadius was 60dp)
    setElevation(cornerRadius/15);
    setOutlineProvider(new ScalingOutlineProvider(cornerRadius));
}
Bö macht Blau
  • 12,820
  • 5
  • 40
  • 61
  • Thank you for this reply, this looks very promising, I will take a proper look at it tomorrow when I am back working on this. – se22as Jan 08 '19 at 20:26
  • This works perfectly as required. It was the onLyaout and clipPath part in onDraw that i was missing. Much appreciated. – se22as Jan 09 '19 at 08:51
  • Any idea how to add elevation so it looks 3D or a slight grey border. If i add "android:elevation="2dp" android:outlineProvider="bounds" " its a grey rectangular shadow which looks terrible with my curved corners – se22as Jan 09 '19 at 09:13
  • I have updated my original question to contain screenshots of what it looks like with elevation and what I am trying to achieve. Thanks in advance – se22as Jan 09 '19 at 09:21
  • @se22as - you could draw a rounded rectangle with a Paint with fill type STROKE. I edited my answer accordingly. – Bö macht Blau Jan 09 '19 at 19:13
  • Thank you so much this is brilliant and exactly the look I want. I very much appreciate your help and quick response. – se22as Jan 10 '19 at 13:28
2

For the shadow follow-up question, you have two options. Use built-in methods or draw your own (depending on needs). The former is likely the correct way to do it, unless you need a custom shadow.

Built-In Methods

See the section on Outline in this blog post: https://android.jlelse.eu/mastering-shadows-in-android-e883ad2c9d5b

Example code from the post:

Create an OutlineProvider

public class ScalingLayoutOutlineProvider extends ViewOutlineProvider {

    @Override
    public void getOutline(View view, Outline outline) {
        outline.setRoundRect(0, 0, width, height, radius);
    }
}

Add the Outline Provider to your View

public class ScalingLayout extends FrameLayout {

    //...
    viewOutline = new ScalingLayoutOutlineProvider(w, h, currentRadius);
    setOutlineProvider(viewOutline);
    //..

}

Drawing Your Own

For the border, you can use View.getElevation() to get the desired height: https://developer.android.com/reference/android/view/View#getElevation() and then draw another shape behind the bar for the shadow.

wblaschko
  • 3,252
  • 1
  • 18
  • 24
0

You should use a CardView take a look at this example:

<androidx.cardview.widget.CardView
  android:layout_width="match_parent"
  android:layout_height="180dp"
  android:layout_margin="@dimen/activity_vertical_margin"
  app:cardPreventCornerOverlap="false"
  app:cardCornerRadius="@dimen/activity_vertical_margin"
  android:foreground="?android:attr/selectableItemBackground">

  <androidx.constraintlayout.widget.ConstraintLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content">

      <View
        android:id="@+id/view1"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#0c21a9"
        app:layout_constraintWidth_percent="0.33"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

      <View
        android:id="@+id/view2"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#51c914"
        app:layout_constraintWidth_percent="0.33"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/view1" />

     <View
        android:id="@+id/view3"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#a90c0c"
        app:layout_constraintWidth_percent="0.33"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/view2" />     

  </androidx.constraintlayout.widget.ConstraintLayout>     

</androidx.cardview.widget.CardView> 

This property prevents overlap in rounded corners app:cardPreventCornerOverlap="false"

Note that I am using the lib androidx recommended by Google AndroidX. If you prefer, you can use the legacy support library.

Diego Alvis
  • 1,785
  • 1
  • 13
  • 14
  • Thank you for the suggestion, this looks promising. I presume I would simply have to set the width and visibility of each of the child views and the main CardView tags are the ones which round the corners of the whole view. – se22as Jan 08 '19 at 20:28
  • Yes, this can help you with the width app:layout_constraintWidth_percent="0.33" ConstraintLayout has many benefits that you can take advantage of to build views using only a parent – Diego Alvis Jan 09 '19 at 17:59
0

For example (adjust colors to desired ones, margins, heights etc.):

enter image description here

You could create this using LinearLayout as root layout with horizontal orientation and assigning some weights, create child layouts and add to them custom backgrounds. Simple without too much nesting layouts.

For example:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:layout_margin="20dp"
    android:orientation="horizontal">

    <LinearLayout
        android:id="@+id/statsLayout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:clickable="true"
        android:background="@drawable/left_background">

        <TextView
            android:id="@+id/stats"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="A%"
            android:textAppearance="@style/TextAppearance.AppCompat.Title"
            android:textColor="@android:color/black" />
    </LinearLayout>


    <LinearLayout
        android:id="@+id/ncaaInfoLayout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:clickable="true"
        android:background="@drawable/middle_background">

        <TextView
            android:id="@+id/ncaaInfo"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="B%"
            android:textAppearance="@style/TextAppearance.AppCompat.Title"
            android:textColor="@android:color/black" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/accountLayout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:clickable="true"
        android:background="@drawable/right_background">

        <TextView
            android:id="@+id/account"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="C%"
            android:textAppearance="@style/TextAppearance.AppCompat.Title"
            android:textColor="@android:color/black" />
    </LinearLayout>
</LinearLayout>

Than create background for each child LinearLayout for left one left_bacground.xml:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
    android:bottomLeftRadius="30dp"
    android:topLeftRadius="30dp" />
<solid android:color="@android:color/holo_green_dark" />

Than for LinearLayout in middlemiddle_background.xml`:

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

And last one LinearLayout for the right right_bacgkround.xml:

  <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
    android:bottomRightRadius="30dp"
    android:topRightRadius="30dp" />
<solid android:color="@android:color/holo_blue_dark" />

Yupi
  • 4,402
  • 3
  • 18
  • 37
  • Thank you for this suggestion. This however won't work if for example the left color is 0% and therefore should not be displayed, as now the middle color needs the corners specified on its background. So it comes back to the issue of specifying the rounded corners on whichever color programmatically – se22as Jan 08 '19 at 20:25
  • You can easily set that by changing visibility of layout from `VISIBLE` to `GONE` and assigning corresponding `background` from `drawable`. I think this is the most simple solution. – Yupi Jan 08 '19 at 20:40