116 | * Note: By default offsetX will be ignored, so in order for it to take effect, you 117 | * must call setCenterByX(false). 118 | * 119 | * Also if you want to use dp, you should convert the values to px by yourself. 120 | * The constructor expects absolute pixel values. 121 | *
122 | * 123 | * @param offsetX InfoWindow's offset by x from marker's screen location. 124 | * Value must be in px. 125 | * @param offsetY InfoWindow's offset by y from marker's screen location. 126 | * Value must be in px. 127 | * 128 | * @see #setCenterByX(boolean) 129 | * @see #setCenterByY(boolean) 130 | */ 131 | public MarkerSpecification(int offsetX, int offsetY) { 132 | this.offsetX = offsetX; 133 | this.offsetY = offsetY; 134 | } 135 | 136 | public int getOffsetX() { 137 | return offsetX; 138 | } 139 | 140 | public void setOffsetX(int offsetX) { 141 | this.offsetX = offsetX; 142 | } 143 | 144 | public int getOffsetY() { 145 | return offsetY; 146 | } 147 | 148 | public void setOffsetY(int offsetY) { 149 | this.offsetY = offsetY; 150 | } 151 | 152 | public boolean centerByX() { 153 | return centerByX; 154 | } 155 | 156 | /** 157 | * Set whether the InfoWindow's center by x should be the same as the marker's 158 | * screen x coordinate. 159 | * If false, offsetX will be used and applied on the x position of the view. Default value is true. 160 | * 161 | * @param centerByX Pass true if you want InfoWindow's x center to be the 162 | * same as the marker's screen x coordinate. Pass false if you want 163 | * offsetX to be used instead. 164 | * 165 | * @see com.appolica.interactiveinfowindow.InfoWindowManager#centerInfoWindow(InfoWindow, android.view.View) 166 | */ 167 | public void setCenterByX(boolean centerByX) { 168 | this.centerByX = centerByX; 169 | } 170 | 171 | public boolean centerByY() { 172 | return centerByY; 173 | } 174 | 175 | /** 176 | * Set whether the InfoWindow's center by y should be the same as the marker's 177 | * screen y coordinate. 178 | * If false, offsetY will be used and applied on the y position of the view. Default value is false. 179 | * 180 | * @param centerByY Pass true if you want InfoWindow's y center to be the 181 | * same as the marker's screen y coordinate. Pass false if you want 182 | * offsetX to be used instead. 183 | * 184 | * @see com.appolica.interactiveinfowindow.InfoWindowManager#centerInfoWindow(InfoWindow, android.view.View) 185 | */ 186 | public void setCenterByY(boolean centerByY) { 187 | this.centerByY = centerByY; 188 | } 189 | 190 | @Override 191 | public boolean equals(Object o) { 192 | 193 | if (o instanceof MarkerSpecification) { 194 | final MarkerSpecification markerSpecification = (MarkerSpecification) o; 195 | 196 | final boolean offsetCheck = markerSpecification.getOffsetY() == offsetY; 197 | 198 | return offsetCheck; 199 | } 200 | 201 | return super.equals(o); 202 | } 203 | } 204 | 205 | @Override 206 | public boolean equals(Object o) { 207 | 208 | if (o instanceof InfoWindow) { 209 | final InfoWindow queryWindow = (InfoWindow) o; 210 | 211 | final boolean markerCheck = queryWindow.getPosition().equals(position); 212 | final boolean specCheck = queryWindow.getMarkerSpec().equals(markerSpec); 213 | final boolean fragmentCheck = queryWindow.getWindowFragment() == windowFragment; 214 | 215 | return markerCheck && specCheck && fragmentCheck; 216 | } 217 | 218 | return super.equals(o); 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /InteractiveInfoWindowAndroid/interactive-info-window/src/main/java/com/appolica/interactiveinfowindow/InfoWindowManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Appolica Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 | * except in compliance with the License. 6 | * 7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under 10 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 11 | * either express or implied. See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.appolica.interactiveinfowindow; 16 | 17 | import android.content.Context; 18 | import android.graphics.Point; 19 | import android.graphics.Rect; 20 | import android.graphics.drawable.Drawable; 21 | import android.os.Build; 22 | import android.os.Bundle; 23 | import android.view.GestureDetector; 24 | import android.view.MotionEvent; 25 | import android.view.View; 26 | import android.view.ViewGroup; 27 | import android.view.ViewTreeObserver; 28 | import android.view.animation.Animation; 29 | import android.view.animation.DecelerateInterpolator; 30 | import android.view.animation.ScaleAnimation; 31 | import android.widget.FrameLayout; 32 | 33 | import androidx.annotation.NonNull; 34 | import androidx.annotation.Nullable; 35 | import androidx.core.content.ContextCompat; 36 | import androidx.core.view.ViewCompat; 37 | import androidx.fragment.app.Fragment; 38 | import androidx.fragment.app.FragmentManager; 39 | 40 | import com.appolica.interactiveinfowindow.animation.SimpleAnimationListener; 41 | import com.appolica.interactiveinfowindow.customview.DisallowInterceptLayout; 42 | import com.appolica.interactiveinfowindow.customview.TouchInterceptFrameLayout; 43 | import com.appolica.mapanimations.R; 44 | import com.google.android.gms.maps.CameraUpdate; 45 | import com.google.android.gms.maps.CameraUpdateFactory; 46 | import com.google.android.gms.maps.GoogleMap; 47 | import com.google.android.gms.maps.Projection; 48 | import com.google.android.gms.maps.model.LatLng; 49 | import com.google.android.gms.maps.model.Polygon; 50 | import com.google.android.gms.maps.model.Polyline; 51 | 52 | /** 53 | * This is where all the magic happens. Use this class to show your interactive {@link InfoWindow} 54 | * above your {@link com.google.android.gms.maps.model.Marker}. 55 | */ 56 | public class InfoWindowManager 57 | implements GoogleMap.OnCameraIdleListener, 58 | GoogleMap.OnCameraMoveStartedListener, 59 | GoogleMap.OnCameraMoveListener, 60 | GoogleMap.OnCameraMoveCanceledListener, 61 | GoogleMap.OnMapClickListener, 62 | GoogleMap.OnPolylineClickListener, 63 | GoogleMap.OnPolygonClickListener{ 64 | 65 | public static final String FRAGMENT_TAG_INFO = "InfoWindow"; 66 | 67 | private static final String TAG = "InfoWindowManager"; 68 | 69 | public static final int DURATION_WINDOW_ANIMATION = 200; 70 | public static final int DURATION_CAMERA_ENSURE_VISIBLE_ANIMATION = 500; 71 | 72 | private GoogleMap googleMap; 73 | 74 | private FragmentManager fragmentManager; 75 | 76 | private InfoWindow currentWindow; 77 | private ViewGroup parent; 78 | private View currentContainer; 79 | 80 | private ContainerSpecification containerSpec; 81 | 82 | private FragmentContainerIdProvider idProvider; 83 | 84 | private GoogleMap.OnMapClickListener onMapClickListener; 85 | 86 | private GoogleMap.OnCameraIdleListener onCameraIdleListener; 87 | private GoogleMap.OnCameraMoveStartedListener onCameraMoveStartedListener; 88 | private GoogleMap.OnCameraMoveListener onCameraMoveListener; 89 | private GoogleMap.OnCameraMoveCanceledListener onCameraMoveCanceledListener; 90 | private GoogleMap.OnPolylineClickListener onPolylineClickListener; 91 | private GoogleMap.OnPolygonClickListener onPolygonClickListener; 92 | 93 | private Animation showAnimation; 94 | private Animation hideAnimation; 95 | 96 | private WindowShowListener windowShowListener; 97 | 98 | private boolean hideOnFling = false; 99 | 100 | public InfoWindowManager(@NonNull final FragmentManager fragmentManager) { 101 | 102 | this.fragmentManager = fragmentManager; 103 | } 104 | 105 | /** 106 | * Call this method if you are not using 107 | * {@link com.appolica.interactiveinfowindow.fragment.MapInfoWindowFragment}. If you are calling 108 | * it from a Fragment we suggest you to call it in {@link Fragment#onViewCreated(View, Bundle)} 109 | * and if you are calling it from an Activity you should call it in 110 | * {@link android.app.Activity#onCreate(Bundle)}. 111 | * 112 | * @param parent The parent of your {@link com.google.android.gms.maps.MapView} or 113 | * {@link com.google.android.gms.maps.SupportMapFragment}. 114 | * @param savedInstanceState The saved state Bundle from your Fragment/Activity. 115 | */ 116 | public void onParentViewCreated( 117 | @NonNull final TouchInterceptFrameLayout parent, 118 | @Nullable final Bundle savedInstanceState) { 119 | 120 | this.parent = parent; 121 | this.idProvider = new FragmentContainerIdProvider(savedInstanceState); 122 | this.containerSpec = generateDefaultContainerSpecs(parent.getContext()); 123 | 124 | parent.setDetector( 125 | new GestureDetector( 126 | parent.getContext(), 127 | new GestureDetector.SimpleOnGestureListener() { 128 | 129 | @Override 130 | public boolean onScroll( 131 | MotionEvent e1, MotionEvent e2, 132 | float distanceX, float distanceY) { 133 | 134 | if (isOpen()) { 135 | centerInfoWindow(currentWindow, currentContainer); 136 | } 137 | 138 | return true; 139 | } 140 | 141 | @Override 142 | public boolean onFling( 143 | MotionEvent e1, MotionEvent e2, 144 | float velocityX, float velocityY) { 145 | 146 | if (isOpen()) { 147 | if (hideOnFling) { 148 | hide(currentWindow); 149 | } else { 150 | centerInfoWindow(currentWindow, currentContainer); 151 | } 152 | } 153 | 154 | return true; 155 | } 156 | 157 | @Override 158 | public boolean onDoubleTap(MotionEvent e) { 159 | 160 | if (isOpen()) { 161 | centerInfoWindow(currentWindow, currentContainer); 162 | } 163 | 164 | return true; 165 | } 166 | })); 167 | 168 | currentContainer = parent.findViewById(idProvider.currentId); 169 | 170 | if (currentContainer == null) { 171 | currentContainer = createContainer(parent); 172 | 173 | parent.addView(currentContainer); 174 | } 175 | 176 | final Fragment oldFragment = fragmentManager.findFragmentByTag(FRAGMENT_TAG_INFO); 177 | if (oldFragment != null) { 178 | fragmentManager.beginTransaction() 179 | .remove(oldFragment) 180 | .commitNow(); 181 | } 182 | 183 | } 184 | 185 | private View createContainer(@NonNull final ViewGroup parent) { 186 | final DisallowInterceptLayout container = new DisallowInterceptLayout(parent.getContext()); 187 | 188 | container.setDisallowParentIntercept(true); 189 | container.setLayoutParams(generateDefaultLayoutParams()); 190 | container.setId(idProvider.getNewId()); 191 | container.setVisibility(View.INVISIBLE); 192 | 193 | return container; 194 | } 195 | 196 | private FrameLayout.LayoutParams generateDefaultLayoutParams() { 197 | 198 | return generateLayoutParams( 199 | ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 200 | } 201 | 202 | private FrameLayout.LayoutParams generateLayoutParams( 203 | final int infoWindowWidth, final int infoWindowHeight) { 204 | 205 | return new FrameLayout.LayoutParams(infoWindowWidth, infoWindowHeight); 206 | } 207 | 208 | /** 209 | * Same as callingtoggle(currentWindow, true);
210 | *
211 | * @param infoWindow The {@link InfoWindow} that is to be shown/hidden.
212 | * @see #toggle(InfoWindow, boolean)
213 | */
214 | public void toggle(@NonNull final InfoWindow infoWindow) {
215 | toggle(infoWindow, true);
216 | }
217 |
218 | /**
219 | * Open/hide the given {@link InfoWindow}.
220 | *
221 | * @param infoWindow The {@link InfoWindow} that is to be shown/hidden.
222 | * @param animated true
if you want to toggle it with animation,
223 | * false
otherwise.
224 | */
225 | public void toggle(@NonNull final InfoWindow infoWindow, final boolean animated) {
226 |
227 | if (isOpen()) {
228 | // If the toggled window is tha same as the already opened one, close it.
229 | // Otherwise close the currently opened window and open the new one.
230 | if (infoWindow.equals(currentWindow)) {
231 | hide(infoWindow, animated);
232 | } else {
233 | show(infoWindow, animated);
234 | }
235 |
236 | } else {
237 | show(infoWindow, animated);
238 | }
239 |
240 | }
241 |
242 | /**
243 | * Same as calling show(currentWindow, true);
244 | *
245 | * @param infoWindow The {@link InfoWindow} that is to be shown.
246 | * @see #show(InfoWindow, boolean)
247 | */
248 | public void show(@NonNull final InfoWindow infoWindow) {
249 | show(infoWindow, true);
250 | }
251 |
252 | /**
253 | * Show the given {@link InfoWindow}. Pass true
if you want this action
254 | * to be animated, false
otherwise. If another window has been already opened
255 | * it will be closed while opening the new one.
256 | *
257 | * @param window The {@link InfoWindow} that is to be shown.
258 | * @param animated true
if you want to show it with animation,
259 | * false
otherwise.
260 | */
261 | public void show(@NonNull final InfoWindow window, final boolean animated) {
262 | // Check if already opened
263 | if (isOpen()) {
264 |
265 | internalHide(currentContainer, currentWindow);
266 |
267 | currentContainer = createContainer(parent);
268 | parent.addView(currentContainer);
269 | }
270 |
271 | setCurrentWindow(window);
272 |
273 | internalShow(window, currentContainer, animated);
274 | }
275 |
276 | private void internalShow(@NonNull final InfoWindow infoWindow,
277 | @NonNull final View container,
278 | final boolean animated) {
279 |
280 | addFragment(infoWindow.getWindowFragment(), container);
281 | prepareView(container, infoWindow);
282 |
283 | if (animated) {
284 |
285 | animateWindowOpen(infoWindow, container);
286 |
287 | } else {
288 | propagateShowEvent(infoWindow, InfoWindow.State.SHOWN);
289 | container.setVisibility(View.VISIBLE);
290 |
291 | }
292 | }
293 |
294 | private void prepareView(final View view, final InfoWindow infoWindow) {
295 |
296 | updateWithContainerSpec(view);
297 |
298 | view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
299 | @Override
300 | public boolean onPreDraw() {
301 |
302 | centerInfoWindow(infoWindow, view);
303 | ensureVisible(view);
304 |
305 | view.getViewTreeObserver().removeOnPreDrawListener(this);
306 | return true;
307 | }
308 | });
309 | }
310 |
311 | private void updateWithContainerSpec(final View view) {
312 | ViewCompat.setBackground(view, containerSpec.background);
313 | }
314 |
315 | private void animateWindowOpen(@NonNull final InfoWindow infoWindow,
316 | @NonNull final View container) {
317 |
318 | final SimpleAnimationListener animationListener = new SimpleAnimationListener() {
319 |
320 | @Override
321 | public void onAnimationStart(Animation animation) {
322 |
323 | container.setVisibility(View.VISIBLE);
324 | propagateShowEvent(infoWindow, InfoWindow.State.SHOWING);
325 |
326 | }
327 |
328 | @Override
329 | public void onAnimationEnd(Animation animation) {
330 |
331 | propagateShowEvent(infoWindow, InfoWindow.State.SHOWN);
332 | setCurrentWindow(infoWindow);
333 |
334 | }
335 | };
336 |
337 | if (showAnimation == null) {
338 |
339 | container.getViewTreeObserver().addOnPreDrawListener(
340 | new ViewTreeObserver.OnPreDrawListener() {
341 |
342 | @Override
343 | public boolean onPreDraw() {
344 | final int containerWidth = container.getWidth();
345 | final int containerHeight = container.getHeight();
346 |
347 | final float pivotX = container.getTranslationX() + containerWidth / 2;
348 | final float pivotY = container.getTranslationY() + containerHeight;
349 |
350 | final ScaleAnimation scaleAnimation = new ScaleAnimation(
351 | 0f, 1f,
352 | 0f, 1f,
353 | pivotX, pivotY);
354 |
355 | scaleAnimation.setDuration(DURATION_WINDOW_ANIMATION);
356 | scaleAnimation.setInterpolator(new DecelerateInterpolator());
357 | scaleAnimation.setAnimationListener(animationListener);
358 |
359 | container.startAnimation(scaleAnimation);
360 |
361 | container.getViewTreeObserver().removeOnPreDrawListener(this);
362 | return true;
363 | }
364 | });
365 | } else {
366 | showAnimation.setAnimationListener(animationListener);
367 | container.startAnimation(showAnimation);
368 | }
369 | }
370 |
371 | /**
372 | * Same as calling hide(currentWindow, true);
373 | *
374 | * @param infoWindow The {@link InfoWindow} that is to be hidden.
375 | * @see #hide(InfoWindow, boolean)
376 | */
377 | public void hide(@NonNull final InfoWindow infoWindow) {
378 | hide(infoWindow, true);
379 | }
380 |
381 | /**
382 | * Hides the given {@link InfoWindow}. Pass true
if you want this action
383 | * to be animated, false
otherwise.
384 | *
385 | * @param infoWindow The {@link InfoWindow} that is to be hidden.
386 | * @param animated true
if you want to hide it with animation,
387 | * false
otherwise.
388 | */
389 | public void hide(@NonNull final InfoWindow infoWindow, final boolean animated) {
390 | internalHide(currentContainer, infoWindow, animated);
391 | }
392 |
393 | private void internalHide(@NonNull final View container, @NonNull final InfoWindow infoWindow) {
394 | internalHide(container, infoWindow, true);
395 | }
396 |
397 | private void internalHide(
398 | @NonNull final View container,
399 | @NonNull final InfoWindow toHideWindow,
400 | final boolean animated) {
401 |
402 | if (animated) {
403 |
404 | final Animation animation;
405 |
406 | if (hideAnimation == null) {
407 |
408 | final int containerWidth = container.getWidth();
409 | final int containerHeight = container.getHeight();
410 |
411 | final float pivotX = container.getTranslationX() + containerWidth / 2;
412 | final float pivotY = container.getTranslationY() + containerHeight;
413 |
414 | animation = new ScaleAnimation(
415 | 1f, 0f,
416 | 1f, 0f,
417 | pivotX, pivotY);
418 |
419 | animation.setDuration(DURATION_WINDOW_ANIMATION);
420 | animation.setInterpolator(new DecelerateInterpolator());
421 |
422 |
423 | } else {
424 | animation = hideAnimation;
425 | }
426 |
427 | animation.setAnimationListener(new SimpleAnimationListener() {
428 |
429 | @Override
430 | public void onAnimationStart(Animation animation) {
431 | toHideWindow.setWindowState(InfoWindow.State.HIDING);
432 | propagateShowEvent(toHideWindow, InfoWindow.State.HIDING);
433 | }
434 |
435 | @Override
436 | public void onAnimationEnd(Animation animation) {
437 | removeWindow(toHideWindow, container);
438 |
439 | if (container.getId() != InfoWindowManager.this.currentContainer.getId()) {
440 | parent.removeView(container);
441 | }
442 |
443 | toHideWindow.setWindowState(InfoWindow.State.HIDDEN);
444 | propagateShowEvent(toHideWindow, InfoWindow.State.HIDDEN);
445 | }
446 | });
447 |
448 | this.currentContainer.startAnimation(animation);
449 |
450 | } else {
451 |
452 | removeWindow(toHideWindow, container);
453 | propagateShowEvent(toHideWindow, InfoWindow.State.HIDDEN);
454 |
455 | }
456 | }
457 |
458 | private void propagateShowEvent(
459 | @NonNull final InfoWindow infoWindow,
460 | @NonNull final InfoWindow.State state) {
461 |
462 | infoWindow.setWindowState(state);
463 |
464 | if (windowShowListener != null) {
465 | switch (state) {
466 | case SHOWING:
467 | windowShowListener.onWindowShowStarted(infoWindow);
468 | break;
469 | case SHOWN:
470 | windowShowListener.onWindowShown(infoWindow);
471 | break;
472 | case HIDING:
473 | windowShowListener.onWindowHideStarted(infoWindow);
474 | break;
475 | case HIDDEN:
476 | windowShowListener.onWindowHidden(infoWindow);
477 | break;
478 | }
479 | }
480 | }
481 |
482 | private void centerInfoWindow(@NonNull final InfoWindow infoWindow,
483 | @NonNull final View container) {
484 | final InfoWindow.MarkerSpecification markerSpec = infoWindow.getMarkerSpec();
485 | final Projection projection = googleMap.getProjection();
486 |
487 | final Point windowScreenLocation = projection.toScreenLocation(infoWindow.getPosition());
488 |
489 | final int containerWidth = container.getWidth();
490 | final int containerHeight = container.getHeight();
491 |
492 | final int x;
493 | if (markerSpec.centerByX()) {
494 | x = windowScreenLocation.x - containerWidth / 2;
495 | } else {
496 | x = windowScreenLocation.x + markerSpec.getOffsetX();
497 | }
498 |
499 | final int y;
500 | if (markerSpec.centerByY()) {
501 | y = windowScreenLocation.y - containerHeight / 2;
502 | } else {
503 | y = windowScreenLocation.y - containerHeight - markerSpec.getOffsetY();
504 | }
505 |
506 | final int pivotX = containerWidth / 2;
507 | final int pivotY = containerHeight;
508 |
509 | container.setPivotX(pivotX);
510 | container.setPivotY(pivotY);
511 |
512 | container.setX(x);
513 | container.setY(y);
514 | }
515 |
516 | private boolean ensureVisible(@NonNull final View infoWindowContainer) {
517 |
518 | final int[] infoWindowLocation = new int[2];
519 | infoWindowContainer.getLocationOnScreen(infoWindowLocation);
520 |
521 | final boolean visible = true;
522 | final Rect infoWindowRect = new Rect();
523 | infoWindowContainer.getHitRect(infoWindowRect);
524 |
525 | final int[] parentPosition = new int[2];
526 | parent.getLocationOnScreen(parentPosition);
527 |
528 | final Rect parentRect = new Rect();
529 | parent.getGlobalVisibleRect(parentRect);
530 |
531 | infoWindowContainer.getGlobalVisibleRect(infoWindowRect);
532 |
533 | final int visibleWidth = infoWindowRect.width();
534 | final int actualWidth = infoWindowContainer.getWidth();
535 |
536 | final int visibleHeight = infoWindowRect.height();
537 | final int actualHeight = infoWindowContainer.getHeight();
538 |
539 | int scrollX = (visibleWidth - actualWidth);
540 | int scrollY = (visibleHeight - actualHeight);
541 |
542 | if (scrollX != 0) {
543 | if (infoWindowRect.left == parentRect.left) {
544 | scrollX = -Math.abs(scrollX);
545 | } else {
546 | scrollX = Math.abs(scrollX);
547 | }
548 | }
549 |
550 | if (scrollY != 0) {
551 | if (infoWindowRect.top < parentRect.top) {
552 | scrollY = Math.abs(scrollY);
553 | } else {
554 | scrollY = -Math.abs(scrollY);
555 | }
556 | }
557 |
558 | final CameraUpdate cameraUpdate = CameraUpdateFactory.scrollBy(scrollX, scrollY);
559 | googleMap.animateCamera(cameraUpdate, DURATION_CAMERA_ENSURE_VISIBLE_ANIMATION, null);
560 |
561 | return visible;
562 | }
563 |
564 | private void removeWindow(@NonNull final InfoWindow window, @NonNull final View container) {
565 |
566 | container.setVisibility(View.INVISIBLE);
567 | container.setScaleY(1f);
568 | container.setScaleX(1f);
569 | container.clearAnimation();
570 |
571 | removeWindowFragment(window.getWindowFragment());
572 | }
573 |
574 | private void addFragment(@NonNull final Fragment fragment, @NonNull final View container) {
575 | fragmentManager.beginTransaction()
576 | .replace(container.getId(), fragment, FRAGMENT_TAG_INFO)
577 | .commitNow();
578 | }
579 |
580 | private void removeWindowFragment(final Fragment windowFragment) {
581 | fragmentManager.beginTransaction()
582 | .remove(windowFragment)
583 | .commitNow();
584 | }
585 |
586 | /**
587 | * Generate default {@link ContainerSpecification} for the container view.
588 | *
589 | * @param context used to work with Resources.
590 | * @return New instance of the generated default container specs.
591 | */
592 | public ContainerSpecification generateDefaultContainerSpecs(Context context) {
593 | final Drawable drawable =
594 | ContextCompat.getDrawable(context, R.drawable.infowindow_background);
595 |
596 | return new ContainerSpecification(drawable);
597 | }
598 |
599 | private boolean isOpen() {
600 | return currentContainer != null && currentContainer.getVisibility() == View.VISIBLE;
601 | }
602 |
603 | /**
604 | * Set a callback which will be invoked when an {@link InfoWindow} is changing its state.
605 | *
606 | * @param windowShowListener The callback that will run.
607 | * @see WindowShowListener
608 | */
609 | public void setWindowShowListener(WindowShowListener windowShowListener) {
610 | this.windowShowListener = windowShowListener;
611 | }
612 |
613 | private void setCurrentWindow(InfoWindow currentWindow) {
614 | this.currentWindow = currentWindow;
615 | }
616 |
617 | /**
618 | * Set the container specifications. These specifications are global for all
619 | * {@link InfoWindow}s.
620 | *
621 | * @param containerSpec The container specifications used for the InfoWindow container view.
622 | */
623 | public void setContainerSpec(ContainerSpecification containerSpec) {
624 | this.containerSpec = containerSpec;
625 | }
626 |
627 | /**
628 | * Get the specification of the {@link InfoWindow}'s container.
629 | *
630 | * @return {@link InfoWindow}'s container specification.
631 | * @see ContainerSpecification
632 | */
633 | public ContainerSpecification getContainerSpec() {
634 | return containerSpec;
635 | }
636 |
637 | private class FragmentContainerIdProvider {
638 | private final static String BUNDLE_KEY_ID = "BundleKeyFragmentContainerIdProvider";
639 | private int currentId;
640 |
641 | public FragmentContainerIdProvider(@Nullable final Bundle savedInstanceState) {
642 | if (savedInstanceState != null) {
643 | currentId = savedInstanceState.getInt(BUNDLE_KEY_ID, R.id.infoWindowContainer1);
644 | } else {
645 | currentId = R.id.infoWindowContainer1;
646 | }
647 | }
648 |
649 | public int getCurrentId() {
650 | return currentId;
651 | }
652 |
653 | public int getNewId() {
654 | if (currentId == R.id.infoWindowContainer1) {
655 | currentId = R.id.infoWindowContainer2;
656 | } else {
657 | currentId = R.id.infoWindowContainer1;
658 | }
659 |
660 | return currentId;
661 | }
662 |
663 | public void onSaveInstanceState(@NonNull final Bundle outState) {
664 | outState.putInt(BUNDLE_KEY_ID, currentId);
665 | }
666 | }
667 |
668 | /**
669 | * This method must be called from activity's or fragment's onSaveInstanceState(Bundle outState).
670 | * There is no need of calling this method if you are using
671 | * {@link com.appolica.interactiveinfowindow.fragment.MapInfoWindowFragment}
672 | *
673 | * @param outState Bundle from activity's of fragment's onSaveInstanceState(Bundle outState).
674 | */
675 | public void onSaveInstanceState(@NonNull final Bundle outState) {
676 | idProvider.onSaveInstanceState(outState);
677 | }
678 |
679 | /**
680 | * This method must be called from activity's or fragment's onDestroy().
681 | * There is no need of calling this method if you are using
682 | * {@link com.appolica.interactiveinfowindow.fragment.MapInfoWindowFragment}
683 | */
684 | public void onDestroy() {
685 |
686 | currentContainer = null;
687 | parent = null;
688 |
689 | }
690 |
691 | /**
692 | * Call this method in your onMapReady(GoogleMap googleMap) callback if you are not using
693 | * {@link com.appolica.interactiveinfowindow.fragment.MapInfoWindowFragment}.
694 | * Keep in mind that this method sets all camera listeners and map click listener
696 | * to the googleMap object and you shouldn't set them by yourself. However if you want
697 | * to listen for these events you can use the methods below:
699 | * {@link #setOnCameraMoveStartedListener(GoogleMap.OnCameraMoveStartedListener)}
700 | *
701 | * {@link #setOnCameraMoveCanceledListener(GoogleMap.OnCameraMoveCanceledListener)}
702 | *
703 | * {@link #setOnCameraMoveListener(GoogleMap.OnCameraMoveListener)}
704 | *
705 | * {@link #setOnCameraIdleListener(GoogleMap.OnCameraIdleListener)}
706 | *
true
if you want to hide your {@link InfoWindow}
878 | * when fling event occurs, pass false
if you want your window
879 | * to move with the map.
880 | */
881 | public void setHideOnFling(final boolean hideOnFling) {
882 | this.hideOnFling = hideOnFling;
883 | }
884 |
885 | /**
886 | * Interface definition for callbacks to be invoked when an {@link InfoWindow}'s
887 | * state has been changed.
888 | */
889 | public interface WindowShowListener {
890 | void onWindowShowStarted(@NonNull final InfoWindow infoWindow);
891 |
892 | void onWindowShown(@NonNull final InfoWindow infoWindow);
893 |
894 | void onWindowHideStarted(@NonNull final InfoWindow infoWindow);
895 |
896 | void onWindowHidden(@NonNull final InfoWindow infoWindow);
897 | }
898 |
899 | /**
900 | * Class containing {@link InfoWindow}'s container details.
901 | */
902 | public static class ContainerSpecification {
903 | private Drawable background;
904 |
905 | /**
906 | * Create a new instance of ContainerSpecification by providing the container background.
907 | *
908 | * @param background the background of the container.
909 | */
910 | public ContainerSpecification(Drawable background) {
911 | this.background = background;
912 | }
913 |
914 | /**
915 | * This is what is called to set the background of the container view.
916 | *
917 | * @return the background of the container view.
918 | */
919 | public Drawable getBackground() {
920 | return background;
921 | }
922 |
923 | public void setBackground(Drawable background) {
924 | this.background = background;
925 | }
926 | }
927 | }
928 |
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/java/com/appolica/interactiveinfowindow/animation/SimpleAnimationListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2016 Appolica Ltd.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under
10 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 |
15 | package com.appolica.interactiveinfowindow.animation;
16 |
17 | import android.view.animation.Animation;
18 |
19 | /**
20 | * Created by Bogomil Kolarov on 02.09.16.
21 | */
22 | public class SimpleAnimationListener implements Animation.AnimationListener {
23 | @Override
24 | public void onAnimationStart(Animation animation) {
25 |
26 | }
27 |
28 | @Override
29 | public void onAnimationEnd(Animation animation) {
30 |
31 | }
32 |
33 | @Override
34 | public void onAnimationRepeat(Animation animation) {
35 |
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/java/com/appolica/interactiveinfowindow/animation/SimpleAnimatorListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2016 Appolica Ltd.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under
10 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 |
15 | package com.appolica.interactiveinfowindow.animation;
16 |
17 | import android.animation.Animator;
18 |
19 | /**
20 | * Created by Bogomil Kolarov on 02.09.16.
21 | */
22 | public class SimpleAnimatorListener implements Animator.AnimatorListener {
23 | @Override
24 | public void onAnimationStart(Animator animator) {
25 |
26 | }
27 |
28 | @Override
29 | public void onAnimationEnd(Animator animator) {
30 |
31 | }
32 |
33 | @Override
34 | public void onAnimationCancel(Animator animator) {
35 |
36 | }
37 |
38 | @Override
39 | public void onAnimationRepeat(Animator animator) {
40 |
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/java/com/appolica/interactiveinfowindow/customview/DisallowInterceptLayout.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2016 Appolica Ltd.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under
10 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 |
15 | package com.appolica.interactiveinfowindow.customview;
16 |
17 | import android.annotation.TargetApi;
18 | import android.content.Context;
19 | import android.os.Build;
20 | import android.util.AttributeSet;
21 | import android.view.MotionEvent;
22 | import android.view.ViewParent;
23 | import android.widget.FrameLayout;
24 |
25 | /**
26 | * This class simply calls {@link android.view.ViewGroup#requestDisallowInterceptTouchEvent(boolean)}
27 | * in {@link android.view.ViewGroup#dispatchTouchEvent(MotionEvent)} and passes to it
28 | * {@link #disallowParentIntercept}.
29 | */
30 | public class DisallowInterceptLayout extends FrameLayout {
31 |
32 | private boolean disallowParentIntercept = false;
33 |
34 | public DisallowInterceptLayout(Context context) {
35 | super(context);
36 | }
37 |
38 | public DisallowInterceptLayout(Context context, AttributeSet attrs) {
39 | super(context, attrs);
40 | }
41 |
42 | public DisallowInterceptLayout(Context context, AttributeSet attrs, int defStyleAttr) {
43 | super(context, attrs, defStyleAttr);
44 | }
45 |
46 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
47 | public DisallowInterceptLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
48 | super(context, attrs, defStyleAttr, defStyleRes);
49 | }
50 |
51 | @Override
52 | public boolean dispatchTouchEvent(MotionEvent ev) {
53 | if (disallowParentIntercept) {
54 | final ViewParent parent = getParent();
55 | if (parent != null) {
56 | parent.requestDisallowInterceptTouchEvent(disallowParentIntercept);
57 | }
58 | }
59 | return super.dispatchTouchEvent(ev);
60 | }
61 |
62 | public void setDisallowParentIntercept(boolean disallowParentIntercept) {
63 | this.disallowParentIntercept = disallowParentIntercept;
64 | }
65 |
66 | public boolean willDisallowParentIntercept() {
67 | return disallowParentIntercept;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/java/com/appolica/interactiveinfowindow/customview/TouchInterceptFrameLayout.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2016 Appolica Ltd.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under
10 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 |
15 | package com.appolica.interactiveinfowindow.customview;
16 |
17 | import android.annotation.TargetApi;
18 | import android.content.Context;
19 | import android.os.Build;
20 | import android.util.AttributeSet;
21 | import android.view.GestureDetector;
22 | import android.view.MotionEvent;
23 | import android.widget.FrameLayout;
24 |
25 | public class TouchInterceptFrameLayout extends FrameLayout {
26 |
27 | private GestureDetector detector;
28 |
29 | public TouchInterceptFrameLayout(Context context) {
30 | super(context);
31 | }
32 |
33 | public TouchInterceptFrameLayout(Context context, AttributeSet attrs) {
34 | super(context, attrs);
35 | }
36 |
37 | public TouchInterceptFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
38 | super(context, attrs, defStyleAttr);
39 | }
40 |
41 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
42 | public TouchInterceptFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
43 | super(context, attrs, defStyleAttr, defStyleRes);
44 | }
45 |
46 | @Override
47 | public boolean onInterceptTouchEvent(MotionEvent ev) {
48 | if (detector != null) {
49 | detector.onTouchEvent(ev);
50 | }
51 | return super.onInterceptTouchEvent(ev);
52 | }
53 |
54 | @Override
55 | public boolean onTouchEvent(MotionEvent event) {
56 | if (detector != null) {
57 | detector.onTouchEvent(event);
58 | }
59 | return super.onTouchEvent(event);
60 | }
61 |
62 | public void setDetector(GestureDetector detector) {
63 | this.detector = detector;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/java/com/appolica/interactiveinfowindow/fragment/MapInfoWindowFragment.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2016 Appolica Ltd.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 | * except in compliance with the License.
6 | *
7 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under
10 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 | * either express or implied. See the License for the specific language governing permissions and
12 | * limitations under the License.
13 | */
14 |
15 | package com.appolica.interactiveinfowindow.fragment;
16 |
17 | import android.os.Bundle;
18 | import android.view.LayoutInflater;
19 | import android.view.View;
20 | import android.view.ViewGroup;
21 |
22 | import androidx.annotation.Nullable;
23 | import androidx.fragment.app.Fragment;
24 |
25 | import com.appolica.interactiveinfowindow.InfoWindow;
26 | import com.appolica.interactiveinfowindow.InfoWindowManager;
27 | import com.appolica.interactiveinfowindow.customview.TouchInterceptFrameLayout;
28 | import com.appolica.mapanimations.R;
29 | import com.google.android.gms.maps.GoogleMap;
30 | import com.google.android.gms.maps.OnMapReadyCallback;
31 | import com.google.android.gms.maps.SupportMapFragment;
32 |
33 | public class MapInfoWindowFragment extends Fragment {
34 |
35 | private static final String TAG = "MapInfoWindowFragment";
36 |
37 | private GoogleMap googleMap;
38 | private InfoWindowManager infoWindowManager;
39 |
40 | @Nullable
41 | @Override
42 | public View onCreateView(
43 | LayoutInflater inflater,
44 | @Nullable ViewGroup container,
45 | @Nullable Bundle savedInstanceState) {
46 |
47 | final View view = inflater.inflate(R.layout.fragment_map_infowindow, container, false);
48 |
49 | return view;
50 | }
51 |
52 | @Override
53 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
54 | super.onViewCreated(view, savedInstanceState);
55 |
56 | infoWindowManager = new InfoWindowManager(getChildFragmentManager());
57 | infoWindowManager.onParentViewCreated((TouchInterceptFrameLayout) view, savedInstanceState);
58 | }
59 |
60 | private void setUpMapIfNeeded() {
61 | // Do a null check to confirm that we have not already instantiated the map.
62 | if (googleMap == null) {
63 | // Try to obtain the map from the SupportMapFragment.
64 |
65 | final SupportMapFragment mapFragment =
66 | (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.mapFragment);
67 |
68 | mapFragment
69 | .getMapAsync(new OnMapReadyCallback() {
70 | @Override
71 | public void onMapReady(GoogleMap googleMap) {
72 | MapInfoWindowFragment.this.googleMap = googleMap;
73 | setUpMap();
74 | }
75 | });
76 | }
77 | }
78 |
79 | private void setUpMap() {
80 | infoWindowManager.onMapReady(googleMap);
81 | }
82 |
83 | /**
84 | * Get the {@link InfoWindowManager}, used for showing/hiding and positioning the
85 | * {@link InfoWindow}.
86 | *
87 | * @return The {@link InfoWindowManager}
88 | */
89 | public InfoWindowManager infoWindowManager() {
90 | return infoWindowManager;
91 | }
92 |
93 | @Override
94 | public void onResume() {
95 | super.onResume();
96 | setUpMapIfNeeded();
97 | }
98 |
99 | @Override
100 | public void onSaveInstanceState(Bundle outState) {
101 | super.onSaveInstanceState(outState);
102 | infoWindowManager.onSaveInstanceState(outState);
103 | }
104 |
105 | @Override
106 | public void onDestroyView() {
107 | super.onDestroyView();
108 | infoWindowManager.onDestroy();
109 | }
110 |
111 | /**
112 | * Use this method to get the {@link GoogleMap} object asynchronously from our fragment.
113 | *
114 | * @param onMapReadyCallback The callback that will be called providing you the GoogleMap
115 | * object.
116 | */
117 | public void getMapAsync(OnMapReadyCallback onMapReadyCallback) {
118 | final SupportMapFragment mapFragment =
119 | (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.mapFragment);
120 |
121 | mapFragment.getMapAsync(onMapReadyCallback);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/drawable-hdpi/infowindow_background.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Appolica/InteractiveInfoWindowAndroid/1566be134b7443f1485ce3e3f1e4df759229cbb2/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/drawable-hdpi/infowindow_background.9.png
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/drawable-mdpi/infowindow_background.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Appolica/InteractiveInfoWindowAndroid/1566be134b7443f1485ce3e3f1e4df759229cbb2/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/drawable-mdpi/infowindow_background.9.png
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/drawable-xhdpi/infowindow_background.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Appolica/InteractiveInfoWindowAndroid/1566be134b7443f1485ce3e3f1e4df759229cbb2/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/drawable-xhdpi/infowindow_background.9.png
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/drawable-xxhdpi/infowindow_background.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Appolica/InteractiveInfoWindowAndroid/1566be134b7443f1485ce3e3f1e4df759229cbb2/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/drawable-xxhdpi/infowindow_background.9.png
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/drawable-xxxhdpi/infowindow_background.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Appolica/InteractiveInfoWindowAndroid/1566be134b7443f1485ce3e3f1e4df759229cbb2/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/drawable-xxxhdpi/infowindow_background.9.png
--------------------------------------------------------------------------------
/InteractiveInfoWindowAndroid/interactive-info-window/src/main/res/layout/fragment_map_infowindow.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |