What is the android equivalent of UIView's convertRect / convertPoint functions? - android

UIView has the following:
- convertPoint:toView:
- convertPoint:fromView:
- convertRect:toView:
- convertRect:fromView:
What is the Android equivalent? More generally, given two Views, how do I get the second View's rect in the coordinate system of the first?

I don't think there is an equivalent as part of the sdk, but it seems like you could write your own implementation very easily using getLocationOnScreen:
public static Point convertPoint(Point fromPoint, View fromView, View toView){
int[] fromCoord = new int[2];
int[] toCoord = new int[2];
fromView.getLocationOnScreen(fromCoord);
toView.getLocationOnScreen(toCoord);
Point toPoint = new Point(fromCoord[0] - toCoord[0] + fromPoint.x,
fromCoord[1] - toCoord[1] + fromPoint.y);
return toPoint;
}
public static Rect convertRect(Rect fromRect, View fromView, View toView){
int[] fromCoord = new int[2];
int[] toCoord = new int[2];
fromView.getLocationOnScreen(fromCoord);
toView.getLocationOnScreen(toCoord);
int xShift = fromCoord[0] - toCoord[0];
int yShift = fromCoord[1] - toCoord[1];
Rect toRect = new Rect(fromRect.left + xShift, fromRect.top + yShift,
fromRect.right + xShift, fromRect.bottom + yShift);
return toRect;
}

In Android it might not have the method that exactly like that
however, you could do by
View.getLocationOnScreen(int[] location) and View.getLocationInWindowint[] location()
which
getLocationOnScreen(int[] location)
Computes the coordinates of this view in
its window.
getLocationInWindow(int[] location)
Computes the coordinates of this view on
the screen.
or View.getLeft() and View.getTop()
which
getLeft()
Left position of this view relative to its parent.
getTop()
Top position of this view relative to its parent.
for you case, I would use getLeft() and getTop() to find it's space between 2 View and get range of it
this link has an example of how to find it using getLeft() and getTop()
private int getRelativeLeft(View myView) {
if (myView.getParent() == myView.getRootView())
return myView.getLeft();
else
return myView.getLeft() + getRelativeLeft((View) myView.getParent());
}
private int getRelativeTop(View myView) {
if (myView.getParent() == myView.getRootView())
return myView.getTop();
else
return myView.getTop() + getRelativeTop((View) myView.getParent());
}

The way to do this is with getGlobalVisibleRect(Rect r, Point globalOffset).
/**
* If some part of this view is not clipped by any of its parents, then
* return that area in r in global (root) coordinates. To convert r to local
* coordinates (without taking possible View rotations into account), offset
* it by -globalOffset (e.g. r.offset(-globalOffset.x, -globalOffset.y)).
* If the view is completely clipped or translated out, return false.
*
* #param r If true is returned, r holds the global coordinates of the
* visible portion of this view.
* #param globalOffset If true is returned, globalOffset holds the dx,dy
* between this view and its root. globalOffet may be null.
* #return true if r is non-empty (i.e. part of the view is visible at the
* root level.
*/
So you give it a Point and get the offset relative to the root View. They key is that your views get a location relative to a common item. You can then use this offset to calculate relative positions. The doc says to get local coordinates, you offset the Rect with -globalOffset. If you want to get the second view's Rect in coordinates of the first, then offset the Rect of the first view with -globalOffset of the second view.
Similarly, you can use the globalOffset to transform a point to relative coordinates if you know it's coordinates in the root view. This should be a subtraction: anyPoint - globalOffset.
Remember to check that getGlobalVisibleRect() returned true and that globalOffset is not null before you do the calculations.

Related

How to Convert View Coordinates to Canvas Coordinates on Android?

I'm porting an app from ios to android that involves drag and drop items from a list view into a view that has a rectangle area.
On Swift I used this Code to validate that the item is inside the main area:
func isInsideMainArea(point: CGPoint) -> Bool {
let nodePoint = convertPoint(fromView: point);
return mainArea?.contains(nodePoint);
}
But on android I cannot find the correct way to convert the View coordinates to the canvas coordinates so I can evalute if the dropped item is inside the main area.
So far this is my validation code but does not work as expected, because if I scale or translate the canvas the coordinates change:
public Boolean isInsideMainArea(Room testRoom, Float textX, Float testY) {
RectF rectangle = new RectF(
x,
y,
room.getWidth() * CONSTANTS.PIX_PER_METER * metrics.density,
room.getHeight() * CONSTANTS.PIX_PER_METER * metrics.density
);
if(mainArea.contains(rectangle)){
return true;
}
return false;
}
Is there a equivalent to the function convertPoint on android or a way to do it?

Converting Camera Coordinates to Custom View Coordinates

I am trying to make a simple face detection app consisting of a SurfaceView (essentially a camera preview) and a custom View (for drawing purposes) stacked on top. The two views are essentially the same size, stacked on one another in a RelativeLayout. When a person's face is detected, I want to draw a white rectangle on the custom View around their face.
The Camera.Face.rect object returns the face bound coordinates using the coordinate system explained here and the custom View uses the coordinate system described in the answer to this question. Some sort of conversion is needed before I can use it to draw on the canvas.
Therefore, I wrote an additional method ScaleFacetoView() in my custom view class (below) I redraw the custom view every time a face is detected by overriding the OnFaceDetection() method. The result is the white box appears correctly when a face is in the center. The problem I noticed is that it does not correct track my face when it moves to other parts of the screen.
Namely, if I move my face:
Up - the box goes left
Down - the box goes right
Right - the box goes upwards
Left - the box goes down
I seem to have incorrectly mapped the values when scaling the coordinates. Android docs provide this method of converting using a matrix, but it is rather confusing and I have no idea what it is doing. Can anyone provide some code on the correct way of converting Camera.Face coordinates to View coordinates?
Here's the code for my ScaleFacetoView() method.
public void ScaleFacetoView(Face[] data, int width, int height, TextView a){
//Extract data from the face object and accounts for the 1000 value offset
mLeft = data[0].rect.left + 1000;
mRight = data[0].rect.right + 1000;
mTop = data[0].rect.top + 1000;
mBottom = data[0].rect.bottom + 1000;
//Compute the scale factors
float xScaleFactor = 1;
float yScaleFactor = 1;
if (height > width){
xScaleFactor = (float) width/2000.0f;
yScaleFactor = (float) height/2000.0f;
}
else if (height < width){
xScaleFactor = (float) height/2000.0f;
yScaleFactor = (float) width/2000.0f;
}
//Scale the face parameters
mLeft = mLeft * xScaleFactor; //X-coordinate
mRight = mRight * xScaleFactor; //X-coordinate
mTop = mTop * yScaleFactor; //Y-coordinate
mBottom = mBottom * yScaleFactor; //Y-coordinate
}
As mentioned above, I call the custom view like so:
#Override
public void onFaceDetection(Face[] arg0, Camera arg1) {
if(arg0.length == 1){
//Get aspect ratio of the screen
View parent = (View) mRectangleView.getParent();
int width = parent.getWidth();
int height = parent.getHeight();
//Modify xy values in the view object
mRectangleView.ScaleFacetoView(arg0, width, height);
mRectangleView.setInvalidate();
//Toast.makeText( cc ,"Redrew the face.", Toast.LENGTH_SHORT).show();
mRectangleView.setVisibility(View.VISIBLE);
//rest of code
Using the explanation Kenny gave I manage to do the following.
This example works using the front facing camera.
RectF rectF = new RectF(face.rect);
Matrix matrix = new Matrix();
matrix.setScale(1, 1);
matrix.postScale(view.getWidth() / 2000f, view.getHeight() / 2000f);
matrix.postTranslate(view.getWidth() / 2f, view.getHeight() / 2f);
matrix.mapRect(rectF);
The returned Rectangle by the matrix has all the right coordinates to draw into the canvas.
If you are using the back camera I think is just a matter of changing the scale to:
matrix.setScale(-1, 1);
But I haven't tried that.
The Camera.Face class returns the face bound coordinates using the image frame that the phone would save into its internal storage, rather than using the image displayed in the Camera Preview. In my case, the images were saved in a different manner from the camera, resulting in a incorrect mapping. I had to manually account for the discrepancy by taking the coordinates, rotating it counter clockwise 90 degrees and flipping it on the y-axis prior to scaling it to the canvas used for the custom view.
EDIT:
It would also appear that you can't change the way the face bound coordinates are returned by modifying the camera capture orientation using the Camera.Parameters.setRotation(int) method either.

What is location of getLocationOnScreen return for?

As i understand that when we pass int[2] location array into view.getLocationOnScreen(location), we will get left,top coordinates of that view. But when i debug this function, i see that location return for right,top coordinates of that view. my screen is 480x800, i margin right for view is 40, so the right coordinate of view is 440 , it equal to location[0] return from view.getLocationOnScreen(location). Why location[0] return for right coordinate of view?
it is exact return for left,top coordinates of that view. I get wrong value because my view still not rendered before i call view.getLocationOnScreen(location).
To confirm the accepted answer, the coordinate does appear to be the top-left corner of the view as per the implementation of getLocationOnScreen; also, the y-axis is inverted.
public void getLocationOnScreen(#Size(2) int[] outLocation) {
getLocationInWindow(outLocation);
final AttachInfo info = mAttachInfo;
if (info != null) {
outLocation[0] += info.mWindowLeft;
outLocation[1] += info.mWindowTop;
}
}

When invalidate method called on an array of Views, the first View is not redrawn

I have an app in which I display nine views within a 3x3 GridView using an adapter class. Within each of the nine views that are contained within the cells of the GridView, I use Canvas and Paint objects to display a 2-dimensional line graphic; these line graphics are subsequently modified and re-displayed by invoking the invalidate() method of each view.
The line graphics in all nine views are displayed correctly when the views are created in the Adapter class’s overridden getView() method, but when I attempt to modify and then redisplay the line graphics subsequently, all of the views are refreshed successfully except for the first view in the top-left-hand corner of the grid, which continues to show the original line drawing. I’ve stepped through my code to establish that the first view is definitely being invalidated, and it is, so I’m baffled as to why the call to the invalidate() method on the first view doesn’t cause it to be redrawn, while the same call on all of the remaining views causes them to be redrawn successfully. I’ve also logged calls to the view’s onDraw method, and this shows that the first view’s onDraw method is called each time, so I’m pretty sure this problem is not being caused by any bugs in the application code.
The code that modifies and refreshes the nine views is as follows:
void updateViews(int parentTestCanvas) {
TestCanvasView testCanvas = testCanvass[parentTestCanvas];
double[] parentGenome = testCanvas.getGenome();
// Assign the parent genome to the testCanvass in the array
// The inherited genome will be subject to mutation in all cases except the first testCanvas
for(int testCanvasItem = 0; testCanvasItem < TestCanvasApp.geneCount; testCanvasItem++) {
testCanvas = testCanvass[testCanvasItem];
testCanvas.setGenome(parentGenome);
// Invalidate the testCanvas view to force it to be redrawn using the new genome
testCanvas.invalidate();
}
}
The onDraw method in the TestCanvasView class is as follows:
protected void onDraw(Canvas canvas) {
float xOrigin = getMeasuredWidth() / 2;
float yOrigin = getMeasuredHeight() / 2;
canvas.drawPaint(cellPaint);
canvas.translate(xOrigin, yOrigin);
drawBranch(canvas, linePaint, 0, 0, this.length, this.direction, this.xInc, this.yInc, this.scale);
Log.d("TestCanvasView", "Drawing testCanvas " + mCellIndex);
}
private void drawBranch(Canvas canvas, Paint linePaint, double startX,
double startY, double branchLen, int branchDir, double[] xInc,
double[] yInc, double scale) {
branchDir = (branchDir + 8) % 8;
double newX = startX + branchLen * xInc[branchDir];
double newY = startY + branchLen * yInc[branchDir];
canvas.drawLine((float) (startX / scale), (float) (-startY / scale),
(float) (newX / scale), (float) (-newY / scale), linePaint);
if (branchLen > 1) {
drawBranch(canvas, linePaint, newX, newY, branchLen - 1, branchDir + 1, xInc, yInc, scale);
drawBranch(canvas, linePaint, newX, newY, branchLen - 1, branchDir - 1, xInc, yInc, scale);
}
}
Anyone got any ideas as to why the first view is not being redrawn?
OK, finally got to the bottom of this - it turns out that the getView method in the TestCanvasAdapter class was called twice for testCanvass[0], but only once for all the other elements. I had naively assumed that the Adapter class's getView method would be called exactly once for each element in the array, but this post confirms that getView may be called more than once for various obscure reasons that are unlikely to be readily apparent to inexperienced Android developers like me. Once I understood this, I was easily able to add the logic below to check that testCanvass[position] was not null before assigning the view reference to it within the TestCanvasAdapter.getView method, which resolved the problem.
// Add the newly created TestCanvasView object to the array on the TestCanvasApp object
if (position >= 0 && position < TestCanvasApp.viewCount
&& mTestCanvasApp.testCanvass[position] == null) {
mTestCanvasApp.testCanvass[position] = testCanvasView;
}
Many thanks to Romain Guy for taking the trouble to reply to this query.
If the first view's onDraw() method is invoked, then the invalidate worked. onDraw() is not called if the View doesn't intersect with the clipping rectangle. You can verify this by opening the Dev Tools app (on the emulator but you can also install it on a phone) and in the development settings screen, turn on "Show screen updates." It will flash regions of the screen that get invalidated/redrawn.

How can you detect which view you are passing over when performing a touch event?

I want to know how I can detect child views if I move a view from one ViewGroup to another ViewGroup, particularly when doing a touch event. Is there a method I can call that will let me know which views i'm "hovering" over?
What I'm doing right now is when I detect an ACTION_MOVE event on my view i'm raising it to the top level parent so that it can move and be drawn within the entire window ( and not just inside it's original parent bounds ), then I want to move the view across to a different ViewGroup and on ACTION_UP attach the view to that ViewGroup.
Inspired by Ami's response, but discovering that MotionEvent#getX()/getY() along with View#getTop()/etc return coordinates wrt the parent View, I ended up doing the following below to operate in screen coordinates, allowing me to work across ViewGroups:
private boolean inRegion(float x, float y, View v) {
v.getLocationOnScreen(mCoordBuffer);
return mCoordBuffer[0] + v.getWidth() > x && // right edge
mCoordBuffer[1] + v.getHeight() > y && // bottom edge
mCoordBuffer[0] < x && // left edge
mCoordBuffer[1] < y; // top edge
}
whose usage inside an OnTouchListener is e.g.:
boolean inside = inRegion(event.getRawX(), event.getRawY(), targetView);
I think I found a simpler way to do this.
Create an ArrayList of possible targets
Call this method from your touch event, supplying your targets list and the coords
private View findView(float x, float y, ArrayList<View> targets)
{
final int count = targets.size();
for (int i = 0; i < count; i++) {
final View target = targets.get(i);
if (target.getRight() > x && target.getTop() < y
&& target.getBottom() > y && target.getLeft() < x) {
return target;
}
}
return null;
}
I found Sebastian Roth's answer very helpful with resources, but since it wasn't really an answer to my question, I thought I'd share what I came up with.
Here is the code I use to detect views ( only views that will accept a drop that is ) given a coordinate on the screen.
private DropView findDropTarget( int x, int y, int[] dropCoordinates ){
final Rect r = mRectTemp;
final ArrayList<DropView> dropTargets = ((main) context).getBoardDropTargets();
final int count = dropTargets.size();
for (int i=count-1; i>=0; i--) {
final DropView target = dropTargets.get(i);
target.getHitRect(r);
target.getLocationOnScreen(dropCoordinates);
r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop());
if (r.contains(x, y)) {
dropCoordinates[0] = x - dropCoordinates[0];
dropCoordinates[1] = y - dropCoordinates[1];
return target;
}
}
}
Ok, first off mRectTemp is just an allocated Rectangle so you don't have to keep creating new ones ( I.E. final Rect r = new Rect() )
The next line dropTargets is a list of views that will accept a drop in my app.
Next I loop through each view.
I then use getHitRect(r) to return the screen coordiantes of the view.
I then offset the coordiantes to account for the notification bar or any other view that could displace the coordiantes.
finally I see if x and y are inside the coordinates of the given rectangle r ( x and y are the event.rawX() and event.rawY() ).
It actually turned out to be simpler then expected and works very well.
Read this:
http://developer.android.com/reference/android/view/ViewGroup.html#onInterceptTouchEvent(android.view.MotionEvent)
http://developer.android.com/reference/android/view/View.html#onTouchEvent(android.view.MotionEvent)
I had implemented a Drag and Drop using that method.
I also highly recommend a read of the HomeScreen sourcecode, which contains this thing (kind of):
https://android.googlesource.com/platform/packages/apps/Launcher2

Categories

Resources