Ravers
Ravers

Reputation: 1039

Rotate a two layer marker icon in GoogleMap

In my application I add a set number of markers to my map like this:

private fun addMarker(googleMap: GoogleMap, location: Location) {
    val options = MarkerOptions()
    options.position(LatLng(location.latitude, location.longitude))
    options.rotation(location.bearing)
    options.anchor(0.5f, 0.5f)
    options.flat(true)

    val drawable = ContextCompat.getDrawable(context, R.drawable.background_vehicle) as LayerDrawable
    val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    drawable.setBounds(0, 0, canvas.width, canvas.height)
    drawable.draw(canvas)

    options.icon(BitmapDescriptorFactory.fromBitmap(bitmap))
    googleMap.addMarker(options)
}

And this is my drawable:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@drawable/icon_vehicle_marker" />
    <item android:id="@+id/vehicle_image" android:bottom="5dp" 
          android:drawable="@drawable/icon_car" android:left="5dp" 
          android:right="5dp" android:top="10dp" />

</layer-list>

Which makes something like this:

example

My problem is that making the icon flat and setting a rotation, makes the car icon inside the drawable rotate to. I just want for the first layer to rotate. Ideally, I just want the first layer (the blue arrow) to be flat and rotate and the second layer (the car icon) to not be flat and not rotate.

Is there any way to make a two-layer marker icon with different options or something like that?

Upvotes: 2

Views: 665

Answers (1)

Andrii Omelchenko
Andrii Omelchenko

Reputation: 13343

To do that you need two points-centers of rotation (see Fig. 1):

Rotation center points

Fig.1 - Rotation center points

P1 - rotation center for "flat" part of marker;

P2 - rotation center for "non-flat" part of marker.

So, rotate inner "non flat" part is impossible via default markers - they have only one rotation center point - P1. Also it's hard to determine P2 coordinates for composite drawable: that needs exactly inner part of drawable pathData coordinates reading, bounding box and center point calculation etc.

But if you have separately placeholder and car drawables then no need to make a two-layer marker icon: you can determine rotation center of inner icon (P2 on fig 1) as offset between placeholder (outer "flat") and inner icons and rotation can be implemented in MapView-based custom view via custom drawing each drawable over map canvas (placeholder need to be drawn with rotation).

TLDR;

For example, with placeholder drawable (icon_vehicle_marker.xml) like:

<vector android:height="24dp" android:viewportHeight="511.999"
    android:viewportWidth="511.999" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="#006DF0" android:pathData="M405.961,62.116C365.906,22.06 312.649,0 256,0c-56.648,0 -109.905,22.06 -149.962,62.116C64.694,103.46 44.023,157.77 44.023,212.077s20.672,108.617 62.016,149.961L256,511.999L405.96,362.037c41.345,-41.345 62.016,-95.653 62.016,-149.961C467.976,157.77 447.306,103.461 405.961,62.116zM384.751,340.828L256,469.579L127.249,340.828c-35.497,-35.497 -53.244,-82.124 -53.244,-128.751s17.748,-93.255 53.244,-128.751C161.64,48.936 207.365,29.996 256,29.996c48.636,0 94.36,18.94 128.751,53.33c35.497,35.497 53.245,82.124 53.245,128.751S420.247,305.331 384.751,340.828z"/>
</vector>

Placeholder icon

and inner car drawable (icon_car.xml) like:

<vector android:height="24dp" android:viewportHeight="459"
    android:viewportWidth="459" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="#006DF0" android:pathData="M405.45,51c-5.101,-15.3 -20.4,-25.5 -35.7,-25.5H89.25c-17.85,0 -30.6,10.2 -35.7,25.5L0,204v204c0,15.3 10.2,25.5 25.5,25.5H51c15.3,0 25.5,-10.2 25.5,-25.5v-25.5h306V408c0,15.3 10.2,25.5 25.5,25.5h25.5c15.3,0 25.5,-10.2 25.5,-25.5V204L405.45,51zM89.25,306C68.85,306 51,288.15 51,267.75s17.85,-38.25 38.25,-38.25s38.25,17.85 38.25,38.25S109.65,306 89.25,306zM369.75,306c-20.4,0 -38.25,-17.85 -38.25,-38.25s17.85,-38.25 38.25,-38.25S408,247.35 408,267.75S390.15,306 369.75,306zM51,178.5L89.25,63.75h280.5L408,178.5H51z"/>
</vector>

inner icon

With MarkersMapView custom view like:

public class MarkersMapView extends MapView implements OnMapReadyCallback {

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

    private int mPlaceholderWidth = 150;
    private int mPlaceholderHeight = 150;

    private int mCarWidth = 75;
    private int mCarHeight = 75;
    private int mCarOffset = 90;

    private Drawable mPlaceholderDrawable;
    private Drawable mCarDrawable;

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

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

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

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


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


    private void init(Context context) {
        setWillNotDraw(false);

        mPlaceholderDrawable = ContextCompat.getDrawable(context, R.drawable.icon_vehicle_marker);
        mPlaceholderDrawable.setBounds(0, 0 , mPlaceholderWidth, mPlaceholderHeight);

        mCarDrawable = ContextCompat.getDrawable(context, R.drawable.icon_car);
        mCarDrawable.setBounds(0, 0 , mCarWidth, mCarHeight);

        postInvalidate();
    }


    @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);
        }
    }


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


        Projection mapProjection = mGoogleMap.getProjection();

        // get screen coordinates of marker
        final Point pointMarker = mapProjection.toScreenLocation(mMarker.getPosition());

        canvas.save();
        // move origin to screen coordinates of marker shifted by placeholder icon sizes
        canvas.translate(pointMarker.x - mPlaceholderWidth / 2, pointMarker.y - mPlaceholderHeight);
        // rotate canvas according bearing of GoogleMap camera view
        canvas.rotate(-mGoogleMap.getCameraPosition().bearing, mPlaceholderWidth / 2, mPlaceholderHeight);
        mPlaceholderDrawable.draw(canvas);
        // revert origin back
        canvas.restore();

        // calculate position of inner icon center point
        float dx = (float) (mCarOffset * Math.sin(Math.toRadians(-mGoogleMap.getCameraPosition().bearing))) - mCarWidth / 2;
        float dy = (float) (-mCarOffset * Math.cos(Math.toRadians(-mGoogleMap.getCameraPosition().bearing))) - mCarHeight / 2;
        // move origin to screen coordinates of inner icon center point shifted by placeholder icon size
        canvas.translate(pointMarker.x + dx, pointMarker.y + dy);

        mCarDrawable.draw(canvas);
    }

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

    public void removeMarker() {
        mGoogleMap.clear();
        mMarker = null;
    }

}

MainActivity.java like:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();
    private static final String MAP_VIEW_BUNDLE_KEY = "MapViewBundleKey";
    private static final LatLng CAR = new LatLng(50.450311, 30.523730);

    private GoogleMap mGoogleMap;
    private MarkersMapView 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 = (MarkersMapView) findViewById(R.id.mapview);
        mMapView.onCreate(mapViewBundle);
        mMapView.getMapAsync(new OnMapReadyCallback() {
            @Override
            public void onMapReady(GoogleMap googleMap) {
                mGoogleMap = googleMap;

                mMapView.addMarker(new MarkerOptions()
                        .position(CAR)
                        .flat(true)
                        .draggable(false));
            }
        });
    }

    @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();
    }
}

and activity_main.xml 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="activities.MainActivity">

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

</RelativeLayout>

you'll got something like that:

Marker with rotated inner part

mCarOffset - P1-P2 distance measured on "placeholder" and "inner" icons of marker and hardcoded.

NB! This is only demo for one marker. If you have, for example, many (hundreds of) markers you should determine which markers exactly should be drawn etc. for increasing performance.

Upvotes: 2

Related Questions