5

How to add scale bar on the map when zoom-in and zoom out value should change (scale bar) and got displaying zoom controls at bottom right corner but i want to replace just above it.

How to make it possible?

Thank you

xarlymg89
  • 2,552
  • 2
  • 27
  • 41
pallavi
  • 161
  • 1
  • 3
  • 12

5 Answers5

10

In the API there is no method to set the scale bar. In this case you should implement it yourself. This is simple example of how you can do this. I have adapt the code from the answer How to add a map scale in MapView on Android? to work with the GoogleMap.

class ScaleBar extends ImageView {
    float mXOffset = 10;
    float mYOffset = 10;
    float mLineWidth = 3;
    int mTextSize = 25;

    boolean mIsImperial = false;
    boolean mIsNautical = false;

    boolean mIsLatitudeBar = true;
    boolean mIsLongitudeBar = true;

    private GoogleMap mMap;

    float mXdpi;
    float mYdpi;

    public ScaleBar(Context context, GoogleMap map) {
        super(context);

        mMap = map;

        mXdpi = context.getResources().getDisplayMetrics().xdpi;
        mYdpi = context.getResources().getDisplayMetrics().ydpi;
    }

    @Override
    public void onDraw(Canvas canvas) {
        canvas.save();

        drawScaleBarPicture(canvas);

        canvas.restore();
    }

    private void drawScaleBarPicture(Canvas canvas) {
        // We want the scale bar to be as long as the closest round-number miles/kilometers
        // to 1-inch at the latitude at the current center of the screen.

        Projection projection = mMap.getProjection();

        if (projection == null) {
            return;
        }

        final Paint barPaint = new Paint();
        barPaint.setColor(Color.BLACK);
        barPaint.setAntiAlias(true);
        barPaint.setStrokeWidth(mLineWidth);

        final Paint textPaint = new Paint();
        textPaint.setColor(Color.BLACK);
        textPaint.setAntiAlias(true);
        textPaint.setTextSize(mTextSize);

        drawXMetric(canvas, textPaint, barPaint);

        drawYMetric(canvas, textPaint, barPaint);
    }

    private void drawXMetric(Canvas canvas, Paint textPaint, Paint barPaint) {
        Projection projection = mMap.getProjection();

        if (projection != null) {
            LatLng p1 = projection.fromScreenLocation(new Point((int) ((getWidth() / 2) - (mXdpi / 2)), getHeight() / 2));
            LatLng p2 = projection.fromScreenLocation(new Point((int) ((getWidth() / 2) + (mXdpi / 2)), getHeight() / 2));

            Location locationP1 = new Location("ScaleBar location p1");
            Location locationP2 = new Location("ScaleBar location p2");

            locationP1.setLatitude(p1.latitude);
            locationP2.setLatitude(p2.latitude);
            locationP1.setLongitude(p1.longitude);
            locationP2.setLongitude(p2.longitude);

            float xMetersPerInch = locationP1.distanceTo(locationP2);

            if (mIsLatitudeBar) {
                String xMsg = scaleBarLengthText(xMetersPerInch, mIsImperial, mIsNautical);
                Rect xTextRect = new Rect();
                textPaint.getTextBounds(xMsg, 0, xMsg.length(), xTextRect);

                int textSpacing = (int) (xTextRect.height() / 5.0);

                canvas.drawRect(mXOffset, mYOffset, mXOffset + mXdpi, mYOffset + mLineWidth, barPaint);
                canvas.drawRect(mXOffset + mXdpi, mYOffset, mXOffset + mXdpi + mLineWidth, mYOffset +
                        xTextRect.height() + mLineWidth + textSpacing, barPaint);

                if (!mIsLongitudeBar) {
                    canvas.drawRect(mXOffset, mYOffset, mXOffset + mLineWidth, mYOffset +
                            xTextRect.height() + mLineWidth + textSpacing, barPaint);
                }
                canvas.drawText(xMsg, (mXOffset + mXdpi / 2 - xTextRect.width() / 2),
                        (mYOffset + xTextRect.height() + mLineWidth + textSpacing), textPaint);
            }
        }
    }

    private void drawYMetric(Canvas canvas, Paint textPaint, Paint barPaint) {
        Projection projection = mMap.getProjection();

        if (projection != null) {
            Location locationP1 = new Location("ScaleBar location p1");
            Location locationP2 = new Location("ScaleBar location p2");

            LatLng p1 = projection.fromScreenLocation(new Point(getWidth() / 2,
                    (int) ((getHeight() / 2) - (mYdpi / 2))));
            LatLng p2 = projection.fromScreenLocation(new Point(getWidth() / 2,
                    (int) ((getHeight() / 2) + (mYdpi / 2))));

            locationP1.setLatitude(p1.latitude);
            locationP2.setLatitude(p2.latitude);
            locationP1.setLongitude(p1.longitude);
            locationP2.setLongitude(p2.longitude);

            float yMetersPerInch = locationP1.distanceTo(locationP2);

            if (mIsLongitudeBar) {
                String yMsg = scaleBarLengthText(yMetersPerInch, mIsImperial, mIsNautical);
                Rect yTextRect = new Rect();
                textPaint.getTextBounds(yMsg, 0, yMsg.length(), yTextRect);

                int textSpacing = (int) (yTextRect.height() / 5.0);

                canvas.drawRect(mXOffset, mYOffset, mXOffset + mLineWidth, mYOffset + mYdpi, barPaint);
                canvas.drawRect(mXOffset, mYOffset + mYdpi, mXOffset + yTextRect.height() +
                        mLineWidth + textSpacing, mYOffset + mYdpi + mLineWidth, barPaint);

                if (!mIsLatitudeBar) {
                    canvas.drawRect(mXOffset, mYOffset, mXOffset + yTextRect.height() +
                            mLineWidth + textSpacing, mYOffset + mLineWidth, barPaint);
                }

                float x = mXOffset + yTextRect.height() + mLineWidth + textSpacing;
                float y = mYOffset + mYdpi / 2 + yTextRect.width() / 2;

                canvas.rotate(-90, x, y);
                canvas.drawText(yMsg, x, y + textSpacing, textPaint);
            }
        }
    }

    private String scaleBarLengthText(float meters, boolean imperial, boolean nautical) {
        if (this.mIsImperial) {
            if (meters >= 1609.344) {
                return (meters / 1609.344) + "mi";
            } else if (meters >= 1609.344/10) {
                return ((meters / 160.9344) / 10.0) + "mi";
            } else {
                return (meters * 3.2808399) + "ft";
            }
        } else if (this.mIsNautical) {
            if (meters >= 1852) {
                return ((meters / 1852)) + "nm";
            } else if (meters >= 1852/10) {
                return (((meters / 185.2)) / 10.0) + "nm";
            } else {
                return ((meters * 3.2808399)) + "ft";
            }
        } else {
            if (meters >= 1000) {
                return ((meters / 1000)) + "km";
            } else if (meters > 100) {
                return ((meters / 100.0) / 10.0) + "km";
            } else {
                return meters + "m";
            }
        }
    }
}

Then you can add this to your layout this way:

    RelativeLayout container = (RelativeLayout) findViewById(R.id.main_view);
    RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(800, 800);

    params.addRule(RelativeLayout.CENTER_HORIZONTAL);
    params.addRule(RelativeLayout.CENTER_VERTICAL);

    mScaleBar = new ScaleBar(this, mMap);
    mScaleBar.setLayoutParams(params);

Hope it will be useful for somebody.

Community
  • 1
  • 1
Arkadiusz Cieśliński
  • 5,307
  • 3
  • 23
  • 19
  • 2
    works great. one just has to add some onTouch handler, and add mScaleBar.invalidate() – dariusiv Jun 30 '15 at 05:58
  • @dariusiv in the class constructor you can add the next code to invalidate the view: `mMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() { @Override public void onCameraMove() { invalidate(); } });` – ClarkXP Dec 07 '16 at 19:21
5

I've created small library based on previous answers.
https://github.com/pengrad/MapScaleView

Add dependency

compile 'com.github.pengrad:mapscaleview:1.4.1'

Include in layout over Map

<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">

    <fragment
        android:id="@+id/mapFragment"
        class="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <com.github.pengrad.mapscaleview.MapScaleView
        android:id="@+id/scaleView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="4dp"/>
</FrameLayout>

Update in code

MapScaleView scaleView = (MapScaleView) findViewById(R.id.scaleView);
CameraPosition cameraPosition = map.getCameraPosition();
// need to pass zoom and latitude
scaleView.update(cameraPosition.zoom, cameraPosition.target.latitude);
Stas Parshin
  • 7,973
  • 3
  • 24
  • 43
  • 1
    Thanks! For other users, use the latest available version. With version 1.2.0 the scale is wrong. – Tim Autin Jul 31 '18 at 11:38
  • Great contribution, thanks! Just. note for others, you need to add `repositories{maven {url "https://jitpack.io"}` to your `build.gradle` to build this. – Peter Jul 21 '22 at 15:46
4

Just add this code..

    googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
    googleMap.getUiSettings().setZoomControlsEnabled(true);
    googleMap.getUiSettings().setCompassEnabled(true);
    googleMap.getUiSettings().setMyLocationButtonEnabled(true);
    googleMap.getUiSettings().setAllGesturesEnabled(true);
    googleMap.setMyLocationEnabled(true);
An-droid
  • 6,433
  • 9
  • 48
  • 93
2

As said before, there is no mention of a scale bar in the Google Maps API. One needs to program one them self. I took the answer from Arkadiusz Cieśliński and enhanced on that to get a more Google Maps like scale view.

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.location.Location;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.Projection;
import com.google.android.gms.maps.model.LatLng;

import java.util.Locale;

import thermcam.georg.locationhistoryviewer.R;
import thermcam.georg.locationhistoryviewer.util.ResourcesUtil;

/**
 * Created by georg on 18.04.17.
 */

public class MapScaleBar extends AppCompatImageView{

    public enum ColorMode {
        COLOR_MODE_AUTO,
        COLOR_MODE_DARK,
        COLOR_MODE_WHITE
    }

    private static final int FT_IN_MILE = 5280;

    private static final int[] METERS = {1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000,
            10000, 20000, 50000, 100000, 200000, 500000, 1000000, 2000000};

    private static final int[] FT = {1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000,
            FT_IN_MILE, 2 * FT_IN_MILE, 5 * FT_IN_MILE, 10 * FT_IN_MILE, 20 * FT_IN_MILE, 50 * FT_IN_MILE,
            100 * FT_IN_MILE, 200 * FT_IN_MILE, 500 * FT_IN_MILE, 1000 * FT_IN_MILE, 2000 * FT_IN_MILE};

    float mXOffset = 10;  // in px
    float mYOffset = 10;  // in px
    private float mLineWidth = 4;  // in dp
    private float mTextSize = 15; // in dp
    private float mTextPadding = 2; // in dp

    public ColorMode mColorMode = ColorMode.COLOR_MODE_AUTO;

    private GoogleMap mMap;

    float mXdpi;
    float mYdpi;

    public MapScaleBar(Context context) {
        this(context, null);
    }

    public MapScaleBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MapScaleBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mXdpi = context.getResources().getDisplayMetrics().xdpi;
        mYdpi = context.getResources().getDisplayMetrics().ydpi;

        setLineWidth(mLineWidth);
        setTextSize(mTextSize);
        setTextPadding(mTextPadding);
    }

    public void addTarget(GoogleMap map) {
        this.mMap = map;

        map.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
            @Override
            public void onCameraMove() {
                invalidate();
            }
        });
    }

    public void setLineWidth(float lineWidth) {
        this.mLineWidth = pxToDp(lineWidth);
    }

    public void setTextSize(float textSize) {
        this.mTextSize = pxToDp(textSize);
    }

    public void setTextPadding(float textPadding) {
        this.mTextPadding = pxToDp(textPadding);
    }

    public float pxToDp(float px) {
        return px * (mYdpi / 160);
    }

    @Override
    public void onDraw(Canvas canvas) {
        if (mMap == null)
            return;

        canvas.save();

        drawScaleBarPicture(canvas);

        canvas.restore();
    }

    private void drawScaleBarPicture(Canvas canvas) {
        // We want the scale bar to be as long as the closest round-number miles/kilometers
        // to 1-inch at the latitude at the current center of the screen.

        int darkC = ResourcesUtil.getColor(getResources(), R.color.textDarkPrimary, null);
        int whiteC = ResourcesUtil.getColor(getResources(), R.color.textLightPrimary, null);

        if (mColorMode == ColorMode.COLOR_MODE_WHITE ||
                mColorMode == ColorMode.COLOR_MODE_AUTO && (mMap.getMapType() == GoogleMap.MAP_TYPE_SATELLITE || mMap.getMapType() == GoogleMap.MAP_TYPE_HYBRID))
            drawXMetric(canvas, whiteC, darkC);
        else
            drawXMetric(canvas, darkC, whiteC);
    }

    private void drawXMetric(Canvas canvas, int fillColor, int outlineColor) {
        Projection projection = mMap.getProjection();

        LatLng p1 = projection.fromScreenLocation(new Point(
                (int) ((getWidth() / 2) - (mXdpi / 2)), getHeight() / 2));
        LatLng p2 = projection.fromScreenLocation(new Point((int) (
                (getWidth() / 2) + (mXdpi / 2)), getHeight() / 2));

        Location locationP1 = new Location("ScaleBar location p1");
        Location locationP2 = new Location("ScaleBar location p2");

        locationP1.setLatitude(p1.latitude);
        locationP2.setLatitude(p2.latitude);
        locationP1.setLongitude(p1.longitude);
        locationP2.setLongitude(p2.longitude);


        // PAINTS
        final Paint barPaintFill = new Paint(Paint.ANTI_ALIAS_FLAG);
        barPaintFill.setStyle(Paint.Style.FILL);
        barPaintFill.setColor(fillColor);

        final Paint barPaintStroke = new Paint(barPaintFill);
        barPaintStroke.setColor(outlineColor);
        barPaintStroke.setStyle(Paint.Style.STROKE);
        barPaintStroke.setStrokeWidth(mLineWidth/5);

        final Paint textPaintFill = new Paint(barPaintFill);
        textPaintFill.setTextSize(mTextSize);

        final Paint textPaintStroke = new Paint(barPaintStroke);
        textPaintStroke.setTextSize(mTextSize);

        // LENGTH
        float xMetersPerInch = locationP1.distanceTo(locationP2);

        int nearestM = findNextSmallestInUnit(xMetersPerInch, false);
        int nearestFT = findNextSmallestInUnit(xMetersPerInch, true);
        float lengthM = mXdpi / xMetersPerInch * nearestM;
        float lengthFT = mXdpi / xMetersPerInch * (nearestFT / 3.2808f);
        String msgM = scaleBarLengthText(nearestM, false);
        String msgFT = scaleBarLengthText(nearestFT, true);

        float longest = Math.max(lengthFT, lengthM);

        // Get text rects & cords
        Rect textRectM = new Rect();
        textPaintFill.getTextBounds(msgM, 0, msgM.length(), textRectM);
        Rect textRectFT = new Rect();
        textPaintFill.getTextBounds(msgFT, 0, msgFT.length(), textRectFT);

        PointF textCoordM = new PointF(canvas.getWidth() - mXOffset - textRectM.width() - mTextPadding,
                canvas.getHeight() - mYOffset - textRectFT.height() - mTextPadding * 2 - mLineWidth);
        PointF textCoordFT = new PointF(canvas.getWidth() - mXOffset - textRectFT.width() - mTextPadding,
                canvas.getHeight() - mYOffset - textRectFT.height() - textRectFT.top);
        // PointF textCoordFT = new PointF()

        // All movements are based in the bottom, right corner
        Path barP = new Path();
        barP.setFillType(Path.FillType.EVEN_ODD);
        barP.moveTo(canvas.getWidth() - mXOffset, canvas.getHeight() - mYOffset - textRectFT.height() - mTextPadding);

        barP.rLineTo(- lengthFT + mLineWidth, 0);
        barP.rLineTo(0, textRectFT.height() * 0.6f);
        barP.rLineTo(- mLineWidth, 0);
        barP.rLineTo(0, -textRectFT.height() * 0.6f);
        barP.rLineTo(lengthFT - longest, 0);
        barP.rLineTo(0, - mLineWidth);
        barP.rLineTo(longest - lengthM, 0);
        barP.rLineTo(0, -textRectM.height() * 0.6f);
        barP.rLineTo(mLineWidth, 0);
        barP.rLineTo(0, textRectM.height() * 0.6f);
        barP.rLineTo(lengthM - mLineWidth, 0);
        barP.close();

        canvas.drawPath(barP, barPaintFill);
        canvas.drawPath(barP, barPaintStroke);

        canvas.drawText(msgM, textCoordM.x, textCoordM.y, textPaintStroke);
        canvas.drawText(msgM, textCoordM.x, textCoordM.y, textPaintFill);

        canvas.drawText(msgFT, textCoordFT.x, textCoordFT.y, textPaintStroke);
        canvas.drawText(msgFT, textCoordFT.x, textCoordFT.y, textPaintFill);
    }

    private int findNextSmallestInUnit(float meters, boolean imperial) {
        int[] data = imperial ? FT : METERS;
        float value = meters * (imperial ? 3.2808f : 1);

        int closest = data[0];

        for (int elem: data) {
            if (elem - value > 0)
                break;
            else
                closest = elem;
        }

        return closest;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension((int)mXdpi, (int)(mTextPadding * 4 + mTextSize * 2 + mLineWidth));
    }

    private String scaleBarLengthText(int value, boolean imperial) {
        if (imperial) {
            if (value >= FT_IN_MILE)
                return String.format(Locale.getDefault(), "%d mi", value / FT_IN_MILE);
            else
                return String.format(Locale.getDefault(), "%d ft", value);
        } else {
            if (value >= 1000)
                return String.format(Locale.getDefault(), "%d km", value / 1000);
            else
                return String.format(Locale.getDefault(), "%d m", value);
        }
    }
}

And can use it, by adding it into your layout.xml. For example like this:

<package.id.MapScaleBar
    android:id="@+id/scaleBar"
    android:layout_centerInParent="true"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

You then just need to add a Google Map instance in the addTarget method of it, for example in onMapReady(GoogleMap googleMap) function.

MapScaleBar scaleBar = (MapScaleBar)findViewById(R.id.scaleBar);
scaleBar.mColorMode = MapScaleBar.ColorMode.COLOR_MODE_DARK;  // normal 
scaleBar.mColorMode = MapScaleBar.ColorMode.COLOR_MODE_WHITE;  // inverted
scaleBar.mColorMode = MapScaleBar.ColorMode.COLOR_MODE_AUTO;  // detect automatically after Google Map style
scaleBar.addTarget(mMap);
Georg Friedrich
  • 183
  • 2
  • 12
1

Here is my code (using the last version of pengrad).

Add this in gradle file.

dependencies {
    implementation 'com.github.pengrad:mapscaleview:1.4.2'
}

Then you have to add the scale component on the layout file with your map.

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/mapFragment"
        class="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <com.github.pengrad.mapscaleview.MapScaleView
        android:id="@+id/scaleView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="4dp"/>
</FrameLayout>

The last step is to link the two components in your activity or fragment (note that OnCameraChangeListener is deprecated now).

public class MyFragment extends Fragment implements OnMapReadyCallback, GoogleMap.OnCameraIdleListener, GoogleMap.OnCameraMoveListener, GoogleMap.OnCameraChangeListener {

   
    private GoogleMap googleMap;
    private MapScaleView mapScaleView;

....

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        viewModel =
                ViewModelProviders.of(getActivity()).get(ViewModel.class);

        View root = inflater.inflate(R.layout.fragment_layout, container, false);
        // Get the SupportMapFragment and request notification
        // when the map is ready to be used.
        SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.home_map);
        mapFragment.getMapAsync(this);

        // scale view for map
        mapScaleView = (MapScaleView) root.findViewById(R.id.home_map_scale_view);

}
....

 @Override
    public void onMapReady(GoogleMap googleMap) {
        
        this.googleMap = googleMap;
       
        googleMap.setOnCameraMoveListener(this);
        googleMap.setOnCameraIdleListener(this);
        googleMap.setOnCameraChangeListener(this);
    }
...
   @Override
    public void onCameraMove() {
        CameraPosition cameraPosition = googleMap.getCameraPosition();
        if(mapScaleView != null)
            mapScaleView.update(cameraPosition.zoom, cameraPosition.target.latitude);
    }

    @Override
    public void onCameraIdle() {
        CameraPosition cameraPosition = googleMap.getCameraPosition();
        if(mapScaleView != null)
            mapScaleView.update(cameraPosition.zoom, cameraPosition.target.latitude);
    }

    @Override
    public void onCameraChange(CameraPosition cameraPosition) {
        if(mapScaleView != null)
            mapScaleView.update(cameraPosition.zoom, cameraPosition.target.latitude);
    }

}

Then you can customize the viewScale eg :

mapScaleView.metersAndMiles() // default
mapScaleView.metersOnly()
mapScaleView.milesOnly()

More information can be found on the MapScaleView GitHub.

Matt N.
  • 53
  • 1
  • 10
Zhar
  • 3,330
  • 2
  • 24
  • 25