Custom Drawing on Mapbox Map Canvas

I would like to be able to manually draw complex shapes on a mapbox map using the android sdk. I have inherited the map view class and overridden the ondraw event but unfortunately whatever I draw gets over drawn by the map itself.

As an example I need to be able to draw polygons with diamond shaped borders among other complex shapes. This i can do no problem in GoogleMaps using a custom tile provider and overriding ondraw.

  • Resizing ImageView to fit to aspect ratio
  • Add animation to an ExpandableListView
  • Documentation for styles in android resources
  • Convert dip to px in Android
  • getting error msg install failed missing shared library
  • Creating a custom editText with tag-like feature
  • Here is the only code I have so far for mapbox:

        @Override
        public void onDraw(Canvas canvas) {        
            super.onDraw(canvas);
    
            Paint stroke = new Paint();
            stroke.setColor(Color.BLACK);
            stroke.setStyle(Paint.Style.STROKE);
            stroke.setStrokeWidth(5);
            stroke.setAntiAlias(true); 
    
            canvas.drawLine(0f,0f,1440f,2464f,stroke);
        }
    

    enter image description here

    Related posts:

    How to get screen resolution of an android device using LIBGDX?
    Android Custom URL Scheme Refuses To Work / How to Navigate Back to Android App After OAuth
    Accidentally deleted Android layout file
    About the Full Screen And No Titlebar from manifest
    Rendering in cocos2d-x using values from a math function
    Android: How to get return result from activity when calling from Fragment?
  • How to enable both hardware and virtual keyboards on Android ice cream sandwich
  • How to draw Bitmap fast in onDraw() method in canvas android
  • UDP or RTP streaming solution for android
  • Is there a size limit for Android Market downloads?
  • Disable one key on custom keyboard in Android
  • android services - error: service not registered
  • 2 Solutions collect form web for “Custom Drawing on Mapbox Map Canvas”

    You can do what You want by 2 ways:

    1) as You propose: “inherit the MapView class and overridden the onDraw() event”. But MapView extends FrameLayout which is ViewGroup, so You should override dispatchDraw() instead of onDraw().

    This approach requires custom view, which extends MapViewand implements:

    • drawing over the MapView;

    • customizing line styles (“diamonds instead of a simple line”);

    • binding path to Lat/Lon coordinates of MapView.

    For drawing over the MapView You should override dispatchDraw(), for example like this:

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

    For customizing line styles You can use setPathEffect() method of Paint class. For this You should create path for “diamond stamp” (in pixels), which will repeated every “advance” (in pixels too):

            mPathDiamondStamp = new Path();
            mPathDiamondStamp.moveTo(-DIAMOND_WIDTH / 2, 0);
            mPathDiamondStamp.lineTo(0, DIAMOND_HEIGHT / 2);
            mPathDiamondStamp.lineTo(DIAMOND_WIDTH / 2, 0);
            mPathDiamondStamp.lineTo(0, -DIAMOND_HEIGHT / 2);
            mPathDiamondStamp.close();
    
            mPathDiamondStamp.moveTo(-DIAMOND_WIDTH / 2 + DIAMOND_BORDER_WIDTH, 0);
            mPathDiamondStamp.lineTo(0, -DIAMOND_HEIGHT / 2 + DIAMOND_BORDER_WIDTH / 2);
            mPathDiamondStamp.lineTo(DIAMOND_WIDTH / 2 - DIAMOND_BORDER_WIDTH, 0);
            mPathDiamondStamp.lineTo(0, DIAMOND_HEIGHT / 2 - DIAMOND_BORDER_WIDTH / 2);
            mPathDiamondStamp.close();
    
            mPathDiamondStamp.setFillType(Path.FillType.EVEN_ODD);
    
            mDiamondPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mDiamondPaint.setColor(Color.BLUE);
            mDiamondPaint.setStrokeWidth(2);
            mDiamondPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            mDiamondPaint.setStyle(Paint.Style.STROKE);
            mDiamondPaint.setPathEffect(new PathDashPathEffect(mPathDiamondStamp, DIAMOND_ADVANCE, DIAMOND_PHASE, PathDashPathEffect.Style.ROTATE));
    

    (in this case there are 2 Path – first one (clockwise) for outer border and second (counter-clockwise) for inner border for “diamond” transparent “hole”).

    For binding path on screen to Lat/Lon coordinates of MapView You should have MapboxMap object of MapView – for that getMapAsync() and onMapReady() should be overridden:

    @Override
    public void getMapAsync(OnMapReadyCallback callback) {
        mMapReadyCallback = callback;
        super.getMapAsync(this);
    }
    
    @Override
    public void onMapReady(MapboxMap mapboxMap) {
        mMapboxMap = mapboxMap;
        if (mMapReadyCallback != null) {
            mMapReadyCallback.onMapReady(mapboxMap);
        }
    }
    

    Than You can use it in “lat/lon-to-screen” convertating:

            mBorderPath = new Path();
            LatLng firstBorderPoint = mBorderPoints.get(0);
            PointF firstScreenPoint = mMapboxMap.getProjection().toScreenLocation(firstBorderPoint);
            mBorderPath.moveTo(firstScreenPoint.x, firstScreenPoint.y);
    
            for (int ixPoint = 1; ixPoint < mBorderPoints.size(); ixPoint++) {
                PointF currentScreenPoint = mMapboxMap.getProjection().toScreenLocation(mBorderPoints.get(ixPoint));
                mBorderPath.lineTo(currentScreenPoint.x, currentScreenPoint.y);
            }
    

    Full source code:

    Custom DrawMapView.java

    public class DrawMapView extends MapView implements OnMapReadyCallback{
    
        private float DIAMOND_WIDTH = 42;
        private float DIAMOND_HEIGHT = 18;
        private float DIAMOND_ADVANCE = 1.5f * DIAMOND_WIDTH;       // spacing between each stamp of shape
        private float DIAMOND_PHASE = DIAMOND_WIDTH / 2;            // amount to offset before the first shape is stamped
        private float DIAMOND_BORDER_WIDTH = 6;                     // width of diamond border
    
        private Path mBorderPath;
        private Path mPathDiamondStamp;
        private Paint mDiamondPaint;
        private OnMapReadyCallback mMapReadyCallback;
        private MapboxMap mMapboxMap = null;
        private List<LatLng> mBorderPoints;
    
        public DrawMapView(@NonNull Context context) {
            super(context);
            init();
        }
    
        public DrawMapView(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public DrawMapView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        public DrawMapView(@NonNull Context context, @Nullable MapboxMapOptions options) {
            super(context, options);
            init();
        }
    
        public void setBorderPoints(List<LatLng> borderPoints) {
            mBorderPoints = borderPoints;
        }
    
        @Override
        public void getMapAsync(OnMapReadyCallback callback) {
            mMapReadyCallback = callback;
            super.getMapAsync(this);
        }
    
        @Override
        public void onMapReady(MapboxMap mapboxMap) {
            mMapboxMap = mapboxMap;
            if (mMapReadyCallback != null) {
                mMapReadyCallback.onMapReady(mapboxMap);
            }
        }
    
        @Override
        public void dispatchDraw(Canvas canvas) {
            super.dispatchDraw(canvas);
            canvas.save();
            drawDiamondsPath(canvas);
            canvas.restore();
        }
    
        private void drawDiamondsPath(Canvas canvas) {
            if (mBorderPoints == null || mBorderPoints.size() == 0) {
                return;
            }
    
            mBorderPath = new Path();
    
            LatLng firstBorderPoint = mBorderPoints.get(0);
            PointF firstScreenPoint = mMapboxMap.getProjection().toScreenLocation(firstBorderPoint);
            mBorderPath.moveTo(firstScreenPoint.x, firstScreenPoint.y);
    
            for (int ixPoint = 1; ixPoint < mBorderPoints.size(); ixPoint++) {
                PointF currentScreenPoint = mMapboxMap.getProjection().toScreenLocation(mBorderPoints.get(ixPoint));
                mBorderPath.lineTo(currentScreenPoint.x, currentScreenPoint.y);
            }
    
            mPathDiamondStamp = new Path();
            mPathDiamondStamp.moveTo(-DIAMOND_WIDTH / 2, 0);
            mPathDiamondStamp.lineTo(0, DIAMOND_HEIGHT / 2);
            mPathDiamondStamp.lineTo(DIAMOND_WIDTH / 2, 0);
            mPathDiamondStamp.lineTo(0, -DIAMOND_HEIGHT / 2);
            mPathDiamondStamp.close();
    
            mPathDiamondStamp.moveTo(-DIAMOND_WIDTH / 2 + DIAMOND_BORDER_WIDTH, 0);
            mPathDiamondStamp.lineTo(0, -DIAMOND_HEIGHT / 2 + DIAMOND_BORDER_WIDTH / 2);
            mPathDiamondStamp.lineTo(DIAMOND_WIDTH / 2 - DIAMOND_BORDER_WIDTH, 0);
            mPathDiamondStamp.lineTo(0, DIAMOND_HEIGHT / 2 - DIAMOND_BORDER_WIDTH / 2);
            mPathDiamondStamp.close();
    
            mPathDiamondStamp.setFillType(Path.FillType.EVEN_ODD);
    
            mDiamondPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mDiamondPaint.setColor(Color.BLUE);
            mDiamondPaint.setStrokeWidth(2);
            mDiamondPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            mDiamondPaint.setStyle(Paint.Style.STROKE);
            mDiamondPaint.setPathEffect(new PathDashPathEffect(mPathDiamondStamp, DIAMOND_ADVANCE, DIAMOND_PHASE, PathDashPathEffect.Style.ROTATE));
    
            canvas.drawPath(mBorderPath, mDiamondPaint);
        }
    
        private void init() {
            mBorderPath = new Path();
            mPathDiamondStamp = new Path();
        }
    }
    

    ActivityMain.java

    public class MainActivity extends AppCompatActivity {
    
        private DrawMapView mapView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            MapboxAccountManager.start(this, getString(R.string.access_token));
            setContentView(R.layout.activity_main);
    
            mapView = (DrawMapView) findViewById(R.id.mapView);
            mapView.onCreate(savedInstanceState);
            mapView.getMapAsync(new OnMapReadyCallback() {
                @Override
                public void onMapReady(MapboxMap mapboxMap) {
    
                    mapView.setBorderPoints(Arrays.asList(new LatLng(-36.930129, 174.958843),
                            new LatLng(-36.877860, 174.978108),
                            new LatLng(-36.846373, 174.901841),
                            new LatLng(-36.829215, 174.814659),
                            new LatLng(-36.791326, 174.779337),
                            new LatLng(-36.767680, 174.823242)));
                }
            });
        }
    
        @Override
        public void onResume() {
            super.onResume();
            mapView.onResume();
        }
    
        @Override
        public void onPause() {
            super.onPause();
            mapView.onPause();
        }
    
        @Override
        public void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            mapView.onSaveInstanceState(outState);
        }
    
        @Override
        public void onLowMemory() {
            super.onLowMemory();
            mapView.onLowMemory();
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mapView.onDestroy();
        }
    
    }
    

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:mapbox="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="ua.com.omelchenko.mapboxlines.MainActivity">
    
        <ua.com.omelchenko.mapboxlines.DrawMapView
            android:id="@+id/mapView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            mapbox:center_latitude="-36.841362"
            mapbox:center_longitude="174.851110"
            mapbox:style_url="@string/style_mapbox_streets"
            mapbox:zoom="10"/>
    </RelativeLayout>
    

    Finally, as a result, you should get something like this:

    enter image description here

    And You should take into account some “special cases”, for example if all points of path is outside current view of map, there is no lines on it, even line should cross view of map and should be visible.

    2) (better way) create and publish map with your additional lines and custom style for them (especially take a look at “Line patterns with Images” sections). You can use Mapbox Studio for this. And in this approach all “special cases” and performance issues is solved on Mabpox side.

    If I understand correctly, you are trying to add a diamond shape to the map (user isn’t drawing the shape)? If this is the case, you have a few options:

    1. Use Polygon, simply add the list of points and it will draw the shape (in this case, a diamond). This would be the easiest, but I assume you already tried and it doesn’t work for you.

      List<LatLng> polygon = new ArrayList<>();
      polygon.add(<LatLng Point 1>);
      polygon.add(<LatLng Point 2>);
      
      ...
      
      mapboxMap.addPolygon(new PolygonOptions()
        .addAll(polygon)
        .fillColor(Color.parseColor("#3bb2d0")));
      
    2. Add a Fill layer using the new Style API introduced in 4.2.0 (still in beta). Doing this will first require you to create a GeoJSON object with points and then to add it to the map. The closest example I have to doing this would be this example, found in the demo app.

    3. Use onDraw which would essential just translate the canvas to a GeoJSON object and add as a layer like explained in step 2. I’d only recommend this if you are having the user draw shapes during runtime, in this case the coordinates would be uncertain.

    I’ll edit this answer if you are looking for something different.

    Android Babe is a Google Android Fan, All about Android Phones, Android Wear, Android Dev and Android Games Apps and so on.