Skip to content

Instantly share code, notes, and snippets.

@corsair992
Last active April 11, 2016 09:44
Show Gist options
  • Save corsair992/8313269 to your computer and use it in GitHub Desktop.
Save corsair992/8313269 to your computer and use it in GitHub Desktop.
This is a wrapper for MapView that can enable live custom info windows in the Android Google Maps API v2. It accomplishes this by wrapping both the MapView and info window View. The MapView wrapper passes on touch events to the info window if one is active, while the info window wrapper refreshes the info window by calling showInfoWindow() on th…
package com.example.liveinfowindow;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.InfoWindowAdapter;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
/*
* This code and the idea behind it was derived from this StackOverflow
* answer: http://stackoverflow.com/a/15040761/3153792
*/
public class MapViewWrapper extends FrameLayout {
private static final int REFRESH_BUFFER_INTERVAL = 150;
private static final Matrix matrix = new Matrix();
private GoogleMap map;
private InfoWindowAdapter infoWindowAdapter, infoWindowAdapterWrapper;
private Map<Marker, Point> markerInfoWindowAnchorOffsetMap;
private Marker marker;
private Point infoWindowAnchorOffset;
private View infoView;
private boolean useCachedInfoView;
public MapViewWrapper(Context context) {
super(context);
}
public MapViewWrapper(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MapViewWrapper(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setInfoWindowAdapter(GoogleMap map, InfoWindowAdapter adapter) {
if (this.map != null && this.map != map) {
map.setInfoWindowAdapter(null);
clearMarkers();
}
this.map = map;
infoWindowAdapter = adapter;
if (infoWindowAdapterWrapper == null)
infoWindowAdapterWrapper = new InfoWindowAdapterWrapper();
map.setInfoWindowAdapter(infoWindowAdapterWrapper);
}
public void removeInfoWindowAdapter() {
infoWindowAdapter = null;
infoWindowAdapterWrapper = null;
map.setInfoWindowAdapter(null);
}
public void addMarker(Marker marker, MarkerOptions options, int iconResId) {
Drawable icon = getContext().getResources().getDrawable(iconResId);
addMarker(marker, options, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
}
public void addMarker(Marker marker, MarkerOptions options, Bitmap icon) {
addMarker(marker, options, icon.getWidth(), icon.getHeight());
}
public void addMarker(Marker marker, MarkerOptions options, int iconWidth, int iconHeight) {
if (map == null)
throw new IllegalStateException("GoogleMap not initialized");
if (markerInfoWindowAnchorOffsetMap == null)
markerInfoWindowAnchorOffsetMap = new HashMap<Marker, Point>();
Point infoWindowAnchorOffset = markerInfoWindowAnchorOffsetMap.get(marker);
if (infoWindowAnchorOffset == null) markerInfoWindowAnchorOffsetMap.put(
marker, infoWindowAnchorOffset = new Point());
float[] anchorPoints = new float[] {
iconWidth * options.getAnchorU(),
iconHeight * options.getAnchorV(),
iconWidth * options.getInfoWindowAnchorU(),
iconHeight * options.getInfoWindowAnchorV()
};
matrix.setRotate(options.getRotation(), anchorPoints[0], anchorPoints[1]);
matrix.mapPoints(anchorPoints);
infoWindowAnchorOffset.set(
Math.round(anchorPoints[2]) - Math.round(anchorPoints[0]),
Math.round(anchorPoints[3]) - Math.round(anchorPoints[1])
);
}
public void removeMarker(Marker marker) {
markerInfoWindowAnchorOffsetMap.remove(marker);
if (this.marker == marker) {
this.marker = null;
infoWindowAnchorOffset = null;
infoView = null;
removeInfoWindow();
}
}
public void clearMarkers() {
markerInfoWindowAnchorOffsetMap.clear();
marker = null;
infoWindowAnchorOffset = null;
infoView = null;
removeInfoWindow();
}
private void removeInfoWindow() {
for (int i = 0, count = getChildCount(); i < count; i++) {
if (getChildAt(i) instanceof InfoViewWrapper) {
removeViewAt(i);
break;
}
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (map != null && marker != null && infoView != null && marker.isInfoWindowShown()) {
Point infoViewCoords = map.getProjection().toScreenLocation(marker.getPosition());
infoViewCoords.offset(infoWindowAnchorOffset.x - (infoView.getWidth() / 2),
infoWindowAnchorOffset.y - infoView.getHeight());
ev.offsetLocation(-infoViewCoords.x, -infoViewCoords.y);
boolean handled = infoView.dispatchTouchEvent(ev);
ev.offsetLocation(infoViewCoords.x, infoViewCoords.y);
if (handled) return true;
}
return super.dispatchTouchEvent(ev);
}
private class InfoWindowAdapterWrapper implements InfoWindowAdapter {
private View infoWindow, infoContents;
@Override
public View getInfoWindow(Marker marker) {
if (useCachedInfoView) useCachedInfoView = false;
else {
View infoWindow = infoWindowAdapter.getInfoWindow(marker);
if (infoWindow == null) return null;
infoWindowAnchorOffset = markerInfoWindowAnchorOffsetMap.get(marker);
if (infoWindowAnchorOffset == null) return infoWindow;
new InfoViewWrapper(infoWindow);
this.infoWindow = infoWindow;
}
MapViewWrapper.this.marker = marker;
return infoWindow;
}
@Override
public View getInfoContents(Marker marker) {
if (useCachedInfoView) useCachedInfoView = false;
else {
View infoContents = infoWindowAdapter.getInfoContents(marker);
if (infoContents == null) return null;
infoWindowAnchorOffset = markerInfoWindowAnchorOffsetMap.get(marker);
if (infoWindowAnchorOffset == null) return infoContents;
new InfoViewWrapper(infoContents);
this.infoContents = infoContents;
}
MapViewWrapper.this.marker = marker;
return infoContents;
}
}
private class InfoViewWrapper extends FrameLayout {
private long lastRefreshTime;
private boolean isRefreshing;
public InfoViewWrapper(View infoView) {
super(infoView.getContext());
addView(infoView, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
setVisibility(GONE);
removeInfoWindow();
MapViewWrapper.this.addView(this,
new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
MapViewWrapper.this.infoView = infoView;
}
@Override
public void requestLayout() {
refreshInfoWindow();
}
@Override
public void forceLayout() {
refreshInfoWindow();
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
refreshInfoWindow();
return null;
}
@Override
public void invalidate() {
if (marker == null || !marker.isInfoWindowShown()) return;
useCachedInfoView = true;
marker.showInfoWindow();
isRefreshing = false;
lastRefreshTime = SystemClock.uptimeMillis();
}
private void refreshInfoWindow() {
if (marker == null || !marker.isInfoWindowShown() || isRefreshing) return;
postInvalidateDelayed(REFRESH_BUFFER_INTERVAL -
(SystemClock.uptimeMillis() - lastRefreshTime));
isRefreshing = true;
}
}
}
@hateum
Copy link

hateum commented Mar 24, 2016

How we can use it ?
there is an example ?

I always have a crash:

java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
                                                                      at android.view.ViewGroup.addViewInner(ViewGroup.java:4273)
                                                                      at android.view.ViewGroup.addView(ViewGroup.java:4123)
                                                                      at android.view.ViewGroup.addView(ViewGroup.java:4064)
                                                                      at android.view.ViewGroup.addView(ViewGroup.java:4037)
                                                                      at maps.ei.h.a(Unknown Source)
                                                                      at maps.ei.h.a(Unknown Source)
                                                                      at maps.dg.c.b(Unknown Source)
                                                                      at maps.dg.d.c(Unknown Source)
                                                                      at maps.ei.s.g(Unknown Source)
                                                                      at maps.ei.t.b(Unknown Source)
                                                                      at maps.dg.c.a(Unknown Source)
                                                                      at maps.dz.w.a(Unknown Source)
                                                                      at maps.dz.ae.a(Unknown Source)
                                                                      at maps.dz.ad.a(Unknown Source)
                                                                      at maps.dz.an.d(Unknown Source)
                                                                      at maps.dz.q.onSingleTapConfirmed(Unknown Source)
                                                                      at maps.dc.f.onSingleTapConfirmed(Unknown Source)
                                                                      at maps.dc.g$a.handleMessage(Unknown Source)
                                                                      at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                      at android.os.Looper.loop(Looper.java:145)
                                                                      at android.app.ActivityThread.main(ActivityThread.java:6837)
                                                                      at java.lang.reflect.Method.invoke(Native Method)
                                                                      at java.lang.reflect.Method.invoke(Method.java:372)
                                                                      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1404)
                                                                      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1199)

@henriquedesousa
Copy link

Yes, a proof-of-concept app will be very useful. I already "lost" some time implementing chose007's solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment