Keep map centered regardless of where you pinch zoom on android

前端 未结 5 763
谎友^
谎友^ 2020-12-01 11:10

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

相关标签:
5条回答
  • 2020-12-01 11:44

    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

    0 讨论(0)
  • 2020-12-01 11:48

    I've founded complete solution after spending about 3 days to search on google. My answer is edited from https://stackoverflow.com/a/32734436/3693334.

    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).

    0 讨论(0)
  • 2020-12-01 12:02

    Here's the code for what MechEthan is thinking of.

    1. 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);
          }
      }
      
    2. 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();
          }
      }
      
    3. 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();
      
    4. 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);

    0 讨论(0)
  • 2020-12-01 12:05

    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.)

    0 讨论(0)
  • 2020-12-01 12:08

    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.

    1. 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);
      }
      
    2. 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" />
    
    0 讨论(0)
提交回复
热议问题