I'm looking to do something similar to the way Uber handles pinch zoom events. No matter where you pinch on the screen, it keeps the map centered and zooms in on the center location. Is there a way to do this without having some sort of overlay over the map fragment? Or should I just disable maps' events, create an overlay over the map fragment, and handle all zoom / other events from the overlay?
I've founded complete solution after spending about 3 days to search on google. My answer is edited from https://mcmap.net/q/89305/-android-googlemaps-v2-disable-scroll-on-pan-or-zoom.
public class CustomMapView extends MapView {
private int fingers = 0;
private GoogleMap googleMap;
private long lastZoomTime = 0;
private float lastSpan = -1;
private Handler handler = new Handler();
private ScaleGestureDetector scaleGestureDetector;
private GestureDetector gestureDetector;
public CustomMapView(Context context) {
super(context);
}
public CustomMapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomMapView(Context context, AttributeSet attrs, int style) {
super(context, attrs, style);
}
public CustomMapView(Context context, GoogleMapOptions options) {
super(context, options);
}
public void init(GoogleMap map) {
scaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.OnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (lastSpan == -1) {
lastSpan = detector.getCurrentSpan();
} else if (detector.getEventTime() - lastZoomTime >= 50) {
lastZoomTime = detector.getEventTime();
googleMap.animateCamera(CameraUpdateFactory.zoomBy(getZoomValue(detector.getCurrentSpan(), lastSpan)), 50, null);
lastSpan = detector.getCurrentSpan();
}
return false;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
lastSpan = -1;
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
lastSpan = -1;
}
});
gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
disableScrolling();
googleMap.animateCamera(CameraUpdateFactory.zoomIn(), 400, null);
return true;
}
});
googleMap = map;
}
private float getZoomValue(float currentSpan, float lastSpan) {
double value = (Math.log(currentSpan / lastSpan) / Math.log(1.55d));
return (float) value;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
gestureDetector.onTouchEvent(ev);
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_DOWN:
fingers = fingers + 1;
break;
case MotionEvent.ACTION_POINTER_UP:
fingers = fingers - 1;
break;
case MotionEvent.ACTION_UP:
fingers = 0;
break;
case MotionEvent.ACTION_DOWN:
fingers = 1;
break;
}
if (fingers > 1) {
disableScrolling();
} else if (fingers < 1) {
enableScrolling();
}
if (fingers > 1) {
return scaleGestureDetector.onTouchEvent(ev);
} else {
return super.dispatchTouchEvent(ev);
}
}
private void enableScrolling() {
if (googleMap != null && !googleMap.getUiSettings().isScrollGesturesEnabled()) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
googleMap.getUiSettings().setAllGesturesEnabled(true);
}
}, 50);
}
}
private void disableScrolling() {
handler.removeCallbacksAndMessages(null);
if (googleMap != null && googleMap.getUiSettings().isScrollGesturesEnabled()) {
googleMap.getUiSettings().setAllGesturesEnabled(false);
}
}
}
and customize MapFragment
public class CustomMapFragment extends Fragment {
CustomMapView view;
Bundle bundle;
GoogleMap map;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
bundle = savedInstanceState;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_map, container, false);
view = (CustomMapView) v.findViewById(R.id.mapView);
view.onCreate(bundle);
view.onResume();
map = view.getMap();
view.init(map);
MapsInitializer.initialize(getActivity());
return v;
}
public GoogleMap getMap() {
return map;
}
@Override
public void onResume() {
super.onResume();
view.onResume();
}
@Override
public void onPause() {
super.onPause();
view.onPause();
}
@Override
public void onDestroy() {
super.onDestroy();
view.onDestroy();
}
@Override
public void onLowMemory() {
super.onLowMemory();
view.onLowMemory();
}
}
Finally, in your activity:
....
<fragment
android:id="@+id/map"
class="yourpackage.CustomMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
...
I've already tested on Android 4.1 (API 16) and latter, it work fine and smooth. (About API < 16, I haven't any device to test).
Math.log(1.55d)
? And of all the zoom calculation (Math.log(currentSpan / lastSpan) / Math.log(1.55d))
? –
Holily Here's the code for what MechEthan is thinking of.
First you have to detect double-tap on an overlay view.
public class TouchableWrapper extends FrameLayout { private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDoubleTap(MotionEvent e) { //Notify the event bus (I am using Otto eventbus of course) that you have just received a double-tap event on the map, inside the event bus event listener EventBus_Singleton.getInstance().post(new EventBus_Poster("double_tapped_map")); return true; } }; public TouchableWrapper(Context context) { super(context); mGestureDetector = new GestureDetectorCompat(context, mGestureListener); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { mGestureDetector.onTouchEvent(ev); return super.onInterceptTouchEvent(ev); } }
Wherever it is that you are grabbing your mapView, wrap that mapView inside the TouchableWrapper created above. This is how I do it because I have the issue of needing to add a mapFragment into another fragment so I need a custom SupportMapFragment to do this
public class CustomMap_Fragment extends SupportMapFragment { TouchableWrapper mTouchView; public CustomMap_Fragment() { super(); } public static CustomMap_Fragment newInstance() { return new CustomMap_Fragment(); } @Override public View onCreateView(LayoutInflater arg0, ViewGroup arg1, Bundle arg2) { View mapView = super.onCreateView(arg0, arg1, arg2); Fragment fragment = getParentFragment(); if (fragment != null && fragment instanceof OnMapReadyListener) { ((OnMapReadyListener) fragment).onMapReady(); } mTouchView = new TouchableWrapper(getActivity()); mTouchView.addView(mapView); return mTouchView; } public static interface OnMapReadyListener { void onMapReady(); } }
Inside my Map_Fragment (which in the end will sit inside a FrameLayout in an activity that supports navigation drawer and fragment transactions for switching the views)
mMapFragment = CustomMap_Fragment.newInstance(); getChildFragmentManager().beginTransaction().replace(R.id.map_container, mMapFragment).commit();
Now finally inside this same Fragment where I just got my map, the EventBus receiver will do the following action when it receives "double_tapped_map":
@Subscribe public void eventBus_ListenerMethod(AnswerAvailableEvent event) { //Construct a CameraUpdate object that will zoom into the exact middle of the map, with a zoom of currentCameraZoom + 1 unit zoomInUpdate = CameraUpdateFactory.zoomIn(); //Run that with a speed of 400 ms. map.animateCamera(zoomInUpdate, 400, null); }
Note: To achieve this perfectly you disable zoomGestures on your map (meaning you do myMap.getUiSettings().setZoomGesturesEnabled(false);
. If you don't do that, you will be able to double-tap very quickly on the map and you will see that it will zoom away from the center because the implementation of double tap is exactly as I had in the first answer I posted, which is that they subtract current time from previous tap-time, so in that window you can slip in a third tap and it will not trigger the event bus event and google map will catch it instead; So disable Zoom gestures.
But then, you will see that pinch-in/out will not work anymore and you have to handle pinch also, which I've also done but needs like 1 more hour and I havent gotten the time to do that yet but 100% I will update the answer when I do that.
TIP: Uber has disabled rotate gestures on the map also. map.getUiSettings().setRotateGesturesEnabled(false);
myMap.getUiSettings().setZoomGesturesEnabled(false);
-tip, one helped me –
Susceptive Personally, I would disable only zoom gestures on the map, detect pinch on an overlay, and then pass everything else through to the map.
The google-maps v2 API doesn't have anything explicit for custom zoom handling. Although I'm sure you could inject something, doing the overlay approach insulates you from google-maps changes, and lets you more easily support other map providers if needed.
(Just for completeness: you could also handle the post-camera change events and re-center, but that would be a janky, bad user experience.)
I had the same requirement as well. I had to understand how the events are handled in android to solve this problem, because we have to intercept the touch event for zoom and pass the scroll event to the map. To achieve this, we need a custom View over Google map View. Our custom view intercepts touch events, and decides whether to handle the follow-up events by not giving a chance for underlying map to handle or just leave the underlying map to handle all by itself.
Now code time - We need two things here - a custom fragment, a custom view.
Custom fragment
public class CustomMapFragment extends SupportMapFragment implements OnMapReadyCallback { public View mapView = null; public WrapperView wrapperView = null; @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { mapView = super.onCreateView(inflater, parent, savedInstanceState); wrapperView = new WrapperView(getActivity()); wrapperView.addView(mapView); SupportMapFragment mapFragment = (SupportMapFragment) getActivity().getSupportFragmentManager().findFragmentById(R.id.map); mapFragment.getMapAsync(this); return wrapperView; } @Override public View getView() { return mapView; } @Override public void onMapReady(GoogleMap googleMap) { wrapperView.setGoogleMap(googleMap); }
Custom View
public class WrapperView extends FrameLayout { private GoogleMap googleMap; Activity activity = null; ScaleGestureDetector scaleGestureDetector; public WrapperView(Activity activity) { super(activity); this.activity=activity; scaleGestureDetector = new ScaleGestureDetector(activity ,new MyOnScaleGestureListener()); } public void setGoogleMap(GoogleMap map){ googleMap = map; } private boolean isZoomInProgress(MotionEvent event){ if(event.getPointerCount()>1){ return true; } return false; } @Override public boolean onInterceptTouchEvent(MotionEvent event){ return isZoomInProgress(event); } @Override public boolean onTouchEvent(MotionEvent event){ return scaleGestureDetector.onTouchEvent(event); } public class MyOnScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector) { float previousSpan = detector.getPreviousSpan(); float currentSpan = detector.getCurrentSpan(); float targetSpan; if(previousSpan>currentSpan){ targetSpan = previousSpan-currentSpan; }else{ targetSpan = currentSpan-previousSpan; } float scaleFactor = detector.getScaleFactor(); if (scaleFactor > 1) { if(googleMap.getCameraPosition().zoom!=googleMap.getMaxZoomLevel()) { for(int j=0;j<(targetSpan*2);j++){ googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(googleMap.getCameraPosition().target, googleMap.getCameraPosition().zoom + 0.002f)); } } } else { if (googleMap.getCameraPosition().zoom != googleMap.getMinZoomLevel()) { for(int j=0;j<(targetSpan*2);j++){ googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(googleMap.getCameraPosition().target, googleMap.getCameraPosition().zoom - 0.002f)); } } } return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) {} }
Use the new custom fragment in your view like below -
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:map="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/map"
android:name="yourpackage.CustomMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" />
You can use LatLngBounds to limit the map be move from the position you want. (You can set both Northeast and Southwest corner of the bound be the same point) .
Please check the below link.
https://developers.google.com/maps/documentation/android-api/views
This worked for me in compose
Use the below CustomMapView
in place of MapView
internal class CustomMapView : MapView {
private var googleMap: GoogleMap? = null
private var gestureDetector: ScaleGestureDetector? = null
private val mHandler = Handler(Looper.getMainLooper())
private var touchDownTime = 0L
private var fingers = 0
private var doubleClicked = false
private val doubleClickInterval = ViewConfiguration.getDoubleTapTimeout().toLong()
private var clickCount = 0
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, style: Int) : super(context, attrs, style)
constructor(context: Context, options: GoogleMapOptions?) : super(context, options)
fun init(map: GoogleMap?, context: Context) {
map ?: return
this.googleMap = map
initScaleGestureDetector(map, context)
}
override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
when (motionEvent.action.and(MotionEvent.ACTION_MASK)) {
MotionEvent.ACTION_POINTER_DOWN -> fingers += 1
MotionEvent.ACTION_POINTER_UP -> fingers -= 1
MotionEvent.ACTION_UP -> {
val time = System.currentTimeMillis() - touchDownTime
if (clickCount == 2) {
doubleClicked = time <= doubleClickInterval
clickCount = 0
} else {
doubleClicked = false
fingers = 0
}
}
MotionEvent.ACTION_DOWN -> {
doubleClicked = false
fingers = 1
if (System.currentTimeMillis() - touchDownTime >= 300) {
clickCount = 0
}
touchDownTime = System.currentTimeMillis()
clickCount++
}
}
if (fingers > 1 || doubleClicked) {
disableScrolling()
if (doubleClicked) {
try {
val zoomValue: Float = googleMap!!.cameraPosition.zoom + 1.0f
googleMap!!.animateCamera(CameraUpdateFactory.zoomTo(zoomValue))
} catch (e: Exception) {
e.printStackTrace()
}
}
} else if (fingers < 1) {
enableScrolling()
}
gestureDetector?.onTouchEvent(motionEvent)
return super.dispatchTouchEvent(motionEvent)
}
var lastSpan = -1f
var lastZoomTime = 0L
private fun initScaleGestureDetector(map: GoogleMap, context: Context) {
gestureDetector = ScaleGestureDetector(
context,
object : ScaleGestureDetector.OnScaleGestureListener {
override fun onScale(detector: ScaleGestureDetector): Boolean {
if (lastSpan == -1f) {
lastSpan = detector.currentSpan
} else if (detector.eventTime - lastZoomTime >= 50) {
lastZoomTime = detector.eventTime
map.moveCamera(
CameraUpdateFactory.zoomBy(
getZoomValue(
detector.currentSpan,
lastSpan
)
)
)
lastSpan = detector.currentSpan
}
return false
}
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
lastSpan = -1f
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector) {
lastSpan = -1f
}
}
)
}
private fun getZoomValue(currentSpan: Float, lastSpan: Float): Float {
val value = ln((currentSpan / lastSpan).toDouble()) / ln(1.55)
return value.toFloat()
}
private fun enableScrolling() {
val map = googleMap ?: return
if (!map.uiSettings.isScrollGesturesEnabled) {
mHandler.postDelayed(
{ map.uiSettings.setAllGesturesEnabled(true) },
50
)
}
}
private fun disableScrolling() {
val map = googleMap ?: return
if (map.uiSettings.isScrollGesturesEnabled) {
map.uiSettings.setAllGesturesEnabled(false)
}
}
}
© 2022 - 2024 — McMap. All rights reserved.