1

I am using google maps on my project. I want to put arrow to screen if marker not showing on the screen. How can I control screen?

Eli gu
  • 27
  • 2
  • 8

1 Answers1

1

Actually, the answer for your question is in lakshman.pasala comment, but its implementation is a little bit complex (TLDR).

So, best way to get Google Maps Screen Control - is implement custom view which extends MapView class. In that case you can get full control of drawing on view canvas. To do that you should override dispatchDraw() method (because MapView extends FrameLayout which is ViewGroup) and implement within it arrow drawing. Something like that:

@Override
public void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    canvas.save();
    drawArrowToMarker(canvas);
    canvas.restore();
}

and you need to call it via invalidate() on every map move/zoom/rotate. For detect map move/zoom/rotate you need GoogleMap (exactly GoogleMap.setOnCameraMoveListener() method). You can declare GoogleMap object inside your custom MapView class and set it via setter, but better if custom MapView class will implement OnMapReadyCallback interface and get it in onMapReady() callback. Also your need several utility methods for adding/removing marker, determine segments intersections, directions etc. Full source code of custom view (e.g. EnhanchedMapView) can be like:

public class EnhanchedMapView extends MapView implements OnMapReadyCallback {

    private final static int ARROW_PADDING = 50;
    private final static double ARROW_ANGLE = Math.PI / 6;
    private final static double ARROW_LENGTH = 100;
    private final static double ARROW_SIZE = 50;

    private OnMapReadyCallback mMapReadyCallback;
    private GoogleMap mGoogleMap;
    private Marker mMarker;
    private Paint mPaintArrow;

    public EnhanchedMapView(@NonNull Context context) {
        super(context);
        init();
    }

    public EnhanchedMapView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public EnhanchedMapView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public EnhanchedMapView(@NonNull Context context, @Nullable GoogleMapOptions options) {
        super(context, options);
        init();
    }

    @Override
    public void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.save();
        drawArrowToMarker(canvas);
        canvas.restore();
    }

    private void drawArrowToMarker(Canvas canvas) {
        if (mGoogleMap == null || mMarker == null) {
            return;
        }

        VisibleRegion visibleRegion = mGoogleMap.getProjection().getVisibleRegion();
        LatLngBounds screenBounds = visibleRegion.latLngBounds;

        LatLng mapCenter = screenBounds.getCenter();
        Projection mapProjection = mGoogleMap.getProjection();

        final Point pointMapCenter = mGoogleMap.getProjection().toScreenLocation(mapCenter);

        final Point pointTopLeft = mapProjection.toScreenLocation(visibleRegion.farLeft);
        final Point pointTopRight = mapProjection.toScreenLocation(visibleRegion.farRight);
        final Point pointBottomLeft = mapProjection.toScreenLocation(visibleRegion.nearLeft);
        final Point pointBottomRight = mapProjection.toScreenLocation(visibleRegion.nearRight);
        final Point pointMarker = mapProjection.toScreenLocation(mMarker.getPosition());

        final Point tl = new Point(pointTopLeft.x + ARROW_PADDING, pointTopLeft.y + ARROW_PADDING);
        final Point tr = new Point(pointTopRight.x - ARROW_PADDING, pointTopRight.y + ARROW_PADDING);
        final Point br = new Point(pointBottomRight.x - ARROW_PADDING, pointBottomRight.y - ARROW_PADDING);
        final Point bl = new Point(pointBottomLeft.x + ARROW_PADDING, pointBottomLeft.y - ARROW_PADDING);

        final Point pointIntersection = getBoundsIntersection(tl, tr, br, bl, pointMapCenter, pointMarker);
        if (pointIntersection != null) {
            double angle = Math.atan2(pointMarker.y - pointMapCenter.y, pointMarker.x - pointMapCenter.x);

            int arrowX, arrowY;
            arrowX = (int) (pointIntersection.x - ARROW_LENGTH * Math.cos(angle));
            arrowY = (int) (pointIntersection.y - ARROW_LENGTH * Math.sin(angle));
            canvas.drawLine(pointIntersection.x, pointIntersection.y, arrowX, arrowY, mPaintArrow);

            arrowX = (int) (pointIntersection.x - ARROW_SIZE * Math.cos(angle + ARROW_ANGLE));
            arrowY = (int) (pointIntersection.y - ARROW_SIZE * Math.sin(angle + ARROW_ANGLE));
            canvas.drawLine(pointIntersection.x, pointIntersection.y, arrowX, arrowY, mPaintArrow);

            arrowX = (int) (pointIntersection.x - ARROW_SIZE * Math.cos(angle - ARROW_ANGLE));
            arrowY = (int) (pointIntersection.y - ARROW_SIZE * Math.sin(angle - ARROW_ANGLE));
            canvas.drawLine(pointIntersection.x, pointIntersection.y, arrowX, arrowY, mPaintArrow);
        }
    }

    private void init() {
        setWillNotDraw(false);

        mPaintArrow = new Paint();
        mPaintArrow.setColor(Color.BLUE);
        mPaintArrow.setStrokeWidth(15);
    }

    @Override
    public void getMapAsync(OnMapReadyCallback callback) {
        mMapReadyCallback = callback;
        super.getMapAsync(this);
    }

    @Override
    public void onMapReady(GoogleMap googleMap) {
        mGoogleMap = googleMap;
        mGoogleMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
            @Override
            public void onCameraMove() {
                invalidate();
            }
        });
        if (mMapReadyCallback != null) {
            mMapReadyCallback.onMapReady(googleMap);
        }
    }

    public void addMarker(MarkerOptions markerOptions) {
        removeMarker();
        mMarker = mGoogleMap.addMarker(markerOptions);
    }

    public void removeMarker() {
        mGoogleMap.clear();
    }

    private static boolean floatEquals(float f1, float f2) {
        final double EPS = 1e-6;
        return (Math.abs(f1 - f2) < EPS);
    }

    private static Point getBoundIntersection(Point p11, Point p12, Point p21, Point p22) {
        double x, y;
        Point intersectionPoint = null;

        // test intersection with vertical bound
        if (floatEquals(p12.x, p11.x) || floatEquals(p22.x, p21.x)) {
            if (floatEquals(p12.x, p11.x) && floatEquals(p22.x, p21.x) && !floatEquals(p11.x, p21.x)) {
                return null;
            } else {
                if (floatEquals(p12.x, p11.x)) {
                    x = p11.x;
                    y = (x - p21.x) / (p22.x - p21.x) * (p22.y - p21.y) + p21.y;

                    if (x >= Math.min(p21.x, p22.x) && x <= Math.max(p21.x, p22.x)
                            && y >= Math.min(p11.y, p12.y) && y <= Math.max(p11.y, p12.y)) {
                        intersectionPoint = new Point((int) x, (int) y);
                    }
                } else {
                    x = p21.x;
                    y = (x - p11.x) / (p12.x - p11.x) * (p12.y - p11.y) + p11.y;

                    if (x >= Math.min(p11.x, p12.x) && x <= Math.max(p11.x, p12.x)
                            && y >= Math.min(p21.y, p22.y) && y <= Math.max(p21.y, p22.y)) {
                        intersectionPoint = new Point((int) x, (int) y);
                    }
                }
            }
        } else {
            // test intersection with horizontal bound
            if (floatEquals(p12.y, p11.y) || floatEquals(p22.y, p21.y)) {
                if (floatEquals(p12.y, p11.y) && floatEquals(p22.y, p21.y) && !floatEquals(p11.y, p21.y)) {
                    return null;
                } else {
                    if (floatEquals(p12.y, p11.y)) {
                        y = p12.y;
                        x = (y - p21.y) / (p22.y - p21.y) * (p22.x - p21.x) + p21.x;

                        if (x >= Math.min(p11.x, p12.x) && x <= Math.max(p11.x, p12.x)
                                && y >= Math.min(p21.y, p22.y) && y <= Math.max(p21.y, p22.y)) {
                            intersectionPoint = new Point((int) x, (int) y);
                        }
                    } else {
                        y = p21.y;
                        x = (y - p11.y) / (p12.y - p11.y) * (p12.x - p11.x) + p11.x;

                        if (x >= Math.min(p21.x, p22.x) && x <= Math.max(p21.x, p22.x)
                                && y >= Math.min(p11.y, p12.y) && y <= Math.max(p11.y, p12.y)) {
                            intersectionPoint = new Point((int) x, (int) y);
                        }
                    }

                }
            }
        }

        return intersectionPoint;
    }

    private static Point getBoundsIntersection(Point tl, Point tr, Point br, Point bl, Point p1, Point p2) {
        Point intersectionPoint = null;
        if ((intersectionPoint = getBoundIntersection(tl, tr, p1, p2)) != null) {
            return intersectionPoint;
        } else if ((intersectionPoint = getBoundIntersection(tr, br, p1, p2)) != null) {
            return intersectionPoint;
        } else if ((intersectionPoint = getBoundIntersection(br, bl, p1, p2)) != null) {
            return intersectionPoint;
        } else if ((intersectionPoint = getBoundIntersection(bl, tl, p1, p2)) != null) {
            return intersectionPoint;
        }
        return null;
    }

}

And you can use it from MainActivity this way:

public class MainActivity extends AppCompatActivity {

    private static final String MAP_VIEW_BUNDLE_KEY = "MapViewBundleKey";
    static final LatLng KYIV = new LatLng(50.450311, 30.523730);

    private EnhanchedMapView mMapView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Bundle mapViewBundle = null;
        if (savedInstanceState != null) {
            mapViewBundle = savedInstanceState.getBundle(MAP_VIEW_BUNDLE_KEY);
        }

        mMapView = (EnhanchedMapView) findViewById(R.id.mapview);
        mMapView.onCreate(mapViewBundle);
        mMapView.getMapAsync(new OnMapReadyCallback() {
            @Override
            public void onMapReady(GoogleMap googleMap) {
                mMapView.addMarker(new MarkerOptions().position(KYIV).title("Kyiv"));
            }
        });

    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        Bundle mapViewBundle = outState.getBundle(MAP_VIEW_BUNDLE_KEY);
        if (mapViewBundle == null) {
            mapViewBundle = new Bundle();
            outState.putBundle(MAP_VIEW_BUNDLE_KEY, mapViewBundle);
        }

        mMapView.onSaveInstanceState(mapViewBundle);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mMapView.onResume();
    }

    @Override
    protected void onStart() {
        super.onStart();
        mMapView.onStart();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mMapView.onStop();
    }
    @Override
    protected void onPause() {
        mMapView.onPause();
        super.onPause();
    }
    @Override
    protected void onDestroy() {
        mMapView.onDestroy();
        super.onDestroy();
    }
    @Override
    public void onLowMemory() {
        super.onLowMemory();
        mMapView.onLowMemory();
    }

}

Where activity_main.xml can be like:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="{YOUR_PACKAGE_NAME}.MainActivity">

    <{YOUR_PACKAGE_NAME}.EnhanchedMapView
        android:id="@+id/mapview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        />

</RelativeLayout>

As a result you should get something like that:

Arrow to marker

Andrii Omelchenko
  • 13,183
  • 12
  • 43
  • 79