colourful polylines in android maps api v2 - android
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.
Related
Get draw bounds/rect of a view
I'm developing an app where a lot of views can be rotated - it's something like a map of physical objects. I have to detect when 2 objects (all objects are rectangles/squares) are overlapping and if a user has performed a single/double/long tap on an object. For this reason I need to know the drawing bounds of a view. Let's look at the example image bellow - the green rectangle is rotated 45 degrees. I need to get the coordinates of the 4 corners of the green rectangle. If I use view.getHitRect() it returns the bounding box (marked in red) of the view, which is of no use to me. Do you know how could I get the coordinates of the edges of a view? The only solution I could think of is to subclass a View, manually store the initial coordinates of the corners and calculate their new values on every modification to the view - translation, scale and rotation but I was wondering if there is a better method. P.S. The app should be working on Android 2.3 but 4.0+ solutions are also welcomed.
Thanks to pskink I explored again the Matrix.mapPoints method and managed to get the proper coordinates of the corners of the rectangle. If you are running on Android 3.0+ you can easily get the view's matrix by calling myView.getMatrix() and map the points of interest. I had to use 0,0 for the upper left corner and getWidth(),getHeight() for the bottom right corner and map these coordinates to the matrix. After that add view's X and Y values to get the real values of the corners. Something like: float points[] = new float[2]; points[0] = myView.getWidth(); points[1] = myView.getHeight(); myView.getViewMatrix().mapPoints(points); Paint p = new Paint(); p.setColor(Color.RED); //offset the point and draw it on the screen canvas.drawCircle(center.getX() + points[0], center.getY() + points[1], 5f, p); If you have to support lower versions of Android you can use NineOldAndroids. Then I've copied and modified one of its internal methods to get the view's matrix: public Matrix getViewMatrix() { Matrix m = new Matrix(); Camera mCamera = new Camera(); final float w = this.getWidth(); final float h = this.getHeight(); final float pX = ViewHelper.getPivotX(this); final float pY = ViewHelper.getPivotY(this); final float rX = ViewHelper.getRotationX(this);; final float rY = ViewHelper.getRotationY(this); final float rZ = ViewHelper.getRotation(this); if ((rX != 0) || (rY != 0) || (rZ != 0)) { final Camera camera = mCamera; camera.save(); camera.rotateX(rX); camera.rotateY(rY); camera.rotateZ(-rZ); camera.getMatrix(m); camera.restore(); m.preTranslate(-pX, -pY); m.postTranslate(pX, pY); } final float sX = ViewHelper.getScaleX(this); final float sY = ViewHelper.getScaleY(this);; if ((sX != 1.0f) || (sY != 1.0f)) { m.postScale(sX, sY); final float sPX = -(pX / w) * ((sX * w) - w); final float sPY = -(pY / h) * ((sY * h) - h); m.postTranslate(sPX, sPY); } m.postTranslate(ViewHelper.getTranslationX(this), ViewHelper.getTranslationY(this)); return m; } I've put this method in an overloaded class of a view (in my case - extending TextView). From there on it's the same as in Android 3.0+ but instead of calling myView.getMatrix() you call myView.getViewMatrix().
Draw bubble programmatically
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(); }
how can we define dynamic(in parabola curve) Path of ViewObject(Bitmap)
I am Currently working on One 2D Android Game, In this game One ViewObject(Bitmap) is moving Across Screen On Parabola Path Like in this Image, But this Path is Static, the Static path is getting throught the Drawing with Fingure on canvas, As Same as signature Drawing. The Bitmap Move code On this Static Path is //animation step private static int iMaxAnimationStep = 900; private int iCurStep = 0; private Path ptCurve = new Path(); //curve private PathMeasure pm; //curve measure private float fSegmentLen; //curve segment length //init smooth curve PointF point = aPoints.get(0); ptCurve.moveTo(point.x, point.y); for(int i = 0; i < aPoints.size() - 1; i++){ point = aPoints.get(i); PointF next = aPoints.get(i+1); ptCurve.quadTo(point.x, point.y, (next.x + point.x) / 2, (point.y + next.y) / 2); } pm = new PathMeasure(ptCurve, false); fSegmentLen = pm.getLength() / iMaxAnimationStep;//20 animation steps //animate the Bitmap Matrix mxTransform = new Matrix(); if (iCurStep <= iMaxAnimationStep) { pm.getMatrix(fSegmentLen * iCurStep, mxTransform, PathMeasure.POSITION_MATRIX_FLAG); mxTransform.preTranslate(-Bitmap.getWidth(), -Bitmap.getHeight()); canvas.drawBitmap(Bitmap, mxTransform, null); iCurStep++; //advance to the next step mPauseViewHandler.post(mPauseViewRunnable); } else { iCurStep = 0; } But My Problem is I want to Move This ViewObject(Bitmap) On Dynamic Path(in parabola curve) & that Dynamic curved path will work in Any Device. I have searched Lot but i can't Find Solution How to get Dynamic Path (in parabola curve). help! If you have Any Solution,Suggestion, idea ,tutorial regarding this post is Mostly Appreciated.
It's simple enough to fill aPoints array based on your screen size, and get a parabolic path based on those points. I've removed all your bitmap/animation code, this code below will calculate the path and draw it on the screen. We need a new variable to set how many curves we want in the screen. If you prefer it's easy to change the math and define the size of the curve instead. private int numberOfCurves = 5; With that it's simple to calculate 3 points for each parabola: public void calculatePoints(){ float w = v.getWidth(); //Screen width float h = v.getHeight(); //Screen height float curveSize = w/numberOfCurves; // Curve size float curveHeight = (h/100) * 20; //80% of the screen size Log.d(TAG,"h:"+h +" - w:" + w); float lastX = 0; //last used X coordinate for (int i=0;i<numberOfCurves;i++){ //for each curve we'll need 3 points float newX = lastX + curveSize; PointF p = new PointF(lastX, h); //first point is the last point PointF p1 = new PointF((lastX + newX)/2, curveHeight); //the middle point is halfway between the last and the new point PointF p2 = new PointF(newX,h); // the new point is last point + the size of our curve aPoints.add(p); //fill in the array aPoints.add(p1); aPoints.add(p2); lastX = newX; //update last point } //log our points for (PointF p : aPoints){ Log.d(TAG,p.x +"-"+p.y); } } Now we have a set of points defining each parabola, we need to draw it. Instead of using quadTo, use cubicTo. It takes 3 points and draws a curve connecting them. Put it onDraw, and you have your parabolas drawn on the screen. private Path ptCurve = new Path(); //curve #Override public void onDraw(Canvas canvas) { calculatePoints(); Log.d(TAG,"DRAWING"); PointF point = aPoints.get(0); ptCurve.moveTo(point.x, point.y); for(int i = 0; i < aPoints.size() - 1; i+=3){ point = aPoints.get(i); PointF middle = aPoints.get(i+1); PointF next = aPoints.get(i+2); ptCurve.cubicTo(point.x, point.y, middle.x,middle.y, next.x , next.y); } canvas.drawPath(ptCurve, paint); } So your ptCurve variable is now filled with a parabolic path, with as many curves as you've defined earlier, and it will work on any screen size.
How to get total area covered while drawing path on canvas android?
Im using below code to draw line on bitmap canvas while finger touch move... here i posted partial code and it is working fine.. As shown in below image, the black and white bitmap erased on touch drag.. I made canvas transparent so the parent layout background(color image) is getting visible. I want to know , how much area is erased(like 50% or 60% of bitmap ).. is there any way to find that? //Erasing paint mDrawPaint = new Paint(); mDrawPaint.setAntiAlias(true); mDrawPaint.setDither(true); mDrawPaint.setStyle(Paint.Style.STROKE); mDrawPaint.setStrokeJoin(Paint.Join.ROUND); mDrawPaint.setStrokeCap(Paint.Cap.ROUND); mDrawPaint.setStrokeWidth(50); mDrawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); BlurMaskFilter mBlur = new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL); mDrawPaint.setMaskFilter(mBlur); private void doDraw(Canvas c) { c.drawBitmap(mBitmap, 0, 0,null ); } private float mX, mY; private static final float TOUCH_TOLERANCE = 1; void touch_start(float x, float y) { mPath.reset(); mPath.moveTo(x, y); mX = x; mY = y; } void touch_move(float x, float y) { float dx = Math.abs(x - mX); float dy = Math.abs(y - mY); if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) { mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2); mX = x; mY = y; } canvas.drawPath(mPath, mDrawPaint ); //Erasing Black and white image } void touch_up() { mPath.lineTo(mX, mY); // commit the path to our offscreen mCanvas.drawPath(mPath, mDrawPaint); // kill this so we don't double draw mPath.reset(); }
Try to use Monte Carlo method to estimate percentage of transparent area. I think it is a fastest and easiest way to do this. Take about 50 (depends on accuracy you need) random pixels on your transparency mask and check their color. Then calc ans = TransparentPixelsCount/TestPixelCount. It is very hard to calculate square of user's drawings using path coordinates. And it's quite long to iterate over all pixels. So, IMHO Monte Carlo is your choise.
To get an exact (and slow) answer, you need to inspect every pixel and count the number are transparent and divide by the total number of pixels. If your requirements allow for some estimation, it is probably best to sample the image. You could downsize the image and run and the above procedure on the smaller image. That has the disadvantage that the scaling operation might be going through all the pixels making it slow. I would recommend a grid sampling, it is similar to downsizing, but skips over pixels. Basically, we evenly space x sample points on a grid over the image. Then count the number of sample points that are transparent. The estimate of transparent percentage is the total transparent samples/number of transparent samples. You can get reasonable accuracy (usually within 5%) with a small number, say 100, samples. Here is a code function that implements this method -- bm is the Bitmap and scale is the number of samples per axis, so setting scale = 10 gives 100 total samples (10x10 sampling grid over the image). static public float percentTransparent(Bitmap bm, int scale) { final int width = bm.getWidth(); final int height = bm.getHeight(); // size of sample rectangles final int xStep = width/scale; final int yStep = height/scale; // center of the first rectangle final int xInit = xStep/2; final int yInit = yStep/2; // center of the last rectangle final int xEnd = width - xStep/2; final int yEnd = height - yStep/2; int totalTransparent = 0; for(int x = xInit; x <= xEnd; x += xStep) { for(int y = yInit; y <= yEnd; y += yStep) { if (bm.getPixel(x, y) == Color.TRANSPARENT) { totalTransparent++; } } } return ((float)totalTransparent)/(scale * scale); } For reference, the slow method that would give you the results by counting every pixel is below. It can be used for reference on testing the above estimator. static public float percentTransparent(Bitmap bm) { final int width = bm.getWidth(); final int height = bm.getHeight(); int totalTransparent = 0; for(int x = 0; x < width; x++) { for(int y = 0; y < height; y++) { if (bm.getPixel(x, y) == Color.TRANSPARENT) { totalTransparent++; } } } return ((float)totalTransparent)/(width * height); }
A different approach on this: you can calculate the size of each path using ComputeBounds. Then it should be simple to compare this with the size of your view and decide the % of the drawing. Jus you need to keep in mind that the path can be drawn over itself, so you need to be careful and handle that in the calculation.
Store all point x and y value in two different sorted sets, one for x value of point and other for y value of point. The final value of your bound will be point(min_x,min_y) and point(max_x,max_y).
You need to detect the points lying inside the drawn polygon. Here is the functions which takes array that contains all the drawn point, and second parameter are the points itself i.e. x ,y. // Return true if the dot { x,y } is within any of the polygons in the list function pointInPolygons( polygons, dot ) for (i=1, [polygons count] i++) { if (pointInPolygon( polygons[i], dot )) return true } return false end // Returns true if the dot { x,y } is within the polygon //defined by points table { {x,y},- --{x,y},{x,y},... } function pointInPolygon( points, dot ) local i, j = #points, #points local oddNodes = false for i=1, #points do if ((points[i].y < dot.y and points[j].y>=dot.y or points[j].y< dot.y and points[i].y>=dot.y) and (points[i].x<=dot.x or points[j].x<=dot.x)) then if (points[i].x+(dot.y-points[i].y)/(points[j].y-points[i].y)*(points[j].x-points[i].x)<dot.x) then oddNodes = not oddNodes end end j = i end return oddNodes end
Sprite collideswith circle (Android AndEngine)?
I have circular sprites and I need to check to see if they collide with any other circle. I tried: public boolean collision(){ boolean collide=false; if(spriteNum>0) for(int x=0;x<spriteNum;x++) if(yourSprite[spriteNum].collidesWith(yourSprite[x])) collide=true; return collide; } But that creates a rectangle around it which kind of throws it off. I could use the distance formula to manually calculate if two sprites are in contact, but that seems taxing and each sprite is attached with a circle physics body, meaning there centers are constantly moving (and I don't know how to find the center). Any ideas?
As Alexandru points out, no circle collision detection is supported by AndEngine so far. The best way is to implement it yourself. His solution works fine (fast), but just in case you need a bit more precision, I will post another approximation: // There is no need to use Sprites, we will use the superclass Entity boolean collidesWith(Entity circle){ final float x1 = this.getX(); final float y1 = this.getY(); final float x2 = circle.getX(); final float y2 = circle.getY(); final float xDifference = x2 - x1; final float yDifference = y2 - y1; // The ideal would be to provide a radius, but as // we assume they are perfect circles, half the // width will be just as good final float radius1 = this.getWidth()/2; final float radius2 = circle.getWidth()/2; // Note we are using inverseSqrt but not normal sqrt, // please look below to see a fast implementation of it. // Using normal sqrt would not need "1.0f/", is more precise // but less efficient final float euclideanDistance = 1.0f/inverseSqrt( xDifference*xDifference + yDifference*yDifference); return euclideanDistance < (radius1+radius2); } /** * Gets an aproximation of the inverse square root with float precision. * #param x float to be square-rooted * #return an aproximation to sqrt(x) */ public static float inverseSqrt(float x) { float xhalf = 0.5f*x; int i = Float.floatToIntBits(x); i = 0x5f3759df - (i>>1); x = Float.intBitsToFloat(i); x = x*(1.5f - xhalf*x*x); return x; } Note I am not the author of the fast inverseSqrt method, it works in Java (and more precisely in Android) because of its floating point representation (see IEEE 754 floating point representation and Java float to byte representation). For further research, see: Quake3 fast inverse Sqrt origins Fast inverse Sqrt implementation in Java
Because there is no circle collision detection in Andengine the only way is to calculate the distance between them boolean collidesWithCircle(Sprite circle) { float x1 = this.getX(); float y1 = this.getY(); float x2 = circle.getX(); float y2 = circle.getY(); double a = x1 - x2; double b = y1 - y2; double c = (a * a) + (b * b); if (c <= this.getWidth()*this.getWidth()) return true; else return false; }
You can create circular bodies if you are using physics world by using PhysicsFactory.createCircularBody() method.