Draw bubble programmatically - android

I would like to have a bubble with a precentage value in my app, I can't use 9 patches as i want it to be customizable and its background color should be changeble.
It should look something like this
How can I do it? This bubble will have views inflated inside of it, like this percentage or some larger layouts.
Also depending on the layout(phone or tablet) it might have one side larger than the other (arrow not at the center) so that's another reason i prefer doing it programmatically

Create a custom Drawable and use it for the background of whatever container you are putting your text or other views into.
You will need to modify the padding of the background to take the bubble's pointer into account.
The code below allows you to set the alignment of the pointer as LEFT, CENTER or RIGHT.
This is just a basic version to give you an idea. You could easily add a setter for the bubble color, or add stroke properties to 'mPaint' for additional 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);
}
}
Usage
The example below shows how you might 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>

Obviously it's never a good idea to have code in your app that you don't understand, so I won't just write out a bunch of equations in java code for you. If however you follow and understand the maths below, then it will be a relatively simple matter for you to use the described equations in your code and draw the arc.
To achieve a rounded tip on the pointer you will need to modify updatePointerPath().
At the moment it just uses rLineTo() to draw three lines forming a 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 tip of the pointer, but you need to work out a few things first.
You can already calculate the coordinates of the three corners of the pointer triangle. This is what updatePointerPath() does already. Now take a look at the diagram below. To use arcTo(), you will need to calculate the following:
The coordinates of point T which is where your arc will start.
The coordinates of the top left and bottom right corners of the bounding RectF
Your starting angle ()
Your sweep angle (2 * )
The start angle can easily be found with basic trig as shown in the diagram below.
Note: It will be best if you stick to using Radians instead of Degrees for all the angles since this is what all the trig functions in the android 'Math' class require.
With this in mind:
There are 2 Radians in a circle
The three angles in a triangle add up to Radians
A right angle is /2 Radians
So adding the three angles in the triangle formed by points C, T and P you get:
+ + /2 =
Therefore
= /2 -
So we have now calculated and .
Next, d is the distance between the point P and the bottom of the bounding box.
You can get it by calculating the distance from point C to point P, and then subtracting the radius r.
Now:
sin() = r / (distance from C to P)
Therefore:
distance from C to P = r / sin()
And so given that the distance d is the distance from point C to point P minus the radius r, we get:
d = (r / sin()) - r
That gives you all the info you need to calculate the coordinates of the top left and bottom right corners of the bounding RectF.
Now all that's left is to work out the coordinates of point T.
First work out the distance from P to T.
Given that:
tan() = r / (distance from P to T)
We get:
distance from P to T = r / tan()
Finally, adding one more point to the diagram....
We can see that:
sin() = (distance from P to A) / (distance from P to T)
So:
distance from P to A = (distance from P to T) * sin()
Similarly:
cos() = (distance from T to A) / (distance from P to T)
So:
distance from T to A = (distance from P to T) * cos()
With this info you can now calculate the coordinates of point T !!
If you understood all that, then the coding from here is easy. If you're unsure of anything, then just ask.
The following gives an idea how the updated updatePointerPath() might look.
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();
}

Related

How can I achieve this ListView animation?

What's up guys, I need a little help with this one. I'm trying to achieve a simple(but not really) folding animation on a listview that is being scrolled. Basically, I'm attempting to fold the listview's first visible child backward as if a sheet of paper is being folded downward along the X axis. This goes on on continuously as the user scrolls up and down the list. This is my first time playing around with Matrix animations and Android's camera from the graphics api, so I'm definitely off the mark here.
This is the effect I'm trying to achieve
And this is the effect I'm getting.
I want the animation to begin at the origin(0,0) but both the left and right side, animating from the top of the list item instead of the upper left corner. I'm not very familiar with matrix translations or animations so If anyone much more experience with these techniques than myself can shed some knowledge, it'll be greatly appreciated.
Basically I'm overriding the onDrawChild method of ListView, grabbing the child's bitmap from a drawing cache, and using a matrix to perform the animation. The lighting and camera implementation is code that I took from another sample app in order to get the animation to look as 3D as possible.
I tried playing around with the ListView animations library, but without much luck. I also tried to hack together a solution using code from the developer guides here that uses object animators to achieve a nice little card flip animation, but it started feeling a bit hacky and I couldn't quite get it the way I wanted.
Here's my current implementation. If anyone can shed some light or direction on this one, or maybe if anyone wrote an awesome library that I didn't come across on my searches, please feel free to share. Thanks
#Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
View first = getChildAt(0);
if (child == first) {
if (child.getTop() < 0) {
Bitmap bitmap = getChildDrawingCache(child);
final int top = child.getTop();
child.getRight();
child.getBottom();
child.getLeft();
final int childCenterY = child.getHeight() / 2;
// final int childCenterX = child.getWidth() / 2;
final int parentCenterY = getHeight() / 2; // center point of
// child relative to list
final int absChildCenterY = child.getTop() + childCenterY;
// final int bottom = child.getBottom();
// distance of child center to the list center final int
int distanceY = parentCenterY - absChildCenterY;
final int r = getHeight() / 2;
if (mAnimate) {
prepareMatrix(mMatrix, distanceY, r);
mMatrix.preTranslate(0, top);
mMatrix.postTranslate(0, -top);
}
canvas.drawBitmap(bitmap, mMatrix, mPaint);
}
else {
super.drawChild(canvas, child, drawingTime);
}
} else {
super.drawChild(canvas, child, drawingTime);
}
return false;
}
private void prepareMatrix(final Matrix outMatrix, int distanceY, int r) { // clip
// the
// distance
final int d = Math.min(r, Math.abs(distanceY)); //
// circle formula
final float translateZ = (float) Math.sqrt((r * r) - (d * d));
double radians = Math.acos((float) d / r);
double degree = 45 - (180 / Math.PI) * radians;
// double degree = -180;
mCamera.save();
mCamera.translate(0, 0, r - translateZ);
mCamera.rotateX((float) degree);
if (distanceY < 0) {
degree = 360 - degree;
}
mCamera.rotateY((float) degree);
mCamera.getMatrix(outMatrix);
mCamera.restore();
// highlight elements in the middle
mPaint.setColorFilter(calculateLight((float) degree));
}
private Bitmap getChildDrawingCache(final View child) {
Bitmap bitmap = child.getDrawingCache();
if (bitmap == null) {
child.setDrawingCacheEnabled(true);
child.buildDrawingCache();
bitmap = child.getDrawingCache();
}
return bitmap;
}
private LightingColorFilter calculateLight(final float rotation) {
final double cosRotation = Math.cos(Math.PI * rotation / 180);
int intensity = AMBIENT_LIGHT + (int) (DIFFUSE_LIGHT * cosRotation);
int highlightIntensity = (int) (SPECULAR_LIGHT * Math.pow(cosRotation,
SHININESS));
if (intensity > MAX_INTENSITY) {
intensity = MAX_INTENSITY;
}
if (highlightIntensity > MAX_INTENSITY) {
highlightIntensity = MAX_INTENSITY;
}
final int light = Color.rgb(intensity, intensity, intensity);
final int highlight = Color.rgb(highlightIntensity, highlightIntensity,
highlightIntensity);
return new LightingColorFilter(light, highlight);
}
JazzyListView
has a lot of stuff that's similar to what you want if not exactly what you want. Take a look at how they're defined under jazzy effect and mix and match. I think reverse fly or maybe flip is close to what you want.

Drawable animation with AnimatorSet not working - drawable disappears

I'm trying to develop simple board game. The board is of size 9x9 fields. The balls are appearing on the fields and when the user clicks on the field with the ball, the ball starts to jumping. I implemented the animation in two ways. The first one is working, but it's not easy to add another one following animation (like little stretch or something). And the second one, which seems to be better (there is used the AnimatorSet) is not working. When user clicks on the field with the ball, the ball disappears. I have no idea why :-(.
The first class implements the board and it is the child of View:
public class BoardView extends View {
...
/**
* Initializes fields of the board.
*/
private void initializeFields() {
this.fields = new ArrayList<Field>();
for (int row = 0; row < BoardView.FIELDS_NUMBER; row++) {
for (int column = 0; column < BoardView.FIELDS_NUMBER; column++) {
this.fields.add(new Field(this, row, column));
}
}
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(BoardView.COLOR_ACTIVITY);
if (this.fields == null) {
this.initializeFields();
}
for (int i = 0; i < this.fields.size(); i++) {
this.fields.get(i).draw(canvas);
}
}
...
}
The second one implements the field:
public class Field {
...
/**
* Draws itself on the screen.
*
* #param Canvas canvas
*/
public void draw(Canvas canvas) {
Rect field = this.getRect();
int round = (int)Math.floor(this.board.getFieldSize() / 4);
this.board.getPainter().setStyle(Paint.Style.FILL);
this.board.getPainter().setColor(Field.COLOR_DEFAULT);
// draw field
canvas.drawRoundRect(new RectF(field), round, round, this.board.getPainter());
// draw selected field
if (this.selected) {
this.board.getPainter().setColor(Field.COLOR_SELECTED);
canvas.drawRoundRect(new RectF(field), round, round, this.board.getPainter());
}
// draw ball
if (this.ball != null) {
Point fieldOrigin = new Point(field.left, field.top);
if (this.selected) {
this.ball.animate(canvas, fieldOrigin);
} else {
this.ball.draw(canvas, fieldOrigin);
}
}
}
...
}
And the last one implements the ball:
Here is the first method, which completely works, but it's not flexible enough:
public class Ball {
...
/**
* Draws itself on the screen.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void draw(Canvas canvas, Point fieldOrigin) {
// set painter
Paint painter = this.field.getBoard().getPainter();
painter.setStyle(Paint.Style.FILL);
painter.setColor(Ball.COLORS[this.color]);
// calculate parameters
float halfSize = this.field.getBoard().getFieldSize() / 2;
float cX = fieldOrigin.x + halfSize;
float cY = fieldOrigin.y + halfSize + this.dy;
float radius = 0.6f * halfSize;
// draw circle
canvas.drawCircle(cX, cY, radius, painter);
// the code continues, because of the shadow and light simulation (radial gradients)
}
/**
* Draws jumping animation.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void animate(Canvas canvas, Point fieldOrigin) {
float currentDy = (this.dy - 0.1f);
this.setDy((float)Math.abs(Math.sin(currentDy)) * (-0.15f * this.field.getBoard().getFieldSize()));
this.draw(canvas, fieldOrigin);
this.setDy(currentDy);
try {
Thread.sleep(Ball.ANIMATION_DELAY);
} catch (InterruptedException e) {}
this.field.invalidate();
}
...
}
As you can see, the animation is implemented by sleeping the current Thread and changing parameter dy.
The second method is showing the ball on the field, but the animation is not working as I said in the beginning of the post (after click, the ball disappears):
public class BallShape {
private Field field;
private LayerDrawable ball;
private int color;
private float diameter,
x, y; // top left corner - THE GETTERS AND SETTERS ARE IMPLEMENTED (because of Animator)
...
/**
* Initializes the ball.
*
* #param Field field
* #param int color
*/
public BallShape(Field field, int color) {
this.field = field;
this.color = ((color == Ball.COLOR_RANDOM) ? Ball.randomColor() : color);
// create ball
float halfSize = this.field.getBoard().getFieldSize() / 2;
this.diameter = 0.6f * field.getBoard().getFieldSize();
float radius = this.diameter / 2;
Rect fieldArea = field.getRect();
this.x = fieldArea.left + halfSize - radius;
this.y = fieldArea.top + halfSize - radius;
// color circle
OvalShape circleShape = new OvalShape();
circleShape.resize(this.diameter, this.diameter);
ShapeDrawable circle = new ShapeDrawable(circleShape);
this.initPainter(circle.getPaint());
// the code continues, because of the shadow and light simulation (radial gradients)
// compound shape - ball
ShapeDrawable[] compound = { circle };//, shadow, light };
this.ball = new LayerDrawable(compound);
}
/**
* Draws itself on the screen.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void draw(Canvas canvas, Point fieldOrigin) {
canvas.save();
canvas.translate(this.x, this.y);
this.ball.draw(canvas);
canvas.restore();
}
/**
* Draws jumping animation.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void animate(Canvas canvas, Point fieldOrigin) {
// common data
float halfSize = this.field.getBoard().getFieldSize() / 2;
float radius = this.diameter / 2;
float startY = fieldOrigin.y + halfSize - radius;
float endY = startY - halfSize + 2;
// bounce animation
ValueAnimator bounceAnimation = ObjectAnimator.ofFloat(this, "y", startY, endY);
bounceAnimation.setDuration(BallShape.ANIMATION_LENGTH);
bounceAnimation.setInterpolator(new AccelerateInterpolator());
bounceAnimation.setRepeatCount(ValueAnimator.INFINITE);
bounceAnimation.setRepeatMode(ValueAnimator.REVERSE);
//bounceAnimation.start();
// animation
AnimatorSet bouncer = new AnimatorSet();
bouncer.play(bounceAnimation);
// start the animation
bouncer.start();
}
...
}
Any idea why it's not working? What I've done wrong?
Thank you very, very much.
Two things I would fix.
First of all you start animation in draw() method. You should either start it in onClick() or at least set this.selected to false, to not start it on every draw(). Secondly, after your value animator changes a property, you need to redraw the BallShape. Otherwise nothing will change. For instance you can define setY(float Y) method, change Y there and call invalidate().

how to rotate image around the circumference of a circle?

I'm new to this so don't blame me. I am trying to develop an android app that would make music. I am trying to make a bar that rotates over a bunch of buttons that are displayed in the form of a circle, and when it does to play the sound represented by every button. However so far I managed to make an image rotate around the middle of the screen by setting x and y coordinates representing the centre of the circle, but when I try to put the formula (x + radius*sin(angle)), (y + radius*cos(angle)), it just moves the image I want to rotate at that point. So basically I am trying to rotate an Image around an circle defined by buttons or coordinates rather then an actual circle image. So I need to rotate an image or imageView around a circle, not just a point.
I have added the code ass well so you could have a look at what I'm doing wrong.
ImageView bara = (ImageView) findViewById(R.id.floating_image);
layoutParams[9] = new RelativeLayout.LayoutParams
(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
toop = Math.round(size.x/2); // + 90*Math.sin(ANGLE));
lefft = Math.round(size.y/2); // + 90*Math.cos(ANGLE));
top = (int) toop;
left = (int) lefft;
layoutParams[9].setMargins(top, left, 0, 0);
bara.setLayoutParams(layoutParams[9]);
RotateAnimation rAnim = new RotateAnimation(0.0f, 360.0f, Animation.RELATIVE_TO_SELF, 0 , Animation.RELATIVE_TO_SELF, 0);
rAnim.setRepeatCount(Animation.INFINITE);
rAnim.setInterpolator(new LinearInterpolator());
rAnim.setDuration(8000);
bara.startAnimation(rAnim);
Any help would be really appreciated !!
the code looks like :
private float mCalcX;//x-coord of object
private float mCalcY;//y-coord of object
private double mCenterX;//x-coord of center of circle
private double mCenterY;//y-coord of center of circle
private double mRadius;//circle radius
private double mAngleRadians;//angle of your object to draw in RADs
// whenever you draw the object, calculate the new X and Y coords
mCalcX = (float) (mCenterX+(mRadius*Math.cos(mAngleRadians)));
mCalcY = (float) (mCenterY+(mRadius*Math.sin(mAngleRadians)));
public void setRadius(double r)
{
mRadius = r;
}
public void setStartingAngle(double radians)
{
mAngleRadians = radians;
}
public void setRotationSpeed(double radians)
{
mRotationSpeed = radians;
}
public void increaseRotationAngle()
{
mAngleRadians += mRotationSpeed;
}
public void decreaseRotationAngle()
{
mAngleRadians -= mRotationSpeed;
}
x^2 + y^2 = r^2
Reference: http://www.mathwarehouse.com/geometry/circle/equation-of-a-circle.php
You should animate the centre of your object around all the (x,y) that satisfy that equation for your chosen value of r (the radius of the circle).
I'm not a graphics guy, so forgive the terseness of my response.

colourful polylines in android maps api v2

I want to draw polyline in android maps api version 2. I want it to have many colors, preferably with gradients. It seems to me though, that polylines are allowed to have only single color.
How can I do that? I already have api-v1 overlay drawing what I like, so presumably I can reuse some code
public class RouteOverlayGoogle extends Overlay {
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
//(...) draws line with color representing speed
}
I know it's been a pretty long time since this has been asked, but there are still no gradient polylines (as of writing, ~may 2015) and drawing multiple polylines really doesn't cut it (jagged edges, quite a bit of lag when dealing with several hundred of points, just not very visually appealing).
When I had to implement gradient polylines, what I ended up doing was implementing a TileOverlay that would render the polyline to a canvas and then rasterize it (see this gist for the specific code I wrote to do it https://gist.github.com/Dagothig/5f9cf0a4a7a42901a7b2).
The implementation doesn't try to do any sort of viewport culling because I ended up not needing it to reach the performance I wanted (I'm not sure about the numbers, but it was under a second per tiles, and multiple tiles will be rendered at the same time).
Rendering the gradient polyline can be pretty tricky to get properly however since you're dealing with varying viewports (positions and size): more than that, I hit a few issues with the limit on float precision at high zoom levels (20+). In the end I didn't use the scale and translate functions from the canvas because I would get weird corruption issues.
Something else to watch out for if you use a similar data structure to what I had (latitudes, longitudes and timestamps) is that you need multiple segments to render the gradient properly (I ended up working with 3 points at a time).
For posterity's sake, I'm going to also leave the code from the gist here:
(the projections are done using https://github.com/googlemaps/android-maps-utils if you're wondering where com.google.maps.android.projection.SphericalMercatorProjection comes from)
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileProvider;
import com.google.maps.android.SphericalUtil;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import java.io.ByteArrayOutputStream;
import java.util.List;
/**
* Tile overlay used to display a colored polyline as a replacement for the non-existence of gradient
* polylines for google maps
*/
public class ColoredPolylineTileOverlay<T extends ColoredPolylineTileOverlay.PointHolder> implements TileProvider {
public static final double LOW_SPEED_CLAMP_KMpH = 0;
public static final double LOW_SPEED_CLAMP_MpS = 0;
// TODO: calculate speed as highest speed of pointsCollection
public static final double HIGH_SPEED_CLAMP_KMpH = 50;
public static final double HIGH_SPEED_CLAMP_MpS = HIGH_SPEED_CLAMP_KMpH * 1000 / (60 * 60);
public static final int BASE_TILE_SIZE = 256;
public static int[] getSpeedColors(Context context) {
return new int[] {
context.getResources().getColor(R.color.polyline_low_speed),
context.getResources().getColor(R.color.polyline_med_speed),
context.getResources().getColor(R.color.polyline_high_speed)
};
}
public static float getSpeedProportion(double metersPerSecond) {
return (float)(Math.max(Math.min(metersPerSecond, HIGH_SPEED_CLAMP_MpS), LOW_SPEED_CLAMP_MpS) / HIGH_SPEED_CLAMP_MpS);
}
public static int interpolateColor(int[] colors, float proportion) {
int rTotal = 0, gTotal = 0, bTotal = 0;
// We correct the ratio to colors.length - 1 so that
// for i == colors.length - 1 and p == 1, then the final ratio is 1 (see below)
float p = proportion * (colors.length - 1);
for (int i = 0; i < colors.length; i++) {
// The ratio mostly resides on the 1 - Math.abs(p - i) calculation :
// Since for p == i, then the ratio is 1 and for p == i + 1 or p == i -1, then the ratio is 0
// This calculation works BECAUSE p lies within [0, length - 1] and i lies within [0, length - 1] as well
float iRatio = Math.max(1 - Math.abs(p - i), 0.0f);
rTotal += (int)(Color.red(colors[i]) * iRatio);
gTotal += (int)(Color.green(colors[i]) * iRatio);
bTotal += (int)(Color.blue(colors[i]) * iRatio);
}
return Color.rgb(rTotal, gTotal, bTotal);
}
protected final Context context;
protected final PointCollection<T> pointsCollection;
protected final int[] speedColors;
protected final float density;
protected final int tileDimension;
protected final SphericalMercatorProjection projection;
// Caching calculation-related stuff
protected LatLng[] trailLatLngs;
protected Point[] projectedPts;
protected Point[] projectedPtMids;
protected double[] speeds;
public ColoredPolylineTileOverlay(Context context, PointCollection pointsCollection) {
super();
this.context = context;
this.pointsCollection = pointsCollection;
speedColors = getSpeedColors(context);
density = context.getResources().getDisplayMetrics().density;
tileDimension = (int)(BASE_TILE_SIZE * density);
projection = new SphericalMercatorProjection(BASE_TILE_SIZE);
calculatePointsAndSpeeds();
}
public void calculatePointsAndSpeeds() {
trailLatLngs = new LatLng[pointsCollection.getPoints().size()];
projectedPts = new Point[pointsCollection.getPoints().size()];
projectedPtMids = new Point[Math.max(pointsCollection.getPoints().size() - 1, 0)];
speeds = new double[Math.max(pointsCollection.getPoints().size() - 1, 0)];
List<T> points = pointsCollection.getPoints();
for (int i = 0; i < points.size(); i++) {
T point = points.get(i);
LatLng latLng = point.getLatLng();
trailLatLngs[i] = latLng;
projectedPts[i] = projection.toPoint(latLng);
// Mids
if (i > 0) {
LatLng previousLatLng = points.get(i - 1).getLatLng();
LatLng latLngMid = SphericalUtil.interpolate(previousLatLng, latLng, 0.5);
projectedPtMids[i - 1] = projection.toPoint(latLngMid);
T previousPoint = points.get(i - 1);
double speed = SphericalUtil.computeDistanceBetween(latLng, previousLatLng) / ((point.getTime() - previousPoint.getTime()) / 1000.0);
speeds[i - 1] = speed;
}
}
}
#Override
public Tile getTile(int x, int y, int zoom) {
// Because getTile can be called asynchronously by multiple threads, none of the info we keep in the class will be modified
// (getTile is essentially side-effect-less) :
// Instead, we create the bitmap, the canvas and the paints specifically for the call to getTile
Bitmap bitmap = Bitmap.createBitmap(tileDimension, tileDimension, Bitmap.Config.ARGB_8888);
// Normally, instead of the later calls for drawing being offset, we would offset them using scale() and translate() right here
// However, there seems to be funky issues related to float imprecisions that happen at large scales when using this method, so instead
// The points are offset properly when drawing
Canvas canvas = new Canvas(bitmap);
Matrix shaderMat = new Matrix();
Paint gradientPaint = new Paint();
gradientPaint.setStyle(Paint.Style.STROKE);
gradientPaint.setStrokeWidth(3f * density);
gradientPaint.setStrokeCap(Paint.Cap.BUTT);
gradientPaint.setStrokeJoin(Paint.Join.ROUND);
gradientPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
gradientPaint.setShader(new LinearGradient(0, 0, 1, 0, speedColors, null, Shader.TileMode.CLAMP));
gradientPaint.getShader().setLocalMatrix(shaderMat);
Paint colorPaint = new Paint();
colorPaint.setStyle(Paint.Style.STROKE);
colorPaint.setStrokeWidth(3f * density);
colorPaint.setStrokeCap(Paint.Cap.BUTT);
colorPaint.setStrokeJoin(Paint.Join.ROUND);
colorPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
// See https://developers.google.com/maps/documentation/android/views#zoom for handy info regarding what zoom is
float scale = (float)(Math.pow(2, zoom) * density);
renderTrail(canvas, shaderMat, gradientPaint, colorPaint, scale, x, y);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
return new Tile(tileDimension, tileDimension, baos.toByteArray());
}
public void renderTrail(Canvas canvas, Matrix shaderMat, Paint gradientPaint, Paint colorPaint, float scale, int x, int y) {
List<T> points = pointsCollection.getPoints();
double speed1, speed2;
MutPoint pt1 = new MutPoint(), pt2 = new MutPoint(), pt3 = new MutPoint(), pt1mid2 = new MutPoint(), pt2mid3 = new MutPoint();
// Guard statement: if the trail is only 1 point, just render the point by itself as a speed of 0
if (points.size() == 1) {
pt1.set(projectedPts[0], scale, x, y, tileDimension);
speed1 = 0;
float speedProp = getSpeedProportion(speed1);
colorPaint.setStyle(Paint.Style.FILL);
colorPaint.setColor(interpolateColor(speedColors, speedProp));
canvas.drawCircle((float) pt1.x, (float) pt1.y, colorPaint.getStrokeWidth() / 2f, colorPaint);
colorPaint.setStyle(Paint.Style.STROKE);
return;
}
// Guard statement: if the trail is exactly 2 points long, just render a line from A to B at d(A, B) / t speed
if (points.size() == 2) {
pt1.set(projectedPts[0], scale, x, y, tileDimension);
pt2.set(projectedPts[1], scale, x, y, tileDimension);
speed1 = speeds[0];
float speedProp = getSpeedProportion(speed1);
drawLine(canvas, colorPaint, pt1, pt2, speedProp);
return;
}
// Because we want to be displaying speeds as color ratios, we need multiple points to do it properly:
// Since we use calculate the speed using the distance and the time, we need at least 2 points to calculate the distance;
// this means we know the speed for a segment, not a point.
// Furthermore, since we want to be easing the color changes between every segment, we have to use 3 points to do the easing;
// every line is split into two, and we ease over the corners
// This also means the first and last corners need to be extended to include the first and last points respectively
// Finally (you can see about that in getTile()) we need to offset the point projections based on the scale and x, y because
// weird display behaviour occurs
for (int i = 2; i < points.size(); i++) {
pt1.set(projectedPts[i - 2], scale, x, y, tileDimension);
pt2.set(projectedPts[i - 1], scale, x, y, tileDimension);
pt3.set(projectedPts[i], scale, x, y, tileDimension);
// Because we want to split the lines in two to ease over the corners, we need the middle points
pt1mid2.set(projectedPtMids[i - 2], scale, x, y, tileDimension);
pt2mid3.set(projectedPtMids[i - 1], scale, x, y, tileDimension);
// The speed is calculated in meters per second (same format as the speed clamps); because getTime() is in millis, we need to correct for that
speed1 = speeds[i - 2];
speed2 = speeds[i - 1];
float speed1Prop = getSpeedProportion(speed1);
float speed1to2Prop = getSpeedProportion((speed1 + speed2) / 2);
float speed2Prop = getSpeedProportion(speed2);
// Circle for the corner (removes the weird empty corners that occur otherwise)
colorPaint.setStyle(Paint.Style.FILL);
colorPaint.setColor(interpolateColor(speedColors, speed1to2Prop));
canvas.drawCircle((float)pt2.x, (float)pt2.y, colorPaint.getStrokeWidth() / 2f, colorPaint);
colorPaint.setStyle(Paint.Style.STROKE);
// Corner
// Note that since for the very first point and the very last point we don't split it in two, we used them instead.
drawLine(canvas, shaderMat, gradientPaint, colorPaint, i - 2 == 0 ? pt1 : pt1mid2, pt2, speed1Prop, speed1to2Prop);
drawLine(canvas, shaderMat, gradientPaint, colorPaint, pt2, i == points.size() - 1 ? pt3 : pt2mid3, speed1to2Prop, speed2Prop);
}
}
/**
* Note: it is assumed the shader is 0, 0, 1, 0 (horizontal) so that it lines up with the rotation
* (rotations are usually setup so that the angle 0 points right)
*/
public void drawLine(Canvas canvas, Matrix shaderMat, Paint gradientPaint, Paint colorPaint, MutPoint pt1, MutPoint pt2, float ratio1, float ratio2) {
// Degenerate case: both ratios are the same; we just handle it using the colorPaint (handling it using the shader is just messy and ineffective)
if (ratio1 == ratio2) {
drawLine(canvas, colorPaint, pt1, pt2, ratio1);
return;
}
shaderMat.reset();
// PS: don't ask me why this specfic orders for calls works but other orders will mess up
// Since every call is pre, this is essentially ordered as (or my understanding is that it is):
// ratio translate -> ratio scale -> scale to pt length -> translate to pt start -> rotate
// (my initial intuition was to use only post calls and to order as above, but it resulted in odd corruptions)
// Setup based on points:
// We translate the shader so that it is based on the first point, rotated towards the second and since the length of the
// gradient is 1, then scaling to the length of the distance between the points makes it exactly as long as needed
shaderMat.preRotate((float) Math.toDegrees(Math.atan2(pt2.y - pt1.y, pt2.x - pt1.x)), (float)pt1.x, (float)pt1.y);
shaderMat.preTranslate((float)pt1.x, (float)pt1.y);
float scale = (float)Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
shaderMat.preScale(scale, scale);
// Setup based on ratio
// By basing the shader to the first ratio, we ensure that the start of the gradient corresponds to it
// The inverse scaling of the shader means that it takes the full length of the call to go to the second ratio
// For instance; if d(ratio1, ratio2) is 0.5, then the shader needs to be twice as long so that an entire call (1)
// Results in only half of the gradient being used
shaderMat.preScale(1f / (ratio2 - ratio1), 1f / (ratio2 - ratio1));
shaderMat.preTranslate(-ratio1, 0);
gradientPaint.getShader().setLocalMatrix(shaderMat);
canvas.drawLine(
(float)pt1.x,
(float)pt1.y,
(float)pt2.x,
(float)pt2.y,
gradientPaint
);
}
public void drawLine(Canvas canvas, Paint colorPaint, MutPoint pt1, MutPoint pt2, float ratio) {
colorPaint.setColor(interpolateColor(speedColors, ratio));
canvas.drawLine(
(float)pt1.x,
(float)pt1.y,
(float)pt2.x,
(float)pt2.y,
colorPaint
);
}
public interface PointCollection<T extends PointHolder> {
List<T> getPoints();
}
public interface PointHolder {
LatLng getLatLng();
long getTime();
}
public static class MutPoint {
public double x, y;
public MutPoint set(Point point, float scale, int x, int y, int tileDimension) {
this.x = point.x * scale - x * tileDimension;
this.y = point.y * scale - y * tileDimension;
return this;
}
}
}
Note that this implementation assumes two relatively large things:
the polyline is already complete
that there is only one polyline.
I would assume handling (1) would not be very difficult. However, if you intend to draw multiple polylines this way, you may need to look at some ways to enhance performance (keeping a bounding box of the polylines to be able to easily discard those that do not fit the viewport for one).
One more thing to remember regarding using a TileOverlay is that it is rendered after movements are done, not during; so you may want to back up the overlay with an actual monochrome polyline underneath it to give it some continuity.
PS: this is the first time I try to answer a question, so if there's anything I should fix or do differently please tell me.
One simple solution: draw multiple polylines and individually set the color.

Draw a shape, and make it bigger when user touches

I have seen how to draw a shape in Android, but what I want to know is how to rescale the shape when the user touches over the shape.
Imagine a square into a screen corner, so when you touch it, it grows until fitting the whole screen. I'd like to make that with a transition, animated, not instant.
Any idea of how to do that, or any known resource?
Android has built-in support for Animations. You can find many examples by searching the Web. This one is a good start.
In order to make your shapes touchable, you can implement them by overriding the View class (a nice example can be found here). Then you can use View.OnTouchListener.
The built in Animations are nice in Android but they aren't the most efficient by any means. When performance is a must I would recommend creating your own method. What I would do is create a class that extends View and give it a bounding box (Rect/RectF) and a circle. Then you can use the bounding box to detect when the circle is touched.
public class Circle extends View {
public static final float SCALE_AMOUNT = 1.0f;
public RectF boundingBox;
private Paint paint;
private float circleCenterX, circleCenterY, circleRadius;
private float x, y;
public Circle(Context context) {
super(context);
// Create paint
paint = new Paint();
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
// Set circle start radius
circleRadius = 50.0f;
// Set start x and y (this is the upper left hand corner)
x = 100.0f;
y = 100.0f;
// Create boundingBox
boundingBox = new RectF();
boundingBox.left = x;
boundingBox.top = y;
boundingBox.right = x + (circleRadius*2);
boundingBox.bottom = y + (circleRadius*2);
// Set circleCenterX and circleCenterY (the center of the bounding box and circle)
circleCenterX = x + circleRadius;
circleCenterY = y + circleRadius;
}
public void scale(boolean scaleUp) {
float scaleBy = (scaleUp) ? SCALE_AMOUNT : -SCALE_AMOUNT;
// Update circleRadius
circleRadius += scaleBy;
// Update the bounding box
boundingBox.left = x;
boundingBox.top = y;
boundingBox.right = x + (circleRadius*2);
boundingBox.bottom = y + (circleRadius*2);
// Update the circle center positions
circleCenterX = x + circleRadius;
circleCenterY = y + circleRadius;
}
#Override
public void onDraw(Canvas canvas) {
canvas.drawCircle(circleCenterX, circleCenterY, circleRadius, paint);
}
}
... Then in your Activity class override the onTouchEvent() method and check if your Circle is touched.
Circle circle = new Circle(this);
#Override
public void onDraw(Canvas canvas) {
circle.onDraw(canvas);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float x = event.getX();
float y = event.getY();
// Detect if pointer goes down on screen
if(action == MotionEvent.ACTION_DOWN) {
if(circle.boundingBox.contains(x, y) == true) {
// Circle was touched so scale it
circle.scale(true); // true is scale up, false is scale down
}
}
return true;
}
... This will scale your circle/rectangle every time you touch it. If you wanted to make it continually grow you could have a boolean variable that gets set to true when you touch the shape and grows until you pick your finger up. I haven't tried this code, just typed it up real quick so it may not compile but this is going to be you're best bet. It is really easy to add many shapes and detect touches on all of the shapes. Add different effects to each one... etc. I didn't want to do all of it for you but this should point you in the right direction.
Maybe this github project could help you: https://github.com/markushi/android-circlebutton

Categories

Resources