Android - Drawing Lines In A TextView - java

I have an app that takes in input from the user and then on the clicking of a button displays the calculated results in a format like so:
123456
213456
214356
124365
I need a line ( preferably blue) to join each of the number 2's in the list as they make their way down the TextView.
I also need the option of not having this line if the user does not want it.
What I have tried so far:
I extended a TextView class and overrode the onDraw(Canvas) method and tried to get something working and managed to display vertical blue lines but couldnt get the joining of the number 2's. I am unsure how android decides when onDraw() is called, as I dont call it in my code but I would rather I could so I could control when it displays the lines or not.

This is only idea:
What you have:
Given the text size lets say: 20px
X and Y coordinates of the view.
Gravity supplied to the content by the TextView if any
Text size in px. getTextSize() or paint.measureText(); [rect.right on Canvas is the text size]
Text ems. getEms()
Optional: Padding or margin if any
What you need to find:
Each x-coordinate of each letter on the canvas.
Each y-coordinate of each letter on the canvas.
How to:
x = letter position in the text * font size(or ems). if there is padding or margin add padding left + padding right to the x; [rect.left on Canvas is x]
Reset letter position each new line
y = line number * font size (or ems). + padding top + padding bottom [rect.bottom on Canvas is y]
Collect all approximate x and y's to form a Point.
Match each Point in onDraw()
If you think you want to use paint.measureText("13332", 0, "13332".indexOf("2")), measured width will be approximate and absolute to itself x-coordinate of "2". Relative to its parent might be much more complex to find.
Edit:
What I wrote above can be easily obtained using paint.[getTextBounds()][1] method which will give you Rect from which you can form a Point

I would proceed this way.
The user enters the value and clicks the button
As the button is tapped hide the TextView and replace it with an ImageView or keep the TextView and put the ImageView on the top of the TextView (Overlap of the ImageView on the TextView can be achieved with a FrameLayout
Draw in the ImageView the lines and the numbers in the OnDraw() method
As the user taps on the ImageView, hide the ImageView and display the TextView again.
Finally if the position of the 2's is fixed you don't need to measure the text.
If the posision is not fixed you can put the numbers in an array. Here is some code in mixed order (not tested)
var resStr1 = Integer.toString(resultNumber);
var position = 0;
...
// METHOD 1, multiple 2's in a number
var j = 0;
for (int i = 0; i < resStr1.length(); i++){
char c = s.charAt(i);
if (c == '2') {
position[j] = i;
j++;
}
}
// METHOD 2. ONLY ONE 2 in a number
position = resStr1.indexOf('2');
// CONTINUE
...
Paint p = new Paint();
...
float left = customMarginLeft + p.measureText(resStr1.substring(0, position));
float top = customMarginTop;
...
canvas.drawRect( bla bla bla...
drawText() // ??
Get a look also to getTextBounds() if you wanna to get the text height too.
About the onDraw method, don't take care of how much time it will be called by the system, if performance is critical just mantain the scope of the variables containing the results and the precalculations global, put in the main class the properties and methods that controls the behaviour of the drawing method. The system will redraw every time the lines again and again as soon as another element overlaps your control or something happens in the system so the fact that the onDraw is called again and again is normal, otherwise your lines will not be redrawn again and could disappear from the screen if something happens.
Of course the code above can be also put in a custom control (combined control).
To clear the lines you should call the invalidate() or postInvalidate() method. This methods will clear the whole area and force the onDraw() to be called again. Then put globally a flag like
shouldRedrawLines = false;
and in the onDraw() do something like this:
if (shouldRedrawLines) { // please note that the onDraw is called again and again and this condition allows you to check if in another part of the program you decided to clear the lines
DrawLines(); // contains the code for redrawing lines
}
DrawNumbersFromResult(); // contains the code for redrawing Numbers
Simple not ?

Related

Is it possible to select/click multiple parts of one image?

For an assignment I am making a Boardgame. (In java) This Boardgame has a map, with multiple fields/lands that have to be used. Units can be placed on them, they can move. Other things are also placed on them.
For the map I have one image I use. I looked online for solutions, but the only ones I found where for a grid game (such as chess or checkers) and the map of this game can not be divided in just squares. I tried this, but the field shapes are to different to make that work.
I had a few faint ideas as to how to work this out, but I can't quite put them into code examples and have no clue if they would work, or how.
The ideas I had:
Make some invisible buttons and bind them to specific coordinates in the picture. The problem I had with this solution was that it also had to be able to display things placed on it. It would also be very inconvenient if not all of the field was clickable.
I have a 'overlay' image with the outlines of all the fields and the 'insides' removed. I made this overlay so I could add a faint color overlay over the board. Would it be possible to use this in any kind of way?
First I though of cutting out all the loose fields and putting them together to form the one image. Only, I don't know how I would do this. Not just where to place it, but also, how can I make sure that the elements are Always in the same place compared to eachother, and my board doesn't mess up when changing screen/resolution size?
I am using javafx for the graphical elements in my game.
If there are any suggestions of something I haven't thought of myself, those are also very welcome.
If it's sufficient to retrieve the color of the pixel where the mouse was clicked, then you can do that fairly easily. If you know the image is displayed in the image view unscaled and uncropped, then all you need is:
imageView.setOnMouseClicked(e -> {
Color color = imageView.getImage().getPixelReader().getColor((int)e.getX(), (int)e.getY());
// ...
});
More generally, you may need to map the image view coordinates to the image coordinates:
imageView.setOnMouseClicked(e -> {
double viewX = e.getX();
double viewY = e.getY();
double viewW = imageView.getBoundsInLocal().getWidth();
double viewH = imageView.getBoundsInLocal().getHeight();
Rectangle2D viewport = imageView.getViewport();
double imgX = viewport.getMinX() + e.getX() * viewport.getWidth() / viewW;
double imgY = viewport.getMinY() + e.getY() * viewport.getHeight() / viewH ;
Color color = imageView.getImage().getPixelReader().getColor((int)imgX, (int)imgY);
// ...
});
Once you have the color you can do some simple analysis to see if it approximately matches the color of various items in your image, e.g. check the hue component, or check if the "distance" from a fixed color is suitably small.
A typical implementation of that might look like:
// choose a color based on what is in your image:
private final Color FIELD_GREEN = Color.rgb(10, 10, 200);
private double distance(Color c1, Color c2) {
double deltaR = c1.getRed() - c2.getRed();
double deltaG = c1.getGreen() - c2.getGreen();
double deltaB = c1.getBlue() - c2.getBlue();
return Math.sqrt(deltaR * deltaR + deltaG * deltaG + deltaB * deltaB);
}
private boolean colorsApproximatelyEqual(Color c1, Color c2, double tolerance) {
return distance(c1, c2) < tolerance ;
}
And the back in the handler you can do
if (colorsApproximatelyEqual(color, FIELD_GREEN, 0.1)) {
// process click on field...
}
Whether or not this is a viable approach depends on the nature of the image map. If the coloring in the map is too complex (or objects are not easily distinguishable by color), then you will likely need to place other elements in the scene graph and register handlers on each of them, as you describe in the question.

MPAndroidChart Display Label Over Horizontal Line on Graph

Is it possible to display custom text centered between 2 points on the graph?
I've got MPAndroidChart setup to display a step function type graph (representing hours spent doing a specific task) with horizontal and vertical lines only. What I would like to be able to do is show a label over the horizontal sections indicating the size of the section (aka the time spent calculated by taking the difference between the x values). Is there a way to do this? I've been look into modifying the library but I can't seem to figure out where would be the correct place to do so.
My best guess would be some changes in BarLineChartBase onDraw() method or maybe in the LineChartRenderer drawLinear() method.
Here is what I am able to produce:
Here is an example of what I am trying to produce:
Figured it out! Just add a new method drawTime() to the LineChart class at the end of onDraw() right after drawDescription(). Since each horizontal line is described by 2 Entry points I simply loop through 2 entries at a time for my single data set and calculate the difference:
protected void drawTime(Canvas c)
{
Paint timePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
timePaint.setTextSize(Utils.convertDpToPixel(16));
timePaint.setColor(Color.BLUE);
timePaint.setTextAlign(Paint.Align.CENTER);
MPPointD position;
LineData data = this.getLineData();
ILineDataSet dataSet = data.getDataSetByIndex(0);
for (int i = 1; i < dataSet.getEntryCount(); i+=2)
{
Entry e1 = dataSet.getEntryForIndex(i-1);
Entry e2 = dataSet.getEntryForIndex(i);
float time = e2.getX() - e1.getX();
position = getPixelForValues(e1.getX() + time/2, e1.getY() - 0.05f, YAxis.AxisDependency.LEFT);
c.drawText(String.valueOf(time), (float)position.x, (float)position.y, timePaint);
}
}
The resulting graph looks like this

Truly top-aligning text in Android TextView

I am trying to display a TextView in Android such that the text in the view is top-aligned:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create container layout
FrameLayout layout = new FrameLayout(this);
// Create text label
TextView label = new TextView(this);
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, 25); // 25 pixels tall
label.setGravity(Gravity.TOP + Gravity.CENTER); // Align text top-center
label.setPadding(0, 0, 0, 0); // No padding
Rect bounds = new Rect();
label.getPaint().getTextBounds("gdyl!", 0, 5, bounds); // Measure height
label.setText("good day, world! "+bounds.top+" to "+bounds.bottom);
label.setTextColor (0xFF000000); // Black text
label.setBackgroundColor(0xFF00FFFF); // Blue background
// Position text label
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(300, 25, Gravity.LEFT + Gravity.TOP);
// also 25 pixels tall
layoutParams.setMargins(50, 50, 0, 0);
label.setLayoutParams(layoutParams);
// Compose screen
layout.addView(label);
setContentView(layout);
}
This code outputs the following image:
The things to note:
The blue box is 25 pixels tall, just like requested
The text bounds are also reported as 25 pixels tall as requested (6 - (-19) = 25)
The text does not start at the top of the label, but has some padding above it, ignoring setPadding()
This leads to the text being clipped at the bottom, even though the box technically is tall enough
How do I tell the TextView to start the text at the very top of the box?
I have two restrictions to possible answers:
I do need to keep the text top-aligned, though, so if there is some trick with bottom-aligning or centering it vertically instead, I can't use it, since I have scenarios where the TextView is taller than it needs to be.
I'm a bit of a compatibility-freak, so if possible I'd like to stick to calls that were available in the early Android APIs (preferably 1, but definitely no higher than 7).
TextViews use the abstract class android.text.Layout to draw the text on the canvas:
canvas.drawText(buf, start, end, x, lbaseline, paint);
The vertical offset lbaseline is calculated as the bottom of the line minus the font's descent:
int lbottom = getLineTop(i+1);
int lbaseline = lbottom - getLineDescent(i);
The two called functions getLineTop and getLineDescent are abstract, but a simple implementation can be found in BoringLayout (go figure... :), which simply returns its values for mBottom and mDesc. These are calculated in its init method as follows:
if (includepad) {
spacing = metrics.bottom - metrics.top;
} else {
spacing = metrics.descent - metrics.ascent;
}
if (spacingmult != 1 || spacingadd != 0) {
spacing = (int)(spacing * spacingmult + spacingadd + 0.5f);
}
mBottom = spacing;
if (includepad) {
mDesc = spacing + metrics.top;
} else {
mDesc = spacing + metrics.ascent;
}
Here, includepad is a boolean that specifies whether the text should include additional padding to allow for glyphs that extend past the specified ascent. It can be set (as #ggc pointed out) by the TextView's setIncludeFontPadding method.
If includepad is set to true (the default value), the text is positioned with its baseline given by the top-field of the font's metrics. Otherwise the text's baseline is taken from the descent-field.
So, technically, this should mean that all we need to do is to turn off IncludeFontPadding, but unfortunately this yields the following result:
The reason for this is that the font reports -23.2 as its ascent, while the bounding box reports a top-value of -19. I don't know if this is a "bug" in the font or if it's supposed to be like this. Unfortunately the FontMetrics do not provide any value that matches the 19 reported by the bounding box, even if you try to somehow incorporate the reported screen resolution of 240dpi vs. the definition of font points at 72dpi, so there is no "official" way to fix this.
But, of course, the available information can be used to hack a solution. There are two ways to do it:
with IncludeFontPadding left alone, i.e. set to true:
double top = label.getPaint().getFontMetrics().top;
label.setPadding(0, (int) (top - bounds.top - 0.5), 0, 0);
i.e. the vertical padding is set to compensate for the difference in the y-value reported from the text bounds and the font-metric's top-value. Result:
with IncludeFontPadding set to false:
double ascent = label.getPaint().getFontMetrics().ascent;
label.setPadding(0, (int) (ascent - bounds.top - 0.5), 0, 0);
label.setIncludeFontPadding(false);
i.e. the vertical padding is set to compensate for the difference in the y-value reported from the text bounds and the font-metric's ascent-value. Result:
Note that there is nothing magical about setting IncludeFontPadding to false. Both version should work. The reason they yield different results are slightly different rounding errors when the font-metric's floating-point values are converted to integers. It just so happens that in this particular case it looks better with IncludeFontPadding set to false, but for different fonts or font sizes this may be different. It is probably fairly easy to adjust the calculation of the top-padding to yield the same exact rounding errors as the calculation used by BoringLayout. I haven't done this yet since I'll rather use a "bug-free" font instead, but I might add it later if I find some time. Then, it should be truly irrelevant whether IncludeFontPadding is set to false or true.
If your TextView is inside an other layout, make sure to check if there is enough space between them. You can add a padding at the bottom of your parent view and see if you get your full text. It worked for me!
Example: you have a textView inside a FrameLayout but the FrameLayout is too small and is cutting your textView. Add a padding to your FrameLayout to see if it work.
Edit: Change this line
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(300, 25, Gravity.LEFT + Gravity.TOP);
for this line
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(300, 50, Gravity.LEFT + Gravity.TOP);
This will make the box bigger and, by the same way, let enough space for your text to be shown.
OR add this line
label.setIncludeFontPadding(false);
This will remove surrounding font padding and let the text be seen. But the only thing that dont work in your case is that it wont show entirely letters like 'g' that goes under the line... Maybe that you will have to change the size of the box or the text just a little (like by 2-3) if you really want it to work.

Centering a list item on the screen when selected in Android?

I have a listView and a timer iterates through each list item. Ideally I'd like to have the currently selected item to automatically center in the listView. I've played around with smoothScrollToPosition() but am lost on how to work out centering the current item.
My corrected code (thanks to Matej Spili):
// Smooth scroll the list item
try {
scrollView.smoothScrollBy(scrollView.getChildAt().getTop(‌​) - (scrollView.getHeight() / 2) + (scrollView.getChildAt().getHeight() / 2), 1500);
}
catch (NullPointerException e)
{
System.out.println("NULL POINTER DEALT WITH");
}
First, get the height of the ListView using getHeight, which returns the height of the ListView in pixels.
Then, get the height of the row's View using the same method.
Then, use setSelectionFromTop and pass in half of the ListView's height minus half of the row's height.
Something like:
int h1 = mListView.getHeight();
int h2 = v.getHeight();
mListView.setSelectionFromTop(position, h1/2 - h2/2);
Or, instead of doing the math, you might just pick a constant for the offset from the top, but I would think it might be more fragile on different devices since the second argument for setSelectionFromTop appears to be in pixels rather than device independent pixels.
I haven't tested this code, but it should work as long as your rows are all roughly the same height.
Or in one line:
scrollView.smoothScrollTo(0, selectedView.getTop() - (scrollView.getHeight() / 2) + (selectedView.getHeight() / 2), 0);

How can I process a food plate aka ellipse to stack onto each other until it reaches 20 plates?

My assignment asks me to code an ellipse similar to a plate, and stack 20 of them using if else statements and everything included from chapters 1-6 in shiffmans processing beginner's book. I need to use a button pressed function to stack 20 plates, once they reach to 20 THE "PLATES" MUST INDIVIDUALLY DISAPPEAR TO 0 ONE BY ONE WITH A MOUSE CLICK FUNCTION. This is what I have came up with so far, the plates have to start at the bottom of the screen.
// Declare global (shared) variables here
float plate1X = 50;
float plate1Y = 200;
int plateColor = (255);
// Do not write any statements here (must be inside methods)
void setup()`enter code here`
{
// Add statements to run once when program starts here. For example:
size(400,400);
plate1X = 200;
plate1Y = 50;
background(255);
plate1X = width/2;
plate1Y = height/2;
} // end of setup method
void draw()
{
// Declare local variables here (new each time through)
// Add statements to run each time screen is updated here
ellipse(plate1X, plate1Y, 200,50);
if(ellipse <= 1 || ellipse <= 0; //draw another plate);
// Screen will be repainted automatically at the end of draw method
} // end of draw method
// Add other methods here
"does not detect the variable 'ellipse'": That's because you didn't declare it. You need to let the compiler know what kind of variable it is before you use it. In this case, it is probably an integer, so you should have the line int ellipse; above the first time you use it.
You also never set ellipse to hold any value! You are trying to check if(ellipse <= 1 || ellipse <= 0), but right now ellipse doesn't have any value.
Your ellipse function seems like it should draw an ellipse at the coordinates that you tell it--
ellipse(x, y, width, height)
so if you want to draw another ellipse, you should call ellipse(...) again with a new x and y value.
You will want to use if statements to make sure you only do this some of the time--specifically when you haven't gotten to 20 plates yet. Do you know what if and else statements do?

Categories

Resources