20

I would try to develop an application in which I can draw a planimetry. So, each room has got its own ID or name and, if I touch a room, I want to show a Toast Message with that ID or name. The problem is how check if and which path is touched!!

I saw a lot of topic discussions that talked about this problem. Someone says to use the getBounds method and, after, contains method for checking if touched point is in Rect. But, I guess getBounds returns the smallest Rect that contains path, right?

So, rooms have different custom geometric forms and, for this reason, if I get bounds about 2 close rooms, method could return a shared set of points. Bad! Each room has got only their area points. How can I solve this problem?

In iOS i could use PathContainsPoint method, but, unfortunaly, Android Path doesn't have something similar.

ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
kinghomer
  • 3,021
  • 2
  • 33
  • 56

5 Answers5

50

Ok i solved my problem. I post the example code:

Path p;
Region r;

@Override
public void onDraw(Canvas canvas) {

    p = new Path();

    p.moveTo(50, 50);
    p.lineTo(100, 50);
    p.lineTo(100, 100);
    p.lineTo(80, 100);
    p.close();      

    canvas.drawPath(p, paint);

    RectF rectF = new RectF();
    p.computeBounds(rectF, true);
    r = new Region();
    r.setPath(p, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));

}   

public boolean onTouch(View view, MotionEvent event) {

    Point point = new Point();
    point.x = event.getX();
    point.y = event.getY();
    points.add(point);
    invalidate();
    Log.d(TAG, "point: " + point);

    if(r.contains((int)point.x,(int) point.y))
        Log.d(TAG, "Touch IN");
    else
        Log.d(TAG, "Touch OUT");

    return true;
}
kinghomer
  • 3,021
  • 2
  • 33
  • 56
12

As Edward Falk say, best way is to use path.op() becouse Region is square. And one point can be in 2 or 3 Regions.

For example:

enter image description here

All regions will contain blue point, but on fact only path4 contains this point.

int x, y;
Path tempPath = new Path(); // Create temp Path
tempPath.moveTo(x,y); // Move cursor to point
RectF rectangle = new RectF(x-1, y-1, x+1, y+1); // create rectangle with size 2xp
tempPath.addRect(rectangle, Path.Direction.CW); // add rect to temp path
tempPath.op(pathToDetect, Path.Op.DIFFERENCE); // get difference with our PathToCheck
if (tempPath.isEmpty()) // if out path cover temp path we get empty path in result
{ 
    Log.d(TAG, "Path contains this point");
    return true;
}
else
{
    Log.d(TAG, "Path don't contains this point");
    return false;
}
Nikita
  • 333
  • 3
  • 7
2

Here's a thought: create a new path which is a tiny square around the point that was touched, and then intersect that path with your path to be tested using path.op() and see if the result is empty.

Edward Falk
  • 9,991
  • 11
  • 77
  • 112
  • Yes, but you are limited to API 19 and above. Is there any other solution ? – Bartando Aug 08 '16 at 06:13
  • If your path is a rectangle, use kinghomer's answer above. If your path is a polygon, see sromku's answer here: http://stackoverflow.com/a/15817043/338479. I have a more general solution that handles a path comprised of arcs, circles, and line segments, but it's a little hairy. I can post it next week if there's enough interest. – Edward Falk Aug 09 '16 at 19:03
  • 2
    Thanks, I have solved it by subclassing Path and keeping track of all points, then I've used this http://stackoverflow.com/a/16391873/3284364 formula and rewritten it into Java. – Bartando Aug 10 '16 at 07:26
  • That's pretty clever actually. And thanks for the link; that's definitely going into my toolbox. (Fun fact: I had a class from Prof. Franklin way back when) – Edward Falk Aug 10 '16 at 16:35
  • 1
    I guess if you want I could post an answer with whole solution. Not sure if thats gonna help anyone – Bartando Aug 10 '16 at 17:17
  • 1
    This answer doesn't work. Perhaps this is intended behaviour, perhaps a bug. I used `Path.op(__, Path.Op.INTERSECT) ` to see whether a path intersects a tiny rectangle centered on (x, y) and it would sometimes return a non-empty intersection when (x,y) was relatively close to the path, but not inside it. NB my paths are made up of straight lines which sometimes go in on themselves. Also note this wasn't simply because my "tiny rectangle" created for `op()` wasn't tiny enough! – HughHughTeotl Jul 01 '17 at 21:15
  • 1
    @Bartando I'm interested in your solution can you post the answer for it? – Robert Jun 22 '18 at 19:58
1

Go through the path and check if the position on the path is near to the touched point. Following method worked for both linear paths and the complex paths.

fun Path.doIntersect(x: Float, y: Float, width: Float): Boolean {
    val measure = PathMeasure(this, false)
    val length = measure.length
    val delta = width / 2f
    val position = floatArrayOf(0f, 0f)
    val bounds = RectF()
    var distance = 0f
    var intersects = false
    while (distance <= length) {
        measure.getPosTan(distance, position, null)
        bounds.set(
            position[0] - delta,
            position[1] - delta,
            position[0] + delta,
            position[1] + delta
        )
        if (bounds.contains(x, y)) {
            intersects = true
            break
        }
        distance += delta / 2f
    }
    return intersects
}
UdaraWanasinghe
  • 2,622
  • 2
  • 21
  • 27
0

Region and path.op() approaches work well, but suffer from a lack of precision. After some tests, it seems like they define a zone at the segment level of the path, like so: image segment zone. Another approach is to work with polygons. While giving better results, it still registers some unexpected touches, and misses obvious ones. But there is another solution, which consists in retrieving a set of points along the path, and registering touches inside an area surrounding them. For that, we can use PathMeasure class to get the desired number of points, and store them in a List<Float>:

PathMeasure pathMeasure = new PathMeasure(path, false);
List<Float> listFloat = new ArrayList<>();
for (float i = 0; i < 1.1; i += 0.1) {
    float[] pointCoordinates = new float[2];
    pathMeasure.getPosTan(pathMeasure.getLength() * i, pointCoordinates, null);
    listFloat.add(pointCoordinates[0]);
    listFloat.add(pointCoordinates[1]);
}

Then, we need to loop over the List<Float> and check if a touch event is inside a defined zone surrounding these points. Here, a zone covers a tenth of the path length, since we collected ten points:

float areaLength = pathMeasure.getLength() / 20; // since we collect ten points, we define a zone covering a tenth of path length    
for (int i = 0; i < listFloat.size() - 1; i += 2) {
    if (touchX > listFloat.get(i) - areaLength
     && touchX < listFloat.get(i) + areaLength
     && touchY > listFloat.get(i+1) - areaLength
     && touchY < listFloat.get(i+1) + areaLength) {
        Log.d(TAG, "path touched");
    }

}

These successive zones capture touch events with a better precision, following more closely the path: image points zone. This method can be improved, by working with Rect class to ensure that these zones are not overlapping each other, and also to bound the start and end point with a better precision.

moun
  • 19
  • 1
  • 5