I am using Android Graph View to display line graphs. I need to change the horizontal labels to have nice rounded numbers. In the screen shot attached the values are
1
11:15
22:29
33:44
I need to adjust the position of the horizontal labels and the vertical lines that come off of it so the labels read
0
11:00
23:00
33:44
The code for the above screen shot uses this base class
protected TextView text;
protected GraphView graph;
#Override
public View onCreateView(final LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.ride_chart_fragment, null, false);
graph = (GraphView) view.findViewById(R.id.graph);
if (graph==null){
Log.e("Rides","Graph should not ne null");
return view;
}
Series series = linePoints();
if (series==null) return view;
graph.getGridLabelRenderer().setNumHorizontalLabels(4);
graph.addSeries(series);
setViewport(graph, series);
text = (TextView)view.findViewById(R.id.textView);
text.setVisibility(View.INVISIBLE);
graph.getGridLabelRenderer().setLabelFormatter(new DefaultLabelFormatter() {
#Override
public String formatLabel(double value, boolean isValueX) {
if (isValueX) {
// show normal x values
return UnitConverter.secondsToInterval((float) value);
} else {
return super.formatLabel(value, isValueX) + units(new Settings(inflater.getContext()));
}
}
});
graph.getGridLabelRenderer().setLabelVerticalWidth((int)getResources().getDimension(R.dimen.graph_label_vertical_width));
graph.getGridLabelRenderer().setLabelHorizontalHeight((int)getResources().getDimension(R.dimen.graph_label_vertical_height));
graph.getGridLabelRenderer().setGridColor(Color.parseColor("#20000000"));
graph.getGridLabelRenderer().setGridStyle(GridLabelRenderer.GridStyle.BOTH);
//graph.getGridLabelRenderer().
return view;
}
and this child class to render the graph
protected Series linePoints() {
//RideDetailActivity activity = (RideDetailActivity)getActivity();
if (activity==null || activity.isFinishing() || activity.ride==null) return null;
Settings settings = new Settings(activity);
long rideId = activity.ride.sqlRide.getId();
LezyneLinkApplication application = (LezyneLinkApplication)activity.getApplicationContext();
DaoSession session = application.getDaoSession();
RideElevationDao table = session.getRideElevationDao();
List<RideElevation> elevations = table
.queryBuilder()
.where(RideElevationDao.Properties.RideId.eq(rideId))
.orderAsc(RideElevationDao.Properties.Index)
.list();
DataPoint dataPoints[] = new DataPoint[elevations.size()];
int index=0;
boolean isMetric = settings.isMetric();
for (RideElevation elevation : elevations){
double y = elevation.getY();
if (!settings.isMetric()){
y = UnitConverter.convertMetersToFeet(y);
}
DataPoint point = new DataPoint(elevation.getX(),y);
dataPoints[index] = point;
index++;
}
LineGraphSeries series = new LineGraphSeries<DataPoint>(dataPoints);
series.setDrawBackground(true);
series.setColor(Color.argb(0xFF, 0x8e, 0x00, 0xe8));
series.setBackgroundColor(Color.argb(0x3F, 0x47, 0x2c, 0x17));
series.setThickness(6);
return series;
}
#Override
protected void setViewport(GraphView graph,Series series) {
graph.getViewport().setXAxisBoundsManual(true);
graph.getViewport().setMaxX(series.getHighestValueX());
graph.getViewport().setYAxisBoundsManual(true);
double lowest = series.getLowestValueY();
double highest = series.getHighestValueY();
graph.getViewport().setMinY(lowest);
Settings settings = new Settings(context);
if (settings.isMetric()){
if (highest<lowest+121) highest = lowest+121;
}
else {
if (highest<lowest+400) highest = lowest+400;
}
graph.getViewport().setMaxY(highest);
}
Im not seeing any way of doing this with the library as is so I am considering changing the source code. Did I miss something in the API? Anybody have any suggestions about where in the code to add this functionality.
you have two ways:
1) (difficult) with dynamic viewport take a look into the source code and find the point where the humanRound is done (GridLabelRenderer.java)
understand it, and modify ;)
2) use a fixed viewport and calculate on your own the min and max bounds and you can change the numberOfHorizontalLabels to get the best match.
Related
I am trying to combine multiple line charts in a single view but i cannot solve problem with different data types. I've tried to use min-max normalization to scale values from different data sets but when i want to show selected value in custom marker view, i cannot convert it back to initial format.
I've tried to use different ValueFormatters for all my data sets but i have not achieved anything.
Normalization code:
private List<Entry> normalizeEntry(List<Entry> entries, int min, int max) {
for (int i = 0; i < entries.size(); i++) {
float lastValue = entries.get(i).getY();
float newValue = normalize(lastValue, min, max, 0, 100);
entries.set(i, new Entry(i, newValue));
}
return entries;
}
Picture which show what i want to achieve:
I was able to solve my problem by adding a parameter corresponding to the value before normalization to the constructor of the Entry object.
before:
new Entry(xValue, normalizedValue);
after:
new Entry(xValue, normalizedValue, initialValueWithOutputFormat);
Example:
entries.add(new Entry(i, normalizedValues.get(i), String.format("%.2f m", inputValues.get(i))));
Then, I can easily update custom marker like in following code:
public class CustomMarkerView extends MarkerView {
private TextView tvContent;
public CustomMarkerView(Context context, int layoutResource) {
super(context, layoutResource);
// this markerview only displays a textview
tvContent = findViewById(R.id.tvContent);
}
#Override
public void refreshContent(Entry e, Highlight highlight) {
tvContent.setText(String.valueOf(e.getData()));
super.refreshContent(e, highlight);
}
#Override
public MPPointF getOffset() {
return new MPPointF(-(getWidth() / 2), -getHeight());
}
}
Hi I am displaying graph with different colors based on condition and graph too works fine. Now my question is, how to display text based on corresponding color in label?
I have attached below screenshot.
In the below screenshot, three colors and after three colors, text been displayed as low, average and high been displayed.
Instead I need to display as, green color with corresponding text as low. red color with corresponding text as average and violet color with corresponding text as high.
What I tried is,
public class CustomBarDataSet extends BarDataSet {
public CustomBarDataSet(List<BarEntry> yVals, String label) {
super(yVals, label);
}
#Override
public int getColor(int index) {
if(getEntryForXIndex(index).getVal() < 20) // green.. low
return mColors.get(0);
else if(getEntryForXIndex(index).getVal() < 50) // 50 red average
return mColors.get(1);
else if(getEntryForXIndex(index).getVal() < 100) // 50 violet high
return mColors.get(2);
else // greater or equal than 100 red
return mColors.get(2); //violet
}
//i don't know how to use this.
#Override
public String getLabel() {
return super.getLabel();
}
}
In MainActivity:
CustomBarDataSet set = new CustomBarDataSet(entries, "low,average,high");
set.setColors(new int[]{getResources().getColor(R.color.green_color),
getResources().getColor(R.color.red_button),
getResources().getColor(R.color.violet_color)});
ArrayList<BarDataSet> dataSets = new ArrayList<>();
dataSets.add(set);
BarDataSet dataset = new BarDataSet(entries, "");
BarData data = new BarData(labels, set);
This should help you:
Legend l = chart.getLegend();
l.setCustom(new int[] {Color.GREEN, Color.RED, Color.rgb(238,130,238)},new String[] {"low", "average", "high"});
I am developing a chart based application, I am using MPAndroidChart library, I need to place the text value inside of circle, i tried to display,Thanks for if any suggestions related this,
i attached a screenshot related to that issue. I need to be do like this
but i get like this image:
Thanks Again for helping this issue,
ArrayList<Entry> e1 = new ArrayList<Entry>();
float[] values = new float[]{48, 59, 79, 29, 39, 50, 60};
for (int i = 0; i < values.length; i++) {
e1.add(new Entry(values[i], i, "line3"));
}
int[] color = {Color.parseColor("#D13385"), Color.parseColor("#37D04E"), Color.parseColor("#33D1D1"), Color.parseColor("#D1C933")};
LineDataSet d1 = new LineDataSet(e1, "" + cnt);
d1.setColors(color);
d1.setLineWidth(3.0f);
d1.setCircleSize(7.0f);
d1.setDrawValues(true);
d1.setCircleColor(Color.parseColor("#891e9a"));
d1.setCircleColorHole(Color.parseColor("#891e9a"));
d1.setDrawHighlightIndicators(false);
d1.setDrawFilled(false);
d1.setFillAlpha(20);
d1.setHighlightLineWidth(50f);
d1.setValueTextSize(10f);
Currently it is not possible to change the position where the values are drawn by default. You will have to modify the library to get that behaviour.
It is a bit "hacky", but I've managed to achieve such layout You've provided by creating two sets of data and attaching them to same chart. One set (lets call it "dots") contains your data needed to be displayed as dots. The second one ("lines") is a bit offset downwards (y value minus some experimentally picked value). Now you can set no line displaying for "lines" and a lines for "dots", no values labels for "dots" and white labels for "lines" and by experimentally moving your y values back and forth you can achieve overlaying values labels from one chart on top of another ("dots" will be covered by "lines" values).
UPDATE:
Actually, I've an answer more elegant, that I've provided! Use Highlight[] and create array of highlights.
highlihts = new Highlight[values_dots.size()];
for (int i = 0; i < values_dots.size(); i++) {
Highlight h = new Highlight(values_dots.get(i).getX(),values_dots.get(i).getY(), 0);
highlihts[i] = h;
}
chart.highlightValues(highlihts);
In CustomMarkerView class position marker like so:
#Override
public MPPointF getOffset() {
return new MPPointF(-(getWidth() / 2), -(getHeight() / 2));
}
Boom
P.S. Philipp Jahoda, awesome library!
there was an easy way to do this
custom maker view
Marker view
public class MyMarkerView extends MarkerView {
private final TextView tvContent;
public MyMarkerView(Context context, int layoutResource) {
super(context, layoutResource);
tvContent = findViewById(R.id.tvContent);
}
// runs every time the MarkerView is redrawn, can be used to update the
// content (user-interface)
#SuppressLint("SetTextI18n")
#Override
public void refreshContent(Entry e, Highlight highlight) {
if (e instanceof CandleEntry) {
CandleEntry ce = (CandleEntry) e;
tvContent.setText(Utils.formatNumber(ce.getHigh(), 0, true)+(ce.getData()));
} else {
tvContent.setText(Utils.formatNumber(e.getY(), 0, true)+"\n "+(e.getData()));
}
super.refreshContent(e, highlight);
}
#Override
public MPPointF getOffset() {
return new MPPointF(-(getWidth() / 2), -getHeight());
}
}
and lastly in your activity
values.add(new Entry(i, val,"Custom message per value"));
Preview be like
example screen shot
there was an easy way to do this
custom maker view
<TextView
android:id="#+id/tvContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="7dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:text=""
android:textSize="12sp"
android:textColor="#android:color/white"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall" />
Marker view class
public class MyMarkerView extends MarkerView {
private final TextView tvContent;
public MyMarkerView(Context context, int layoutResource) {
super(context, layoutResource);
tvContent = findViewById(R.id.tvContent);
}
// runs every time the MarkerView is redrawn, can be used to update the
// content (user-interface)
#SuppressLint("SetTextI18n")
#Override
public void refreshContent(Entry e, Highlight highlight) {
if (e instanceof CandleEntry) {
CandleEntry ce = (CandleEntry) e;
tvContent.setText(Utils.formatNumber(ce.getHigh(), 0, true)+(ce.getData()));
} else {
tvContent.setText(Utils.formatNumber(e.getY(), 0, true)+"\n "+(e.getData()));
}
super.refreshContent(e, highlight);
}
#Override
public MPPointF getOffset() {
return new MPPointF(-(getWidth() / 2), -getHeight());
}
}
and lastly in your activity
values.add(new Entry(i, val,"Custom message per value"));
Preview be like
example screen shot
There are two possibilities:
(1) Not so good: Shift the y-value of the label position
Two Data sets one for text and one for the line (including circles)
Modifiy the y-position for the text value with a constant offset
Pro: Easy
Con: The offset is not always constant (see offset is not always similar)
(2) Better: Override the drawValues method from LineChartRenderer
In LineChartRenderer.java -> drawValues the text is vertically shifted by this line:
drawValue(c, formatter.getPointLabel(entry), x, y - valOffset, dataSet.getValueTextColor(j / 2));
So to get rid of the "- valOffset":
1.Override the drawValues method
Create a new java file "CenteredTextLineChartRenderer.java" and override method drawValues from LineChartRenderer
2.Modify the y-valOffset to y+textHeight*0.35f
Add float textHeight = dataSet.getValueTextSize();
public class CenteredTextLineChartRenderer extends LineChartRenderer {
public CenteredTextLineChartRenderer(LineDataProvider chart, ChartAnimator animator, ViewPortHandler viewPortHandler) {
super(chart, animator, viewPortHandler);
}
//Modified drawValues Method
// Center label on coordinate instead of applying a valOffset
#Override
public void drawValues(Canvas c) {
if (isDrawingValuesAllowed(mChart)) {
List<ILineDataSet> dataSets = mChart.getLineData().getDataSets();
for (int i = 0; i < dataSets.size(); i++) {
ILineDataSet dataSet = dataSets.get(i);
float textHeight = dataSet.getValueTextSize();
if (!shouldDrawValues(dataSet) || dataSet.getEntryCount() < 1)
continue;
// apply the text-styling defined by the DataSet
applyValueTextStyle(dataSet);
Transformer trans = mChart.getTransformer(dataSet.getAxisDependency());
// make sure the values do not interfear with the circles
int valOffset = (int) (dataSet.getCircleRadius() * 1.75f);
if (!dataSet.isDrawCirclesEnabled())
valOffset = valOffset / 2;
mXBounds.set(mChart, dataSet);
float[] positions = trans.generateTransformedValuesLine(dataSet, mAnimator.getPhaseX(), mAnimator
.getPhaseY(), mXBounds.min, mXBounds.max);
ValueFormatter formatter = dataSet.getValueFormatter();
MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset());
iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x);
iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y);
for (int j = 0; j < positions.length; j += 2) {
float x = positions[j];
float y = positions[j + 1];
if (!mViewPortHandler.isInBoundsRight(x))
break;
if (!mViewPortHandler.isInBoundsLeft(x) || !mViewPortHandler.isInBoundsY(y))
continue;
Entry entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min);
if (dataSet.isDrawValuesEnabled()) {
//drawValue(c, formatter.getPointLabel(entry), x, y - valOffset, dataSet.getValueTextColor(j / 2));
drawValue(c, formatter.getPointLabel(entry), x, y+textHeight*0.35f, dataSet.getValueTextColor(j / 2));
}
if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) {
Drawable icon = entry.getIcon();
Utils.drawImage(
c,
icon,
(int)(x + iconsOffset.x),
(int)(y + iconsOffset.y),
icon.getIntrinsicWidth(),
icon.getIntrinsicHeight());
}
}
MPPointF.recycleInstance(iconsOffset);
}
}
}
}
3.Set your own LineChart renderer to your modified drawValues class
LineChart mChart = (LineChart) mainActivity.findViewById(R.id.LineChart);
mChart.setRenderer(new CenteredTextLineChartRenderer(mChart,mChart.getAnimator(),mChart.getViewPortHandler()));
Run your code and manually adapt the 0.35f offset in your CenteredTextLineChartRenderer class
Now your text is always vertically centered!
IMPORTANT: With deleting the valOffset your label is not vertically centered as the text anchor is not in the center of your text label. So you have to insert a manual offset "textHeight*0.35f" (just try it out). But the big advantage of method (2) is that the text is always centered with the same offset also for example in landscape mode and on other screen sizes...
So I'm using the awesome MPAndroid Chart library to make a simple LineChart. I was able to customize it heavily using the example project on GitHub.
The problem is, when I move it to my own code, certain methods are no longer able to be resolved:
mLineChart.setExtraOffsets() and mLineChart.setAutoScaleMinMaxEnabled() in particular. There might be others but these are the only two I've noticed.
Everything else works fine though. Any idea why I can't access these two methods? What should I dig into to find out more about why this is the case?
public class MyFragment extends Fragment {
private LineChart mLineChart;
// stuff here
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// stuff here
// Creating numbers line chart for candidate
LineChart numChart = (LineChart)view.findViewById(R.id.numbersLineChart);
setNumChart(numChart, mObject.getNums());
// stuff here
}
public void setNumChart(LineChart lineChart, List<Integer> nums){
mLineChart = lineChart;
mLineChart.setDrawGridBackground(false);
// no description text
mLineChart.setDescription("");
mLineChart.setNoDataTextDescription("You need to provide data for the chart.");
// enable value highlighting
mLineChart.setHighlightEnabled(true);
// enable touch gestures
mLineChart.setTouchEnabled(true);
// enable scaling and dragging
mLineChart.setDragEnabled(false);
mLineChart.setScaleEnabled(false);
// if disabled, scaling can be done on x- and y-axis separately
mLineChart.setPinchZoom(false);
// create a custom MarkerView (extend MarkerView and specify the layout to use for it)
MyMarkerViewv2 mv = new MyMarkerViewv2(getActivity(), R.layout.custom_marker_view, mLineChart);
// set the marker to the chart
mLineChart.setMarkerView(mv);
// disable all axes lines and labels
YAxis leftAxis = mLineChart.getAxisLeft();
leftAxis.setEnabled(false);
mLineChart.getAxisRight().setEnabled(false);
XAxis bottomAxis = mLineChart.getXAxis();
bottomAxis.setEnabled(false);
// add data
setLineChartData(nums);
//THIS METHOD CANNOT BE RESOLVED********************
mLineChart.setExtraOffsets(30f,50f,30f,0f);
// get the legend (only possible after setting data)
Legend l = mLineChart.getLegend();
l.setEnabled(false);
mLineChart.invalidate();
}
public void setLineChartData(List<Integer> nums){
//create xVariables aka strings of the months
ArrayList<String> xVals = new ArrayList<String>();
for (int i = 0; i < nums.size(); i++) {
xVals.add(Month.getMonthfromIndex(i).getAbbrev());
}
//add corresponding numbers
ArrayList<Entry> yVals = new ArrayList<Entry>();
for (int i = 0; i < nums.size(); i++) {
yVals.add(new Entry(nums.get(i), i));
}
// create a dataset and give it a type
LineDataSet set1 = new LineDataSet(yVals, "DataSet");
set1.setColor(Color.BLACK);
set1.setCircleColor(Color.BLACK);
set1.setLineWidth(0.75f);
set1.setDrawCircles(true);
set1.setDrawValues(false);
set1.setCircleSize(1.75f);
set1.setDrawCircleHole(false);
ArrayList<LineDataSet> dataSets = new ArrayList<LineDataSet>();
dataSets.add(set1); // add the datasets
// create a data object with the datasets
LineData data = new LineData(xVals, dataSets);
// set data
mLineChart.setData(data);
}
// stuff here
class MyMarkerViewv2 extends MarkerView {
private TextView markerContent;
private LineChart mChart;
public MyMarkerViewv2(Context context, int layoutResource, LineChart lChart) {
super(context, layoutResource);
mChart = lChart;
markerContent = (TextView) findViewById(R.id.markerContent);
}
// callbacks everytime the MarkerView is redrawn, can be used to update the
// content (user-interface)
#Override
public void refreshContent(Entry e, int dataSetIndex) {
if (e instanceof CandleEntry) {
CandleEntry ce = (CandleEntry) e;
List<String> months = mChart.getLineData().getXVals();
markerContent.setText(months.get(e.getXIndex() % 12) + "\n" + Utils.formatNumber(ce.getHigh(), 0, true) + "%");
} else {
List<String> months = mChart.getLineData().getXVals();
markerContent.setText(months.get(e.getXIndex() % 12) + "\n" + Utils.formatNumber(e.getVal(), 0, true) + "%");
}
}
#Override
public int getXOffset() {
// this will center the marker-view horizontally
return -(getWidth() / 2);
}
#Override
public int getYOffset() {
// this will cause the marker-view to be above the selected value
return -getHeight();
}
}
I also have the LineChart wrapped inside a vertical LinearLayout with other elements and that whole thing wrapped inside a ScrollView. Not sure if that's what's causing the problem or not.
Nevermind, I figured it out. It's because the library downloaded by Maven and Gradle is not exactly the same as the one included in the example project on the website.
I have an Arcgis map with pins.When i tap on a pin i am showing a callout(popover)over the pins which works perfectly fine.But when i zoom in/out the map, callout does't position itself with respect to the pin on the map.How can i always show callout on top of pin while zooming in/out.
tap on pin and callout pops up
and the image where pop up moves away from pin when zoom in
Note: I have made changes to the existing sample project of Arcgis map app i.e. SymbolizingResults
Here are the changes i have made to the SymbolizingResults Activity
public class SymbolizingResults extends Activity {
MapView map;
Button queryBtn;
GraphicsLayer gl;
Callout callout;
/** Called when the activity is first created. */
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
map = (MapView) findViewById(R.id.map);
map.addLayer(new ArcGISTiledMapServiceLayer(
"http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer"));
gl = new GraphicsLayer();
gl.setRenderer(createClassBreaksRenderer());
Point p = new Point(37.6922222, -97.3372222);
HashMap<String, Object> map1 = new HashMap<String, Object>();
map1.put("NAME", "India");
PictureMarkerSymbol pic = new PictureMarkerSymbol(this,getResources().getDrawable(R.drawable.pin_dest));
Graphic gr = new Graphic(p,pic,map1);
gl.addGraphic(gr);
map.addLayer(gl);
queryBtn = (Button) findViewById(R.id.querybtn);
queryBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// Sets query parameter
Query query = new Query();
query.setWhere("STATE_NAME='Kansas'");
query.setReturnGeometry(true);
String[] outfields = new String[] { "NAME", "STATE_NAME",
"POP07_SQMI" };
query.setOutFields(outfields);
query.setOutSpatialReference(map.getSpatialReference());
Query[] queryParams = { query };
AsyncQueryTask qt = new AsyncQueryTask();
qt.execute(queryParams);
}
});
// Sets 'OnSingleTapListener' so that when single tap
// happens, Callout would show 'SQMI' information associated
// with tapped 'Graphic'
map.setOnSingleTapListener(new OnSingleTapListener() {
private static final long serialVersionUID = 1L;
public void onSingleTap(float x, float y) {
if (!map.isLoaded())
return;
Point toDroppedPinPoint = map.toMapPoint(x, y);
System.out.println("X : "+toDroppedPinPoint.getX());
System.out.println("Y : "+toDroppedPinPoint.getY());
int[] uids = gl.getGraphicIDs(x, y, 2);
if (uids != null && uids.length > 0) {
int targetId = uids[0];
Graphic gr = gl.getGraphic(targetId);
callout = map.getCallout();
// Sets Callout style
callout.setStyle(R.xml.countypop);
/* String countyName = (String) gr.getAttributeValue("NAME");
String countyPop = gr.getAttributeValue("POP07_SQMI")
.toString();*/
// Sets custom content view to Callout
callout.setContent(loadView("Anshul", "India"));
callout.show(map.toMapPoint(new Point(x, y)));
} else {
if (callout != null && callout.isShowing()) {
callout.hide();
}
}
}
});
}
// Creates custom content view with 'Graphic' attributes
private View loadView(String countyName, String pop07) {
View view = LayoutInflater.from(CalloutSampleActivity.this).inflate(
R.layout.sqmi, null);
final TextView name = (TextView) view.findViewById(R.id.county_name);
name.setText(countyName + "'s SQMI");
final TextView number = (TextView) view.findViewById(R.id.pop_sqmi);
number.setText(pop07);
final ImageView photo = (ImageView) view
.findViewById(R.id.family_photo);
photo.setImageDrawable(CalloutSampleActivity.this.getResources()
.getDrawable(R.drawable.family));
return view;
}`
The trouble is this line:
callout.show(map.toMapPoint(new Point(x, y)));
Here you're saying you want to show the callout at the point that the user tapped. That's what the sample does, and in the sample it always makes sense because the sample's graphics are all polygons (i.e. counties in Kansas).
But for a point, like the pin you added, you don't want to show the callout at the tapped point. If the tapped point is a couple of pixels away from the pin, and then you zoom out, the difference can be hundreds of miles! Instead, you want to show the callout at the pin graphic's point. But you only want to do that if it's actually a point, so you need to check the graphic's geometry with instanceof.
I replaced the above line with this and it works:
Geometry graphicGeom = gr.getGeometry();
if (graphicGeom instanceof Point) {
callout.show((Point) graphicGeom);
} else {
callout.show(toDroppedPinPoint);
}
I couldn't see the code about how you are zooming in & out. But logically you should update the callout position in zoom-in & zoom-out too like you are doing in onSingleTap().