Skip to content

Commit d89b0b8

Browse files
danielgindirborn
authored andcommitted
Performance improvements for tracksViewChanges (react-native-maps#2487)
* Avoid unnecessary redraws and bitmap allocations * Avoid drawing empty background of ViewAttacherGroup * Moved some ViewAttacherGroup initialization to constructor * Some documentation * Added missing `setTracksViewChanges` (to allow disabling tracksViewChanges) * Added new `redraw` method on MapMarker to cause a redraw of custom marker * Let it render one more time when toggling tracksViewChanges to false
1 parent d0d6f30 commit d89b0b8

File tree

10 files changed

+153
-37
lines changed

10 files changed

+153
-37
lines changed

docs/marker.md

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ To access event data, you will need to use `e.nativeEvent`. For example, `onPres
4444
| `showCallout` | | Shows the callout for this marker
4545
| `hideCallout` | | Hides the callout for this marker
4646
| `animateMarkerToCoordinate` | `coordinate: LatLng, duration: number` | Animates marker movement. **Note**: Android only
47+
| `redraw` | | Causes a redraw of the marker. Useful when there are updates to the marker and `tracksViewChanges` comes with a cost that is too high.
4748

4849

4950

lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java

+78-22
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import android.graphics.Bitmap;
55
import android.graphics.BitmapFactory;
66
import android.graphics.Canvas;
7+
import android.graphics.Color;
78
import android.graphics.drawable.Animatable;
89
import android.graphics.drawable.Drawable;
910
import android.net.Uri;
@@ -75,6 +76,7 @@ public class AirMapMarker extends AirMapFeature {
7576

7677
private boolean tracksViewChanges = true;
7778
private boolean tracksViewChangesActive = false;
79+
private boolean hasViewChanges = true;
7880

7981
private boolean hasCustomMarkerView = false;
8082

@@ -108,7 +110,7 @@ public void onFinalImageSet(
108110
CloseableReference.closeSafely(imageReference);
109111
}
110112
}
111-
update();
113+
update(true);
112114
}
113115
};
114116

@@ -150,12 +152,12 @@ public void setCoordinate(ReadableMap coordinate) {
150152
if (marker != null) {
151153
marker.setPosition(position);
152154
}
153-
update();
155+
update(false);
154156
}
155157

156158
public void setIdentifier(String identifier) {
157159
this.identifier = identifier;
158-
update();
160+
update(false);
159161
}
160162

161163
public String getIdentifier() {
@@ -167,60 +169,60 @@ public void setTitle(String title) {
167169
if (marker != null) {
168170
marker.setTitle(title);
169171
}
170-
update();
172+
update(false);
171173
}
172174

173175
public void setSnippet(String snippet) {
174176
this.snippet = snippet;
175177
if (marker != null) {
176178
marker.setSnippet(snippet);
177179
}
178-
update();
180+
update(false);
179181
}
180182

181183
public void setRotation(float rotation) {
182184
this.rotation = rotation;
183185
if (marker != null) {
184186
marker.setRotation(rotation);
185187
}
186-
update();
188+
update(false);
187189
}
188190

189191
public void setFlat(boolean flat) {
190192
this.flat = flat;
191193
if (marker != null) {
192194
marker.setFlat(flat);
193195
}
194-
update();
196+
update(false);
195197
}
196198

197199
public void setDraggable(boolean draggable) {
198200
this.draggable = draggable;
199201
if (marker != null) {
200202
marker.setDraggable(draggable);
201203
}
202-
update();
204+
update(false);
203205
}
204206

205207
public void setZIndex(int zIndex) {
206208
this.zIndex = zIndex;
207209
if (marker != null) {
208210
marker.setZIndex(zIndex);
209211
}
210-
update();
212+
update(false);
211213
}
212214

213215
public void setOpacity(float opacity) {
214216
this.opacity = opacity;
215217
if (marker != null) {
216218
marker.setAlpha(opacity);
217219
}
218-
update();
220+
update(false);
219221
}
220222

221223
public void setMarkerHue(float markerHue) {
222224
this.markerHue = markerHue;
223-
update();
225+
update(false);
224226
}
225227

226228
public void setAnchor(double x, double y) {
@@ -230,7 +232,7 @@ public void setAnchor(double x, double y) {
230232
if (marker != null) {
231233
marker.setAnchor(anchorX, anchorY);
232234
}
233-
update();
235+
update(false);
234236
}
235237

236238
public void setCalloutAnchor(double x, double y) {
@@ -240,7 +242,7 @@ public void setCalloutAnchor(double x, double y) {
240242
if (marker != null) {
241243
marker.setInfoWindowAnchor(calloutAnchorX, calloutAnchorY);
242244
}
243-
update();
245+
update(false);
244246
}
245247

246248
public void setTracksViewChanges(boolean tracksViewChanges) {
@@ -257,18 +259,36 @@ private void updateTracksViewChanges() {
257259
ViewChangesTracker.getInstance().addMarker(this);
258260
} else {
259261
ViewChangesTracker.getInstance().removeMarker(this);
262+
263+
// Let it render one more time to avoid race conditions.
264+
// i.e. Image onLoad ->
265+
// ViewChangesTracker may not get a chance to render ->
266+
// setState({ tracksViewChanges: false }) ->
267+
// image loaded but not rendered.
268+
updateMarkerIcon();
260269
}
261270
}
262271

263-
public boolean updateCustomMarkerIcon() {
272+
public boolean updateCustomForTracking() {
264273
if (!tracksViewChangesActive)
265274
return false;
266275

267-
marker.setIcon(getIcon());
276+
updateMarkerIcon();
268277

269278
return true;
270279
}
271280

281+
public void updateMarkerIcon() {
282+
if (!hasViewChanges) return;
283+
284+
if (!hasCustomMarkerView) {
285+
// No more updates for this, as it's a simple icon
286+
hasViewChanges = false;
287+
}
288+
289+
marker.setIcon(getIcon());
290+
}
291+
272292
public LatLng interpolate(float fraction, LatLng a, LatLng b) {
273293
double lat = (b.latitude - a.latitude) * fraction + a.latitude;
274294
double lng = (b.longitude - a.longitude) * fraction + a.longitude;
@@ -293,9 +313,11 @@ public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
293313
}
294314

295315
public void setImage(String uri) {
316+
hasViewChanges = true;
317+
296318
if (uri == null) {
297319
iconBitmapDescriptor = null;
298-
update();
320+
update(true);
299321
} else if (uri.startsWith("http://") || uri.startsWith("https://") ||
300322
uri.startsWith("file://") || uri.startsWith("asset://")) {
301323
ImageRequest imageRequest = ImageRequestBuilder
@@ -323,7 +345,7 @@ public void setImage(String uri) {
323345
drawable.draw(canvas);
324346
}
325347
}
326-
update();
348+
update(true);
327349
}
328350
}
329351

@@ -344,7 +366,21 @@ public void addView(View child, int index) {
344366
hasCustomMarkerView = true;
345367
updateTracksViewChanges();
346368
}
347-
update();
369+
update(true);
370+
}
371+
372+
@Override
373+
public void requestLayout() {
374+
super.requestLayout();
375+
376+
if (getChildCount() == 0) {
377+
if (hasCustomMarkerView) {
378+
hasCustomMarkerView = false;
379+
clearDrawableCache();
380+
updateTracksViewChanges();
381+
update(true);
382+
}
383+
}
348384
}
349385

350386
@Override
@@ -404,12 +440,13 @@ private MarkerOptions fillMarkerOptions(MarkerOptions options) {
404440
return options;
405441
}
406442

407-
public void update() {
443+
public void update(boolean updateIcon) {
408444
if (marker == null) {
409445
return;
410446
}
411447

412-
marker.setIcon(getIcon());
448+
if (updateIcon)
449+
updateMarkerIcon();
413450

414451
if (anchorIsSet) {
415452
marker.setAnchor(anchorX, anchorY);
@@ -427,14 +464,33 @@ public void update() {
427464
public void update(int width, int height) {
428465
this.width = width;
429466
this.height = height;
430-
update();
467+
468+
update(true);
469+
}
470+
471+
private Bitmap mLastBitmapCreated = null;
472+
473+
private void clearDrawableCache() {
474+
mLastBitmapCreated = null;
431475
}
432476

433477
private Bitmap createDrawable() {
434478
int width = this.width <= 0 ? 100 : this.width;
435479
int height = this.height <= 0 ? 100 : this.height;
436480
this.buildDrawingCache();
437-
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
481+
482+
// Do not create the doublebuffer-bitmap each time. reuse it to save memory.
483+
Bitmap bitmap = mLastBitmapCreated;
484+
485+
if (bitmap == null ||
486+
bitmap.isRecycled() ||
487+
bitmap.getWidth() != width ||
488+
bitmap.getHeight() != height) {
489+
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
490+
mLastBitmapCreated = bitmap;
491+
} else {
492+
bitmap.eraseColor(Color.TRANSPARENT);
493+
}
438494

439495
Canvas canvas = new Canvas(bitmap);
440496
this.draw(canvas);

lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarkerManager.java

+15-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class AirMapMarkerManager extends ViewGroupManager<AirMapMarker> {
2323
private static final int SHOW_INFO_WINDOW = 1;
2424
private static final int HIDE_INFO_WINDOW = 2;
2525
private static final int ANIMATE_MARKER_TO_COORDINATE = 3;
26+
private static final int REDRAW = 4;
2627

2728
public AirMapMarkerManager() {
2829
}
@@ -133,6 +134,11 @@ public void setOpacity(AirMapMarker view, float opacity) {
133134
view.setOpacity(opacity);
134135
}
135136

137+
@ReactProp(name = "tracksViewChanges", defaultBoolean = true)
138+
public void setTracksViewChanges(AirMapMarker view, boolean tracksViewChanges) {
139+
view.setTracksViewChanges(tracksViewChanges);
140+
}
141+
136142
@Override
137143
public void addView(AirMapMarker parent, View child, int index) {
138144
// if an <Callout /> component is a child, then it is a callout view, NOT part of the
@@ -141,14 +147,14 @@ public void addView(AirMapMarker parent, View child, int index) {
141147
parent.setCalloutView((AirMapCallout) child);
142148
} else {
143149
super.addView(parent, child, index);
144-
parent.update();
150+
parent.update(true);
145151
}
146152
}
147153

148154
@Override
149155
public void removeViewAt(AirMapMarker parent, int index) {
150156
super.removeViewAt(parent, index);
151-
parent.update();
157+
parent.update(true);
152158
}
153159

154160
@Override
@@ -157,7 +163,8 @@ public Map<String, Integer> getCommandsMap() {
157163
return MapBuilder.of(
158164
"showCallout", SHOW_INFO_WINDOW,
159165
"hideCallout", HIDE_INFO_WINDOW,
160-
"animateMarkerToCoordinate", ANIMATE_MARKER_TO_COORDINATE
166+
"animateMarkerToCoordinate", ANIMATE_MARKER_TO_COORDINATE,
167+
"redraw", REDRAW
161168
);
162169
}
163170

@@ -176,7 +183,7 @@ public void receiveCommand(AirMapMarker view, int commandId, @Nullable ReadableA
176183
case HIDE_INFO_WINDOW:
177184
((Marker) view.getFeature()).hideInfoWindow();
178185
break;
179-
186+
180187
case ANIMATE_MARKER_TO_COORDINATE:
181188
region = args.getMap(0);
182189
duration = args.getInt(1);
@@ -185,6 +192,10 @@ public void receiveCommand(AirMapMarker view, int commandId, @Nullable ReadableA
185192
lat = region.getDouble("latitude");
186193
view.animateToCoodinate(new LatLng(lat, lng), duration);
187194
break;
195+
196+
case REDRAW:
197+
view.updateMarkerIcon();
198+
break;
188199
}
189200
}
190201

lib/android/src/main/java/com/airbnb/android/react/maps/AirMapView.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,6 @@ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
183183
attacherLayoutParams.leftMargin = 99999999;
184184
attacherLayoutParams.topMargin = 99999999;
185185
attacherGroup.setLayoutParams(attacherLayoutParams);
186-
attacherGroup.setVisibility(VISIBLE);
187-
attacherGroup.setAlpha(0.0f);
188-
attacherGroup.setRemoveClippedSubviews(false);
189-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
190-
attacherGroup.setClipBounds(new Rect(0, 0, 0, 0));
191-
}
192-
attacherGroup.setOverflow("hidden");
193186
addView(attacherGroup);
194187
}
195188

@@ -540,9 +533,16 @@ public void addFeature(View child, int index) {
540533
annotation.addToMap(map);
541534
features.add(index, annotation);
542535

536+
// Allow visibility event to be triggered later
543537
int visibility = annotation.getVisibility();
544538
annotation.setVisibility(INVISIBLE);
539+
540+
// Add to the parent group
545541
attacherGroup.addView(annotation);
542+
543+
// Trigger visibility event if necessary.
544+
// With some testing, seems like it is not always
545+
// triggered just by being added to a parent view.
546546
annotation.setVisibility(visibility);
547547

548548
Marker marker = (Marker) annotation.getFeature();

lib/android/src/main/java/com/airbnb/android/react/maps/ViewAttacherGroup.java

+13
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
package com.airbnb.android.react.maps;
22

33
import android.content.Context;
4+
import android.graphics.Canvas;
5+
import android.graphics.Rect;
6+
import android.os.Build;
47

8+
import com.facebook.react.uimanager.ViewProps;
59
import com.facebook.react.views.view.ReactViewGroup;
610

711
public class ViewAttacherGroup extends ReactViewGroup {
812

913
public ViewAttacherGroup(Context context) {
1014
super(context);
15+
16+
this.setWillNotDraw(true);
17+
this.setVisibility(VISIBLE);
18+
this.setAlpha(0.0f);
19+
this.setRemoveClippedSubviews(false);
20+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
21+
this.setClipBounds(new Rect(0, 0, 0, 0));
22+
}
23+
this.setOverflow("hidden"); // Change to ViewProps.HIDDEN until RN 0.57 is base
1124
}
1225

1326
// This should make it more performant, avoid trying to hard to overlap layers with opacity.

0 commit comments

Comments
 (0)