I'm working on a method to remove the transparent bounds of a Bitmap. The method looks like this:
private static final int STEP = 4;//Don't check all the pixel, only a sampling
private Bitmap clipTransparent(Bitmap image) {
int x1, x2, y1, y2;
final int width = image.getWidth();
final int height = image.getHeight();
for (x1 = 0; x1 < width - 1; x1 += STEP) {
if (checkColumn(image, x1)) {
break;
}
}
x1 = Math.max(0, x1 - STEP);
for (x2 = width - 1; x2 > x1; x2 -= STEP) {
if (checkColumn(image, x2)) {
break;
}
}
x2 = Math.min(width, x2 + STEP);
for (y1 = 0; y1 < height - 1; y1 += STEP) {
if (checkRow(x1, x2, image, y1)) {
break;
}
}
y1 = Math.max(0, y1 - STEP);
for (y2 = height - 1; y2 > 0; y2 -= STEP) {
if (checkRow(x1, x2, image, y2)) {
break;
}
}
y2 = Math.min(height, y2 + STEP);
try {
image = Bitmap.createBitmap(image, x1, y1, x2 - x1, y2 - y1);
} catch (Throwable t) {
t.printStackTrace();
}
return image;
}
private boolean checkColumn(Bitmap image, int x1) {
for (int y = 0; y < image.getHeight(); y += STEP) {
if (Color.alpha(image.getPixel(x1, y)) > 0) {
return true;
}
}
return false;
}
private boolean checkRow(int x1, int x2, Bitmap image, int y1) {
for (int x = x1; x < x2; x += STEP) {
if (Color.alpha(image.getPixel(x, y1)) > 0) {
return true;
}
}
return false;
}
It works great, but not as fast as I would like it to be.
The bottleneck of the code is to get the color of a pixel.
Now I read that value by calling image.getPixel(x, y), but looking at the source code of Android, getPixel checks the indexes and does other stuff that slows the code down (x>=0 && x<getWidth() && y>=0 && y<getHeight() && !isRecycled)...
Is there a way to access the pixel data without any index check or other useless stuff (useless in my case of course)?
PS: I already tryed to use getPixels() that returns a int array containing all the colors. But the image is big and allocating all the memory triggers a GC... the result was an even slower method
According to the docs you can pass an array to getPixels() which you can later reuse. This way no GC should ever happen.
Related
When using scaling, MotionEvent coordinates can be corrected by dividing by the ScaleFactor.
Further, when scaling and paning, divide by scalefactor and subtract offset.
When dealing with zoom, however, it isn't as easy. Dividing does get the correct relative coordinates, but because pan is involved, 0 isn't 0. 0 can be -2000 in offset.
So how can I correct the TouchEvents to give the correct coordinates after zoom and pan?
Code:
Zoom:
class Scaler extends ScaleGestureDetector {
public Scaler(Context context, OnScaleGestureListener listener) {
super(context, listener);
}
#Override
public float getScaleFactor() {
return super.getScaleFactor();
}
}
class ScaleListener implements ScaleGestureDetector.OnScaleGestureListener{
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
if(scaleFactor > 2) scaleFactor = 2;
else if(scaleFactor < 0.3f) scaleFactor = 0.3f;
scaleFactor = ((float)((int)(scaleFactor * 100))) / 100;//jitter-protection
scaleMatrix.setScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
return true;
}
#Override
public boolean onScaleBegin(ScaleGestureDetector detector) {return true;}
#Override
public void onScaleEnd(ScaleGestureDetector detector) {
System.out.println("ScaleFactor: " + scaleFactor);
}
}
TouchEvent:
#Override
public boolean onTouchEvent(MotionEvent ev) {
int pointers = ev.getPointerCount();
if(pointers == 2 ) {
zoom = true;
s.onTouchEvent(ev);
}else if(pointers == 1 && zoom){
if(ev.getAction() == MotionEvent.ACTION_UP)
zoom = false;
return true;
}else {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//scaled physical coordinates
x = ev.getX() /*/ mScaleFactorX*/;//unscaled
y = ev.getY() /*/ mScaleFactorY*/;
sx = ev.getX() / scaleFactor;//scaled
sy = ev.getY() / scaleFactor;
//////////////////////////////////////////
tox = toy = true;
} else if (ev.getAction() == MotionEvent.ACTION_UP) {
if (tox && toy) {
x = ev.getX() /*/ mScaleFactorX*/;
y = ev.getY() /*/ mScaleFactorY*/;
sx = ev.getX() / scaleFactor;
sy = ev.getY() / scaleFactor;
System.out.println("XY: " + sx + "/" + sy);
Rect cursor = new Rect((int) x, (int) y, (int) x + 1, (int) y + 1);
Rect scaledCursor = new Rect((int)sx, (int)sy, (int)sx+1, (int)sy+1);
...
}
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
//This is where the pan happens.
float currX = ev.getX() / scaleFactor;
float currY = ev.getY() / scaleFactor;
float newOffsetX = (sx - currX),
newOffsetY = (sy - currY);
if (newOffsetY < Maths.convertDpToPixel(1, c) && newOffsetY > -Maths.convertDpToPixel(1, c))
newOffsetY = 0;
else tox = false;
if (newOffsetX < Maths.convertDpToPixel(1, c) && newOffsetX > -Maths.convertDpToPixel(1, c))
newOffsetX = 0;
else toy = false;
this.newOffsetX = newOffsetX;
this.newOffsetY = newOffsetY;
offsetX += newOffsetX;
offsetY += newOffsetY;
sx = ev.getX() / scaleFactor;
sy = ev.getY() / scaleFactor;
}
}
return true;
}
Implementation of the zooming matrix:
Matrix scaleMatrix = new Matrix();
public void render(Canvas c) {
super.draw(c);
if (c != null) {
backgroundRender(c);
c.setMatrix(scaleMatrix);
//Example rendering:
c.drawRect(0 - offsetX,0 - offsetY,10 - offsetX,10 - offsetY,paint);
c.setMatrix(null);//null the matrix to allow for unscaled rendering after this line. For UI objects.
}
}
What the issue is, is that when zooming 0 shifts but the coordinates of the objects does not. Meaning objects rendered at e.g. -2500, -2500 will appear to be rendered at over 0,0. Their coordinates are different from the TouchEvent. So how can I correct the touch events?
What I have tried:
This causes laggy zoom and the objects flying away. ev = MotionEvent in onTouchEvent. Doesn't correct the coordinates
Matrix invert = new Matrix(scaleMatrix);
invert.invert(invert);
ev.transform();
This doesn't work because the coordinates are wrong compared to objects. Objects with coordinates < 0 show over 0 meaning MotionEvents are wrong no matter what.
int sx = ev.getX() / scaleFactor;//same with y, but ev.getY()
Found a solution after doing a ton more research
Whenever getting the scaled coordinates, get the clipBounds of the canvas and add the top and left coordinates to X/Y coordinates:
sx = ev.getX() / scaleFactor + clip.left;
sy = ev.getY() / scaleFactor + clip.top ;
clip is a Rect defined as the clipBounds of the Canvas.
public void render(Canvas c) {
super.draw(c);
if (c != null) {
c.setMatrix(scaleMatrix);
clip = c.getClipBounds();
(...)
}
}
I am able to plot bar graph using canvas and drawing rectangle in the view. But the onTouch interaction works for the whole view and hence I am not able to interact with each bar separately.I am not looking for using any library for plotting graphs. Any suggestions would be helpful. Thanks!
protected void onDraw(Canvas canvas) {
float border = 20;
float horstart = border * 2;
float height = getHeight();
float width = getWidth() - 1;
float max = getMax();
float min = getMin();
float diff = max - min;
float graphheight = height - (2 * border);
float graphwidth = width - (2 * border);
paint.setTextAlign(Align.LEFT);
int vers = verlabels.length - 1;
for (int i = 0; i < verlabels.length; i++) {
paint.setColor(Color.BLUE);
float y = ((graphheight / vers) * i) + border;
//canvas.drawLine(horstart, y, width, y, paint);
paint.setColor(Color.BLUE);
canvas.drawText(verlabels[i], 0, y, paint);
}
int hors = horlabels.length - 1;
for (int i = 0; i < horlabels.length; i++) {
paint.setColor(Color.BLUE);
float x = ((graphwidth / hors) * i) + horstart;
//canvas.drawLine(x, height - border, x, border, paint);
paint.setTextAlign(Align.CENTER);
if (i == horlabels.length - 1)
paint.setTextAlign(Align.RIGHT);
if (i == 0)
paint.setTextAlign(Align.LEFT);
paint.setColor(Color.BLUE);
canvas.drawText(horlabels[i], x, height - 4, paint);
}
paint.setTextAlign(Align.CENTER);
canvas.drawText(title, (graphwidth / 2) + horstart, border - 4, paint);
if (max != min) {
paint.setColor(Color.BLUE);
if (type == BAR) {
float datalength = values.length;
float colwidth = (graphwidth / hors);
for (int i = 0; i < values.length; i++) {
float val = values[i] - min;
float rat = val / diff;
float h = graphheight * rat;
canvas.drawRect((i * colwidth) + horstart, (border - h)
+ graphheight+curY, ((i * colwidth) + horstart)
+ (colwidth - 1), height - (border - 1), paint);
}
} else {
float datalength = values.length;
float colwidth = (width - (2 * border)) / datalength;
float halfcol = colwidth / 2;
float lasth = 0;
float h = 0;
for (int i = 0;i<values.length;i++)
canvas.drawLine(((i - 1) * colwidth) + (horstart + 1)
+ halfcol, (border - lasth) + graphheight+curY,
(i * colwidth) + (horstart + 1) + halfcol,
(border - h) + graphheight, paint);
lasth = h;
}
}
}
onTouch method for the view :
public boolean onTouch(View v, MotionEvent event)
{
boolean result=false;
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
curX= (int)event.getX();
curY= (int)event.getY();
result=true;
break;
case MotionEvent.ACTION_MOVE:
curX= (int)event.getX();
curY= (int)event.getY();
result=true;
break;
case MotionEvent.ACTION_UP:
curX= (int)event.getX();
curY= (int)event.getY();
result=true;
break;
}
if (result) invalidate();
return result;
}
Whenever an user touches that view, it causes the onTouch method gets called. Once it's getting called, you're given two float numbers: x and y indicating where user's finger touches your view relative to the view coordination system.
As you might grasped the idea, you should get those numbers and internally in your custom view (i.e. bar chart) calculate which bar is affected. Then, you can for example apply a hover effect or do something else.
Note: For updating appearance of your view, you should store changes in data models of your chart view and then issue invalidate(). Subsequently, as a result, your onDraw is invoked and in which you can re-draw your chart. (i.e. You should each time, re-draw your whole chart again)
I am working on a tile based game which should be scrollable but only inside the boundaries of the world. So I set up the pan() method as several examples/tutorials suggest but it is not always working properly. Sometimes it is jumping back to the starting point of the last gesture or is only moving very slow. Additionally the borders are not working either. Maybe somebody can point out the mistakes I made.
public void pan(float x, float y, float deltaX, float deltaY) {
moveCamera(x, y);
}
private void moveCamera(float x, float y) {
Vector3 new_position = getNewCameraPosition((int) x, (int)y);
if(!cameraOutOfLimit(new_position))
this.getViewport().getCamera().translate(new_position.sub(this.getViewport().getCamera().position));
lastTouchDown.set(x, y, 0);
}
private Vector3 getNewCameraPosition(int x, int y) {
Vector3 newPosition = lastTouchDown;
newPosition.sub(x, y, 0);
newPosition.y = -newPosition.y;
newPosition.add(this.getViewport().getCamera().position);
return newPosition;
}
private boolean cameraOutOfLimit( Vector3 position ) {
int x_left_limit = (int) (Global.SCREEN_WIDTH / 2);
int x_right_limit = (int) (Global.COLS_OF_TILES * Global.TILE_WIDTH - (Global.SCREEN_WIDTH / 2));
int y_bottom_limit = (int) (Global.SCREEN_HEIGHT / 2);
int y_top_limit = (int) (Global.ROWS_OF_TILES * Global.TILE_HEIGHT - Global.SCREEN_HEIGHT / 2);
if( position.x < x_left_limit || position.x > x_right_limit )
return true;
else if( position.y < y_bottom_limit || position.y > y_top_limit )
return true;
else
return false;
}
The code above seems convoluted to me, and I don't really understandwhy you modify a vector called lastTouchPosition in ways that have nothing to do with touch position.
I would do something like this. These methods clamp your target X and Y positions whenever you want to move your camera.
float clampCamTargetX(float x) {
x = Math.max(x, (int) (Global.SCREEN_WIDTH / 2));
x = Math.min(x, (int) (Global.COLS_OF_TILES * Global.TILE_WIDTH - (Global.SCREEN_WIDTH / 2)));
return x;
}
float clampCamTargetY (float y){
y = Math.max(y,(int)(Global.SCREEN_HEIGHT/2));
y = Math.min(y,(int)(Global.ROWS_OF_TILES*Global.TILE_HEIGHT-Global.SCREEN_HEIGHT/2));
return y;
}
Then if you want to pan it, you would do something like this:
void panCamera(float deltaX, float deltaY) {
Camera camera = this.getViewport().getCamera();
Vector3 camPosition = camera.position;
camPosition.x = clampCamTargetX(camPosition.x + deltaX);
camPosition.y = clampCamTargetY(camPosition.y + deltaY);
camera.update();
}
Or if you want a complete solution for smoothly moving the camera to the last position touched, try this:
float startXCam, startYCam, targetXCam, targetYCam;
float elapsedTimeCam;
boolean panningCam = false;
static final float CAM_PAN_DURATION = 0.4f;
public void render (){
//...
panCameraToTouchPoint(Gdx.graphics.getDeltaTime());
//...
}
void panCameraToTouchPoint (float deltaTime){
Camera camera = getViewport().getCamera();
Vector3 camPosition = camera.position;
if (Gdx.input.justTouched()) {
startXCam = camPosition.x;
startYCam = camPosition.y;
targetXCam = clampCamTargetX(Gdx.input.getX());
targetYCam = clampCamTargetY(Gdx.input.getY());
elapsedTimeCam = 0;
panningCam = true;
}
if (panningCam){
elapsedTimeCam += deltaTime;
float alpha = elapsedTimeCam / CAM_PAN_DURATION;
if (alpha >= 1){
alpha = 1;
panningCam = false;
}
camPosition.x = Interpolation.pow2Out.apply(startXCam, targetXCam, alpha);
camPosition.y = Interpolation.pow2Out.apply(startYCam, targetYCam, alpha);
camera.update();
}
}
I have a sprite in Android OpenGL. This sprite (a small beetlebug) is always moving in a forward direction and I use:
sprite.setPosition(posX, posY);
Now I have a rotation method, when the user gestures left or right the bug rotates:
private void applyRotation() {
for(int i=0;i<beetleBug.size;i++) {
Sprite s = beetleBug.get(i);
s.setOrigin(s.getWidth() / 2, s.getHeight() / 2);
s.setRotation(angle);
}
}
Now when the bug is moving forward which he always does the new x and y coordinates have to be calculated which depend on the rotation-angle, so that the bug is always moving forward. Does anybody have an algorithm to calculate the direction by the rotation-angle?
Here is the whole Bug-class:
public class Bug {
private SpriteBatch spriteBatch = null;
private TextureAtlas spriteSheet;
private Array<Sprite> beetleBug;
private int currentFrame = 0;
private final float frameLength = 0.10f; //in seconds, how long a frame last
private float animationElapsed = 0.0f;
private float angle = 0.0f;
private float posX = 0.0f;
private float posY = 0.0f;
private float sizeX = 100.0f;
private float sizeY = 100.0f;
private float offSet = 50.0f;
public Bug() {
spriteBatch = new SpriteBatch();
spriteSheet = new TextureAtlas("assets/data/bug.txt");
beetleBug = spriteSheet.createSprites("bug");
// dont forget to set the size of your sprites!
for(int i=0; i<beetleBug.size; i++){
beetleBug.get(i).setSize(sizeX, sizeY);
}
applyPosition();
}
public void handleInput() {
boolean leftKey = Gdx.input.isKeyPressed(Input.Keys.LEFT);
boolean rightKey = Gdx.input.isKeyPressed(Input.Keys.RIGHT);
if(rightKey) {
if(angle <= 0) {
angle = 360;
}
angle -= 2f;
applyRotation();
}
if(leftKey) {
if(angle >= 360) {
angle = 0;
}
angle += 2f;
applyRotation();
}
applyPosition();
}
private void applyPosition() {
float x = (float) Math.cos(angle);
float y = (float) Math.sin(angle);
posX = posX + x;
posY = posY + y;
for(int i=0; i<beetleBug.size; i++){
beetleBug.get(i).setPosition(posX - offSet, posY -offSet); // optional: center the sprite to screen
}
}
private void applyRotation() {
for(int i=0;i<beetleBug.size;i++) {
Sprite s = beetleBug.get(i);
s.setOrigin(s.getWidth() / 2, s.getHeight() / 2);
s.setRotation(angle);
}
}
public void render(OrthographicCamera cam) {
float dt = Gdx.graphics.getDeltaTime();
animationElapsed += dt;
while(animationElapsed > frameLength){
animationElapsed -= frameLength;
currentFrame = (currentFrame == beetleBug.size - 1) ? 0 : ++currentFrame;
}
spriteBatch.setProjectionMatrix(cam.combined);
spriteBatch.begin();
beetleBug.get(currentFrame).draw(spriteBatch);
spriteBatch.end();
}
}
Works perfectly now:
Converted degrees to radians
Set x-coordintae to -
private void applyPosition() {
float radians = (float) Math.toRadians(angle);
float x = -(float) Math.sin(radians);
float y = (float) Math.cos(radians);
posX = posX + x;
posY = posY + y;
for(int i=0; i<beetleBug.size; i++){
beetleBug.get(i).setPosition(posX - offSet, posY -offSet);
}
}
Create a normalized vector to represent the beetle's direction, then multiply by the speed. Add that vector to the beetle's current position and you've got his new position.
Create the normalized vector (i.e. has a length of 1) using your angle. vx = cos(angle), vy = sin(angle)
Multiply by your beetle's speed. vx = vx*speed, vy = vy*speed
Add it to the current position. x = x + vx, y = y + vy
Repeat
Some gotchas: Watch out that your sprite's graphical rotation and your own internal representation of rotation go the same way. Some frameworks flip which way they rotate graphics. The above [cos(angle), sin(angle)] is for an angle of zero pointing towards the positive x axis. Many implementations of cos/sin/tan use radians instead of degrees for their calculations, so convert as appropriate.
[cos angle, sin angle]is for zero to the right (positive x), counterclockwise. [-sin angle, cos angle]is for zero pointing up (positive y), counterclockwise.
This might work:
int currentX = 100; //beetleCurrentX
int currentY = 100; //beetleCurrentY
int angle = 200; //beetleAngle
int len = 2; //Step that the beetle makes (jumps 2 in this case)
int x2Pos = sin(angle)*len + currentX;
int y2Pos = cos(angle)*len + currentY;
sprite.setPosition(x2Pos,y2Pos);
If you execute this each frame you will have your beetle moving in the angles direction.
In my application, I have some images and when I tap on that image, it should be rotated by 90 degree. I am able to rotate image once but can't rotate on second tap. Can anyone help me to solve this problem? How can I rotate image on every touch event?
if (event.getAction() == MotionEvent.ACTION_DOWN) {
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.quartercircle1);
Matrix m = new Matrix();
imgvwQrtr1.setScaleType(ScaleType.MATRIX);
m.setRotate(90f, imgvwQrtr1.getDrawable().getBounds().width()/2, imgvwQrtr1.getDrawable().getBounds().height()/2);
bm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), m, true);
imgvwQrtr1.setImageBitmap(bm);
ClipData data = ClipData.newPlainText("", "");
DragShadowBuilder shadowBuilder = new DragShadowBuilder(v);
v.startDrag(data, shadowBuilder, v, 0);
return true;
}
you need to create global variable like:
int degree = 0;
and in the code
.....
//shortest way
degree = degree + 90;
if(degree % 360 == 0) {
degree = 0;
}
//if(degree >= 270) {
//degree = 0;
//} else {
//degree+=90;
//}
m.setRotate(degree, imgvwQrtr1.getDrawable().getBounds().width()/2,
imgvwQrtr1.getDrawable().getBounds().height()/2);
....
try this
public static float getRotationAngle(final float x1, final float y1, final float x2, final float y2) {
final float CLOCK_WISE = 1.0f;
final float ANTI_CLOCK_WISE = -1.0f;
final float deltaX = Math.abs(x2 - x1);
final float deltaY = Math.abs(y2 - y1);
if (deltaX == 0) {
return 0.0f;
}
final float angle = (float) Math.toDegrees(Math.atan(deltaY / deltaX));
if (x1 <= x2 && y2 <= y1) {
return CLOCK_WISE * (90.0f - angle);
} else if (x2 <= x1 && y2 <= y1) {
return ANTI_CLOCK_WISE * (90.0f - angle);
} else if (x2 <= x1 && y1 <= y2) {
return ANTI_CLOCK_WISE * (90.0f + angle);
} else if (x1 <= x2 && y1 <= y2) {
return CLOCK_WISE * (90.0f + angle);
} else {
return 0.0f;
}
}