Draw a bubble programmatically

I would like to have a bubble with the precentage value in my application, I cannot use 9 patches, because I want it to be customizable and its background color needs to be changed. It should look something like this enter image description here

How can i do this? This bubble will have the appearance swollen inside it, like this percentage or some larger layouts. Also, depending on the layout (phone or tablet), it may have one side larger than the other (the arrow is not in the center), so there is another reason that I prefer to do programmatically

+7
android android canvas
source share
2 answers

Create a custom Drawable and use it for the background of any container in which you place your text or other views.
You will need to change the background fill to account for the bubble pointer.
In the code below, you can set the pointer alignment as LEFT, CENTER or RIGHT.
This is just a basic version to give you an idea. You can easily add a setter for the color of the bubble or add stroke properties to "mPaint" for added flexibility.

public class BubbleDrawable extends Drawable { // Public Class Constants //////////////////////////////////////////////////////////// public static final int LEFT = 0; public static final int CENTER = 1; public static final int RIGHT = 2; // Private Instance Variables //////////////////////////////////////////////////////////// private Paint mPaint; private int mColor; private RectF mBoxRect; private int mBoxWidth; private int mBoxHeight; private float mCornerRad; private Rect mBoxPadding = new Rect(); private Path mPointer; private int mPointerWidth; private int mPointerHeight; private int mPointerAlignment; // Constructors //////////////////////////////////////////////////////////// public BubbleDrawable(int pointerAlignment) { setPointerAlignment(pointerAlignment); initBubble(); } // Setters //////////////////////////////////////////////////////////// public void setPadding(int left, int top, int right, int bottom) { mBoxPadding.left = left; mBoxPadding.top = top; mBoxPadding.right = right; mBoxPadding.bottom = bottom; } public void setCornerRadius(float cornerRad) { mCornerRad = cornerRad; } public void setPointerAlignment(int pointerAlignment) { if (pointerAlignment < 0 || pointerAlignment > 3) { Log.e("BubbleDrawable", "Invalid pointerAlignment argument"); } else { mPointerAlignment = pointerAlignment; } } public void setPointerWidth(int pointerWidth) { mPointerWidth = pointerWidth; } public void setPointerHeight(int pointerHeight) { mPointerHeight = pointerHeight; } // Private Methods //////////////////////////////////////////////////////////// private void initBubble() { mPaint = new Paint(); mPaint.setAntiAlias(true); mColor = Color.RED; mPaint.setColor(mColor); mCornerRad = 0; setPointerWidth(40); setPointerHeight(40); } private void updatePointerPath() { mPointer = new Path(); mPointer.setFillType(Path.FillType.EVEN_ODD); // Set the starting point mPointer.moveTo(pointerHorizontalStart(), mBoxHeight); // Define the lines mPointer.rLineTo(mPointerWidth, 0); mPointer.rLineTo(-(mPointerWidth / 2), mPointerHeight); mPointer.rLineTo(-(mPointerWidth / 2), -mPointerHeight); mPointer.close(); } private float pointerHorizontalStart() { float x = 0; switch (mPointerAlignment) { case LEFT: x = mCornerRad; break; case CENTER: x = (mBoxWidth / 2) - (mPointerWidth / 2); break; case RIGHT: x = mBoxWidth - mCornerRad - mPointerWidth; } return x; } // Superclass Override Methods //////////////////////////////////////////////////////////// @Override public void draw(Canvas canvas) { mBoxRect = new RectF(0.0f, 0.0f, mBoxWidth, mBoxHeight); canvas.drawRoundRect(mBoxRect, mCornerRad, mCornerRad, mPaint); updatePointerPath(); canvas.drawPath(mPointer, mPaint); } @Override public int getOpacity() { return 255; } @Override public void setAlpha(int alpha) { // TODO Auto-generated method stub } @Override public void setColorFilter(ColorFilter cf) { // TODO Auto-generated method stub } @Override public boolean getPadding(Rect padding) { padding.set(mBoxPadding); // Adjust the padding to include the height of the pointer padding.bottom += mPointerHeight; return true; } @Override protected void onBoundsChange(Rect bounds) { mBoxWidth = bounds.width(); mBoxHeight = getBounds().height() - mPointerHeight; super.onBoundsChange(bounds); } } 

Using
The example below shows how you can use BubbleDrawable.

MainActivity.java

 public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); LinearLayout linearLayout = (LinearLayout)findViewById(R.id.myLayout); BubbleDrawable myBubble = new BubbleDrawable(BubbleDrawable.CENTER); myBubble.setCornerRadius(20); myBubble.setPointerAlignment(BubbleDrawable.RIGHT); myBubble.setPadding(25, 25, 25, 25); linearLayout.setBackgroundDrawable(myBubble); } } 

activity_main.xml

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" > <LinearLayout android:id="@+id/myLayout" android:orientation="vertical" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Some Text" android:textAppearance="?android:attr/textAppearanceLarge" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Some Other Text" android:textAppearance="?android:attr/textAppearanceLarge" /> </LinearLayout> </RelativeLayout> 
+12
source share

Obviously, it is never recommended that you have code in your application that you don’t understand, so I won’t just write a bunch of equations in Java code for you. If, however, you follow and understand the math below, then it will be relatively simple for you to use the described equations in your code and draw an arc.


To get the rounded tip of the pointer, you need to change updatePointerPath() .
For now, it just uses rLineTo () to draw the three lines that make up the triangle.
There is another method in the android Path class called arcTo() , which takes the form:

  arcTo(RectF oval, float startAngle, float sweepAngle) 

You can use this method to draw your arc at the end of the pointer, but first you need to work out a few things.

You can already calculate the coordinates of the three corners of the pointer triangle. This is what updatePointerPath() already does. Now take a look at the chart below. To use arcTo() , you will need to calculate the following:

  • The coordinates of point T where your arc begins.
  • Coordinates of the upper left and lower right corners of the bounding RectF
  • Your starting angle ( Alpha )
  • Sweep Angle (2 * Phi )

Diagram 2

Starting angle Alpha can be easily found using the base trigger, as shown in the diagram below.

Diagram 1

Note. . It is best if you use Radians instead of Degrees for all angles, as this is what all the trigger functions in the android 'Math' class require. With that in mind:

  • There are 2 Pi Radians in a circle
  • Three angles in a triangle make up Pi Radians
  • Right angle Pi / 2 Radians

So, adding three triangles to the triangle formed by points C , T and P , you get:

Alpha + Phi + Pi / 2 = Pi

therefore

Phi = Pi / 2 - Alpha

So now we have calculated Alpha and Phi .

Next, d is the distance between point P and the bottom border of the frame.
You can get it by calculating the distance from point C to point P , and then subtracting the radius r .
Now:

sin ( Alpha ) = r / (distance from C to P )

Consequently:

distance from C to P = r / sin ( Alpha )

And therefore, given that the distance d is the distance from point C to point P minus the radius r , we obtain:

d = ( r / sin ( Alpha )) - r

This gives you all the information you need to calculate the coordinates of the upper left and lower right corners of the bounding RectF.

Now all that remains is to work out the coordinates of the point T.

First determine the distance from P to T.
Given that:

tan ( Alpha ) = r / (distance from P to T )

We get:

distance from P to T = r / tan ( Alpha )

Finally, adding another point to the chart ....

Diagram 3

We can see that:

sin ( Alpha ) = (distance from P to A ) / (distance from P to T )

So:

distance from P to A = (distance from P to T ) * sin ( Alpha )

Similarly:

cos ( Alpha ) = (distance from T to A ) / (distance from P to T )

So:

distance from T to A = (distance from P to T ) * cos ( Alpha )

Using this information, you can calculate the coordinates of the point T !

If you all understand this, then coding is easy. If you don’t know anything, just ask.


Below is an idea of ​​what an updated updatePointerPath() might look like.

 private void updatePointerPath() { float xDistance; float yDistance; mPointer = new Path(); mPointer.setFillType(Path.FillType.EVEN_ODD); // Set the starting point (top left corner of the pointer) mPointer.moveTo(pointerHorizontalStart(), mBoxHeight); // Define the lines // First draw a line to the top right corner xDistance= mPointerWidth; yDistance= 0; mPointer.rLineTo(xDistance, yDistance); // Next draw a line down to point T xDistance= (mPointerWidth / 2) - distancePtoA; yDistance= mPointerHeight - distanceTtoA; mPointer.rLineTo(-xDistance, yDistance); //Note: Negative sign because we are moving back to the left // Next draw the arc // Note: The RectF used in arcTo() is defined in absolute screen coordinates float boundingBoxLeft = pointerHorizontalStart() + (mPointerWidth / 2) - (2 * radius); float boundingBoxTop = mBoxHeight - distanceD - (2 * radius); float boundingBoxRight = boundingBoxLeft + (2 * radius); float boundingBoxBottom = boundingBoxTop + (2 * radius); RectF boundingBox = new RectF(boundingBoxLeft, boundingBoxTop, boundingBoxRight, boundingBoxBottom); // Note: While the methods in the android Math class like sin() and asin() all use Radians, // for some reason it was decided that arcTo() in the Path class would use Degrees, // so we have to convert the angles float startAngleInDegrees = angleAlpha * (180 / Math.PI); float sweepAngleInDegrees = 2 * anglePhi * (180 / Math.PI); mPointer.arcTo(boundingBox, startAngleInDegrees, sweepAngleInDegrees); // Finally draw the line from the end of the arc back up to the top left corner // Note: The distances are the same as from the top right corner to point T, // just the direction is different. mPointer.rLineTo(-xDistance, -yDistance); // Note: Negative in front of yDistance because we are moving up // Close off the path mPointer.close(); } 
+10
source share

All Articles