Search code examples
androidandroid-layoutandroid-drawableandroid-vectordrawableandroid-graphics

Is there a straightforward way to measure the distance between a point and a VectorDrawable group?


In my Android App I want to know the distance between the point where the user clicks and a specific VectorDrawable group.

I want the distance to a group like blue in the VectorDrawable:

<vector android:height="24dp" android:viewportHeight="1052.3622"
    android:viewportWidth="744.0945" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="#ff0000"
        android:name="blue"
        android:pathData="M182.9,349.5m-74.7,0a74.7,74.7 0,1 1,149.3 0a74.7,74.7 0,1 1,-149.3 0"
        android:strokeAlpha="1" android:strokeColor="#000000" android:strokeWidth="4.23501825"/>
    <path android:fillColor="#00ff00"
        android:name="red"
        android:pathData="M474.3,392.4a84.3,102.9 0,1 0,168.6 0a84.3,102.9 0,1 0,-168.6 0z"
        android:strokeAlpha="1" android:strokeColor="#000000" android:strokeWidth="5"/>>
</vector>

Is there a straightforward way to calculate this distance in Android?


Solution

  • I am not sure easy method to solve the issue exists but this can be done like this:

    Parse vector XML so you have all those variables in runtime. Parsing is not covered here, let's assume you have following data structure that we will work with later:

    private static class VectorData {
        private int width = 24;
        private int height = 24;
        private double viewportHeight = 1052.3622;
        private double viewportWidth = 744.0945;
        private String path = "M182.9,349.5m-74.7,0a74.7,74.7 0,1 1,149.3 0a74.7,74.7 0,1 1,-149.3 0";
    
        private double scaleVectorX(Context context) {
            return dpToPx(context, width) / viewportWidth;
        }
    
        private double scaleVectorY(Context context) {
            return dpToPx(context, height) / viewportHeight;
        }
    
        private static float dpToPx(Context context, float dp) {
            return dp * context.getResources().getDisplayMetrics().density;
        }
    }
    

    as you see all fields are hardcoded for simplicity.

    Next step is to parse vector path data converting it to android.graphics.Path:

    android.graphics.Path path = android.util.PathParser.createPathFromPathData(vectorData.path);
    

    android.util.PathParser is not included, but you can find source here: https://android.googlesource.com/platform/frameworks/base/+/17e64ffd852f8fe23b8e2e2ff1b62ee742af17a6/core/java/android/util/PathParser.java. Not sure how legal it is to copy and use it though.

    Having path we will need to find N its points (coordinates). More points - more precise result will be and slower processing:

    final Collection<Point> points = getPoints(path, iv.getX(), iv.getY(), vectorData);
    
    private static class Point {
        private float x;
        private float y;
    
        Point(float x, float y) {
            this.x = x;
            this.y = y;
        }
    
        @Override
        public String toString() {
            return "Point{" +
                    "x=" + x +
                    ", y=" + y +
                    '}';
        }
    }
    
    private Collection<Point> getPoints(Path path, float viewX, float viewY, VectorData vectorData) {
        Collection<Point> points = new ArrayList<>();
        PathMeasure pm = new PathMeasure(path, false);
        float length = pm.getLength();
        float distance = 0f;
        int size = N;
        float speed = length / size;
        int counter = 0;
        float[] aCoordinates = new float[2];
    
        while ((distance < length) && (counter < size)) {
            // get point from the path
            pm.getPosTan(distance, aCoordinates, null);
            float pathX = aCoordinates[0];
            float pathY = aCoordinates[1];
    
            float x = (float) (vectorData.scaleVectorX(this) * pathX) + viewX;
            float y = (float) (vectorData.scaleVectorY(this) * pathY) + viewY;
    
            points.add(new Point(x, y));
            counter++;
            distance = distance + speed;
        }
    
        return points;
    } 
    

    path - is our path that we get before, iv - is vector container (ImageView, for example), we need it in order to adjust points coordinates. vectorData - is structure that we got before parsing our vector.

    Now we need to define region to handle case where path is closed and we want to treat click inside path as 0 distance:

    final Region region = new Region();
    RectF rectF = new RectF();
    path.computeBounds(rectF, true);
    region.setPath(path, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
    

    In order to calculate min distance following method should be used:

    private int getMinDistance(float eventX, float eventY, Collection<Point> pathPoints, Region pathRegion, VectorData vectorData) {
        int minDistance = Integer.MAX_VALUE;
    
        boolean contains = pathRegion.contains((int) (eventX / vectorData.scaleVectorX(this)), (int) (eventY / vectorData.scaleVectorY(this)));
    
        if (contains) {
            minDistance = 0;
        } else {
            for (Point point : pathPoints) {
                int distance = getDistanceBetweenPoints((int) eventX, (int) eventY, (int) point.x, (int) point.y);
                if (distance < minDistance) {
                    minDistance = distance;
                }
            }
        }
        return minDistance;
    }
    
    private int getDistanceBetweenPoints(int x, int y, int x1, int y1) {
        return (int) Math.sqrt((x1 - x) * (x1 - x) + (y1 - y) * (y1 - y));
    }