[android] - add binding integration for expressions (#10654)

This commit is contained in:
Tobrun
2017-12-19 08:55:48 +01:00
committed by GitHub
parent 9759ba7836
commit 757cc0f2be
13 changed files with 4144 additions and 173 deletions

View File

@@ -5,14 +5,15 @@ import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import android.widget.Toast;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.style.functions.stops.Stops;
import com.mapbox.mapboxsdk.style.layers.FillLayer;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
import com.mapbox.mapboxsdk.style.sources.Source;
@@ -23,13 +24,16 @@ import java.io.IOException;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.style.functions.Function.composite;
import static com.mapbox.mapboxsdk.style.functions.Function.property;
import static com.mapbox.mapboxsdk.style.functions.Function.zoom;
import static com.mapbox.mapboxsdk.style.functions.stops.Stop.stop;
import static com.mapbox.mapboxsdk.style.functions.stops.Stops.categorical;
import static com.mapbox.mapboxsdk.style.functions.stops.Stops.exponential;
import static com.mapbox.mapboxsdk.style.functions.stops.Stops.interval;
import static com.mapbox.mapboxsdk.style.expressions.Expression.color;
import static com.mapbox.mapboxsdk.style.expressions.Expression.exponential;
import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate;
import static com.mapbox.mapboxsdk.style.expressions.Expression.linear;
import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
import static com.mapbox.mapboxsdk.style.expressions.Expression.match;
import static com.mapbox.mapboxsdk.style.expressions.Expression.step;
import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillAntialias;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillOpacity;
@@ -63,12 +67,26 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
// Add a parks layer
addParksLayer();
// Add debug overlay
setupDebugZoomView();
// Center and Zoom (Amsterdam, zoomed to streets)
mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(52.379189, 4.899431), 14));
}
});
}
private void setupDebugZoomView() {
final TextView textView = (TextView) findViewById(R.id.textZoom);
mapboxMap.setOnCameraChangeListener(new MapboxMap.OnCameraChangeListener() {
@Override
public void onCameraChange(CameraPosition position) {
textView.setText(String.format(getString(R.string.debug_zoom), position.zoom));
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_data_driven_style, menu);
@@ -159,12 +177,11 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
assert layer != null;
layer.setProperties(
fillColor(
zoom(
exponential(
stop(1, fillColor(Color.RED)),
stop(5, fillColor(Color.BLUE)),
stop(10, fillColor(Color.GREEN))
).withBase(0.5f)
interpolate(
exponential(0.5f), zoom(),
stop(1, color(Color.RED)),
stop(5, color(Color.BLUE)),
stop(10, color(Color.GREEN))
)
)
);
@@ -178,12 +195,11 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
assert layer != null;
layer.setProperties(
fillColor(
zoom(
interval(
stop(1, fillColor(Color.RED)),
stop(5, fillColor(Color.BLUE)),
stop(10, fillColor(Color.GREEN))
)
step(zoom(),
color(Color.CYAN),
stop(1, color(Color.RED)),
stop(5, color(Color.BLUE)),
stop(10, color(Color.GREEN))
)
)
);
@@ -197,13 +213,12 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
assert layer != null;
layer.setProperties(
fillColor(
property(
"stroke-width",
exponential(
stop(1f, fillColor(Color.RED)),
stop(5f, fillColor(Color.BLUE)),
stop(10f, fillColor(Color.GREEN))
).withBase(0.5f)
interpolate(
exponential(0.5f),
get("stroke-width"),
stop(1f, color(Color.RED)),
stop(5f, color(Color.BLUE)),
stop(10f, color(Color.GREEN))
)
)
);
@@ -217,13 +232,13 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
assert layer != null;
layer.setProperties(
fillColor(
property(
"name",
categorical(
stop("Westerpark", fillColor(Color.RED)),
stop("Jordaan", fillColor(Color.BLUE)),
stop("Prinseneiland", fillColor(Color.GREEN))
))
match(
get("name"),
literal("Westerpark"), color(Color.RED),
literal("Jordaan"), color(Color.BLUE),
literal("Prinseneiland"), color(Color.GREEN),
color(Color.CYAN)
)
)
);
@@ -236,9 +251,7 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
assert layer != null;
layer.setProperties(
fillOpacity(
property(
"fill-opacity",
Stops.<Float>identity())
get("fill-opacity")
)
);
@@ -251,13 +264,13 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
assert layer != null;
layer.setProperties(
fillColor(
property(
"stroke-width",
interval(
stop(1f, fillColor(Color.RED)),
stop(5f, fillColor(Color.BLUE)),
stop(10f, fillColor(Color.GREEN))
))
step(
get("stroke-width"),
color(Color.CYAN),
stop(1f, color(Color.RED)),
stop(2f, color(Color.BLUE)),
stop(3f, color(Color.GREEN))
)
)
);
@@ -270,16 +283,30 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
assert layer != null;
layer.setProperties(
fillColor(
composite(
"stroke-width",
exponential(
stop(1, 1, fillColor(Color.RED)),
stop(10, 2, fillColor(Color.BLUE)),
stop(22, 3, fillColor(Color.GREEN)),
stop(1, 1, fillColor(Color.CYAN)),
stop(10, 2, fillColor(Color.GRAY)),
stop(22, 3, fillColor(Color.YELLOW))
).withBase(1f)
interpolate(
exponential(1f),
zoom(),
stop(12, step(
get("stroke-width"),
color(Color.BLACK),
stop(1f, color(Color.RED)),
stop(2f, color(Color.WHITE)),
stop(3f, color(Color.BLUE))
)),
stop(15, step(
get("stroke-width"),
color(Color.BLACK),
stop(1f, color(Color.YELLOW)),
stop(2f, color(Color.LTGRAY)),
stop(3f, color(Color.CYAN))
)),
stop(18, step(
get("stroke-width"),
color(Color.BLACK),
stop(1f, color(Color.WHITE)),
stop(2f, color(Color.GRAY)),
stop(3f, color(Color.GREEN)))
)
)
)
);
@@ -288,21 +315,36 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
}
private void addCompositeIntervalFunction() {
Timber.i("Add composite exponential function");
Timber.i("Add composite interval function");
FillLayer layer = mapboxMap.getLayerAs(AMSTERDAM_PARKS_LAYER);
assert layer != null;
layer.setProperties(
fillColor(
composite(
"stroke-width",
interval(
stop(1, 1, fillColor(Color.RED)),
stop(10, 2, fillColor(Color.BLUE)),
stop(22, 3, fillColor(Color.GREEN)),
stop(1, 1, fillColor(Color.CYAN)),
stop(10, 2, fillColor(Color.GRAY)),
stop(22, 3, fillColor(Color.YELLOW))
interpolate(
linear(),
zoom(),
stop(12, step(
get("stroke-width"),
color(Color.BLACK),
stop(1f, color(Color.RED)),
stop(2f, color(Color.WHITE)),
stop(3f, color(Color.BLUE))
)),
stop(15, step(
get("stroke-width"),
color(Color.BLACK),
stop(1f, color(Color.YELLOW)),
stop(2f, color(Color.LTGRAY)),
stop(3f, color(Color.CYAN))
)),
stop(18, step(
get("stroke-width"),
color(Color.BLACK),
stop(1f, color(Color.WHITE)),
stop(2f, color(Color.GRAY)),
stop(3f, color(Color.GREEN))
))
)
)
);
@@ -315,30 +357,92 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
assert layer != null;
layer.setProperties(
fillColor(
composite(
"name",
categorical(
stop(7f, "Westerpark", fillColor(Color.RED)),
stop(8f, "Westerpark", fillColor(Color.BLUE)),
stop(9f, "Westerpark", fillColor(Color.RED)),
stop(10f, "Westerpark", fillColor(Color.BLUE)),
stop(11f, "Westerpark", fillColor(Color.RED)),
stop(12f, "Westerpark", fillColor(Color.BLUE)),
stop(13f, "Westerpark", fillColor(Color.RED)),
stop(14f, "Westerpark", fillColor(Color.BLUE)),
stop(15f, "Westerpark", fillColor(Color.RED)),
stop(16f, "Westerpark", fillColor(Color.BLUE)),
stop(17f, "Westerpark", fillColor(Color.RED)),
stop(18f, "Westerpark", fillColor(Color.BLUE)),
stop(19f, "Westerpark", fillColor(Color.RED)),
stop(20f, "Westerpark", fillColor(Color.BLUE)),
stop(21f, "Westerpark", fillColor(Color.RED)),
stop(22f, "Westerpark", fillColor(Color.BLUE)),
stop(14f, "Jordaan", fillColor(Color.GREEN)),
stop(18f, "Jordaan", fillColor(Color.CYAN)),
stop(14f, "Prinseneiland", fillColor(Color.WHITE)),
stop(18f, "Prinseneiland", fillColor(Color.BLACK))
step(zoom(),
color(Color.BLACK),
stop(7f, match(
get("name"),
literal("Westerpark"), color(Color.RED),
color(Color.BLACK)
)),
stop(8f, match(
get("name"),
literal("Westerpark"), color(Color.BLUE),
color(Color.BLACK)
)),
stop(9f, match(
get("name"),
literal("Westerpark"), color(Color.RED),
color(Color.BLACK)
)),
stop(10f, match(
get("name"),
literal("Westerpark"), color(Color.BLUE),
color(Color.BLACK)
)),
stop(11f, match(
get("name"),
literal("Westerpark"), color(Color.RED),
color(Color.BLACK)
)),
stop(12f, match(
get("name"),
literal("Westerpark"), color(Color.BLUE),
color(Color.BLACK)
)),
stop(13f, match(
get("name"),
literal("Westerpark"), color(Color.RED),
color(Color.BLACK)
)),
stop(14f, match(
get("name"),
literal("Westerpark"), color(Color.BLUE),
literal("Jordaan"), color(Color.GREEN),
literal("PrinsenEiland"), color(Color.WHITE),
color(Color.BLACK)
)),
stop(15f, match(
get("name"),
literal("Westerpark"), color(Color.RED),
color(Color.BLACK)
)),
stop(16f, match(
get("name"),
literal("Westerpark"), color(Color.BLUE),
color(Color.BLACK)
)),
stop(17f, match(
get("name"),
literal("Westerpark"), color(Color.RED),
color(Color.BLACK)
)),
stop(18f, match(
get("name"),
literal("Westerpark"), color(Color.BLUE),
literal("Jordaan"), color(Color.CYAN),
color(Color.BLACK)
)),
stop(19f, match(
get("name"),
literal("Westerpark"), color(Color.RED),
color(Color.BLACK)
)),
stop(20f, match(
get("name"),
literal("Westerpark"), color(Color.BLUE),
color(Color.BLACK)
)),
stop(21f, match(
get("name"),
literal("Westerpark"), color(Color.RED),
color(Color.BLACK)
)),
stop(22f, match(
get("name"),
literal("Westerpark"), color(Color.BLUE),
color(Color.BLACK)
))
)
)
);
@@ -359,7 +463,6 @@ public class DataDrivenStyleActivity extends AppCompatActivity {
return;
}
// Add a fill layer
mapboxMap.addLayer(new FillLayer(AMSTERDAM_PARKS_LAYER, source.getId())
.withProperties(

View File

@@ -13,9 +13,6 @@ import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.style.functions.Function;
import com.mapbox.mapboxsdk.style.functions.stops.ExponentialStops;
import com.mapbox.mapboxsdk.style.functions.stops.Stop;
import com.mapbox.mapboxsdk.style.layers.CircleLayer;
import com.mapbox.mapboxsdk.style.layers.FillLayer;
import com.mapbox.mapboxsdk.style.layers.Layer;
@@ -42,9 +39,11 @@ import java.util.List;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.style.functions.Function.zoom;
import static com.mapbox.mapboxsdk.style.functions.stops.Stop.stop;
import static com.mapbox.mapboxsdk.style.functions.stops.Stops.exponential;
import static com.mapbox.mapboxsdk.style.expressions.Expression.exponential;
import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate;
import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;
import static com.mapbox.mapboxsdk.style.expressions.Expression.color;
import static com.mapbox.mapboxsdk.style.layers.Filter.all;
import static com.mapbox.mapboxsdk.style.layers.Filter.eq;
import static com.mapbox.mapboxsdk.style.layers.Filter.gte;
@@ -453,32 +452,21 @@ public class RuntimeStyleActivity extends AppCompatActivity {
}
// Set a zoom function to update the color of the water
layer.setProperties(fillColor(
zoom(
exponential(
stop(1, fillColor(Color.GREEN)),
stop(4, fillColor(Color.BLUE)),
stop(12, fillColor(Color.RED)),
stop(20, fillColor(Color.BLACK))
).withBase(0.8f)
layer.setProperties(
fillColor(
interpolate(
exponential(0.8f),
zoom(),
stop(1, color(Color.GREEN)),
stop(4, color(Color.BLUE)),
stop(12, color(Color.RED)),
stop(20, color(Color.BLACK))
)
)
));
);
// do some animations to show it off properly
mapboxMap.animateCamera(CameraUpdateFactory.zoomTo(1), 1500);
PropertyValue<String> fillColor = layer.getFillColor();
Function<Float, String> function = (Function<Float, String>) fillColor.getFunction();
if (function != null) {
ExponentialStops<Float, String> stops = (ExponentialStops) function.getStops();
Timber.d("Fill color base: %s", stops.getBase());
Timber.d("Fill color #stops: %s", stops.size());
if (function.getStops() != null) {
for (Stop<Float, String> stop : stops) {
Timber.d("Fill color #stops: %s", stop);
}
}
}
}
private void addCustomTileSource() {

View File

@@ -20,6 +20,7 @@ import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.style.layers.Filter;
import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
import com.mapbox.mapboxsdk.style.sources.Source;
@@ -38,8 +39,24 @@ import java.util.List;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.style.expressions.Expression.concat;
import static com.mapbox.mapboxsdk.style.expressions.Expression.division;
import static com.mapbox.mapboxsdk.style.expressions.Expression.downcase;
import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
import static com.mapbox.mapboxsdk.style.expressions.Expression.pi;
import static com.mapbox.mapboxsdk.style.expressions.Expression.product;
import static com.mapbox.mapboxsdk.style.expressions.Expression.upcase;
import static com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_BOTTOM;
import static com.mapbox.mapboxsdk.style.layers.Property.TEXT_ANCHOR_TOP;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAnchor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconOffset;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textAnchor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textField;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textSize;
/**
* Test activity showcasing using a symbol generator that generates Bitmaps from Android SDK Views.
@@ -49,7 +66,10 @@ public class SymbolGeneratorActivity extends AppCompatActivity implements OnMapR
private static final String SOURCE_ID = "com.mapbox.mapboxsdk.style.layers.symbol.source.id";
private static final String LAYER_ID = "com.mapbox.mapboxsdk.style.layers.symbol.layer.id";
private static final String FEATURE_ID = "brk_name";
private static final String FEATURE_VALUE = "name_sort";
private static final String FEATURE_RANK = "scalerank";
private static final String FEATURE_NAME = "name_sort";
private static final String FEATURE_TYPE = "type";
private static final String FEATURE_REGION = "continent";
private MapView mapView;
private MapboxMap mapboxMap;
@@ -68,7 +88,7 @@ public class SymbolGeneratorActivity extends AppCompatActivity implements OnMapR
public void onMapReady(final MapboxMap map) {
mapboxMap = map;
addSymbolClickListener();
new LoadDataTask(map, SymbolGeneratorActivity.this).execute();
new LoadDataTask(this).execute();
}
private void addSymbolClickListener() {
@@ -82,7 +102,7 @@ public class SymbolGeneratorActivity extends AppCompatActivity implements OnMapR
Timber.v("Feature was clicked with data: %s", feature.toJson());
Toast.makeText(
SymbolGeneratorActivity.this,
"hello from: " + feature.getStringProperty(FEATURE_VALUE),
"hello from: " + feature.getStringProperty(FEATURE_NAME),
Toast.LENGTH_LONG).show();
}
}
@@ -100,6 +120,12 @@ public class SymbolGeneratorActivity extends AppCompatActivity implements OnMapR
if (item.getItemId() == R.id.menu_action_icon_overlap) {
SymbolLayer layer = mapboxMap.getLayerAs(LAYER_ID);
layer.setProperties(iconAllowOverlap(!layer.getIconAllowOverlap().getValue()));
return true;
} else if (item.getItemId() == R.id.menu_action_filter) {
SymbolLayer layer = mapboxMap.getLayerAs(LAYER_ID);
layer.setFilter(Filter.eq(FEATURE_RANK, 1));
//layer.setFilter(eq(get(FEATURE_RANK), 1));
return true;
}
return super.onOptionsItemSelected(item);
}
@@ -178,19 +204,17 @@ public class SymbolGeneratorActivity extends AppCompatActivity implements OnMapR
private static class LoadDataTask extends AsyncTask<Void, Void, FeatureCollection> {
private final MapboxMap mapboxMap;
private final Context context;
private SymbolGeneratorActivity activity;
LoadDataTask(MapboxMap mapboxMap, Context context) {
this.mapboxMap = mapboxMap;
this.context = context;
LoadDataTask(SymbolGeneratorActivity activity) {
this.activity = activity;
}
@Override
protected FeatureCollection doInBackground(Void... params) {
try {
// read local geojson from raw folder
String tinyCountriesJson = ResourceUtils.readRawResource(context, R.raw.tiny_countries);
String tinyCountriesJson = ResourceUtils.readRawResource(activity, R.raw.tiny_countries);
// convert geojson to a model
FeatureCollection featureCollection = new GsonBuilder()
@@ -204,30 +228,54 @@ public class SymbolGeneratorActivity extends AppCompatActivity implements OnMapR
}
}
@Override
protected void onPostExecute(FeatureCollection featureCollection) {
super.onPostExecute(featureCollection);
if (featureCollection == null) {
if (featureCollection == null || activity == null) {
return;
}
// add a geojson to the map
Source source = new GeoJsonSource(SOURCE_ID, featureCollection);
mapboxMap.addSource(source);
// create layer use
mapboxMap.addLayer(new SymbolLayer(LAYER_ID, SOURCE_ID)
.withProperties(
iconImage("{" + FEATURE_ID + "}"), // { } is a token notation
iconAllowOverlap(false)
)
);
new GenerateSymbolTask(mapboxMap, context).execute(featureCollection);
activity.onDataLoaded(featureCollection);
}
}
public void onDataLoaded(@NonNull FeatureCollection featureCollection) {
// add a geojson to the map
Source source = new GeoJsonSource(SOURCE_ID, featureCollection);
mapboxMap.addSource(source);
// create layer use
mapboxMap.addLayer(new SymbolLayer(LAYER_ID, SOURCE_ID)
.withProperties(
// icon configuration
iconImage(get(literal(FEATURE_ID))),
iconAllowOverlap(false),
iconSize(
division(get(literal(FEATURE_RANK)), literal(2))
),
iconAnchor(ICON_ANCHOR_BOTTOM),
iconOffset(new Float[] {0.0f, -5.0f}),
// text field configuration
textField(
concat(
upcase(literal("a ")),
get(literal(FEATURE_TYPE)),
downcase(literal(" IN ")),
get(literal(FEATURE_REGION))
)
),
textSize(
product(get(literal(FEATURE_RANK)), pi())
),
textAnchor(TEXT_ANCHOR_TOP)
)
);
new GenerateSymbolTask(mapboxMap, this).execute(featureCollection);
}
private static class GenerateSymbolTask extends AsyncTask<FeatureCollection, Void, HashMap<String, Bitmap>> {
private MapboxMap mapboxMap;

View File

@@ -9,4 +9,16 @@
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/textZoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_gravity="bottom|start"
android:layout_margin="8dp"
android:textIsSelectable="true"
android:textSize="14sp"/>
</RelativeLayout>

View File

@@ -5,4 +5,8 @@
android:id="@+id/menu_action_icon_overlap"
android:title="@string/menuitem_change_icon_overlap"
app:showAsAction="never"/>
<item
android:id="@+id/menu_action_filter"
android:title="@string/menuitem_filter"
app:showAsAction="never"/>
</menu>

View File

@@ -11,6 +11,7 @@
<string name="menuitem_title_change_location_source_mock">Change to mock location source</string>
<string name="menuitem_title_change_location_source_null">Reset location source to null</string>
<string name="menuitem_change_icon_overlap">Toggle icon overlap</string>
<string name="menuitem_filter">Filter layer</string>
<string name="menuitem_change_location">Change location</string>
<string name="menuitem_title_accelerate_decelerate">Accelerate/Decelerate interpolator</string>
<string name="menuitem_title_bounce">Bounce interpolator</string>