Creating ring shape in Android code - android-layout

I have the following shape XML:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:a="http://schemas.android.com/apk/res/android"
a:shape="ring"
a:innerRadiusRatio="3"
a:thicknessRatio="8"
a:useLevel="false">
<!-- some other stuff goes here -->
</gradient>
</shape>
I would like to use code instead to create this shape, since some things need to be calculated on the fly before I do it, so static pre-defined layout doesn't cut it.
I'm new to Android and can't quite figure out how XML translates to code, and there's no RingShape class inheriting from Shape.
In addition to answering just this question, if there's a guide somewhere that details relation between XML and Java code and how XML gets processed in order to end up on the screen I would appreciate a link too. Thanks.

Reuben already pointed out most the most useful observations, so I'll just focus on the implementation side of the story. There's multiple approaches using reflection that'll probably give you what you're looking for.
First one is to (ab)use the private GradientDrawable constructor that takes a GradientState reference. Unfortunately the latter is a final subclass with package visibility, so you can't easily get access to it. In order to use it, you would need to dive further in using reflection or mimic its functionality into your own code.
Second approach is to use reflection to get the private member variable mGradientState, which fortunately has a getter in the form of getConstantState(). This'll give you the ConstantState, which at runtime is really a GradientState and hence we can use reflection to access its members and change them at runtime.
In order to support above statements, here's a somewhat basic implementation to create a ring-shaped drawable from code:
RingDrawable.java
public class RingDrawable extends GradientDrawable {
private Class<?> mGradientState;
public RingDrawable() {
this(Orientation.TOP_BOTTOM, null);
}
public RingDrawable(int innerRadius, int thickness, float innerRadiusRatio, float thicknessRatio) {
this(Orientation.TOP_BOTTOM, null, innerRadius, thickness, innerRadiusRatio, thicknessRatio);
}
public RingDrawable(GradientDrawable.Orientation orientation, int[] colors) {
super(orientation, colors);
setShape(RING);
}
public RingDrawable(GradientDrawable.Orientation orientation, int[] colors, int innerRadius, int thickness, float innerRadiusRatio, float thicknessRatio) {
this(orientation, colors);
try {
setInnerRadius(innerRadius);
setThickness(thickness);
setInnerRadiusRatio(innerRadiusRatio);
setThicknessRatio(thicknessRatio);
} catch (Exception e) {
// fail silently - change to your own liking
e.printStackTrace();
}
}
public void setInnerRadius(int radius) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
if (mGradientState == null) mGradientState = resolveGradientState();
Field innerRadius = resolveField(mGradientState, "mInnerRadius");
innerRadius.setInt(getConstantState(), radius);
}
public void setThickness(int thicknessValue) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
if (mGradientState == null) mGradientState = resolveGradientState();
Field thickness = resolveField(mGradientState, "mThickness");
thickness.setInt(getConstantState(), thicknessValue);
}
public void setInnerRadiusRatio(float ratio) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
if (mGradientState == null) mGradientState = resolveGradientState();
Field innerRadiusRatio = resolveField(mGradientState, "mInnerRadiusRatio");
innerRadiusRatio.setFloat(getConstantState(), ratio);
}
public void setThicknessRatio(float ratio) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
if (mGradientState == null) mGradientState = resolveGradientState();
Field thicknessRatio = resolveField(mGradientState, "mThicknessRatio");
thicknessRatio.setFloat(getConstantState(), ratio);
}
private Class<?> resolveGradientState() {
Class<?>[] classes = GradientDrawable.class.getDeclaredClasses();
for (Class<?> singleClass : classes) {
if (singleClass.getSimpleName().equals("GradientState")) return singleClass;
}
throw new RuntimeException("GradientState could not be found in current GradientDrawable implementation");
}
private Field resolveField(Class<?> source, String fieldName) throws SecurityException, NoSuchFieldException {
Field field = source.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
}
}
Above can be used as follows to create a RingDrawable from code and display it in a standard ImageView.
ImageView target = (ImageView) findViewById(R.id.imageview);
RingDrawable ring = new RingDrawable(10, 20, 0, 0);
ring.setColor(Color.BLUE);
target.setImageDrawable(ring);
This will show a simple, opaque blue ring in the ImageView (10 units inner radius, 20 units thick). You'll need to make sure to not set the ImageView's width and height to wrap_content, unless you add ring.setSize(width, height) to above code in order for it to show up.
Hope this helps you out in any way.

Ring and other shapes are GradientDrawables.
If you look at the source code for GradientDrawable, you'll see it looks like certain properties (like innerRadius) can only be defined through XML... they are not exposed through accessor methods. The relevant state is also unhelpfully private to the class, so subclassing is no help either.

You can do something like this:
private ShapeDrawable newRingShapeDrawable(int color) {
ShapeDrawable drawable = new ShapeDrawable(new OvalShape());
drawable.getPaint().setColor(color);
drawable.getPaint().setStrokeWidth(2);
drawable.getPaint().setStyle(Paint.Style.STROKE);
return drawable;
}

It is possible to do it from code:
int r = dipToPixels(DEFAULT_CORNER_RADIUS_DIP); // this can be used to make it circle
float[] outerR = new float[]{r, r, r, r, r, r, r, r};
int border = dipToPixels(2); // border of circle
RectF rect = new RectF(border, border, border, border);
RoundRectShape rr = new RoundRectShape(outerR, rect, outerR);// must checkout this constructor
ShapeDrawable drawable = new ShapeDrawable(rr);
drawable.getPaint().setColor(badgeColor);// change color of border
// use drawble now

For me it works as follow: (also for Android version > lollipop)
ImageView target = (ImageView) findViewById(R.id.imageview);
GradientDrawable shapeRing = new GradientDrawable();
shapeRing.setShape(GradientDrawable.OVAL);
shapeRing.setColor(centerColor); // transparent
shapeRing.setStroke(stroke, strokeColor);
shapeRing.setSize(width, width);
target.setImageDrawable(ring);

Related

SVG Path Collision in javafx

My question today is relatively simple, is there a way to have collision handling with the Javafx implementation of an SVGPath? EX: if I drop a particle on the screen, and it encounter Any part of the svgPath, it'll signal a collision.
I tried using the regular bounds collision, but it gives me a bounding box that is gigantic for the SVGPath if the path is shaped like an 'L'
The path I'm specifically playing with is:
m 252,12.362183
c 1.03171,23.632637 -4.57241,55.427587 9,69 65.41611,65.416117 361.05896,43.999997 469,43.999997
Do I have to re-interpolate the line and have an array store a set of (x,y) positions which I constantly check against? that seems rather unwieldy, but I simply can't think of any other (simpler) ways?
The code I tried for the regular bounded collision is as follows:
observableBooleanValue colliding = Bindings.createBooleanBinding(new Callable<Boolean>() {
#Override
public Boolean call() throws Exception {
return particle.getBoundsInParent().intersects(path.getBoundsInParent());
}
}, particle.boundsInParentProperty(), path.boundsInParentProperty());
System.out.println("path bounds: " + path.boundsInParentProperty());
colliding.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> obs,
Boolean oldValue, Boolean newValue) {
if (newValue) {
System.out.println("Colliding");
} else {
System.out.println("Not colliding");
}
}
});
note that a particle is simply a circle with radius 2, and the path is just an SVG path loaded up with the aforementioned svg.
-Will
small edit
So after looking into a few other methods, I got it where it'll get close to be an accurate collision, but it's still about 20% too early (it detects a collision before one actually h appens). almost as if the edges of the path are a bit 'blurry'.
code:
particle.layoutYProperty().addListener(new ChangeListener<Number>(){
#Override
public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
Shape intersect = Shape.intersect(path, particle);
if ((intersect.getBoundsInLocal().getHeight() != -1) && (intersect.getBoundsInLocal().getWidth() != -1)) {
System.out.println("Collison!");
}
}
});
I'm really pretty stumped as to why the edge of the svgpath would have such a large buffer on it.
code that creates the path:
/**
* This function helps to make the path for animating particles
*
* #throws IOException
*/
public void makePaths() throws IOException {
PathLoader loader = new PathLoader();
path = new SVGPath();
path.setContent(loader.getPath(1));
path.setStroke(Color.AQUA);
//path.setFill(Color.TRANSPARENT);
//path.setEffect(boxBlur);
}
the pathloader simply loads in exactly the path I mentioned up above, tried with and w/o the blur and the transparency, no effect in tightening up the path collision.

How can I make a TextArea stretch to fill the content, expanding the parent in the process?

So I have a TextArea and as the user pastes paragraphs into it, or just writes in it, I want it to expand vertically to reveal all the available text. I.e. not to use a scrollbar in the text field itself... much like what happens on many web pages. Many users, myself included, don't like to be forced to edit in a small window. Exactly how Facebook status updates box works.
I've tried
myTextArea.autoSize()
wrapped in an
myTextArea.textProperty().addListener(new ChangeListener()....);
but that doesn't work. I think it's happy autosizing to its current size.
The left, right & top anchors are set to it's parent AnchorPane. I've tried it with the bottom attached and not attached. Ideally I'd like to grow the anchor pane as the textarea grows.
I don't mind reading the TextProperty and calculating a trigger size which I set myself... but this seems a hacky approach IF there is already a best practise. The number of properties and sub objects of javafx is sufficiently daunting that it seems like a good point to ask the question here, rather than trying to figure out how many pixels the font/paragraphs etc are taking up.
Update:
So I thought maybe I was overthinking it, and all I needed to do was to switch the scrollbars off and the rest would happen. Alas, looking for available fields and methods for "scroll", "vertical", "vbar" comes up with nothing I can use. ScrollTopProperty looks like it's for something else.
The problem; the height of textArea is wanted to be grown or shrunk while its text is changing by either user's typing or copy-pasting. Here is another approach:
public class TextAreaDemo extends Application {
private Text textHolder = new Text();
private double oldHeight = 0;
#Override
public void start(Stage primaryStage) {
final TextArea textArea = new TextArea();
textArea.setPrefSize(200, 40);
textArea.setWrapText(true);
textHolder.textProperty().bind(textArea.textProperty());
textHolder.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) {
if (oldHeight != newValue.getHeight()) {
System.out.println("newValue = " + newValue.getHeight());
oldHeight = newValue.getHeight();
textArea.setPrefHeight(textHolder.getLayoutBounds().getHeight() + 20); // +20 is for paddings
}
}
});
Group root = new Group(textArea);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
// See the explanation below of the following line.
// textHolder.setWrappingWidth(textArea.getWidth() - 10); // -10 for left-right padding. Exact value can be obtained from caspian.css
}
public static void main(String[] args) {
launch(args);
}
}
But it has a drawback; the textarea's height is changing only if there are line breaks (ie Enter keys) between multiple lines, if the user types long enough the text gets wrapped to multiple line but the height is not changing.
To workaround this drawback I added this line
textHolder.setWrappingWidth(textArea.getWidth() - 10);
after primaryStage.show();. It works well for long typings where user does not linebreaks. However this generates another problem. This problem occurs when the user is deleting the text by hitting "backspace". The problem occurs exactly when the textHolder height is changed and where the textArea's height is set to new value. IMO it maybe a bug, didn't observe deeper.
In both case the copy-pasting is handling properly.
Awaiting a better, i use this hacky solution.
lookup the vertical scrollbar of the textarea.
make it transparent
listen to its visible property
when the scrollbar become visible i add a row to the textarea.
The code:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TextArea;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class GrowGrowTextArea extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
AnchorPane root = new AnchorPane();
root.setStyle("-fx-padding:20;-fx-background-color:dodgerblue;");
final TextArea textArea = new TextArea();
AnchorPane.setTopAnchor(textArea, 10.0);
AnchorPane.setLeftAnchor(textArea, 10.0);
AnchorPane.setRightAnchor(textArea, 10.0);
root.getChildren().add(textArea);
primaryStage.setScene(new Scene(root, 400, 300));
primaryStage.show();
ScrollBar scrollBar = lookupVerticalScrollBar(textArea);
scrollBar.setOpacity(0.0);
scrollBar.visibleProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> source,
Boolean wasVisible,
Boolean isVisible) {
if (isVisible) {
textArea.setPrefRowCount(textArea.getPrefRowCount() + 1);
textArea.requestLayout();
}
}
});
}
private ScrollBar lookupVerticalScrollBar(Node node) {
if (node instanceof ScrollBar && ((ScrollBar)node).getOrientation() == Orientation.VERTICAL) {
return (ScrollBar) node;
}
if (node instanceof Parent) {
ObservableList<Node> children = ((Parent) node).getChildrenUnmodifiable();
for (Node child : children) {
ScrollBar scrollBar = lookupVerticalScrollBar(child);
if (scrollBar != null) {
return scrollBar;
}
}
}
return null;
}
}
I had a similar problem with creating expanding TextArea. I was creating TextArea that looks like TextField and expand vertically every time when there is no more space in line.
I have tested all solutions that I could find on this topic on stack and other sources available. I found few good solutions but neither was good enough.
After many hours of fighting, I figured out this approach.
I extended TextArea class, override layoutChildren() method and add a listener on text height.
#Override
protected void layoutChildren() {
super.layoutChildren();
setWrapText(true);
addListenerToTextHeight();
}
private void addListenerToTextHeight() {
ScrollPane scrollPane = (ScrollPane) lookup(".scroll-pane");
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
StackPane viewport = (StackPane) scrollPane.lookup(".viewport");
Region content = (Region) viewport.lookup(".content");
Text text = (Text) content.lookup(".text");
text.textProperty().addListener(textHeightListener(text));
}
private InvalidationListener textHeightListener(Text text) {
return (property) -> {
// + 1 for little margin
double textHeight = text.getBoundsInLocal().getHeight() + 1;
//To prevent that our TextArena will be smaller than our TextField
//I used DEFAULT_HEIGHT = 18.0
if (textHeight < DEFAULT_HEIGHT) {
textHeight = DEFAULT_HEIGHT;
}
setMinHeight(textHeight);
setPrefHeight(textHeight);
setMaxHeight(textHeight);
};
}
I used some of the code found in the previous answers.
The growTextAreaIfNecessary method will increase the height of textArea until the scrollbar is not visible (limited to 20 lines in this example).
The problem with this approach is that the window needs to be redrawn several times until the perfect height is found.
private ScrollBar lookupVerticalScrollBar(Node node) {
if (node instanceof ScrollBar && ((ScrollBar) node).getOrientation() == Orientation.VERTICAL) {
return (ScrollBar) node;
}
if (node instanceof Parent) {
ObservableList<Node> children = ((Parent) node).getChildrenUnmodifiable();
for (Node child : children) {
ScrollBar scrollBar = lookupVerticalScrollBar(child);
if (scrollBar != null) {
return scrollBar;
}
}
}
return null;
}
private void growTextAreaIfNecessary(TextArea textArea) {
Platform.runLater(() -> {
ScrollBar lookupVerticalScrollBar = lookupVerticalScrollBar(textArea);
int prefRowCount = textArea.getPrefRowCount();
if (lookupVerticalScrollBar.isVisible() && prefRowCount < 20) {
textArea.setPrefRowCount(prefRowCount + 1);
System.out.println("increasing height to: " + (prefRowCount + 1));
growTextAreaIfNecessary(textArea);
}
});
}
I have tried many hacks, most of them had jitters while typing, this to me was the perfect result:
textArea.textProperty().addListener((obs,old,niu)->{
Text t = new Text(old+niu);
t.setFont(textArea.getFont());
StackPane pane = new StackPane(t);
pane.layout();
double height = t.getLayoutBounds().getHeight();
double padding = 20 ;
textArea.setMinHeight(height+padding);
});

Change the size of a Custom View at Runtime Android

I'm changing the height and the width of my CustomView which extends Android View in runtime like this:
#Override
public void onGlobalLayout() {
if(!initialized) {
int containerHeight = instance.getHeight();
int containerWidth = instance.getWidth();
myView.getLayoutParams().height = (int) (containerHeight * HEIGHT_RATIO);
myView.getLayoutParams().width = (int) (containerWidth * WIDTH_RATIO);
instance.getViewTreeObserver().removeGlobalOnLayoutListener(this);
initialized = true;
}
}
this code is in the container view Constructor.
In addition my CustomView onMeasure() is as follows:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// maximum width we should use
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
}
This is the result:
Where the width and height I specified are at the same size of the green rectangle.
My Question is: why does the actual size of my custom view (red rectangle) is not at the same size as I gave as input in the LayoutParams ?
not sure what object you're working with for your view, but you might try something like this:
CustomViewget.Window().setLayout(370, 480); //Controls width and height
My problem was in the drawing of the CustomView. In onDraw(), I took the width and height of the canvas instead of the View itself.
The sizes might not be calculated properly at the time of calling, so you may need to place your code in a handler and run it on post so that it gets done after everything else on the UI thread is done. Try the following code.
Handler handler = new Handler();
handler.post(new Runnable() {
public void run() {
//check sizes and update sizes here
});

Checking Collision of Shapes with JavaFX

I am trying to do some collision detection. For this test I am using simple rectangular Shape, and checking their Bound, to figure if they are colliding. Although the detection does not work as expected. I have tried using different ways to move the object(relocate, setLayoutX,Y) and also different bound checks (boundsInLocal,boundsInParrent etc) but I still cannot get this to work. As you can see the detection works only for one object, even when you have three objects only one detects collision. This is some working code demonstrating the problem:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.util.ArrayList;
public class CollisionTester extends Application {
private ArrayList<Rectangle> rectangleArrayList;
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage) {
primaryStage.setTitle("The test");
Group root = new Group();
Scene scene = new Scene(root, 400, 400);
rectangleArrayList = new ArrayList<Rectangle>();
rectangleArrayList.add(new Rectangle(30.0, 30.0, Color.GREEN));
rectangleArrayList.add(new Rectangle(30.0, 30.0, Color.RED));
rectangleArrayList.add(new Rectangle(30.0, 30.0, Color.CYAN));
for(Rectangle block : rectangleArrayList){
setDragListeners(block);
}
root.getChildren().addAll(rectangleArrayList);
primaryStage.setScene(scene);
primaryStage.show();
}
public void setDragListeners(final Rectangle block) {
final Delta dragDelta = new Delta();
block.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
// record a delta distance for the drag and drop operation.
dragDelta.x = block.getTranslateX() - mouseEvent.getSceneX();
dragDelta.y = block.getTranslateY() - mouseEvent.getSceneY();
block.setCursor(Cursor.NONE);
}
});
block.setOnMouseReleased(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
block.setCursor(Cursor.HAND);
}
});
block.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
block.setTranslateX(mouseEvent.getSceneX() + dragDelta.x);
block.setTranslateY(mouseEvent.getSceneY() + dragDelta.y);
checkBounds(block);
}
});
}
private void checkBounds(Rectangle block) {
for (Rectangle static_bloc : rectangleArrayList)
if (static_bloc != block) {
if (block.getBoundsInParent().intersects(static_bloc.getBoundsInParent())) {
block.setFill(Color.BLUE); //collision
} else {
block.setFill(Color.GREEN); //no collision
}
} else {
block.setFill(Color.GREEN); //no collision -same block
}
}
class Delta {
double x, y;
}
}
Looks like you have a slight logic error in your checkBounds routine - you are correctly detecting collisions (based on bounds) but are overwriting the fill of your block when you perform subsequent collision checks in the same routine.
Try something like this - it adds a flag so that the routine does not "forget" that a collision was detected:
private void checkBounds(Shape block) {
boolean collisionDetected = false;
for (Shape static_bloc : nodes) {
if (static_bloc != block) {
static_bloc.setFill(Color.GREEN);
if (block.getBoundsInParent().intersects(static_bloc.getBoundsInParent())) {
collisionDetected = true;
}
}
}
if (collisionDetected) {
block.setFill(Color.BLUE);
} else {
block.setFill(Color.GREEN);
}
}
Note that the check you are doing (based on bounds in parent) will report intersections of the rectangle enclosing the visible bounds of nodes within the same parent group.
Alternate Implementation
In case you need it, I updated your original sample so that it is able to check based on the visual shape of the Node rather than the bounding box of the visual shape. This lets you to accurately detect collisions for non-rectangular shapes such as Circles. The key for this is the Shape.intersects(shape1, shape2) method.
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.ArrayList;
import javafx.scene.shape.*;
public class CircleCollisionTester extends Application {
private ArrayList<Shape> nodes;
public static void main(String[] args) { launch(args); }
#Override public void start(Stage primaryStage) {
primaryStage.setTitle("Drag circles around to see collisions");
Group root = new Group();
Scene scene = new Scene(root, 400, 400);
nodes = new ArrayList<>();
nodes.add(new Circle(15, 15, 30));
nodes.add(new Circle(90, 60, 30));
nodes.add(new Circle(40, 200, 30));
for (Shape block : nodes) {
setDragListeners(block);
}
root.getChildren().addAll(nodes);
checkShapeIntersection(nodes.get(nodes.size() - 1));
primaryStage.setScene(scene);
primaryStage.show();
}
public void setDragListeners(final Shape block) {
final Delta dragDelta = new Delta();
block.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
// record a delta distance for the drag and drop operation.
dragDelta.x = block.getLayoutX() - mouseEvent.getSceneX();
dragDelta.y = block.getLayoutY() - mouseEvent.getSceneY();
block.setCursor(Cursor.NONE);
}
});
block.setOnMouseReleased(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
block.setCursor(Cursor.HAND);
}
});
block.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
block.setLayoutX(mouseEvent.getSceneX() + dragDelta.x);
block.setLayoutY(mouseEvent.getSceneY() + dragDelta.y);
checkShapeIntersection(block);
}
});
}
private void checkShapeIntersection(Shape block) {
boolean collisionDetected = false;
for (Shape static_bloc : nodes) {
if (static_bloc != block) {
static_bloc.setFill(Color.GREEN);
Shape intersect = Shape.intersect(block, static_bloc);
if (intersect.getBoundsInLocal().getWidth() != -1) {
collisionDetected = true;
}
}
}
if (collisionDetected) {
block.setFill(Color.BLUE);
} else {
block.setFill(Color.GREEN);
}
}
class Delta { double x, y; }
}
Sample program output. In the sample the circles have been dragged around and the user is currently dragging a circle which has been marked as colliding with another circle (by painting it blue) - for demonstration purposes only the circle currently being dragged has it's collision color marked.
Comments based on additional questions
The link I posted to an intersection demo application in a prior comment was to illustrate the use of various bounds types rather than as a specific type of collision detection sample. For your use case, you don't need the additional complexity of the change listener and checking on various different kinds of bounds types - just settling on one type will be enough. Most collision detection is only going to be interested in intersection of visual bounds rather than other JavaFX bounds types such as the layout bounds or local bounds of a node. So you can either:
Check for intersection of getBoundsInParent (as you did in your original question) which works on the smallest rectangular box which will encompass the visual extremities of the node OR
Use the Shape.intersect(shape1, shape2) routine if you need to check based on the visual shape of the Node rather than the bounding box of the visual shape.
Should I be using setLayoutX or translateX for the rectangle
The layoutX and layoutY properties are intended for positioning or laying out nodes. The translateX and translateY properties are intended for temporary changes to the visual location of a node (for example when the node is undergoing an animation). For your example, though either property will work, it is perhaps better form to use the layout properties than the translate ones, that way if you did want to run something like a TranslateTransition on the nodes, it will be more obvious what the start and end translate values should be as those values will be relative to the current layout position of the node rather than the position in the parent group.
Another way you could use these layout and translate co-ordinates in tandem in your sample is if you had something like an ESC to cancel during the course of a drag operation. You could set layoutX,Y to the initial location of your node, start a drag operation which sets translateX,Y values and if the user presses ESC, set translateX,Y back to 0 to cancel the drag operation or if the user releases the mouse set layoutX,Y to layoutX,Y+translateX,Y and set translateX,Y back to 0. The idea is that the translation is values are used for a temporary modification of the visual co-ordinates of the node from it's original layout position.
will the intersect work even though the circles are animated? I mean without dragging the circle by mouse, what will happen if I made them to move around randomly. Will the colour change in this case also?
To do this, just change where the collision detection function is called and the collision handler invoked. Rather than checking for intersections based upon a mouse drag event (like the example above), instead check for collisions within a change listener on each node's boundsInParentProperty().
block.boundsInParentProperty().addListener((observable, oldValue, newValue) ->
checkShapeIntersection(block)
);
Note: if you have lots of shapes being animated, then checking for collisions once per frame within a game loop will be more efficient than running a collision check whenever any node moves (as is done in the boundsInParentProperty change listener above).
Additional info for handling input on non-rectangular shapes
For input detection not collision detection, so not directly related to your question, look at the node.pickOnBounds setting if you need mouse or touch interaction with a non-rectangular node.

Android getMeasuredHeight returns wrong values !

I'm trying to determine the real dimension in pixels of some UI elements !
Those elements are inflated from a .xml file and are initialized with dip width and height so that the GUI will eventually support multiple screen size and dpi (as recommended by android specs).
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="150dip"
android:orientation="vertical">
<ImageView
android:id="#+id/TlFrame"
android:layout_width="110dip"
android:layout_height="90dip"
android:src="#drawable/timeline_nodrawing"
android:layout_margin="0dip"
android:padding="0dip"/></LinearLayout>
This previous xml represent one frame. But I do add many dynamically inside a horizontal layout describe here :
<HorizontalScrollView
android:id="#+id/TlScroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_margin="0dip"
android:padding="0dip"
android:scrollbars="none"
android:fillViewport="false"
android:scrollbarFadeDuration="0"
android:scrollbarDefaultDelayBeforeFade="0"
android:fadingEdgeLength="0dip"
android:scaleType="centerInside">
<!-- HorizontalScrollView can only host one direct child -->
<LinearLayout
android:id="#+id/TimelineContent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_margin="0dip"
android:padding="0dip"
android:scaleType="centerInside"/>
</HorizontalScrollView >
The method defined to add one frame inside my java code :
private void addNewFrame()
{
LayoutInflater inflater = (LayoutInflater) _parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.tl_frame, null);
TextView frameNumber = (TextView) root.findViewById(R.id.FrameNumber);
Integer val = new Integer(_nFramesDisplayed+1); //+1 to display ids starting from one on the user side
frameNumber.setText(val.toString());
++_nFramesDisplayed;
_content.addView(root);
// _content variable is initialized like this in c_tor
// _content = (LinearLayout) _parent.findViewById(R.id.TimelineContent);
}
Then inside my code, I try to get the actual real size in pixel because I need this to draw some opengl stuff over it.
LayoutInflater inflater = (LayoutInflater) _parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.tl_frame, null);
ImageView frame = (ImageView) root.findViewById(R.id.TlFrame);
frame.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
frame.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
final int w = frame.getMeasuredWidth();
final int h = frame.getMeasuredHeight();
Everything seems to work fine except that those values are way bigger than the actual pixel size of the ImageView.
Reported infos from getWindowManager().getDefaultDisplay().getMetrics(metrics);
are the following :
density = 1,5
densityDpi = 240
widthPixel = 600
heightPixel = 1024
Now, I know the rule from android is : pixel = dip * (dpi /160). But nothing makes any sense with the value returned. For that ImageView of (90dip X 110dip), the returned values of the measure() method is (270 x 218) which I assumed is in pixel !
Anyone has any idea why ?
Is the value returned in pixel ?
By the way : I've been testing the same code but with a TextView instead than an ImageView and everything seems to be working fine ! Why !?!?
You're calling measure incorrectly.
measure takes MeasureSpec values which are specially packed by MeasureSpec.makeMeasureSpec. measure ignores LayoutParams. The parent doing the measuring is expected to create a MeasureSpec based on its own measurement and layout strategy and the child's LayoutParams.
If you want to measure the way that WRAP_CONTENT usually works in most layouts, call measure like this:
frame.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST));
If you don't have max values (for example if you're writing something like a ScrollView that has infinite space) you can use the UNSPECIFIED mode:
frame.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
Do that:
frame.measure(0, 0);
final int w = frame.getMeasuredWidth();
final int h = frame.getMeasuredHeight();
Solved!
Ok ! Kind of Answering my own question here...But not completly
1 - It seems that on some devices, The ImageView measuring do not provide with exact values. I've seen lots of reports on this happenning on Nexus and Galaxy devices for example.
2 - A work around that I've come up with :
Set the width and height of your ImageView to "wrap_content" inside xml code.
Inflate the layout inside your code (generally in the UI initialization I suppose).
LayoutInflater inflater = (LayoutInflater)
_parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.tl_frame, null);
ImageView frame = (ImageView) root.findViewById(R.id.TlFrame);
Calculate your own ratio for your image view, based on the typical Android calculation
//ScreenDpi can be acquired by getWindowManager().getDefaultDisplay().getMetrics(metrics);
pixelWidth = wantedDipSize * (ScreenDpi / 160)
Use the calculated size to set your ImageView dynamycally inside your code
frame.getLayoutParams().width = pixeWidth;
And voila ! your ImageView has now the wanted Dip size ;)
view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#SuppressLint("NewApi")
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
//now we can retrieve the width and height
int width = view.getWidth();
int height = view.getHeight();
//this is an important step not to keep receiving callbacks:
//we should remove this listener
//I use the function to remove it based on the api level!
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN){
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}else{
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
});
One should go with How to get width/height of a View
Unfortunately, in Activity lifecycle methods such as Activity#onCreate(Bundle), a layout pass has not yet been performed, so you can't yet retrieve the size of views in your view hierarchy. However, you can explicitly ask Android to measure a view using View#measure(int, int).
As #adamp's answer points out, you have to provide View#measure(int, int) with MeasureSpec values, but it can be a bit daunting figuring out the correct MeasureSpec.
The following method tries to determine the correct MeasureSpec values and measures the passed in view:
public class ViewUtil {
public static void measure(#NonNull final View view) {
final ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
final int horizontalMode;
final int horizontalSize;
switch (layoutParams.width) {
case ViewGroup.LayoutParams.MATCH_PARENT:
horizontalMode = View.MeasureSpec.EXACTLY;
if (view.getParent() instanceof LinearLayout
&& ((LinearLayout) view.getParent()).getOrientation() == LinearLayout.VERTICAL) {
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
horizontalSize = ((View) view.getParent()).getMeasuredWidth() - lp.leftMargin - lp.rightMargin;
} else {
horizontalSize = ((View) view.getParent()).getMeasuredWidth();
}
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
horizontalMode = View.MeasureSpec.UNSPECIFIED;
horizontalSize = 0;
break;
default:
horizontalMode = View.MeasureSpec.EXACTLY;
horizontalSize = layoutParams.width;
break;
}
final int horizontalMeasureSpec = View.MeasureSpec
.makeMeasureSpec(horizontalSize, horizontalMode);
final int verticalMode;
final int verticalSize;
switch (layoutParams.height) {
case ViewGroup.LayoutParams.MATCH_PARENT:
verticalMode = View.MeasureSpec.EXACTLY;
if (view.getParent() instanceof LinearLayout
&& ((LinearLayout) view.getParent()).getOrientation() == LinearLayout.HORIZONTAL) {
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
verticalSize = ((View) view.getParent()).getMeasuredHeight() - lp.topMargin - lp.bottomMargin;
} else {
verticalSize = ((View) view.getParent()).getMeasuredHeight();
}
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
verticalMode = View.MeasureSpec.UNSPECIFIED;
verticalSize = 0;
break;
default:
verticalMode = View.MeasureSpec.EXACTLY;
verticalSize = layoutParams.height;
break;
}
final int verticalMeasureSpec = View.MeasureSpec.makeMeasureSpec(verticalSize, verticalMode);
view.measure(horizontalMeasureSpec, verticalMeasureSpec);
}
}
Then you can simply call:
ViewUtil.measure(view);
int height = view.getMeasuredHeight();
int width = view.getMeasuredWidth();
Alternatively, as #Amit Yadav suggested, you can use OnGlobalLayoutListener to have a listener called after the layout pass has been performed. The following is a method that handles unregistering the listener and method naming changes across versions:
public class ViewUtil {
public static void captureGlobalLayout(#NonNull final View view,
#NonNull final ViewTreeObserver.OnGlobalLayoutListener listener) {
view.getViewTreeObserver()
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
final ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
viewTreeObserver.removeOnGlobalLayoutListener(this);
} else {
//noinspection deprecation
viewTreeObserver.removeGlobalOnLayoutListener(this);
}
listener.onGlobalLayout();
}
});
}
}
Then you can:
ViewUtil.captureGlobalLayout(rootView, new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int width = view.getMeasureWidth();
int height = view.getMeasuredHeight();
}
});
Where rootView can be the root view of your view hierarchy and view can be any view within your hierarchy that you want to know the dimensions of.
You have to create Custom Textview and use it in your layouts and use getActual height function to set the height at runtime
public class TextViewHeightPlus extends TextView {
private static final String TAG = "TextViewHeightPlus";
private int actualHeight=0;
public int getActualHeight() {
return actualHeight;
}
public TextViewHeightPlus(Context context) {
super(context);
}
public TextViewHeightPlus(Context context, AttributeSet attrs) {
super(context, attrs);
setCustomFont(context, attrs);
}
public TextViewHeightPlus(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
actualHeight=0;
actualHeight=(int) ((getLineCount()-1)*getTextSize());
}
}
Probably, because of what you have in AndroidManifest.xml (link) file and from which drawable-XXX directory the xml file comes, Android loads resources with scaling operation. You decide to use "dip" (link) dimension unit which is virtual and the real value (px) can be different.

Resources