Related
I want to do a "zoomable paint", I mean a paint that I can zoom/zoom out and pan/drag the canvas and then draw on it.
I have a problem that I can't solve: when I draw while the canvas is zoomed, I retrieve the X and Y coordinate and effectively drawing it on the canvas. But these coordinates are not correct because of the zoomed canvas.
I tried to correct these (multiply by (zoomHeigh/screenHeight)) but I can't find a way to retrieve where I must draw on the original/none-zoomed screen
This is my code :
public class PaintView extends View {
public static int BRUSH_SIZE = 20;
public static final int DEFAULT_COLOR = Color.BLACK;
public static final int DEFAULT_BG_COLOR = Color.WHITE;
private static final float TOUCH_TOLERANCE = 4;
private float mX, mY;
private SerializablePath mPath;
private Paint mPaint;
private ArrayList<FingerPath> paths = new ArrayList<>();
private ArrayList<FingerPath> tempPaths = new ArrayList<>();
private int currentColor;
private int backgroundColor = DEFAULT_BG_COLOR;
private int strokeWidth;
private boolean emboss;
private boolean blur;
private boolean eraser;
private MaskFilter mEmboss;
private MaskFilter mBlur;
private Bitmap mBitmap;
private Canvas mCanvas;
private Paint mBitmapPaint = new Paint(Paint.DITHER_FLAG);
public boolean zoomViewActivated = false;
//These two constants specify the minimum and maximum zoom
private static float MIN_ZOOM = 1f;
private static float MAX_ZOOM = 5f;
private float scaleFactor = 1.f;
private ScaleGestureDetector detector;
//These constants specify the mode that we're in
private static int NONE = 0;
private static int DRAG = 1;
private static int ZOOM = 2;
private int mode;
//These two variables keep track of the X and Y coordinate of the finger when it first
//touches the screen
private float startX = 0f;
private float startY = 0f;
//These two variables keep track of the amount we need to translate the canvas along the X
//and the Y coordinate
private float translateX = 0f;
private float translateY = 0f;
//These two variables keep track of the amount we translated the X and Y coordinates, the last time we
//panned.
private float previousTranslateX = 0f;
private float previousTranslateY = 0f;
int currentPositionX = 0;
int currentPositionY = 0;
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
return true;
}
}
public ArrayList<FingerPath> getDividedPaths(float i){
for(FingerPath p : this.paths){
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(i, i);
p.path.transform(scaleMatrix);
}
return this.paths;
}
public ArrayList<FingerPath> getPaths() {
return paths;
}
public void dividePath(float i) {
for(FingerPath p : this.paths){
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(i, i);
p.path.transform(scaleMatrix);
}
}
public void setPaths(ArrayList<FingerPath> paths){
this.paths = paths;
}
public void setStrokeWidth(int value){
strokeWidth = value;
}
public PaintView(Context context) {
this(context, null);
detector = new ScaleGestureDetector(getContext(), new ScaleListener());
}
public PaintView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(DEFAULT_COLOR);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setXfermode(null);
mPaint.setAlpha(0xff);
mEmboss = new EmbossMaskFilter(new float[] {1, 1, 1}, 0.4f, 6, 3.5f);
mBlur = new BlurMaskFilter(5, BlurMaskFilter.Blur.NORMAL);
detector = new ScaleGestureDetector(getContext(), new ScaleListener());
}
public void init(DisplayMetrics metrics) {
int height = metrics.heightPixels;
int width = metrics.widthPixels;
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
currentColor = DEFAULT_COLOR;
strokeWidth = BRUSH_SIZE;
}
public void normal() {
emboss = false;
blur = false;
eraser = false;
}
public void emboss() {
emboss = true;
blur = false;
eraser = false;
}
public void blur() {
emboss = false;
blur = true;
eraser = false;
}
public void eraser() {
eraser = true;
}
public void cancel(){
if(paths.size() != 0){
tempPaths.add(paths.get(paths.size()-1));
paths.remove(paths.size()-1);
invalidate();
}
}
public void redo(){
if(tempPaths.size() != 0){
paths.add(tempPaths.get(tempPaths.size()-1));
tempPaths.remove(tempPaths.size()-1);
invalidate();
}
}
public void clear() {
backgroundColor = DEFAULT_BG_COLOR;
paths.clear();
normal();
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
//We're going to scale the X and Y coordinates by the same amount
canvas.scale(scaleFactor, scaleFactor);
//If translateX times -1 is lesser than zero, let's set it to zero. This takes care of the left bound
if((translateX * -1) < 0) {
translateX = 0;
}
//This is where we take care of the right bound. We compare translateX times -1 to (scaleFactor - 1) * displayWidth.
//If translateX is greater than that value, then we know that we've gone over the bound. So we set the value of
//translateX to (1 - scaleFactor) times the display width. Notice that the terms are interchanged; it's the same
//as doing -1 * (scaleFactor - 1) * displayWidth
else if((translateX * -1) > (scaleFactor - 1) * getWidth()) {
translateX = (1 - scaleFactor) * getWidth();
}
if(translateY * -1 < 0) {
translateY = 0;
}
//We do the exact same thing for the bottom bound, except in this case we use the height of the display
else if((translateY * -1) > (scaleFactor - 1) * getHeight()) {
translateY = (1 - scaleFactor) * getHeight();
}
//We need to divide by the scale factor here, otherwise we end up with excessive panning based on our zoom level
//because the translation amount also gets scaled according to how much we've zoomed into the canvas.
canvas.translate(translateX / scaleFactor, translateY / scaleFactor);
/* The rest of your canvas-drawing code */
mCanvas.drawColor(backgroundColor);
if(paths != null){
for (FingerPath fp : paths) {
mPaint.setColor(fp.color);
mPaint.setStrokeWidth(fp.strokeWidth);
mPaint.setMaskFilter(null);
if (fp.emboss)
mPaint.setMaskFilter(mEmboss);
else if (fp.blur)
mPaint.setMaskFilter(mBlur);
if(fp.eraser) {
mPaint.setColor(DEFAULT_BG_COLOR);
}
mCanvas.drawPath(fp.path, mPaint);
}
}
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
canvas.restore();
}
private void touchStart(float x, float y) {
mPath = new SerializablePath();
FingerPath fp = new FingerPath(currentColor, emboss, blur, eraser, strokeWidth, mPath);
paths.add(fp);
tempPaths = new ArrayList<>();
mPath.reset();
mPath.moveTo(x, y);
mX = x;
mY = y;
}
private void touchMove(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;
}
}
private void touchUp() {
mPath.lineTo(mX, mY);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if(zoomViewActivated){
boolean dragged = false;
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mode = DRAG;
//We assign the current X and Y coordinate of the finger to startX and startY minus the previously translated
//amount for each coordinates This works even when we are translating the first time because the initial
//values for these two variables is zero.
startX = event.getX() - previousTranslateX;
startY = event.getY() - previousTranslateY;
break;
case MotionEvent.ACTION_MOVE:
translateX = event.getX() - startX;
translateY = event.getY() - startY;
//We cannot use startX and startY directly because we have adjusted their values using the previous translation values.
//This is why we need to add those values to startX and startY so that we can get the actual coordinates of the finger.
double distance = Math.sqrt(Math.pow(event.getX() - (startX + previousTranslateX), 2) +
Math.pow(event.getY() - (startY + previousTranslateY), 2)
);
if(distance > 0) {
dragged = true;
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
mode = ZOOM;
break;
case MotionEvent.ACTION_UP:
mode = NONE;
dragged = false;
//All fingers went up, so let's save the value of translateX and translateY into previousTranslateX and
//previousTranslate
previousTranslateX = translateX;
previousTranslateY = translateY;
currentPositionX += previousTranslateX;
currentPositionY += previousTranslateY;
break;
case MotionEvent.ACTION_POINTER_UP:
mode = DRAG;
//This is not strictly necessary; we save the value of translateX and translateY into previousTranslateX
//and previousTranslateY when the second finger goes up
previousTranslateX = translateX;
previousTranslateY = translateY;
break;
}
detector.onTouchEvent(event);
//We redraw the canvas only in the following cases:
//
// o The mode is ZOOM
// OR
// o The mode is DRAG and the scale factor is not equal to 1 (meaning we have zoomed) and dragged is
// set to true (meaning the finger has actually moved)
if ((mode == DRAG && scaleFactor != 1f && dragged) || mode == ZOOM) {
invalidate();
}
}else{
float x = event.getX()*(getHeight()/scaleFactor)/getHeight()+currentPositionX;
float y = event.getY()*(getWidth()/scaleFactor)/getWidth()+currentPositionY;
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN :
touchStart(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE :
touchMove(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP :
touchUp();
invalidate();
break;
}
}
return true;
}
}
So, I'm using LWJGL and OpenGL to render some quads and textures. My quads have been working fine, until the addition of some textures. Specifically, when I added the ability to render text to the screen, all of the quads color has changed to black. I've looked around for a solution but have had no luck, the most common answers are to use glEnable(GL_TEXTURE_2D); only when I draw textures, otherwise keep it disabled when drawing quads. This doesn't seem to be working for me, my quads are still black. I've included my Renderer, Font, and Texture classes below as I'm fairly certain the issue is somewhere in there.
Edit: So I've done a bit more testing, I wasn't even trying to draw normal textures (from .png files) however, now that I've tried to do that as well, I've found they also are drawn completely black. So as of now, the only thing rendering correctly is my font/text, even though I create a texture to render them as well
Renderer.java
public class Renderer {
private VertexArrayObject vao;
private VertexBufferObject vbo;
private ShaderProgram shaderProgram;
private FloatBuffer vertices;
private int numVertices;
private boolean drawing;
private Font fontSketchalot;
public void drawQuad(float x, float y, float width, float height, Colors c) {
glDisable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
begin();
if (vertices.remaining() < 5 * 7) flush();
// Calculate Vertex positions
float x1 = x;
float y1 = y;
float x2 = x + width;
float y2 = y - height;
// Calculate color
float r = c.getR();
float g = c.getG();
float b = c.getB();
// Put data into buffer
vertices.put(x1).put(y1).put(r).put(g).put(b).put(0f).put(1f);
vertices.put(x1).put(y2).put(r).put(g).put(b).put(0f).put(0f);
vertices.put(x2).put(y2).put(r).put(g).put(b).put(1f).put(0f);
vertices.put(x2).put(y1).put(r).put(g).put(b).put(1f).put(1f);
// We drew X vertices
numVertices += 4;
end();
}
public void drawTextureFromTexture(Texture texture, float x, float y, float width, float height) {
drawTexture(texture, x, y, x + width, y - height, 0, 0, 1, 1, Colors.white);
}
public void drawTextureFromFont(Texture texture, float x, float y, float width, float height, float s1, float t1, float sWidth, float sHeight, Colors c) {
drawTexture(texture, x, y, x + width, y - height, s1, t1, s1 + sWidth, t1 + sHeight, c);
}
public void drawTexture(Texture texture, float x1, float y1, float x2, float y2, float s1, float t1, float s2, float t2, Colors c) {
glEnable(GL_TEXTURE_2D);
texture.prepare();
begin();
if (vertices.remaining() < 5 * 7) flush();
// Calculate color
float r = c.getR();
float g = c.getG();
float b = c.getB();
// Put data into buffer
vertices.put(x1).put(y1).put(r).put(g).put(b).put(s1).put(t2);
vertices.put(x1).put(y2).put(r).put(g).put(b).put(s1).put(t1);
vertices.put(x2).put(y2).put(r).put(g).put(b).put(s2).put(t1);
vertices.put(x2).put(y1).put(r).put(g).put(b).put(s2).put(t2);
// We drew X vertices
numVertices += 4;
end();
texture.unbind();
}
public void drawText(String text, float x, float y, float scale, Colors c) {
fontSketchalot.drawText(this, text, x, y, scale, c);
}
// Initialize renderer
public void init(){
// Create font
fontSketchalot = new Font(Fonts.SKETCHALOT);
// Set up shader programs
setupShaderProgram();
// Set wrapping and filtering values
setParameter(GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
setParameter(GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
setParameter(GL_TEXTURE_MIN_FILTER, GL_NEAREST);
setParameter(GL_TEXTURE_MAG_FILTER, GL_NEAREST);
// Enable blending (?????)
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
// Set parameter of texture
private void setParameter(int name, int value) {
glTexParameteri(GL_TEXTURE_2D, name, value);
}
// Clears drawing area
public void clear() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
// Begin rendering
public void begin() {
if (drawing) throw new IllegalStateException("Renderer is already drawing.");
drawing = true;
numVertices = 0;
}
// End rendering
public void end() {
if (!drawing) throw new IllegalStateException("Renderer is not drawing.");
drawing = false;
flush();
}
// Flushes data to GPU to get rendered
public void flush() {
if (numVertices > 0) {
vertices.flip();
if (vao != null) vao.bind();
else vbo.bind(GL_ARRAY_BUFFER);
specifyVertexAttributes();
}
shaderProgram.use();
// Upload the new vertex data
vbo.bind(GL_ARRAY_BUFFER);
vbo.uploadSubData(GL_ARRAY_BUFFER, 0, vertices);
// Draw batch
glDrawArrays(GL_QUADS, 0, numVertices);
// Clear vertex data for next batch
vertices.clear();
numVertices = 0;
}
private void setupShaderProgram() {
// Generate VertexArrayObject
if (Game.is32Supported()) {
vao = new VertexArrayObject();
vao.bind();
} else {
throw new RuntimeException("OpenGL 3.2 not supported.");
}
// Generate VertexBufferObject
vbo = new VertexBufferObject();
vbo.bind(GL_ARRAY_BUFFER);
// Create FloatBuffer
vertices = MemoryUtil.memAllocFloat(4096);
// Upload null data to allocate storage for the VBO
long size = vertices.capacity() * Float.BYTES;
vbo.uploadData(GL_ARRAY_BUFFER, size, GL_DYNAMIC_DRAW);
// Initialize variables
numVertices = 0;
drawing = false;
// Load Shaders:
Shader vertexShader, fragmentShader;
if (Game.is32Supported()) {
vertexShader = Shader.loadShader(GL_VERTEX_SHADER, "res/shaders/default.vert");
fragmentShader = Shader.loadShader(GL_FRAGMENT_SHADER, "res/shaders/default.frag");
} else {
throw new RuntimeException("OpenGL 3.2 not supported.");
}
// Create ShaderProgram
shaderProgram = new ShaderProgram();
shaderProgram.attachShader(vertexShader);
shaderProgram.attachShader(fragmentShader);
if (Game.is32Supported()) {
shaderProgram.bindFragmentDataLocation(0, "fragColor");
}
shaderProgram.link();
shaderProgram.use();
// Delete linked shaders
vertexShader.delete();
fragmentShader.delete();
// Get width & height of framebuffer
long window = GLFW.glfwGetCurrentContext();
int width, height;
try (MemoryStack stack = MemoryStack.stackPush()) {
IntBuffer widthBuffer = stack.mallocInt(1);
IntBuffer heightBuffer = stack.mallocInt(1);
GLFW.glfwGetFramebufferSize(window, widthBuffer, heightBuffer);
width = widthBuffer.get();
height = heightBuffer.get();
}
// Specify vertex pointers
specifyVertexAttributes();
// Set Model Matrix to identity matrix
Matrix4f model = new Matrix4f();
int uniModel = shaderProgram.getUniformLocation("model");
shaderProgram.setUniform(uniModel, model);
// Set View Matrix to identity matrix
Matrix4f view = new Matrix4f();
int uniView = shaderProgram.getUniformLocation("view");
shaderProgram.setUniform(uniView, view);
// Set Projection Matrix to an orthographic projection
Matrix4f projection = Matrix4f.orthographic(0f, width, 0f, height, -1f, 1f);
int uniProjection = shaderProgram.getUniformLocation("projection");
shaderProgram.setUniform(uniProjection, projection);
}
// Specifies the vertex shader pointers (attributes)
private void specifyVertexAttributes() {
int posAttrib = shaderProgram.getAttributeLocation("position");
shaderProgram.enableVertexAttribute(posAttrib);
shaderProgram.pointVertexAttribute(posAttrib, 2, 7 * Float.BYTES, 0);
int colAttrib = shaderProgram.getAttributeLocation("color");
shaderProgram.enableVertexAttribute(colAttrib);
shaderProgram.pointVertexAttribute(colAttrib, 3, 7 * Float.BYTES, 2 * Float.BYTES);
int texAttrib = shaderProgram.getAttributeLocation("texcoord");
shaderProgram.enableVertexAttribute(texAttrib);
shaderProgram.pointVertexAttribute(texAttrib, 2, 7 * Float.BYTES, 5 * Float.BYTES);
int uniTex = shaderProgram.getUniformLocation("texImage");
shaderProgram.setUniform(uniTex, 0);
}
}
Font.java
public class Font {
private String fontPath;
private java.awt.Font font;
private Map<Character, Glyph> glyphs;
private float fontHeight;
private Texture texture;
public Font(String fontPath) {
System.out.println("Creating font...");
this.fontPath = fontPath;
loadFont();
glyphs = new HashMap<>();
texture = createFontTexture();
System.out.println("Font created.");
}
public Font() {
System.out.println("Creating font...");
this.fontPath = "DEFAULT";
font = new java.awt.Font(java.awt.Font.MONOSPACED, java.awt.Font.PLAIN, 30);
glyphs = new HashMap<>();
texture = createFontTexture();
System.out.println("Font created.");
}
private void loadFont() {
try {
System.out.println("Loading font...");
font = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, new FileInputStream(fontPath)).deriveFont(java.awt.Font.PLAIN, 30);
System.out.println("Font loaded.");
} catch (Exception e) {
throw new RuntimeException("Could not load font.");
}
}
private Texture createFontTexture() {
int imageWidth = 0;
int imageHeight = 0;
// Add up total width and height
for (int i = 32; i < 256; i++) {
if (i == 127) { // DEL control code
continue;
}
char c = (char) i;
BufferedImage ch = createCharImage(c);
if (ch == null) {
System.out.println("Could not load [CHAR: \"" + c + "\"] from font: " + fontPath);
continue;
}
imageWidth += ch.getWidth();
imageHeight = Math.max(imageHeight, ch.getHeight());
}
fontHeight = Converter.glfwCoordToOpenGLCoord(imageHeight);
/* Image for the texture */
BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
// Set up glyphs (Map<CHAR, GLYPH>)
int xOffsetI = 0;
float xOffsetF = 0;
for (int i = 32; i < 256; i++) {
if (i == 127) { // DEL control code
continue;
}
char c = (char) i;
BufferedImage charImage = createCharImage(c);
if (charImage == null) {
System.out.println("Could not load [CHAR: \"" + c + "\"] from font: " + fontPath);
continue;
}
int charWidth = charImage.getWidth();
int charHeight = charImage.getHeight();
// Create glyph
Glyph ch = new Glyph(Converter.glfwCoordToOpenGLCoord(charWidth), Converter.glfwCoordToOpenGLCoord(charHeight), ((float)charWidth / (float)imageWidth), (float)charHeight / (float)imageHeight, xOffsetF, 0.0f);
// Draw char on image
g.drawImage(charImage, xOffsetI, 0, null);
xOffsetI += charWidth;
xOffsetF += ch.sWidth;
// Put in map
glyphs.put(c, ch);
}
// Flip image Horizontal to get the origin to bottom left
AffineTransform transform = AffineTransform.getScaleInstance(1f, -1f);
transform.translate(0, -image.getHeight());
AffineTransformOp operation = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
image = operation.filter(image, null);
// Get char width & char height of image
int width = image.getWidth();
int height = image.getHeight();
// Put pixel data into int[] pixels
int[] pixels = new int[width * height];
image.getRGB(0, 0, width, height, pixels, 0, width);
// Put pixel data into byte buffer
ByteBuffer buffer = MemoryUtil.memAlloc(width * height * 4);
for (int i = 0; i < height; i++){
for (int j = 0; j < width; j++) {
// Pixel format : OxAARRGGBB
int pixel = pixels[i * width + j];
buffer.put((byte) ((pixel >> 16) & 0xFF)); // 0x000000RR
buffer.put((byte) ((pixel >> 8) & 0xFF)); // 0x000000GG
buffer.put((byte) ((pixel) & 0xFF)); // 0x000000BB
buffer.put((byte) ((pixel >> 24) & 0xFF)); // 0x000000AA
//buffer.put((byte)(0xFF)); // Test
}
}
buffer.flip(); // Set index back to 0
Texture fontTexture = Texture.createTexture(width, height, buffer); // Create texture
MemoryUtil.memFree(buffer); // Free buffer
return fontTexture;
}
private BufferedImage createCharImage(char c) {
// Begin by calculating proper width and height
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
if (Options.ANTIALIAS) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g.setFont(font);
FontMetrics fontMetrics = g.getFontMetrics();
g.dispose();
int charWidth = fontMetrics.charWidth(c);
int charHeight = fontMetrics.getHeight();
if (charWidth == 0) return null;
// Now set up the image
image = new BufferedImage(charWidth, charHeight, BufferedImage.TYPE_INT_ARGB);
g = image.createGraphics();
if (Options.ANTIALIAS) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g.setFont(font);
g.setPaint(Color.WHITE); // Use white so we can set the color while rendering
g.drawString(String.valueOf(c), 0, fontMetrics.getAscent()); // Paint char onto image
g.dispose();
// Finally return the final image
return image;
}
public void drawText(Renderer renderer, CharSequence text, float x, float y, float scale, Colors c) {
float xOffset = 0;
float yOffset = 0;
texture.prepare();
//renderer.begin();
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch == '\n') {
// If we start a new line, change the yOffset to reflect (then continue)
yOffset -= fontHeight;
continue;
}
if (ch == '\r') {
// Skip carriage return
continue;
}
Glyph g = glyphs.get(ch);
renderer.drawTextureFromFont(texture, x + xOffset, y + yOffset, g.width * scale, g.height * scale, g.s, g.t, g.sWidth, g.sHeight, c);
xOffset += g.width * scale;
}
//renderer.end();
texture.unbind();
}
}
Texture.java
public class Texture {
// Store handle
private final int id;
private int width;
private int height;
private ByteBuffer image;
// Draw texture using given renderer
public void draw(Renderer renderer, float x, float y, float scale) {
// Scale texture
float w = ((float)width * scale * 2) / (Game.WIDTH);
float h = ((float)height * scale * 2) / (Game.HEIGHT);
renderer.drawTextureFromTexture(this, x, y, w, h);
}
// Create new texture
public Texture() {
id = glGenTextures();
}
// Prepare texture to be drawn
public void prepare() {
bind();
uploadData(width, height, image);
}
// Bind texture
public void bind() {
glBindTexture(GL_TEXTURE_2D, id);
}
// Set parameter of texture
private void setParameter(int name, int value) {
glTexParameteri(GL_TEXTURE_2D, name, value);
}
// Upload image data with specified width and height
public void uploadData(int width, int height, ByteBuffer data) {
uploadData(GL_RGBA8, width, height, GL_RGBA, data);
}
// Upload image data with specified internal format, width, height, and image format
public void uploadData(int internalFormat, int width, int height, int format, ByteBuffer data) {
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, data);
}
// Delete texture
public void delete() {
glDeleteTextures(id);
}
// Get width
public int getWidth() {
return width;
}
// Set width
public void setWidth(int width) {
if (width > 0) {
this.width = width;
}
}
// Get height
public int getHeight() {
return height;
}
// Set height
public void setHeight(int height) {
if (height > 0) {
this.height = height;
}
}
// Set image
public void setImage(ByteBuffer image) {
this.image = image;
}
public static Texture createTexture(int width, int height, ByteBuffer image) {
Texture texture = new Texture();
texture.setWidth(width);
texture.setHeight(height);
texture.setImage(image);
texture.prepare();
return texture;
}
public static Texture loadTexture(String path) {
ByteBuffer image;
int width, height;
try (MemoryStack stack = MemoryStack.stackPush()) {
IntBuffer w = stack.mallocInt(1);
IntBuffer h = stack.mallocInt(1);
IntBuffer comp = stack.mallocInt(1);
// Load image
stbi_set_flip_vertically_on_load(true);
image = stbi_load(path, w, h, comp, 4);
if (image == null) throw new RuntimeException("Could not load texture.");
width = w.get();
height = h.get();
}
return createTexture(width, height, image);
}
public void unbind() {
glBindTexture(GL_TEXTURE_2D, 0);
}
}
I have a CustomView and an Image view. The CustomView is a ball that moves around the screen and bounces off the walls. The Image is a quarter circle that you can rotate in a circle on touch. I am trying to make my game so that when the filled pixels from the CustomView cross paths with the Filled pixels from the ImageView a collision is detected. The problem that I am having is I do not know how to retrieve where the filled pixels are on each view.
Here is my XML code
<com.leytontaylor.bouncyballz.AnimatedView
android:id="#+id/anim_view"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
/>
<ImageView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="#+id/quartCircle"
android:layout_gravity="center_horizontal"
android:src="#drawable/quartercircle"
android:scaleType="matrix"/>
My MainActivity
public class MainActivity extends AppCompatActivity {
private static Bitmap imageOriginal, imageScaled;
private static Matrix matrix;
private ImageView dialer;
private int dialerHeight, dialerWidth;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// load the image only once
if (imageOriginal == null) {
imageOriginal = BitmapFactory.decodeResource(getResources(), R.drawable.quartercircle);
}
// initialize the matrix only once
if (matrix == null) {
matrix = new Matrix();
} else {
// not needed, you can also post the matrix immediately to restore the old state
matrix.reset();
}
dialer = (ImageView) findViewById(R.id.quartCircle);
dialer.setOnTouchListener(new MyOnTouchListener());
dialer.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// method called more than once, but the values only need to be initialized one time
if (dialerHeight == 0 || dialerWidth == 0) {
dialerHeight = dialer.getHeight();
dialerWidth = dialer.getWidth();
// resize
Matrix resize = new Matrix();
resize.postScale((float) Math.min(dialerWidth, dialerHeight) / (float) imageOriginal.getWidth(), (float) Math.min(dialerWidth, dialerHeight) / (float) imageOriginal.getHeight());
imageScaled = Bitmap.createBitmap(imageOriginal, 0, 0, imageOriginal.getWidth(), imageOriginal.getHeight(), resize, false);
// translate to the image view's center
float translateX = dialerWidth / 2 - imageScaled.getWidth() / 2;
float translateY = dialerHeight / 2 - imageScaled.getHeight() / 2;
matrix.postTranslate(translateX, translateY);
dialer.setImageBitmap(imageScaled);
dialer.setImageMatrix(matrix);
}
}
});
}
MyOnTouchListener class:
private class MyOnTouchListener implements View.OnTouchListener {
private double startAngle;
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startAngle = getAngle(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
double currentAngle = getAngle(event.getX(), event.getY());
rotateDialer((float) (startAngle - currentAngle));
startAngle = currentAngle;
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
}
private double getAngle(double xTouch, double yTouch) {
double x = xTouch - (dialerWidth / 2d);
double y = dialerHeight - yTouch - (dialerHeight / 2d);
switch (getQuadrant(x, y)) {
case 1:
return Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI;
case 2:
return 180 - Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI;
case 3:
return 180 + (-1 * Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
case 4:
return 360 + Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI;
default:
return 0;
}
}
/**
* #return The selected quadrant.
*/
private static int getQuadrant(double x, double y) {
if (x >= 0) {
return y >= 0 ? 1 : 4;
} else {
return y >= 0 ? 2 : 3;
}
}
/**
* Rotate the dialer.
*
* #param degrees The degrees, the dialer should get rotated.
*/
private void rotateDialer(float degrees) {
matrix.postRotate(degrees, dialerWidth / 2, dialerHeight / 2);
dialer.setImageMatrix(matrix);
}
And my AnimatedView
public class AnimatedView extends ImageView {
private Context mContext;
int x = -1;
int y = -1;
private int xVelocity = 10;
private int yVelocity = 5;
private Handler h;
private final int FRAME_RATE = 60;
public AnimatedView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
h = new Handler();
}
private Runnable r = new Runnable() {
#Override
public void run() {
invalidate();
}
};
protected void onDraw(Canvas c) {
BitmapDrawable ball = (BitmapDrawable) mContext.getResources().getDrawable(R.drawable.smallerball);
if (x<0 && y <0) {
x = this.getWidth()/2;
y = this.getHeight()/2;
} else {
x += xVelocity;
y += yVelocity;
if ((x > this.getWidth() - ball.getBitmap().getWidth()) || (x < 0)) {
xVelocity = xVelocity*-1;
}
if ((y > this.getHeight() - ball.getBitmap().getHeight()) || (y < 0)) {
yVelocity = yVelocity*-1;
}
}
c.drawBitmap(ball.getBitmap(), x, y, null);
h.postDelayed(r, FRAME_RATE);
}
public float getX() {
return x;
}
public float getY() {
return y;
}
}
My question is: How can I retrieve the filled pixels from both of these views, and pass them through a function that detects a collision.
Thanks in advance for the help!:)
You could encapsulate your images with Array of points marking your border coordinates(and update them on move/calculate them based on origin) then you'll be able to decide whether oposing object are in touch or not(if any of oposing arrays share the same point)
You really need to define "filled pixels". I assume you mean the non-transparent pixels. The easiest way to find those, is by converting your entire view into a bitmap and iterating through its pixels. You can convert a View into a Bitmap like this:
private Bitmap getBitmapFromView(View view) {
view.buildDrawingCache();
Bitmap returnedBitmap = Bitmap.createBitmap(view.measuredWidth,
view.measuredHeight,
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(returnedBitmap);
canvas.drawColor(Color.WHITE, PorterDuff.Mode.SRC_IN);
Drawable drawable = view.background;
drawable.draw(canvas);
view.draw(canvas);
return returnedBitmap;
}
You'd also need to get the absolute location of the views:
Point getViewLocationOnScreen(View v) {
int[] coor = new int[]{0, 0};
v.getLocationOnScreen(coor);
return new Point(coor[0], coor[1]);
}
Then you just have to iterate through the pixels of each Bitmap, check their colors to know whether they're "filled", and also check whether they're overlapping based on their coordination inside the bitmaps and the absolute location of the views on screen.
Iterating through a bitmap's is done like this:
for (int x = 0; x < myBitmap.getWidth(); x++) {
for (int y = 0; y < myBitmap.getHeight(); y++) {
int color = myBitmap.getPixel(x, y);
}
}
But I must say, I don't think performing this sort of heavy computations on UI thread is really a good idea. There are dozens of much better ways to detect collisions than pixel-perfect checking. This will probably come out extremely laggy.
I'm very new to Android and want to create an image processing app.
I have lifted a code in using the camera of an android phone and shows the captured photo on a imageview... The code works well, the problem is that I can't seem to make the grayscaling code to work. Or I can't seem to show the grayscaled image on the imageview... Please I need your help. Thank you very much.
This is the camera image capturing code that works well
public class CameraActivity extends ActionBarActivity {
static final int REQUEST_IMAGE_CAPTURE = 1;
ImageView imageView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
Button button = (Button) findViewById(R.id.button);
//King ina, button2 for processing
Button button2 = (Button) findViewById(R.id.button2);
imageView = (ImageView) findViewById(R.id.imageView);
//Disable the button if it has no camera
if (!hasCamera())
button.setEnabled(false);
}
//Check if the user has camera
private boolean hasCamera() {
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
}
//Launching the camera
public void launchCamera(View view) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//Take a picture and pass result along to onActivityResult
startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
}
//Show image on imageView
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
//Get the photo
Bundle extras = data.getExtras();
Bitmap image = (Bitmap) extras.get("data");
imageView.setImageBitmap(image);
}
}
}
While this is the code for Grayscaling the image... Can i just repeat the override code which shows the captured image?
Thanks a lot...
public Bitmap imageProcess(Bitmap image) {
int width, height;
height = image.getHeight();
width = image.getWidth();
Bitmap bmpGrayscale = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bmpGrayscale);
Paint paint = new Paint();
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
paint.setColorFilter(f);
c.drawBitmap(image, 0, 0, paint);
return bmpGrayscale;
}
Hi you can make the image black n white using contrast.
See the code..
public static Bitmap createContrast(Bitmap src, double value) {
// image size
int width = src.getWidth();
int height = src.getHeight();
// create output bitmap
Bitmap bmOut = Bitmap.createBitmap(width, height, src.getConfig());
// color information
int A, R, G, B;
int pixel;
// get contrast value
double contrast = Math.pow((100 + value) / 100, 2);
// scan through all pixels
for(int x = 0; x < width; ++x) {
for(int y = 0; y < height; ++y) {
// get pixel color
pixel = src.getPixel(x, y);
A = Color.alpha(pixel);
// apply filter contrast for every channel R, G, B
R = Color.red(pixel);
R = (int)(((((R / 255.0) - 0.5) * contrast) + 0.5) * 255.0);
if(R < 0) { R = 0; }
else if(R > 255) { R = 255; }
G = Color.red(pixel);
G = (int)(((((G / 255.0) - 0.5) * contrast) + 0.5) * 255.0);
if(G < 0) { G = 0; }
else if(G > 255) { G = 255; }
B = Color.red(pixel);
B = (int)(((((B / 255.0) - 0.5) * contrast) + 0.5) * 255.0);
if(B < 0) { B = 0; }
else if(B > 255) { B = 255; }
// set new pixel color to output bitmap
bmOut.setPixel(x, y, Color.argb(A, R, G, B));
}
}
return bmOut;
}
Set the double value to 50 on mathod call. For Example
createContrast(Bitmap src, 50)
For more information on formulae please refer this
After researching and a few editing, I did the grayscaling this way...
bmp = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
operation = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), bmp.getConfig());
double GS_RED = 0.299;
double GS_GREEN = 0.587;
double GS_BLUE = 0.114;
int pixel;
int A, R, G, B;
// get image size
int width = bmp.getWidth();
int height = bmp.getHeight();
// scan through every single pixel
for (int x = 0; x < width; ++x) {
for (int y = 0; y < height; ++y) {
// get one pixel color
pixel = bmp.getPixel(x, y);
// retrieve color of all channels
A = Color.alpha(pixel);
R = Color.red(pixel);
G = Color.green(pixel);
B = Color.blue(pixel);
// take conversion up to one single value
R = G = B = (int) (GS_RED * R + GS_GREEN * G + GS_BLUE * B);
// set new pixel color to output bitmap
operation.setPixel(x, y, Color.argb(A, R, G, B));
//Show grayscaled image
imageView.setImageBitmap(operation);
}
}
Thanks for the answers and this site.
https://xjaphx.wordpress.com/2011/06/21/image-processing-grayscale-image-on-the-fly/
Use this dependency for image transformation
Github link - https://github.com/wasabeef/glide-transformations
repositories { mavenCentral()}
implementation 'jp.wasabeef:glide-transformations:4.3.0'
Glide.with(this).load(R.drawable.demo).apply(RequestOptions.bitmapTransform(GrayscaleTransformation())).into(imageView)
You need to call your imageProcess() method, before setting it in the image view.
//Show image on imageView
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
//Get the photo
Bundle extras = data.getExtras();
Bitmap image = (Bitmap) extras.get("data");
imageView.setImageBitmap(imageProcess(image)); // called here
}
}
I'm new in Android programming, and I've tried to write a simple app recently, just for practice! In this one, I want to color an image on user's tap, but I don't know how to start it. I've read different topics which say to use the "Flood Fill" algorithm. I've found it on the web, but I don't know how to put it into my simple app.
The code I've found:
private void FloodFill(Bitmap bmp, Point pt, int targetColor, int replacementColor)
{
Queue<Point> q = new LinkedList<Point>();
q.add(pt);
while (q.size() > 0) {
Point n = q.poll();
if (bmp.getPixel(n.x, n.y) != targetColor)
continue;
Point w = n, e = new Point(n.x + 1, n.y);
while ((w.x > 0) && (bmp.getPixel(w.x, w.y) == targetColor)) {
bmp.setPixel(w.x, w.y, replacementColor);
if ((w.y > 0) && (bmp.getPixel(w.x, w.y - 1) == targetColor))
q.add(new Point(w.x, w.y - 1));
if ((w.y < bmp.getHeight() - 1) && (bmp.getPixel(w.x, w.y + 1) == targetColor))
q.add(new Point(w.x, w.y + 1));
w.x--;
}
while ((e.x < bmp.getWidth() - 1) && (bmp.getPixel(e.x, e.y) == targetColor)) {
bmp.setPixel(e.x, e.y, replacementColor);
if ((e.y > 0) && (bmp.getPixel(e.x, e.y - 1) == targetColor))
q.add(new Point(e.x, e.y - 1));
if ((e.y < bmp.getHeight() - 1) && (bmp.getPixel(e.x, e.y + 1) == targetColor))
q.add(new Point(e.x, e.y + 1));
e.x++;
}
}
}
I know how to draw lines on the screen following user's finger on touch event, but I'd also like to know how to fill a given image with some color, for example this one:
A little lion!
I saw these other questions on stack overflow:
First topic
Second topic
Third topic
It seems so easy to do, but I can't! Can you show me a little example please? I'd like to know how to set the canvas, the image to color, and how to do it.
android using flood fill algorithm getting out of memory exception. Check the link has an example.
You need the the co-ordinates of x and y touch and you can use asynctask to floofill a closed area. Use a progressdialog untill the floodfill fills the closed area with replacement color.
Note: I have faced problem when coloring large closed are. It took lot of time. I am not sure if using asynctask is the beast way. I hope someone can clarify on that part
You can modify the below according to your needs.
final Point p1 = new Point();
p1.x=(int) x; //x co-ordinate where the user touches on the screen
p1.y=(int) y; //y co-ordinate where the user touches on the screen
FloodFill f= new FloodFill();
f.floodFill(bmp,pt,targetColor,replacementColor);
FloodFill algorithm to fill a closed area
public class FloodFill {
public void floodFill(Bitmap image, Point node, int targetColor,
int replacementColor) {
int width = image.getWidth();
int height = image.getHeight();
int target = targetColor;
int replacement = replacementColor;
if (target != replacement) {
Queue<Point> queue = new LinkedList<Point>();
do {
int x = node.x;
int y = node.y;
while (x > 0 && image.getPixel(x - 1, y) == target) {
x--;
}
boolean spanUp = false;
boolean spanDown = false;
while (x < width && image.getPixel(x, y) == target) {
image.setPixel(x, y, replacement);
if (!spanUp && y > 0 && image.getPixel(x, y - 1) == target) {
queue.add(new Point(x, y - 1));
spanUp = true;
} else if (spanUp && y > 0
&& image.getPixel(x, y - 1) != target) {
spanUp = false;
}
if (!spanDown && y < height - 1
&& image.getPixel(x, y + 1) == target) {
queue.add(new Point(x, y + 1));
spanDown = true;
} else if (spanDown && y < height - 1
&& image.getPixel(x, y + 1) != target) {
spanDown = false;
}
x++;
}
} while ((node = queue.poll()) != null);
}
}
}
Edit:
Edit 8-7-2014 :
Filling a small closed area works fine with the above flood fill algorithm. However for large area the algorithm works slow and consumes lot of memory. Recently i came across a post which uses QueueLinear Flood Fill which is way faster that the above.
Source :
http://www.codeproject.com/Articles/16405/Queue-Linear-Flood-Fill-A-Fast-Flood-Fill-Algorith
Code :
public class QueueLinearFloodFiller {
protected Bitmap image = null;
protected int[] tolerance = new int[] { 0, 0, 0 };
protected int width = 0;
protected int height = 0;
protected int[] pixels = null;
protected int fillColor = 0;
protected int[] startColor = new int[] { 0, 0, 0 };
protected boolean[] pixelsChecked;
protected Queue<FloodFillRange> ranges;
// Construct using an image and a copy will be made to fill into,
// Construct with BufferedImage and flood fill will write directly to
// provided BufferedImage
public QueueLinearFloodFiller(Bitmap img) {
copyImage(img);
}
public QueueLinearFloodFiller(Bitmap img, int targetColor, int newColor) {
useImage(img);
setFillColor(newColor);
setTargetColor(targetColor);
}
public void setTargetColor(int targetColor) {
startColor[0] = Color.red(targetColor);
startColor[1] = Color.green(targetColor);
startColor[2] = Color.blue(targetColor);
}
public int getFillColor() {
return fillColor;
}
public void setFillColor(int value) {
fillColor = value;
}
public int[] getTolerance() {
return tolerance;
}
public void setTolerance(int[] value) {
tolerance = value;
}
public void setTolerance(int value) {
tolerance = new int[] { value, value, value };
}
public Bitmap getImage() {
return image;
}
public void copyImage(Bitmap img) {
// Copy data from provided Image to a BufferedImage to write flood fill
// to, use getImage to retrieve
// cache data in member variables to decrease overhead of property calls
width = img.getWidth();
height = img.getHeight();
image = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(image);
canvas.drawBitmap(img, 0, 0, null);
pixels = new int[width * height];
image.getPixels(pixels, 0, width, 1, 1, width - 1, height - 1);
}
public void useImage(Bitmap img) {
// Use a pre-existing provided BufferedImage and write directly to it
// cache data in member variables to decrease overhead of property calls
width = img.getWidth();
height = img.getHeight();
image = img;
pixels = new int[width * height];
image.getPixels(pixels, 0, width, 1, 1, width - 1, height - 1);
}
protected void prepare() {
// Called before starting flood-fill
pixelsChecked = new boolean[pixels.length];
ranges = new LinkedList<FloodFillRange>();
}
// Fills the specified point on the bitmap with the currently selected fill
// color.
// int x, int y: The starting coords for the fill
public void floodFill(int x, int y) {
// Setup
prepare();
if (startColor[0] == 0) {
// ***Get starting color.
int startPixel = pixels[(width * y) + x];
startColor[0] = (startPixel >> 16) & 0xff;
startColor[1] = (startPixel >> 8) & 0xff;
startColor[2] = startPixel & 0xff;
}
// ***Do first call to floodfill.
LinearFill(x, y);
// ***Call floodfill routine while floodfill ranges still exist on the
// queue
FloodFillRange range;
while (ranges.size() > 0) {
// **Get Next Range Off the Queue
range = ranges.remove();
// **Check Above and Below Each Pixel in the Floodfill Range
int downPxIdx = (width * (range.Y + 1)) + range.startX;
int upPxIdx = (width * (range.Y - 1)) + range.startX;
int upY = range.Y - 1;// so we can pass the y coord by ref
int downY = range.Y + 1;
for (int i = range.startX; i <= range.endX; i++) {
// *Start Fill Upwards
// if we're not above the top of the bitmap and the pixel above
// this one is within the color tolerance
if (range.Y > 0 && (!pixelsChecked[upPxIdx])
&& CheckPixel(upPxIdx))
LinearFill(i, upY);
// *Start Fill Downwards
// if we're not below the bottom of the bitmap and the pixel
// below this one is within the color tolerance
if (range.Y < (height - 1) && (!pixelsChecked[downPxIdx])
&& CheckPixel(downPxIdx))
LinearFill(i, downY);
downPxIdx++;
upPxIdx++;
}
}
image.setPixels(pixels, 0, width, 1, 1, width - 1, height - 1);
}
// Finds the furthermost left and right boundaries of the fill area
// on a given y coordinate, starting from a given x coordinate, filling as
// it goes.
// Adds the resulting horizontal range to the queue of floodfill ranges,
// to be processed in the main loop.
// int x, int y: The starting coords
protected void LinearFill(int x, int y) {
// ***Find Left Edge of Color Area
int lFillLoc = x; // the location to check/fill on the left
int pxIdx = (width * y) + x;
while (true) {
// **fill with the color
pixels[pxIdx] = fillColor;
// **indicate that this pixel has already been checked and filled
pixelsChecked[pxIdx] = true;
// **de-increment
lFillLoc--; // de-increment counter
pxIdx--; // de-increment pixel index
// **exit loop if we're at edge of bitmap or color area
if (lFillLoc < 0 || (pixelsChecked[pxIdx]) || !CheckPixel(pxIdx)) {
break;
}
}
lFillLoc++;
// ***Find Right Edge of Color Area
int rFillLoc = x; // the location to check/fill on the left
pxIdx = (width * y) + x;
while (true) {
// **fill with the color
pixels[pxIdx] = fillColor;
// **indicate that this pixel has already been checked and filled
pixelsChecked[pxIdx] = true;
// **increment
rFillLoc++; // increment counter
pxIdx++; // increment pixel index
// **exit loop if we're at edge of bitmap or color area
if (rFillLoc >= width || pixelsChecked[pxIdx] || !CheckPixel(pxIdx)) {
break;
}
}
rFillLoc--;
// add range to queue
FloodFillRange r = new FloodFillRange(lFillLoc, rFillLoc, y);
ranges.offer(r);
}
// Sees if a pixel is within the color tolerance range.
protected boolean CheckPixel(int px) {
int red = (pixels[px] >>> 16) & 0xff;
int green = (pixels[px] >>> 8) & 0xff;
int blue = pixels[px] & 0xff;
return (red >= (startColor[0] - tolerance[0])
&& red <= (startColor[0] + tolerance[0])
&& green >= (startColor[1] - tolerance[1])
&& green <= (startColor[1] + tolerance[1])
&& blue >= (startColor[2] - tolerance[2]) && blue <= (startColor[2] + tolerance[2]));
}
// Represents a linear range to be filled and branched from.
protected class FloodFillRange {
public int startX;
public int endX;
public int Y;
public FloodFillRange(int startX, int endX, int y) {
this.startX = startX;
this.endX = endX;
this.Y = y;
}
}
}
Thanks to the stackoverflow's users I've gotten to the right solution!
I wanted to know how to use the flood fill algorithm and integrate it in a simple Android project, and this is what I did:
Java code:
import java.util.LinkedList;
import java.util.Queue;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
public class Main extends Activity {
private RelativeLayout dashBoard;
private MyView myView;
public ImageView image;
Button b_red, b_blue, b_green, b_orange, b_clear;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myView = new MyView(this);
setContentView(R.layout.activity_main);
findViewById(R.id.dashBoard);
b_red = (Button) findViewById(R.id.b_red);
b_blue = (Button) findViewById(R.id.b_blue);
b_green = (Button) findViewById(R.id.b_green);
b_orange = (Button) findViewById(R.id.b_orange);
b_red.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
myView.changePaintColor(0xFFFF0000);
}
});
b_blue.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
myView.changePaintColor(0xFF0000FF);
}
});
b_green.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
myView.changePaintColor(0xFF00FF00);
}
});
b_orange.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
myView.changePaintColor(0xFFFF9900);
}
});
dashBoard = (RelativeLayout) findViewById(R.id.dashBoard);
dashBoard.addView(myView);
}
public class MyView extends View {
private Paint paint;
private Path path;
public Bitmap mBitmap;
public ProgressDialog pd;
final Point p1 = new Point();
public Canvas canvas;
//Bitmap mutableBitmap ;
public MyView(Context context) {
super(context);
this.paint = new Paint();
this.paint.setAntiAlias(true);
pd = new ProgressDialog(context);
this.paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(5f);
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.forme).copy(Bitmap.Config.ARGB_8888, true);
this.path = new Path();
}
#Override
protected void onDraw(Canvas canvas) {
this.canvas = canvas;
this.paint.setColor(Color.RED);
canvas.drawBitmap(mBitmap, 0, 0, paint);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
p1.x = (int) x;
p1.y = (int) y;
final int sourceColor = mBitmap.getPixel((int) x, (int) y);
final int targetColor = paint.getColor();
new TheTask(mBitmap, p1, sourceColor, targetColor).execute();
invalidate();
}
return true;
}
public void clear() {
path.reset();
invalidate();
}
public int getCurrentPaintColor() {
return paint.getColor();
}
public void changePaintColor(int color){
this.paint.setColor(color);
}
class TheTask extends AsyncTask<Void, Integer, Void> {
Bitmap bmp;
Point pt;
int replacementColor, targetColor;
public TheTask(Bitmap bm, Point p, int sc, int tc) {
this.bmp = bm;
this.pt = p;
this.replacementColor = tc;
this.targetColor = sc;
pd.setMessage("Filling....");
pd.show();
}
#Override
protected void onPreExecute() {
pd.show();
}
#Override
protected void onProgressUpdate(Integer... values) {
}
#Override
protected Void doInBackground(Void... params) {
FloodFill f = new FloodFill();
f.floodFill(bmp, pt, targetColor, replacementColor);
return null;
}
#Override
protected void onPostExecute(Void result) {
pd.dismiss();
invalidate();
}
}
}
// flood fill
public class FloodFill {
public void floodFill(Bitmap image, Point node, int targetColor, int replacementColor) {
int width = image.getWidth();
int height = image.getHeight();
int target = targetColor;
int replacement = replacementColor;
if (target != replacement) {
Queue<Point> queue = new LinkedList<Point>();
do {
int x = node.x;
int y = node.y;
while (x > 0 && image.getPixel(x - 1, y) == target) {
x--;
}
boolean spanUp = false;
boolean spanDown = false;
while (x < width && image.getPixel(x, y) == target) {
image.setPixel(x, y, replacement);
if (!spanUp && y > 0 && image.getPixel(x, y - 1) == target) {
queue.add(new Point(x, y - 1));
spanUp = true;
} else if (spanUp && y > 0 && image.getPixel(x, y - 1) != target) {
spanUp = false;
}
if (!spanDown && y < height - 1 && image.getPixel(x, y + 1) == target) {
queue.add(new Point(x, y + 1));
spanDown = true;
} else if (spanDown && y < (height - 1) && image.getPixel(x, y + 1) != target) {
spanDown = false;
}
x++;
}
} while ((node = queue.poll()) != null);
}
}
}
}
And this is the XML code:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/drawingLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".Main" >
<RelativeLayout
android:id="#+id/dashBoard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="#+id/b_red"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="10dp" >
</RelativeLayout>
<Button
android:id="#+id/b_red"
android:layout_width="65dp"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:background="#FF0000" />
<Button
android:id="#+id/b_green"
android:layout_width="65dp"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:layout_toRightOf="#+id/b_red"
android:background="#00FF00" />
<Button
android:id="#+id/b_blue"
android:layout_width="65dp"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:layout_toRightOf="#+id/b_green"
android:background="#0000FF" />
<Button
android:id="#+id/b_orange"
android:layout_width="65dp"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:layout_toRightOf="#+id/b_blue"
android:background="#FF9900" />
<Button
android:id="#+id/button5"
android:layout_width="60dp"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="Clear" />
</RelativeLayout>
I hope it to be helpfull for you!!!
Have a nice day!!!