I am currently building an Android app which produces a maze based upon a difficulty setting (easy produces a maze of 5x5 with the borders, medium 8x8, hard 11x11). The maze is made up of black squares in positions that constitutes a path from the top-left to the bottom-right among others in other directions as well as a one-square border around the maze. If there is a black square, you cannot move in that direction, etc... At the moment, only the interface is being built, not any user interaction (I will add that later). The code is also a little messy - I am aware of this and will sort it out after the problem has been fixed.
The issue with this is the maze is not displayed - just a white blank space! I am using a XML file and activity for the main interface, and a custom view in the form of a drawable for the maze.
This is the activity/class that creates the interface in the game. The Level class that is used here holds data about the current score/maze/user position etc. It has been tested and I am very confident that it works.
public class InGameInterface extends Activity {
Level level;;
MazeView maze;
private boolean[][] currentMaze = null;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.in_game_interface);
level = new Level();
currentMaze = level.getNewMaze();
loadMazeDisplay();
}
private void loadMazeDisplay()
{
maze = new MazeView(this, level, currentMaze);
RelativeLayout mazeHolder = (RelativeLayout) findViewById(R.id.maze);
LayoutParams lP = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
mazeHolder.addView(maze, lP);
}
}
Below is the relevant part of the XML file that I am trying to insert the custom view in. The < are missing because Stack Overflow does not like them here.
RelativeLayout
android:id="#+id/maze"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginLeft="7dp"
android:layout_marginRight="15dp"
android:layout_weight="2" >
RelativeLayout
Below is the MazeView that constructs and should display the maze.
public class MazeView extends View
{
private Activity context;
private Level level;
private boolean mazeIsPath[][] = null;
private Integer widthInPixels = 0, heightInPixels = 0;
private Integer cellWidthInPixels = 0, cellHeightInPixels = 0;
private Paint cell, ball, background, target;
public MazeView(Context context, Level level, boolean[][] maze)
{
super(context);
this.context = (Activity) context;
this.level = level;
mazeIsPath = maze;
cell = new Paint();
cell.setColor(getResources().getColor(R.color.textColor));
ball = new Paint();
ball.setColor(getResources().getColor(R.color.splashBackground));
background = new Paint();
background.setColor(getResources().getColor(R.color.gameInterfaceBackground));
target = new Paint();
target.setColor(getResources().getColor(R.color.buttonBackground));
widthInPixels = this.getWidth();
heightInPixels = this.getHeight();
setFocusable(true);
this.setFocusableInTouchMode(true);
}
protected void onSizeChanged(Integer newW, Integer newH, Integer oldW, Integer oldH)
{
widthInPixels = newW;
heightInPixels = newH;
super.onSizeChanged(newW, newH, oldW, oldH);
}
protected void onDraw(Canvas canvas)
{
Integer counterX = 0, counterY = 0;
canvas.drawRect(0, 0, widthInPixels, heightInPixels, background);
for(counterX = 0; counterX < level.getWidth(); counterX++)
{
for(counterY = 0; counterY < level.getHeight(); counterY++)
{
if(mazeIsPath[counterX][counterY] != true)
{
canvas.drawRect((float)(cellWidthInPixels * counterX), (float)(cellHeightInPixels * counterX), (float)(cellWidthInPixels * (counterX + 1)), (float)(cellHeightInPixels * (counterY + 1)), cell);
}
}
}
canvas.drawCircle( (float)((cellWidthInPixels * level.getCurrentX()) + (0.5f*(cellWidthInPixels))), (float)((cellHeightInPixels * level.getCurrentX()) + (0.5f*(cellHeightInPixels))), (float)(cellHeightInPixels * (2f/3f)), ball);
canvas.drawCircle( (float)((cellWidthInPixels * level.getFinalX()) + (0.5f*(cellWidthInPixels))), (float)((cellHeightInPixels * level.getFinalX()) + (0.5f*(cellHeightInPixels))), (float)(cellHeightInPixels * (4f/5f)), target);
}
}
I am wondering if someone can point out where I have gone wrong and how to fix it!
First amendment to onDraw():
protected void onDraw(Canvas canvas)
{
Integer counterX = 0, counterY = 0;
widthInPixels = new Integer(canvas.getWidth());
heightInPixels = new Integer(canvas.getHeight());
canvas.drawRect(0, 0, widthInPixels, heightInPixels, background);
for(counterX = 0; counterX < level.getWidth(); counterX++)
{
for(counterY = 0; counterY < level.getHeight(); counterY++)
{
if(mazeIsPath[counterX][counterY] != true)
{
canvas.drawRect((float)(cellWidthInPixels * counterX), (float)(cellHeightInPixels * counterX), (float)(cellWidthInPixels * (counterX + 1)), (float)(cellHeightInPixels * (counterY + 1)), cell);
}
}
}
canvas.drawCircle( (float)((cellWidthInPixels * level.getCurrentX()) + (0.5f*(cellWidthInPixels))), (float)((cellHeightInPixels * level.getCurrentX()) + (0.5f*(cellHeightInPixels))), (float)(cellHeightInPixels * (2f/3f)), ball);
canvas.drawCircle( (float)((cellWidthInPixels * level.getFinalX()) + (0.5f*(cellWidthInPixels))), (float)((cellHeightInPixels * level.getFinalX()) + (0.5f*(cellHeightInPixels))), (float)(cellHeightInPixels * (4f/5f)), target);
Log.w("onDraw width", widthInPixels.toString());
Log.w("onDraw height", heightInPixels.toString());
Integer cWidth = new Integer(canvas.getWidth());
Integer cHeight = new Integer(canvas.getHeight());
Log.w("canvas width", cWidth.toString());
Log.w("cavas height", cHeight.toString());
}
i'd say, forget about widthInPixels & heightInPixels as they depend on the layout. To use them you'd have to implement onMeasure (that may be insteresting). However as your view is pretty simple, just use the values from canvas.getWidth() and getHeight(). Try the following to see if it works
private static final int CELLS_PER_SCREEN = 5; // the number of cells that fit on a view
protected void onDraw(Canvas canvas) {
int totalWidth=canvas.getWidth(),
totalHeight=canvas.getHeight(),
cellWidthInPixels=totalWidth / CELLS_PER_SCREEN,
cellHeightInPixels=totalHeight / CELLS_PER_SCREEN;
canvas.drawRect(0, 0, totalWidth, totalHeight, background);
for (int counterX = 0, xlen=level.getWidth(); counterX < xlen; counterX++) {
for (int counterY = 0, ylen=level.getHeight(); counterY < ylen; counterY++) {
if (!mazeIsPath[counterX][counterY]) {
canvas.drawRect(
(float)(cellWidthInPixels * counterX),
(float)(cellHeightInPixels * counterX),
(float)(cellWidthInPixels * (counterX + 1)),
(float)(cellHeightInPixels * (counterY + 1)), cell
);
}
}
}
canvas.drawCircle( (float)((cellWidthInPixels * level.getCurrentX()) + (0.5f*(cellWidthInPixels))), (float)((cellHeightInPixels * level.getCurrentX()) + (0.5f*(cellHeightInPixels))), (float)(cellHeightInPixels * (2f/3f)), ball);
canvas.drawCircle( (float)((cellWidthInPixels * level.getFinalX()) + (0.5f*(cellWidthInPixels))), (float)((cellHeightInPixels * level.getFinalX()) + (0.5f*(cellHeightInPixels))), (float)(cellHeightInPixels * (4f/5f)), target);
Log.w("onDraw width", widthInPixels.toString());
Log.w("onDraw height", heightInPixels.toString());
Integer cWidth = new Integer(canvas.getWidth());
Integer cHeight = new Integer(canvas.getHeight());
Log.w("canvas width", cWidth.toString());
Log.w("cavas height", cHeight.toString());
}
Also some advices for onDraw: OnDraw is called very frequently and it's important to be as optimized as possible.
use int rather than Integers, Integer is an Object while int is just a fast primitive type.
Pre-calculate stuff prone to be re-usable, specially float multiplications. Your drawCircles() do a lot of math that only change with cellWidthInPixels ... So calculate them once, everytime cellWidthInPixels change
Related
I have created sound visualization using felixpalmer library. Now i have to modify his code to look like this.How could i achieve empty spaces between bars. like
I will handle the color scheme but don't know how to add empty spaces in bars and shadow effect.
BarGraphRender
public class BarGraphRenderer extends Renderer
{
private int mDivisions;
private Paint mPaint;
private boolean mTop;
/**
* Renders the FFT data as a series of lines, in histogram form
* #param divisions - must be a power of 2. Controls how many lines to draw
* #param paint - Paint to draw lines with
* #param top - whether to draw the lines at the top of the canvas, or the bottom
*/
public BarGraphRenderer(int divisions , Paint paint , boolean top)
{
super();
mDivisions = divisions;
mPaint = paint;
mTop = top;
}
#Override
public void onRender(Canvas canvas, AudioData data, Rect rect)
{
// Do nothing, we only display FFT data
}
#Override
public void onRender(Canvas canvas, FFTData data, Rect rect)
{
for (int i = 0; i < data.bytes.length / mDivisions; i++)
{
mFFTPoints[i * 4] = i * 4 * mDivisions;
mFFTPoints[i * 4 + 2] = i * 4 * mDivisions;
byte rfk = data.bytes[mDivisions * i];
byte ifk = data.bytes[mDivisions * i + 1];
float magnitude = (rfk * rfk + ifk * ifk);
int dbValue = (int) (10 * Math.log10(magnitude));
if(mTop)
{
mFFTPoints[i * 4 + 1] = 0;
mFFTPoints[i * 4 + 3] = (dbValue * 2 - 10);
}
else
{
mFFTPoints[i * 4 + 1] = rect.height();
mFFTPoints[i * 4 + 3] = rect.height() - (dbValue * 2 - 10);
}
}
canvas.drawLines(mFFTPoints, mPaint);
}
}
I have canvas which draws pie chart for me. After I change radio button to disagree position, my pie chart has to show different values. I am doing this with Listener (setted up on radio group). It is working but for updating the canvas (not adding and overdrawing, I have to clear it first and redraw with new values) I use this:
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
Under the piechart I want to write number of percent. It is working without that line what is on top. But after I execute the line, only piechart is redrawn (with new values) but text with percentage is not shown (even if I set text to that again):
if (checkedId == R.id.agreeRadio) {
values[0] += 1;
values[1] -= 1;
setupPieChart();
setupPercentageValueToGraph();
}
Method calculating percentage and writing it:
public void setupPercentageValueToGraph() {
float[] degrees = calculateData(values);
// get percentage number from values
float percentage = (degrees[0] / (degrees[0] + degrees[1])) * 100;
// setup color of the number shown red/green
if (percentage >= 50) {
percentageTV.setTextColor(Color.parseColor("#47B243"));
} else {
percentageTV.setTextColor(Color.parseColor("#DB262A"));
}
// set the text
percentageTV.setText((int) percentage + "%");
}
Method to set piechart to linearLayout:
public void setupPieChart() {
float[] degrees = calculateData(values);
graphLayout.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
graphLayout.addView(new MyGraphView(this, degrees, size));
}
Class which is drawing the piechart where that clear is used:
public class MyGraphView extends View {
public static final int PADDING = 4;
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private float[] value_degree;
int strokeWidth;
private int[] COLORS = { Color.parseColor("#47B243"),
Color.parseColor("#DB262A") };
// size of bigger half circle
RectF rectf = new RectF(2, 2, 62, 62);
// size of smaller half circle
RectF rectf2 = new RectF(9, 9, 55, 55);
// size of the smallest half circle
RectF rectf3 = new RectF(16, 16, 48, 48);
int temp = 0;
public MyGraphView(Context context, float[] values, int size) {
super(context);
// setting up size of pie chart dynamically
int difference = size / 9;
rectf.set(PADDING, PADDING, size + PADDING, size + PADDING);
rectf2.set(difference + PADDING, difference + PADDING, size
- difference + PADDING, size - difference + PADDING);
rectf3.set(difference * 2 + PADDING, difference * 2 + PADDING, size
- difference * 2 + PADDING, size - difference * 2 +
PADDING);
// setting up brush size
strokeWidth = size / 15;
// assign degrees of agree and disagree to array
value_degree = new float[values.length];
for (int i = 0; i < values.length; i++) {
value_degree[i] = values[i];
}
}
#Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
for (int i = 0; i < value_degree.length; i++) {
// set type of "brush"
paint.setStrokeWidth(strokeWidth);
paint.setStyle(Paint.Style.STROKE);
// set shadow
paint.setShadowLayer(2, 1, 1, Color.BLACK);
// setLayerType(LAYER_TYPE_SOFTWARE, paint);
// agree
if (i == 0) {
final Path path = new Path();
paint.setColor(COLORS[i]);
// draw 3 paths to show 3 curves
path.addArc(rectf, 180, value_degree[i] - 4);
path.addArc(rectf2, 180, value_degree[i] - 5);
path.addArc(rectf3, 180, value_degree[i] - 6);
// draw the path
canvas.drawPath(path, paint);
// disagree
} else {
temp += (int) value_degree[i - 1];
paint.setColor(COLORS[i]);
final Path path = new Path();
path.addArc(rectf, temp + 180 + 4, value_degree[i] - 4);
path.addArc(rectf2, temp + 180 + 5, value_degree[i] - 5);
path.addArc(rectf3, temp + 180 + 6, value_degree[i] - 6);
// draw the path
canvas.drawPath(path, paint);
}
}
}
}
Any idea why text is not written? Or any other way how to update piechart after radiobutton is changed?
Finally I found a way how to do it. To draw the text on the same canvas as graph was drawn:
// draw the number of percent under piechart
// setup brush
Paint paint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
// set shadow
paint2.setShadowLayer(3, 2, 2, Color.parseColor("#404040"));
// fill it not stroke
paint2.setStyle(Style.FILL);
// set text family + make it bold
paint2.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
// convert dp to pixels and setup the size of text
int MY_DIP_VALUE = 23; // 5dp
int pixel = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, MY_DIP_VALUE,
getResources()
.getDisplayMetrics());
// setup color of the number shown red/green
if (percent >= 50) {
paint2.setColor(COLORS[0]);
} else {
paint2.setColor(COLORS[1]);
}
// draw the text
paint2.setTextSize(pixel);
float textWidth = paint2.measureText(percent + "%");
canvas.drawText(percent + "%", size / 2 - textWidth / 2 + PADDING,
size, paint2);
I have removed TextView and did few little changes, but the key was to draw it. Still dont understand why I was not able to write text on it but had to draw but it is working!
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().
I need to draw something like this which will be painted and have little transparency
Also it needs to be clickable (onTouch event etc)
I know that in API v1 you have to use Overlay and extend it using canvas and some mathematics.
What is easiest way to do it in Google Map API v2?
PS: Radius is variable.
(For further reference)
EDIT 1:
I implemented CanvasTileProvider subclass and override its onDraw() method:
#Override
void onDraw(Canvas canvas, TileProjection projection) {
// TODO Auto-generated method stub
LatLng tempLocation = moveByDistance(mSegmentLocation, mSegmentRadius, mSegmentAngle);
DoublePoint segmentLocationPoint = new DoublePoint(0, 0);
DoublePoint tempLocationPoint = new DoublePoint(0, 0);
projection.latLngToPoint(mSegmentLocation, segmentLocationPoint);
projection.latLngToPoint(tempLocationPoint, tempLocationPoint);
float radiusInPoints = FloatMath.sqrt((float) (Math.pow(
(segmentLocationPoint.x - tempLocationPoint.x), 2) + Math.pow(
(segmentLocationPoint.y - tempLocationPoint.y), 2)));
RectF segmentArea = new RectF();
segmentArea.set((float)segmentLocationPoint.x - radiusInPoints, (float)segmentLocationPoint.y - radiusInPoints,
(float)segmentLocationPoint.x + radiusInPoints, (float)segmentLocationPoint.y + radiusInPoints);
canvas.drawArc(segmentArea, getAdjustedAngle(mSegmentAngle),
getAdjustedAngle(mSegmentAngle + 60), true, getOuterCirclePaint());
}
Also, I added this from MapActivity:
private void loadSegmentTiles() {
TileProvider tileProvider;
TileOverlay tileOverlay = mMap.addTileOverlay(
new TileOverlayOptions().tileProvider(new SegmentTileProvider(new LatLng(45.00000,15.000000), 250, 30)));
}
Now I'm wondering why my arc isn't on map?
For drawing the circle segments, I would register a TileProvider, if the segments are mainly static. (Tiles are typically loaded only once and then cached.) For checking for click events, you can register an onMapClickListener and loop over your segments to check whether the clicked LatLng is inside one of your segments. (see below for more details.)
Here is a TileProvider example, which you could subclass and just implement the onDraw method.
One important note: The subclass must be thread safe! The onDraw method will be called by multiple threads simultaneously. So avoid any globals which are changed inside onDraw!
/* imports should be obvious */
public abstract class CanvasTileProvider implements TileProvider {
private static int TILE_SIZE = 256;
private BitMapThreadLocal tlBitmap;
#SuppressWarnings("unused")
private static final String TAG = CanvasTileProvider.class.getSimpleName();
public CanvasTileProvider() {
super();
tlBitmap = new BitMapThreadLocal();
}
#Override
// Warning: Must be threadsafe. To still avoid creation of lot of bitmaps,
// I use a subclass of ThreadLocal !!!
public Tile getTile(int x, int y, int zoom) {
TileProjection projection = new TileProjection(TILE_SIZE,
x, y, zoom);
byte[] data;
Bitmap image = getNewBitmap();
Canvas canvas = new Canvas(image);
onDraw(canvas, projection);
data = bitmapToByteArray(image);
Tile tile = new Tile(TILE_SIZE, TILE_SIZE, data);
return tile;
}
/** Must be implemented by a concrete TileProvider */
abstract void onDraw(Canvas canvas, TileProjection projection);
/**
* Get an empty bitmap, which may however be reused from a previous call in
* the same thread.
*
* #return
*/
private Bitmap getNewBitmap() {
Bitmap bitmap = tlBitmap.get();
// Clear the previous bitmap
bitmap.eraseColor(Color.TRANSPARENT);
return bitmap;
}
private static byte[] bitmapToByteArray(Bitmap bm) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.PNG, 100, bos);
byte[] data = bos.toByteArray();
return data;
}
class BitMapThreadLocal extends ThreadLocal<Bitmap> {
#Override
protected Bitmap initialValue() {
Bitmap image = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE,
Config.ARGB_8888);
return image;
}
}
}
Use the projection, which is passed into the onDraw method, to get at first the bounds of the tile. If no segment is inside the bounds, just return. Otherwise draw your seqment into the canvas. The method projection.latLngToPoint helps you to convert from LatLng to the pixels of the canvas.
/** Converts between LatLng coordinates and the pixels inside a tile. */
public class TileProjection {
private int x;
private int y;
private int zoom;
private int TILE_SIZE;
private DoublePoint pixelOrigin_;
private double pixelsPerLonDegree_;
private double pixelsPerLonRadian_;
TileProjection(int tileSize, int x, int y, int zoom) {
this.TILE_SIZE = tileSize;
this.x = x;
this.y = y;
this.zoom = zoom;
pixelOrigin_ = new DoublePoint(TILE_SIZE / 2, TILE_SIZE / 2);
pixelsPerLonDegree_ = TILE_SIZE / 360d;
pixelsPerLonRadian_ = TILE_SIZE / (2 * Math.PI);
}
/** Get the dimensions of the Tile in LatLng coordinates */
public LatLngBounds getTileBounds() {
DoublePoint tileSW = new DoublePoint(x * TILE_SIZE, (y + 1) * TILE_SIZE);
DoublePoint worldSW = pixelToWorldCoordinates(tileSW);
LatLng SW = worldCoordToLatLng(worldSW);
DoublePoint tileNE = new DoublePoint((x + 1) * TILE_SIZE, y * TILE_SIZE);
DoublePoint worldNE = pixelToWorldCoordinates(tileNE);
LatLng NE = worldCoordToLatLng(worldNE);
return new LatLngBounds(SW, NE);
}
/**
* Calculate the pixel coordinates inside a tile, relative to the left upper
* corner (origin) of the tile.
*/
public void latLngToPoint(LatLng latLng, DoublePoint result) {
latLngToWorldCoordinates(latLng, result);
worldToPixelCoordinates(result, result);
result.x -= x * TILE_SIZE;
result.y -= y * TILE_SIZE;
}
private DoublePoint pixelToWorldCoordinates(DoublePoint pixelCoord) {
int numTiles = 1 << zoom;
DoublePoint worldCoordinate = new DoublePoint(pixelCoord.x / numTiles,
pixelCoord.y / numTiles);
return worldCoordinate;
}
/**
* Transform the world coordinates into pixel-coordinates relative to the
* whole tile-area. (i.e. the coordinate system that spans all tiles.)
*
*
* Takes the resulting point as parameter, to avoid creation of new objects.
*/
private void worldToPixelCoordinates(DoublePoint worldCoord, DoublePoint result) {
int numTiles = 1 << zoom;
result.x = worldCoord.x * numTiles;
result.y = worldCoord.y * numTiles;
}
private LatLng worldCoordToLatLng(DoublePoint worldCoordinate) {
DoublePoint origin = pixelOrigin_;
double lng = (worldCoordinate.x - origin.x) / pixelsPerLonDegree_;
double latRadians = (worldCoordinate.y - origin.y)
/ -pixelsPerLonRadian_;
double lat = Math.toDegrees(2 * Math.atan(Math.exp(latRadians))
- Math.PI / 2);
return new LatLng(lat, lng);
}
/**
* Get the coordinates in a system describing the whole globe in a
* coordinate range from 0 to TILE_SIZE (type double).
*
* Takes the resulting point as parameter, to avoid creation of new objects.
*/
private void latLngToWorldCoordinates(LatLng latLng, DoublePoint result) {
DoublePoint origin = pixelOrigin_;
result.x = origin.x + latLng.longitude * pixelsPerLonDegree_;
// Truncating to 0.9999 effectively limits latitude to 89.189. This is
// about a third of a tile past the edge of the world tile.
double siny = bound(Math.sin(Math.toRadians(latLng.latitude)), -0.9999,
0.9999);
result.y = origin.y + 0.5 * Math.log((1 + siny) / (1 - siny))
* -pixelsPerLonRadian_;
};
/** Return value reduced to min and max if outside one of these bounds. */
private double bound(double value, double min, double max) {
value = Math.max(value, min);
value = Math.min(value, max);
return value;
}
/** A Point in an x/y coordinate system with coordinates of type double */
public static class DoublePoint {
double x;
double y;
public DoublePoint(double x, double y) {
this.x = x;
this.y = y;
}
}
}
Finally you need something to check, whether a click on a LatLng-Coordinate is inside of your segment.
I would therefore approximate the segment by a list of LatLng-Coordinates, where in your case a simple triangle may be sufficient. For each list of LatLng coordinates, i.e. for each segment, you may then call something like the following:
private static boolean isPointInsidePolygon(List<LatLng> vertices, LatLng point) {
/**
* Test is based on a horizontal ray, starting from point to the right.
* If the ray is crossed by an even number of polygon-sides, the point
* is inside the polygon, otherwise it is outside.
*/
int i, j;
boolean inside = false;
int size = vertices.size();
for (i = 0, j = size - 1; i < size; j = i++) {
LatLng vi = vertices.get(i);
LatLng vj = vertices.get(j);
if ((vi.latitude > point.latitude) != (vj.latitude > point.latitude)) {
/* The polygonside crosses the horizontal level of the ray. */
if (point.longitude <= vi.longitude
&& point.longitude <= vj.longitude) {
/*
* Start and end of the side is right to the point. Side
* crosses the ray.
*/
inside = !inside;
} else if (point.longitude >= vi.longitude
&& point.longitude >= vj.longitude) {
/*
* Start and end of the side is left of the point. No
* crossing of the ray.
*/
} else {
double crossingLongitude = (vj.longitude - vi.longitude)
* (point.latitude - vi.latitude)
/ (vj.latitude - vi.latitude) + vi.longitude;
if (point.longitude < crossingLongitude) {
inside = !inside;
}
}
}
}
return inside;
}
As you may see, I had a very similar task to solve :-)
Create a View, override its onDraw method to use drawArc on its canvas, and add it to your MapFragment. You can specify the radius in drawArc. Set the onClickListener on the View (or onTouch, any listener you can use for normal views, really).
From the image you can see that the ball fired on the left that fire behind it, does not match the calculated trajectory. Im drawing the ball trajectory using an equation from a SO question, this is modified to take into consideration the box2d steps of 30 frames per second. This does calculate a valid trajectory but it does not match the actual trajectory of the ball, the ball has a smaller trajectory. I am applying a box2d force to the ball, this also has a density set and a shape. The shape radius varies depending on the type of ball. Im setting the start velocity in the touchdown event.
public class ProjectileEquation {
public float gravity;
public Vector2 startVelocity = new Vector2();
public Vector2 startPoint = new Vector2();
public Vector2 gravityVec = new Vector2(0,-10f);
public float getX(float n) {
return startVelocity.x * (n * 1/30f) + startPoint.x;
}
public float getY(float n) {
float t = 1/30f * n;
return 0.5f * gravity * t * t + startVelocity.y * t + startPoint.y;
}
}
#Override
public void draw(SpriteBatch batch, float parentAlpha) {
float t = 0f;
float width = this.getWidth();
float height = this.getHeight();
float timeSeparation = this.timeSeparation;
for (int i = 0; i < trajectoryPointCount; i+=timeSeparation) {
//projectileEquation.getTrajectoryPoint(this.getX(), this.getY(), i);
float x = this.getX() + projectileEquation.getX(i);
float y = this.getY() + projectileEquation.getY(i);
batch.setColor(this.getColor());
if(trajectorySprite != null) batch.draw(trajectorySprite, x, y, width, height);
// t += timeSeparation;
}
}
public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
if(button==1 || world.showingDialog)return false;
touchPos.set(x, y);
float angle = touchPos.sub(playerCannon.position).angle();
if(angle > 270 ) {
angle = 0;
}
else if(angle >70) {
angle = 70;
}
playerCannon.setAngle(angle);
world.trajPath.controller.angle = angle;
float radians = (float) angle * MathUtils.degreesToRadians;
float ballSpeed = touchPos.sub(playerCannon.position).len()*12;
world.trajPath.projectileEquation.startVelocity.x = (float) (Math.cos(radians) * ballSpeed);
world.trajPath.projectileEquation.startVelocity.y = (float) (Math.sin(radians) * ballSpeed);
return true;
}
public CannonBall(float x, float y, float width, float height, float damage, World world, Cannon cannonOwner) {
super(x, y, width, height, damage, world);
active = false;
shape = new CircleShape();
shape.setRadius(width/2);
FixtureDef fd = new FixtureDef();
fd.shape = shape;
fd.density = 4.5f;
if(cannonOwner.isEnemy) { //Enemy cannon balls cannot hit other enemy cannons just the player
fd.filter.groupIndex = -16;
}
bodyDef.type = BodyType.DynamicBody;
bodyDef.position.set(this.position);
body = world.createBody(bodyDef);
body.createFixture(fd);
body.setUserData(this);
body.setBullet(true);
this.cannonOwner = cannonOwner;
this.hitByBall = null;
this.particleEffect = null;
}
private CannonBall createCannonBall(float radians, float ballSpeed, float radius, float damage)
{
CannonBall cannonBall = new CannonBall(CannonEnd().x, CannonEnd().y, radius * ballSizeMultiplier, radius * ballSizeMultiplier, damage, this.world, this);
cannonBall.velocity.x = (float) (Math.cos(radians) * ballSpeed);
//cannonBall.velocity.x = (float) ((Math.sqrt(10) * Math.sqrt(29) *
// Math.sqrt((Math.tan(cannon.angle)*Math.tan(cannon.angle))+1)) / Math.sqrt(2 * Math.tan(cannon.angle) - (2 * 10 * 2)/29))* -1f;
cannonBall.velocity.y = (float) (Math.sin(radians) * ballSpeed);
cannonBall.active = true;
//cannonBall.body.applyLinearImpulse(cannonBall.velocity, cannonBall.position);
cannonBall.body.applyForce(cannonBall.velocity, cannonBall.position );
return cannonBall;
}
trajPath = new TrajectoryActor(-10f);
trajPath.setX(playerCannon.CannonEnd().x);
trajPath.setY(playerCannon.CannonEnd().y);
trajPath.setWidth(10f);
trajPath.setHeight(10f);
stage.addActor(trajPath);
Here is a code that I used for one of my other games, which proved to be very precise. The trick is to apply the impulse on the body and read the initial velocity. Having that I calculate 10 positions where the body will be within 0.5 seconds. The language is called Squirrel which is Lua based with C/C++ like syntax. You should be able to grasp what is going on there. What returns from the getTrajectoryPointsForObjectAtImpulse is an array of 10 positions through which the ball will pass within 0.5 seconds.
const TIMESTER_DIVIDOR = 60.0;
function getTrajectoryPoint( startingPosition, startingVelocity, n )
{
local gravity = box2DWorld.GetGravity();
local t = 1 / 60.0;
local stepVelocity = b2Vec2.Create( t * startingVelocity.x, t * startingVelocity.y );
local stepGravity = b2Vec2.Create( t * t * gravity.x, t * t * gravity.y );
local result = b2Vec2.Create( 0, 0 );
result.x = ( startingPosition.x + n * stepVelocity.x + 0.5 * ( n * n + n ) * stepGravity.x ) * MTP;
result.y = ( startingPosition.y + n * stepVelocity.y + 0.5 * ( n * n + n ) * stepGravity.y ) * -MTP;
return result;
}
function getTrajectoryPointsForObjectAtImpulse (object, impulse)
{
if( !object || !impulse ) return [];
local result = [];
object.bBody.ApplyLinearImpulse( impulse, object.bBody.GetWorldCenter() );
local initialVelocity = object.bBody.GetLinearVelocity();
object.bBody.SetLinearVelocity( b2Vec2.Create(0, 0) );
object.bBody.SetActive(false);
for ( local i = 0.0 ; i < ( 0.5 * TIMESTER_DIVIDOR ) ; )
{
result.append( getTrajectoryPoint(object.bBody.GetPosition(), initialVelocity, i.tointeger() ) );
i += ( (0.5 * TIMESTER_DIVIDOR) * 0.1 );
}
return result;
}
If you do not understand any part of the code, please let me know and I will try to explain.