javaandroidcollision-detectionpixel-perfect

Pixel Perfect Collision detection between a custom view and an ImageView


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!:)


Solution

  • 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.