При масштабировании и переводе холста заполнение цветом не очень хорошо. Я не понимаю, как это решить?
So I have a canvas that I draw a bitmap onto.. When the scale factor of the canvas is 1.0f everything works perfectly and fill color perfectly how I want it to.. However the problem is when I change the scale of the canvas. Either zoomed in or out I run into problems with filling color in bitmap. I just can't work out the right calculation to get my desired result! Please help!
Что я уже пробовал:
public class ColorGFX extends SurfaceView implements Runnable { public Thread mThread; private Handler mHandler; public boolean isThreadBroken = false; private int mRunnableCounter = 0; public int selectedColor = Color.BLACK; public boolean isFillEnabled = false; public boolean isFillModeEnabled = true; public boolean isEraseModeEnabled = false; private SurfaceHolder mOurHolder; private Thread mOurThread = null; public int imageWidth; public int imageHeight; public boolean isNextImage = false; public Bitmap pictureBitmap; // public String paintBitmapName; public Bitmap pictureBitmapBuffer; public Bitmap bitmap; public Canvas pathCanvas; public Canvas fillCanvas; Matrix matrix; public Canvas canvas; private boolean mIsDrawn = false; public Paint paint; public Paint bitmapPaint; public Path mPath; public boolean isSavedData; private boolean[][] mFloodfillList; private boolean[][] mStrokefillList; private Context mContext; private static final int INVALID_POINTER_ID = -1; private int mActivePointerId = INVALID_POINTER_ID; private ScaleGestureDetector mScaleDetector; private float mScaleFactor = 1.f; private float mLastTouchX; private float mLastTouchY; public float mPosX = 10; public float mPosY = 10; private int mImageWidthScaled; private int mImageHeightScaled; public ColorGFX(Context context, int width, int height) { super(context); this.mContext = context; this.imageWidth = width; this.imageHeight = height; mPath = new Path(); bitmapPaint = new Paint(Paint.DITHER_FLAG); mOurHolder = getHolder(); Log.e("imageWidth", "......................." + imageWidth); Log.e("imageHeight", "......................" + imageHeight); mImageWidthScaled = (int) (imageWidth * mScaleFactor); mImageHeightScaled = (int) (imageHeight * mScaleFactor); mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); } public ColorGFX(Context context, int width, int height, boolean isSavedData, Bitmap savedPathBitmap) { super(context); this.mContext = context; this.imageWidth = width; this.imageHeight = height; this.isSavedData = isSavedData; this.bitmap = savedPathBitmap; mPath = new Path(); bitmapPaint = new Paint(Paint.DITHER_FLAG); mOurHolder = getHolder(); } public ColorGFX(Context context) { super(context); this.mContext = context; mPath = new Path(); bitmapPaint = new Paint(Paint.DITHER_FLAG); mOurHolder = getHolder(); } public void pause() { if (mThread != null && mThread.isAlive()) { isThreadBroken = true; } mOurThread.interrupt(); } public void resume() { if (bitmap == null && !isSavedData) { bitmap = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888); } pathCanvas = new Canvas(bitmap); mOurThread = new Thread(this); mOurThread.start(); } public void run() { while (!mOurThread.isInterrupted()) { if (!mOurHolder.getSurface().isValid()) { continue; } canvas = mOurHolder.lockCanvas(); canvas.save(); matrix = new Matrix(); matrix.postTranslate(mPosX, mPosY); canvas.concat(matrix); canvas.scale(mScaleFactor, mScaleFactor); canvas.drawColor(Color.WHITE); if (pictureBitmap != null) { canvas.drawBitmap(pictureBitmap, 0, 0, null); } if (bitmap != null) { if (!bitmap.isRecycled()) { canvas.drawBitmap(bitmap, 0, 0, bitmapPaint); } } if (mIsDrawn) { mPath.reset(); mIsDrawn = false; } canvas.drawPath(mPath, paint); if (pictureBitmap != null) { canvas.drawBitmap(pictureBitmap, 0, 0, null); } if (isNextImage) { loadNewImages(); isNextImage = false; } // Once we are done, unlock the canvas and update the display. mOurHolder.unlockCanvasAndPost(canvas); } if (mOurThread.isInterrupted()) { mOurThread = null; } } public void loadNewImages() { if (pictureBitmap != null) { pictureBitmap.recycle(); } int counter = 0; while (true) { pictureBitmap = pictureBitmapBuffer.copy(Bitmap.Config.ARGB_8888, true); if (pictureBitmap != null) { break; } else if (counter > 1000) { // TODO: throw a timeout exception. Resource is not loading or // something is hanging. Right now we'll just break so we don't // over consume resources. The error might simply crash the // program for the user if this ever happens. break; } counter++; } pictureBitmapBuffer.recycle(); } public void clear() { pathCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); } /** * Handles fill operations when a fill event occurs. */ private void fillHandler(float x, float y, int scaleimageWidth, int scaleimageHeight) { if ((scaleimageWidth > imageWidth || scaleimageHeight > imageHeight)) { Toast.makeText(mContext, "Image Scaling working", Toast.LENGTH_SHORT).show(); } else { if (isFillModeEnabled) { if (isFillEnabled) { if (mRunnableCounter >= 1) { // Signal end of op. isFillEnabled = false; return; } int resPaintId = R.drawable.coloring_book_1_image_1_map; // mContext.getResources().getIdentifier(paintBitmapName, "drawable", mContext.getPackageName()); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), resPaintId, options); int inSampleSize = 1; int fillImageWidth = options.outWidth; int fillImageHeight = options.outHeight; boolean scaleFailed = false; Bitmap paintMap = null; float resizeRatioHeight = 1; if (fillImageWidth > imageWidth || fillImageHeight > imageHeight) { resizeRatioHeight = (float) fillImageHeight / (float) imageHeight; inSampleSize = (int) resizeRatioHeight; if (inSampleSize <= 1) { scaleFailed = true; } } options.inSampleSize = inSampleSize; options.inJustDecodeBounds = false; Bitmap picture = BitmapFactory.decodeResource(getResources(), resPaintId, options); if (scaleFailed) { int newWidth = (int) (picture.getWidth() / resizeRatioHeight); int newHeight = (int) (picture.getHeight() / resizeRatioHeight); paintMap = Bitmap.createScaledBitmap(picture, newWidth, newHeight, true); picture.recycle(); } else { paintMap = picture; } Bitmap mutablePaintMap = paintMap.copy(Config.ARGB_8888, true); paintMap.recycle(); Log.e("TAG", "..........x............." + x); Log.e("TAG", ".........y............." + y); if (picture.getHeight() > y && picture.getWidth() > x) { int targetColor = mutablePaintMap.getPixel((int) x, (int) y); if (targetColor == Color.TRANSPARENT) { Point node = new Point((int) x, (int) y); mRunnableCounter++; LoadViewTask floodfillTask = new LoadViewTask(); floodfillTask.image = mutablePaintMap; floodfillTask.point = node; floodfillTask.target = targetColor; floodfillTask.replacementColor = selectedColor; mHandler = new Handler(); mThread = new Thread(floodfillTask, "FloodFillThread"); mThread.start(); } } else { Log.e(TAG, "Out of bound"); } } } } } public void colorPixels(Bitmap picture, int replacementColor) { for (int i = 0; i < mFloodfillList.length; i++) { for (int j = 0; j < mFloodfillList[i].length; j++) { if (mFloodfillList[i][j] != false) { picture.setPixel(i, j, replacementColor); } if (mStrokefillList[i][j] != false) { picture.setPixel(i, j, replacementColor); } } } } public void clearPixelLists() { mStrokefillList = null; mFloodfillList = null; } private class LoadViewTask implements Runnable { public Point point; public int target; public int replacementColor; public Bitmap image; public boolean[][] list; public boolean[][] strokeList; @Override public void run() { synchronized (mThread) { floodFill(point, target, replacementColor, image); mHandler.post(new Runnable() { @Override public void run() { } }); } mHandler.post(new Runnable() { @Override public void run() { if (!isThreadBroken) { image.recycle(); mFloodfillList = list; mStrokefillList = strokeList; Bitmap fillPicture = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888); colorPixels(fillPicture, replacementColor); Paint addFilter = new Paint(); addFilter.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); pathCanvas.drawBitmap(fillPicture, 0, 0, addFilter); } clearPixelLists(); mRunnableCounter--; if (mRunnableCounter == 0) { isThreadBroken = false; } } }); synchronized (mThread) { mThread.interrupt(); } } public void floodFill(Point node, int targetColor, int replacementColor, Bitmap picture) { int width = picture.getWidth(); int height = picture.getHeight(); list = new boolean[width][height]; strokeList = new boolean[width][height]; int target = targetColor; int replacement = replacementColor; if (target != replacement) { Queue<Point> queue = new LinkedList<Point>(); do { if (isThreadBroken) { break; } int x = node.x; int y = node.y; while (x > 0 && picture.getPixel(x - 1, y) == target) { // Continuously decrement x (AKA bring x as far to the // west as possible given the color constraints). x--; } // Set directional booleans. boolean spanUp = false; boolean spanDown = false; // While x has not reached as far East as it can in the // bitmap (AKA hasn't hit the end of the image and hasn't // reached a color different than the replacement color)... while (x < width && picture.getPixel(x, y) == target) { // Replace the current pixel color. picture.setPixel(x, y, replacement); // Add the pixel to the flood fill list. list[x][y] = true; // If we don't take the stroke paths into consideration // and color them where necessary, we WILL have reduced // aliasing, but not perfect anti-aliasing. By coloring // the paths, we have virtually ZERO aliasing. // TOP if (y + 1 < height - 1 && picture.getPixel(x, y + 1) != target) { strokeList[x][y + 1] = true; } // RIGHT if (x + 1 < width - 1 && picture.getPixel(x + 1, y) != target) { strokeList[x + 1][y] = true; } // LEFT if (x - 1 > 0 && picture.getPixel(x - 1, y) != target) { strokeList[x - 1][y] = true; } // BOTTOM if (y - 1 > 0 && picture.getPixel(x, y - 1) != target) { strokeList[x][y - 1] = true; } // Add one SOUTH point to the queue if it is replaceable // (this will be the next relative point to check from) // and we have not previously moved down. if (!spanUp && y > 0 && picture.getPixel(x, y - 1) == target) { queue.add(new Point(x, y - 1)); spanUp = true; } // If the SOUTH point is unreplaceable or we have // previously moved up set the boolean to false. else if (spanUp && y > 0 && picture.getPixel(x, y - 1) != target) { spanUp = false; } // Add one NORTH point to the queue if it is replaceable // (this will be the next relative point to check from) // and we have not previously moved up. if (!spanDown && y < height - 1 && picture.getPixel(x, y + 1) == target) { queue.add(new Point(x, y + 1)); spanDown = true; } // If the NORTH point is unreplaceable or we have // previously moved up set the boolean to false. else if (spanDown && y < height - 1 && picture.getPixel(x, y + 1) != target) { spanDown = false; } // Increment the x-position, 1 to the east. x++; } } // Remove the head of this queue. Keep looping until no pixels // remain. while ((node = queue.poll()) != null); } // Once the Flood Fill Algorithm has completed, turn the action flag // off. We're done. isFillEnabled = false; } } @Override public boolean onTouchEvent(MotionEvent ev) { // Let the ScaleGestureDetector inspect all events. mScaleDetector.onTouchEvent(ev); final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { Log.e("MotionEvent", "==========ACTION_DOWN==========="); final float x = ev.getX(); final float y = ev.getY(); mLastTouchX = x; mLastTouchY = y; mActivePointerId = ev.getPointerId(0); break; } case MotionEvent.ACTION_POINTER_DOWN: isFillModeEnabled = false; break; case MotionEvent.ACTION_MOVE: { Log.e("MotionEvent", "==========ACTION_MOVE==========="); final int pointerIndex = ev.findPointerIndex(mActivePointerId); final float x = ev.getX(pointerIndex); final float y = ev.getY(pointerIndex); // Only move if the ScaleGestureDetector isn't processing a gesture. if (!mScaleDetector.isInProgress()) { isFillModeEnabled = true; final float dx = x - mLastTouchX; final float dy = y - mLastTouchY; mPosX += dx; mPosY += dy; invalidate(); } else { isFillModeEnabled = false; } mLastTouchX = x; mLastTouchY = y; break; } case MotionEvent.ACTION_UP: { Log.e("MotionEvent", "==========ACTION_UP==========="); setFocusable(false); Log.e("ACTION_UP", "===============imageWidth==========" + imageWidth); Log.e("ACTION_UP", "============imageHeight===========" + imageHeight); mImageWidthScaled = (int) (imageWidth * mScaleFactor); mImageHeightScaled = (int) (imageHeight * mScaleFactor); mActivePointerId = INVALID_POINTER_ID; mActivePointerId = INVALID_POINTER_ID; float mx = ev.getX(); float my = ev.getY(); this.isFillEnabled = true; fillHandler(mx, my, mImageWidthScaled, mImageHeightScaled); invalidate(); break; } case MotionEvent.ACTION_CANCEL: { Log.e("MotionEvent", "==========ACTION_CANCEL==========="); mActivePointerId = INVALID_POINTER_ID; break; } case MotionEvent.ACTION_POINTER_UP: { isFillModeEnabled = false; Log.e("MotionEvent", "==========ACTION_POINTER_UP==========="); final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastTouchX = ev.getX(newPointerIndex); mLastTouchY = ev.getY(newPointerIndex); mActivePointerId = ev.getPointerId(newPointerIndex); } break; } } return true; } private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector) { mScaleFactor *= detector.getScaleFactor(); mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f)); invalidate(); return true; } }