I have an imageview with a layout on top of it which has buttons added at specific x and y coordinates. How do i make sure that everything lines up the same on different screen sizes so the buttons are always in the same position relative to the image regardless of screen, also how to maintain all the positioning if the orientation is horizontal.
I actually built a class specifically or this. You can find it here:
https://gist.github.com/nathan-fiscaletti/190a660620f6130e6a15962f59b21f22
The way you would use it is as follows:
Replace the ImageView in your layout with SizeAwareImageView.
<com.my.app.SizeAwareImageView
android:id="#+id/sizeAwareImageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/viewHeader"
app:srcCompat="#drawable/somePicture" />
You can then determine the placement of those objects and use use the following to figure out where to place them.
PointF location = sizeAwareImageView.getScaledCoordinateWithPadding(new PointF(10f, 10f));
// place the object at the location
This will keep your objects at the same location based on the drawable applied to the image view. No matter what screen it is on, the location variable will always point to the same placement on the image.
I use this to store the location of map markers in a database, they can then be used to move a pin around a static map drawable regardless of what device they are displayed on.
/**
* Update the map markers location and display it.
*
* #param mapPinLocation The new location.
*/
private void updateMarker(final PointF mapPinLocation) {
markerContainer.removeView(mapMarker);
PointF location = mapImageView
.getScaledCoordinateWithPadding(mapPinLocation, 100f);
if (getContext() != null) {
int markerSize = (int) DisplayUtil.dpToPx(25, getContext());
RelativeLayout.LayoutParams lp
= new RelativeLayout.LayoutParams(markerSize, markerSize);
// Android uses top left of the view for it's coords
// so we need to center it at it's X/Y coords.
if (location != null) {
lp.topMargin = (int) location.y - (markerSize / 2);
lp.leftMargin = (int) location.x - (markerSize / 2);
mapMarker.setLayoutParams(lp);
markerContainer.addView(mapMarker);
}
}
}
Hopefully this is actually what you were looking for.
Related
I have a google maps app that I need to be able to upload different floorplans to. So far I am adding the floorplan image to the map using a ground overlay object like this:
LatLngBounds bounds = new LatLngBounds(new LatLng(floorplan.get_swlat(), floorplan.get_swlng()),
new LatLng(floorplan.get_nelat(), floorplan.get_nelng()));
GroundOverlayOptions overlayOptions = new GroundOverlayOptions()
.image(BitmapDescriptorFactory.fromBitmap(floorplan.get_floorplanimage())).positionFromBounds(bounds);
mMap.addGroundOverlay(overlayOptions);
This adds the floorplan to the map, but the problem is that it is only north-south facing, so it won't work for buildings that don't perfectly sit like that. So I was wondering if there was a way to go in and manually place it (and use fingers to rotate and scale the image) where I need it to be. Anyone know where to even begin?
Took some time but I finally figured something out. Instead of adding the overlay using the two corners I use the center lat/lon and the width and height. I add the overlay as follows.
mOverlayOptions = new GroundOverlayOptions()
.image(BitmapDescriptorFactory.fromBitmap(floorplan.get_floorplanimage()))
.position(new LatLng(floorplan.get_clat(), floorplan.get_clng()), (float) floorplan.get_width(), (float) floorplan.get_height())
.bearing((float) floorplan.get_bearing())
.transparency(mOpacity);
mOverlay = mMap.addGroundOverlay(mOverlayOptions);
So you have to give the floorplan initial values to begin.Then I added two markers, one located at the center of the overlay (same lat/lon) and another one located along the center of the right edge of the overlay (mSizeMarker). Make sure they're draggable. I had to do some math to figure out where to place the one on the right edge.
mCenterMarker = mMap.addMarker(new MarkerOptions().position(new LatLng(floorplan.get_clat(), floorplan.get_clng())));
Double new_longitude = floorplan.get_clng() + (floorplan.get_width() / 2 / (6371000)) * (180 / 3.14159265) / Math.cos(floorplan.get_clat() * 3.14159265/180);
mSizeMarker = mMap.addMarker(new MarkerOptions().position(new LatLng(floorplan.get_clat(), new_longitude)));
mCenterMarker.setDraggable(true);
mSizeMarker.setDraggable(true);
All that's left is to add an OnMarkerDragListener:
mMap.setOnMarkerDragListener(new GoogleMap.OnMarkerDragListener() {
#Override
public void onMarkerDragStart(Marker marker) {
}
}
#Override
public void onMarkerDrag(Marker marker) {
if (marker.equals(mCenterMarker)) {
mOverlay.remove();
mOverlayOptions.position(marker.getPosition(), mOverlay.getWidth(), mOverlay.getHeight());
mOverlay = mMap.addGroundOverlay(mOverlayOptions);
mSizeMarker.setPosition(new LatLng(marker.getPosition().latitude, marker.getPosition().longitude + (mOverlay.getWidth() / 2 / (6371000)) * (180 / 3.14159265) / Math.cos(marker.getPosition().latitude * 3.14159265/180)));
}
if (marker.equals(mSizeMarker)) {
marker.setPosition(new LatLng(mCenterMarker.getPosition().latitude, marker.getPosition().longitude));
double newWidth = ((marker.getPosition().longitude - mCenterMarker.getPosition().longitude) / ((180 / 3.14159265) / Math.cos(mCenterMarker.getPosition().latitude * 3.14159265/180))) * 6371000 * 2;
mOverlay.remove();
mOverlayOptions.position(mCenterMarker.getPosition(), (float) newWidth);
mOverlay = mMap.addGroundOverlay(mOverlayOptions);
}
}
#Override
public void onMarkerDragEnd(Marker marker) {
if (marker.equals(mSizeMarker)) {
marker.setPosition(new LatLng(mCenterMarker.getPosition().latitude, marker.getPosition().longitude));
}
}
});
Use the middle marker to drag where you want the floorplan to be located, and the one on the right to change the width (the height will automatically update correspondingly).
Then when the image is positioned correctly, just have a save button somewhere that updates where the floorplan is stored with the position, width, and height of mOverlay. It would be possible to add another marker to manually adjust the bearing, but that's more math than I need to do right now. I'll just keep adjusting it when I set it originally. Hope this helps anyone else who comes along this problem, I know this solution is working great for me so far.
Background
Suppose I have a Google maps view, and another view on top of it, that covers a part of it, hiding some content of the map.
The problem
I need to make the "camera" of the map, to focus and have a marker on a coordinate , yet let it all be in the middle of the visible part of the map.
Something like this:
The original code was focusing on (about) the center of the entire screen, making the marker almost invisible (as the bottom view covers it).
Thing is, I can't find the proper way to set the correct value to the Y coordinate of the map itself (meaning latitude).
What I've tried
I tried, given the height of the bottom view, and the coordinate that I've put the marker on, to calculate the delta (yet of course not change the marker itself) :
final float neededZoom = 6.5f;
int bottomViewHeight = bottomView.getHeight();
LatLng posToFocusOn = ...;
final Point point = mMap.getProjection().toScreenLocation(posToFocusOn);
final float curZoom = mMap.getCameraPosition().zoom;
point.y += bottomViewHeight * curZoom / neededZoom;
posToFocusOn = mMap.getProjection().fromScreenLocation(point);
final CameraUpdate cameraPosition = CameraUpdateFactory.newCameraPosition(new Builder().target(posToFocusOn).zoom(neededZoom).build());
Sadly, this focuses way above the marker.
The question
What's wrong with what I wrote? What can I do to fix it?
ok, I've found a workaround, which I think works on all devices (tested on 3, each with a different screen resolution and size) :
I've measured how many pixels (and then converted to DP) a change of one degree has on the marker itself.
From this, I measured the height of each view, and calculated the delta needed to move the camera.
In my case, it's this way (supposing the zoom is 6.5f) :
//measured as 223 pixels on Nexus 5, which has xxhdpi, so divide by 3
final float oneDegreeInPixels = convertDpToPixels( 223.0f / 3.0f);
final float mapViewCenter = mapViewHeight / 2.0f;
final float bottomViewHeight = ...;
final float posToFocusInPixelsFromTop = (mapViewHeight - bottomViewHeight) / 2.0f ;// can optionally add the height of the view on the top area
final float deltaLatDegreesToMove = (mapViewCenter - posToFocusInPixelsFromTop) / oneDegreeInPixels;
LatLng posToFocusOn = new LatLng(latitude - deltaLatDegreesToMove, longitude);
final CameraUpdate cameraPosition = CameraUpdateFactory.newCameraPosition(new Builder().target(posToFocusOn).zoom(neededZoom).build());
And it worked.
I wonder if it can be adjusted to support any value of zoom.
Your code is almost right, but it goes above the marker because you are taking into account bottomViewHeight when computing point.y instead of bottomViewHeight/2 (When your view's size is 200px, you only need to displace the map 100px to recenter it):
point.y += (bottomViewHeight / 2) * curZoom / neededZoom;
Update:
This is a more general approach taht takes into account the map bounds and calculates a new map bounds according to the height of your bottomView. This is zoom independent.
public void recenter() {
LatLngBounds mapBounds = mMap.getProjection().getVisibleRegion().latLngBounds;
Point nothEastPoint = mMap.getProjection().toScreenLocation(mapBounds.northeast);
Point souhWestPoint = mMap.getProjection().toScreenLocation(mapBounds.southwest);
Point newNorthEast = new Point(nothEastPoint.x, nothEastPoint.y + bottomView.getHeight() / 2);
Point newSouhWestPoint = new Point(souhWestPoint.x, souhWestPoint.y + bottomView.getHeight() / 2);
LatLngBounds newBounds = LatLngBounds.builder()
.include(mMap.getProjection().fromScreenLocation(newNorthEast))
.include(mMap.getProjection().fromScreenLocation(newSouhWestPoint))
.build();
mMap.moveCamera(CameraUpdateFactory.newLatLngBounds(newBounds, 0));
}
Note that each time you call recenter() the map will move.
Background
I'm developing an app for Android that plots data as a line graph using AndroidPlot. Because of the nature of the data, it's important that it be pannable and zoomable. I'm using AndroidPlot's sample code on bitbucket for panning and zooming, modified to allow panning and zooming in both X and Y directions.
Everything works as desired except that there are no X and Y axis lines. It is very disorienting to look at the data without them. The grid helps, but there's no guarantee that grid lines will actually fall on the axis.
To remedy this I have tried adding two series, one that falls on just the X axis and the other on the Y. The problem with this is that if one zooms out too far the axis simply end, and it becomes apparent that I have applied a 'hack'.
Question
Is it possible to add X and Y axis lines to AndroidPlot? Or will my sad hack have to do?
EDIT
Added tags
I figured it out. It wasn't trivial, took a joint effort with a collaborator, and sucked up many hours of our time.
Starting with the sample mentioned in my question, I had to extend XYPlot (which I called GraphView) and override the onPreInit method. Note that I have two PointF's, minXY and maxXY, that are defined in my overridden XYPlot and manipulated when I zoom or scroll.
#Override
protected void onPreInit() {
super.onPreInit();
final Paint axisPaint = new Paint();
axisPaint.setColor(getResources().getColor(R.color.MY_AXIS_COLOR));
axisPaint.setStrokeWidth(3); //or whatever stroke width you want
XYGraphWidget oldWidget = getGraphWidget();
XYGraphWidget widget = new XYGraphWidget(getLayoutManager(),
this,
new SizeMetrics(
oldWidget.getHeightMetric(),
oldWidget.getWidthMetric())) {
//We now override XYGraphWidget methods
RectF mGridRect;
#Override
protected void doOnDraw(Canvas canvas, RectF widgetRect)
throws PlotRenderException {
//In order to draw the x axis, we must obtain gridRect. I believe this is the only
//way to do so as the more convenient routes have private rather than protected access.
mGridRect = new RectF(widgetRect.left + ((isRangeAxisLeft())?getRangeLabelWidth():1),
widgetRect.top + ((isDomainAxisBottom())?1:getDomainLabelWidth()),
widgetRect.right - ((isRangeAxisLeft())?1:getRangeLabelWidth()),
widgetRect.bottom - ((isDomainAxisBottom())?getDomainLabelWidth():1));
super.doOnDraw(canvas, widgetRect);
}
#Override
protected void drawGrid(Canvas canvas) {
super.drawGrid(canvas);
if(mGridRect == null) return;
//minXY and maxXY are PointF's defined elsewhere. See my comment in the answer.
if(minXY.y <= 0 && maxXY.y >= 0) { //Draw the x axis
RectF paddedGridRect = getGridRect();
//Note: GraphView.this is the extended XYPlot instance.
XYStep rangeStep = XYStepCalculator.getStep(GraphView.this, XYAxisType.RANGE,
paddedGridRect, getCalculatedMinY().doubleValue(),
getCalculatedMaxY().doubleValue());
double rangeOriginF = paddedGridRect.bottom;
float yPix = (float) (rangeOriginF + getRangeOrigin().doubleValue() * rangeStep.getStepPix() /
rangeStep.getStepVal());
//Keep things consistent with drawing y axis even though drawRangeTick is public
//drawRangeTick(canvas, yPix, 0, getRangeLabelPaint(), axisPaint, true);
canvas.drawLine(mGridRect.left, yPix, mGridRect.right, yPix, axisPaint);
}
if(minXY.x <= 0 && maxXY.x >= 0) { //Draw the y axis
RectF paddedGridRect = getGridRect();
XYStep domianStep = XYStepCalculator.getStep(GraphView.this, XYAxisType.DOMAIN,
paddedGridRect, getCalculatedMinX().doubleValue(),
getCalculatedMaxX().doubleValue());
double domainOriginF = paddedGridRect.left;
float xPix = (float) (domainOriginF - getDomainOrigin().doubleValue() * domianStep.getStepPix() /
domianStep.getStepVal());
//Unfortunately, drawDomainTick has private access in XYGraphWidget
canvas.drawLine(xPix, mGridRect.top, xPix, mGridRect.bottom, axisPaint);
}
}
};
widget.setBackgroundPaint(oldWidget.getBackgroundPaint());
widget.setMarginTop(oldWidget.getMarginTop());
widget.setMarginRight(oldWidget.getMarginRight());
widget.setPositionMetrics(oldWidget.getPositionMetrics());
getLayoutManager().remove(oldWidget);
getLayoutManager().addToTop(widget);
setGraphWidget(widget);
//More customizations can go here
}
And that was that. I sure wish this was built into AndroidPlot; it'll be nasty trying to fix this when it breaks in an AndroidPlot update...
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.
i'm getting a Point object throw my MapView Projectiion object.
I have a list of Overlays and i want to check if each Overlay is inside my current screen (which is of curse depends on the zoom level).
I couldn't find a way to check if a given point is inside the screen.
What i did found out is that when i'm Logging the Points from the Projection, the points that're not in the screen have negative value..
is it true to say that if a Point has a negative value it is outside my screen ?
GeoPoint yourPoint = null;// w/e point u wanna see is on the screen
Point newPoint = null; // x/y point
mapView.getProjection().toPixels(yourPoint, newPoint); //convert to xy Point
int height = context.getResources().getDisplayMetrics().heightPixels;
int width = context.getResources().getDisplayMetrics().widthPixels;
Rect screen = new Rect(0,0,width,height); //rect that represents your Screen
if(screen.contains(newPoint.x, newPoint.y){
// your point is on the screen
}