I want to draw image on osm map, using osmdroid/osmbonuspack. I have tried Marker and SimpleLocationOverlay. These display the image as overlay, like a pin which doesn't change scale. But I want to show image which becomes part of Map, such that when Map is Zoomed-in, the image should scale up, and when zoomed-out, it should scale down.
With some modifications of GroundOverlay.java and MainActivity.java of osmbonuspack repo:
public class MainActivity extends AppCompatActivity {
MapView mMapView = null;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Context ctx = getApplicationContext();
Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx));
setContentView(R.layout.activity_main);
mMapView = (MapView) findViewById(R.id.map);
mMapView.setTileSource(TileSourceFactory.MAPNIK);
mMapView.setBuiltInZoomControls(true);
mMapView.setMultiTouchControls(true);
GeoPoint overlayCenterPoint = new GeoPoint(50.450667, 30.523193);
IMapController mapController = mMapView.getController();
mapController.setZoom(17f);
mapController.setCenter(overlayCenterPoint);
mMapView.setMapOrientation(0.0f);
GroundOverlay myGroundOverlay = new GroundOverlay();
myGroundOverlay.setPosition(overlayCenterPoint);
Drawable d = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_launcher, null);
myGroundOverlay.setImage(d.mutate());
myGroundOverlay.setDimensions(200.0f);
myGroundOverlay.setTransparency(0.25f);
myGroundOverlay.setBearing(0);
mMapView.getOverlays().add(myGroundOverlay);
mMapView.invalidate();
}
#Override
protected void onResume() {
super.onResume();
mMapView.onResume();
}
#Override
protected void onPause() {
super.onPause();
mMapView.onPause();
}
public class GroundOverlay extends Overlay {
protected Drawable mImage;
protected GeoPoint mPosition;
protected float mBearing;
protected float mWidth, mHeight;
protected float mTransparency;
public final static float NO_DIMENSION = -1.0f;
protected Point mPositionPixels, mSouthEastPixels;
public GroundOverlay() {
super();
mWidth = 10.0f;
mHeight = NO_DIMENSION;
mBearing = 0.0f;
mTransparency = 0.0f;
mPositionPixels = new Point();
mSouthEastPixels = new Point();
}
public void setImage(Drawable image){
mImage = image;
}
public Drawable getImage(){
return mImage;
}
public GeoPoint getPosition(){
return mPosition.clone();
}
public void setPosition(GeoPoint position){
mPosition = position.clone();
}
public float getBearing(){
return mBearing;
}
public void setBearing(float bearing){
mBearing = bearing;
}
public void setDimensions(float width){
mWidth = width;
mHeight = NO_DIMENSION;
}
public void setDimensions(float width, float height){
mWidth = width;
mHeight = height;
}
public float getHeight(){
return mHeight;
}
public float getWidth(){
return mWidth;
}
public void setTransparency(float transparency){
mTransparency = transparency;
}
public float getTransparency(){
return mTransparency;
}
protected void computeHeight(){
if (mHeight == NO_DIMENSION && mImage != null){
mHeight = mWidth * mImage.getIntrinsicHeight() / mImage.getIntrinsicWidth();
}
}
/** #return the bounding box, ignoring the bearing of the GroundOverlay (similar to Google Maps API) */
public BoundingBox getBoundingBox(){
computeHeight();
GeoPoint pEast = mPosition.destinationPoint(mWidth, 90.0f);
GeoPoint pSouthEast = pEast.destinationPoint(mHeight, -180.0f);
double north = mPosition.getLatitude()*2 - pSouthEast.getLatitude();
double west = mPosition.getLongitude()*2 - pEast.getLongitude();
return new BoundingBox(north, pEast.getLongitude(), pSouthEast.getLatitude(), west);
}
public void setPositionFromBounds(BoundingBox bb){
mPosition = bb.getCenterWithDateLine();
GeoPoint pEast = new GeoPoint(mPosition.getLatitude(), bb.getLonEast());
GeoPoint pWest = new GeoPoint(mPosition.getLatitude(), bb.getLonWest());
mWidth = (float)pEast.distanceToAsDouble(pWest);
GeoPoint pSouth = new GeoPoint(bb.getLatSouth(), mPosition.getLongitude());
GeoPoint pNorth = new GeoPoint(bb.getLatNorth(), mPosition.getLongitude());
mHeight = (float)pSouth.distanceToAsDouble(pNorth);
}
#Override public void draw(Canvas canvas, MapView mapView, boolean shadow) {
if (shadow)
return;
if (mImage == null)
return;
computeHeight();
final Projection pj = mapView.getProjection();
pj.toPixels(mPosition, mPositionPixels);
GeoPoint pEast = mPosition.destinationPoint(mWidth/2, 90.0f);
GeoPoint pSouthEast = pEast.destinationPoint(mHeight/2, -180.0f);
pj.toPixels(pSouthEast, mSouthEastPixels);
int hWidth = mSouthEastPixels.x-mPositionPixels.x;
int hHeight = mSouthEastPixels.y-mPositionPixels.y;
mImage.setBounds(-hWidth, -hHeight, hWidth, hHeight);
mImage.setAlpha(255-(int)(mTransparency*255));
drawAt(canvas, mImage, mPositionPixels.x, mPositionPixels.y, false, -mBearing);
}
}
}
you can get something like that:
Main part is:
// overlay center point
GeoPoint overlayCenterPoint = new GeoPoint(50.450667, 30.523193);
IMapController mapController = mMapView.getController();
mapController.setZoom(17f);
mapController.setCenter(overlayCenterPoint);
mMapView.setMapOrientation(0.0f);
GroundOverlay myGroundOverlay = new GroundOverlay();
myGroundOverlay.setPosition(overlayCenterPoint);
Drawable d = ResourcesCompat.getDrawable(getResources(), R.drawable.ic_launcher, null);
myGroundOverlay.setImage(d.mutate());
// overlay width in meters (height calculated automatically) also you can set both width and height
myGroundOverlay.setDimensions(200.0f);
myGroundOverlay.setTransparency(0.25f);
myGroundOverlay.setBearing(0);
mMapView.getOverlays().add(myGroundOverlay);
and it's "self-commented".
Related
I know android.graphics is old, but i am having trouble doing a simple stuff.
I want to draw a line animation where one View points an arrow/line into another View
First Button-------------------------------->Second Button
I have tried creating a custom View class and overriding the onDraw(Canvas c) method and then using the drawLine(startX, startY, stopX, stopY, paint) method from the Canvas Object. But i don't know which coordinates to get in order to point one View to the other View
I don't want to create a static View in the XML layout with a slim height because the View can be added dynamically by the user, which i think drawing the line dynamically is the best way.
Please help me out. Thank you!
For drawing lines between views better if all of it lays on same parent layout. For the conditions of the question (Second Button is exactly to the right of First Button) you can use custom layout like that:
public class ArrowLayout extends RelativeLayout {
public static final String PROPERTY_X = "PROPERTY_X";
public static final String PROPERTY_Y = "PROPERTY_Y";
private final static double ARROW_ANGLE = Math.PI / 6;
private final static double ARROW_SIZE = 50;
private Paint mPaint;
private boolean mDrawArrow = false;
private Point mPointFrom = new Point(); // current (during animation) arrow start point
private Point mPointTo = new Point(); // current (during animation) arrow end point
public ArrowLayout(Context context) {
super(context);
init();
}
public ArrowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ArrowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public ArrowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
setWillNotDraw(false);
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLUE);
mPaint.setStrokeWidth(5);
}
#Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
canvas.save();
if (mDrawArrow) {
drawArrowLines(mPointFrom, mPointTo, canvas);
}
canvas.restore();
}
private Point calcPointFrom(Rect fromViewBounds, Rect toViewBounds) {
Point pointFrom = new Point();
pointFrom.x = fromViewBounds.right;
pointFrom.y = fromViewBounds.top + (fromViewBounds.bottom - fromViewBounds.top) / 2;
return pointFrom;
}
private Point calcPointTo(Rect fromViewBounds, Rect toViewBounds) {
Point pointTo = new Point();
pointTo.x = toViewBounds.left;
pointTo.y = toViewBounds.top + (toViewBounds.bottom - toViewBounds.top) / 2;
return pointTo;
}
private void drawArrowLines(Point pointFrom, Point pointTo, Canvas canvas) {
canvas.drawLine(pointFrom.x, pointFrom.y, pointTo.x, pointTo.y, mPaint);
double angle = Math.atan2(pointTo.y - pointFrom.y, pointTo.x - pointFrom.x);
int arrowX, arrowY;
arrowX = (int) (pointTo.x - ARROW_SIZE * Math.cos(angle + ARROW_ANGLE));
arrowY = (int) (pointTo.y - ARROW_SIZE * Math.sin(angle + ARROW_ANGLE));
canvas.drawLine(pointTo.x, pointTo.y, arrowX, arrowY, mPaint);
arrowX = (int) (pointTo.x - ARROW_SIZE * Math.cos(angle - ARROW_ANGLE));
arrowY = (int) (pointTo.y - ARROW_SIZE * Math.sin(angle - ARROW_ANGLE));
canvas.drawLine(pointTo.x, pointTo.y, arrowX, arrowY, mPaint);
}
public void animateArrows(int duration) {
mDrawArrow = true;
View fromView = getChildAt(0);
View toView = getChildAt(1);
// find from and to views bounds
Rect fromViewBounds = new Rect();
fromView.getDrawingRect(fromViewBounds);
offsetDescendantRectToMyCoords(fromView, fromViewBounds);
Rect toViewBounds = new Rect();
toView.getDrawingRect(toViewBounds);
offsetDescendantRectToMyCoords(toView, toViewBounds);
// calculate arrow sbegin and end points
Point pointFrom = calcPointFrom(fromViewBounds, toViewBounds);
Point pointTo = calcPointTo(fromViewBounds, toViewBounds);
ValueAnimator arrowAnimator = createArrowAnimator(pointFrom, pointTo, duration);
arrowAnimator.start();
}
private ValueAnimator createArrowAnimator(Point pointFrom, Point pointTo, int duration) {
final double angle = Math.atan2(pointTo.y - pointFrom.y, pointTo.x - pointFrom.x);
mPointFrom.x = pointFrom.x;
mPointFrom.y = pointFrom.y;
int firstX = (int) (pointFrom.x + ARROW_SIZE * Math.cos(angle));
int firstY = (int) (pointFrom.y + ARROW_SIZE * Math.sin(angle));
PropertyValuesHolder propertyX = PropertyValuesHolder.ofInt(PROPERTY_X, firstX, pointTo.x);
PropertyValuesHolder propertyY = PropertyValuesHolder.ofInt(PROPERTY_Y, firstY, pointTo.y);
ValueAnimator animator = new ValueAnimator();
animator.setValues(propertyX, propertyY);
animator.setDuration(duration);
// set other interpolator (if needed) here:
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mPointTo.x = (int) valueAnimator.getAnimatedValue(PROPERTY_X);
mPointTo.y = (int) valueAnimator.getAnimatedValue(PROPERTY_Y);
invalidate();
}
});
return animator;
}
}
with .xml layout like:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/layout_main"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<{YOUR_PACKAGE_NAME}.ArrowLayout
android:id="#+id/arrow_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="#+id/first_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:text="First Button"/>
<Button
android:id="#+id/second_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="Second Button"/>
</{YOUR_PACKAGE_NAME}.ArrowLayout>
</RelativeLayout>
and MainActivity.java like:
public class MainActivity extends AppCompatActivity {
private ArrowLayout mArrowLayout;
private Button mFirstButton;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mArrowLayout = (ArrowLayout) findViewById(R.id.arrow_layout);
mFirstButton = (Button) findViewById(R.id.first_button);
mFirstButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mArrowLayout.animateArrows(1000);
}
});
}
}
you got something like that (on First Button click):
For other cases ( Second Button is exactly to the left (or above, or below) or more complex above-right/below-left etc. of First Button) you should modify part for calculating arrow begin and end points:
private Point calcPointFrom(Rect fromViewBounds, Rect toViewBounds) {
Point pointFrom = new Point();
// Second Button above
// ----------+----------
// | |
// Second Button tho the left + First Button + Second Button tho the right
// | |
// ----------+----------
// Second Button below
//
// + - is arrow start point position
if (toViewBounds to the right of fromViewBounds){
pointFrom.x = fromViewBounds.right;
pointFrom.y = fromViewBounds.top + (fromViewBounds.bottom - fromViewBounds.top) / 2;
} else if (toViewBounds to the left of fromViewBounds) {
pointFrom.x = fromViewBounds.left;
pointFrom.y = fromViewBounds.top + (fromViewBounds.bottom - fromViewBounds.top) / 2;
} else if () {
...
}
return pointFrom;
}
Use Path and Pathmeasure for Drawing Animated Line. I have Made and test it.
Make Custom View and pass view coordinates points array to it,
public class AnimatedLine extends View {
private final Paint mPaint;
public Canvas mCanvas;
AnimationListener animationListener;
Path path;
private static long animSpeedInMs = 2000;
private static final long animMsBetweenStrokes = 100;
private long animLastUpdate;
private boolean animRunning = true;
private int animCurrentCountour;
private float animCurrentPos;
private Path animPath;
private PathMeasure animPathMeasure;
float pathLength;
float distance = 0;
float[] pos;
float[] tan;
Matrix matrix;
Bitmap bm;
public AnimatedLine(Context context) {
this(context, null);
mCanvas = new Canvas();
}
public AnimatedLine(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(15);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setColor(context.getResources().getColor(R.color.materialcolorpicker__red));
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
}
bm = BitmapFactory.decodeResource(getResources(), R.drawable.hand1);
bm = Bitmap.createScaledBitmap(bm, 20,20, false);
distance = 0;
pos = new float[2];
tan = new float[2];
matrix = new Matrix();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mCanvas = canvas;
if (path != null) {
if (animRunning) {
drawAnimation(mCanvas);
} else {
drawStatic(mCanvas);
}
}
}
/**
* draw Path With Animation
*
* #param time in milliseconds
*/
public void drawWithAnimation(ArrayList<PointF> points, long time,AnimationListener animationListener) {
animRunning = true;
animPathMeasure = null;
animSpeedInMs = time;
setPath(points);
setAnimationListener(animationListener);
invalidate();
}
public void setPath(ArrayList<PointF> points) {
if (points.size() < 2) {
throw new IllegalStateException("Pass atleast two points.");
}
path = new Path();
path.moveTo(points.get(0).x, points.get(0).y);
path.lineTo(points.get(1).x, points.get(1).y);
}
private void drawAnimation(Canvas canvas) {
if (animPathMeasure == null) {
// Start of animation. Set it up.
animationListener.onAnimationStarted();
animPathMeasure = new PathMeasure(path, false);
animPathMeasure.nextContour();
animPath = new Path();
animLastUpdate = System.currentTimeMillis();
animCurrentCountour = 0;
animCurrentPos = 0.0f;
pathLength = animPathMeasure.getLength();
} else {
// Get time since last frame
long now = System.currentTimeMillis();
long timeSinceLast = now - animLastUpdate;
if (animCurrentPos == 0.0f) {
timeSinceLast -= animMsBetweenStrokes;
}
if (timeSinceLast > 0) {
// Get next segment of path
float newPos = (float) (timeSinceLast) / (animSpeedInMs / pathLength) + animCurrentPos;
boolean moveTo = (animCurrentPos == 0.0f);
animPathMeasure.getSegment(animCurrentPos, newPos, animPath, moveTo);
animCurrentPos = newPos;
animLastUpdate = now;
//start draw bitmap along path
animPathMeasure.getPosTan(newPos, pos, tan);
matrix.reset();
matrix.postTranslate(pos[0], pos[1]);
canvas.drawBitmap(bm, matrix, null);
//end drawing bitmap
//take current position
animationListener.onAnimationUpdate(pos);
// If this stroke is done, move on to next
if (newPos > pathLength) {
animCurrentPos = 0.0f;
animCurrentCountour++;
boolean more = animPathMeasure.nextContour();
// Check if finished
if (!more) {
animationListener.onAnimationEnd();
animRunning = false;
}
}
}
// Draw path
canvas.drawPath(animPath, mPaint);
}
invalidate();
}
private void drawStatic(Canvas canvas) {
canvas.drawPath(path, mPaint);
canvas.drawBitmap(bm, matrix, null);
}
public void setAnimationListener(AnimationListener animationListener) {
this.animationListener = animationListener;
}
public interface AnimationListener {
void onAnimationStarted();
void onAnimationEnd();
void onAnimationUpdate(float[] pos);
}
}
I would like to know if there is any simple solution to creating an overlay where an element would get highlighted.
So the final result would look something like this:
I would like to avoid using ShowcaseViewLibrary from variety of reason (it doesn't have the look I need, it's no longer supported etc.).
I thought about using FrameLayout but I am not sure how to achieve the highlighted existing element. Also putting the arrows or bubbles to the elements so they connect precisely.
A quick and easy way would be to make a copy of the Activity you want to demonstrate with overlays added and just show that. It's what I do and it works fine.
/**
* Created by Nikola D. on 10/1/2015.
*/
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class ShowCaseLayout extends ScrimInsetsFrameLayout {
private static final long DEFAULT_DURATION = 1000;
private static final int DEFAULT_RADIUS = 100;
private Paint mEmptyPaint;
private AbstractQueue<Pair<String, View>> mTargetQueue;
private int mLastCenterX = 600;
private int mLastCenterY = 100;
private ValueAnimator.AnimatorUpdateListener mAnimatorListenerX = new ValueAnimator.AnimatorUpdateListener() {
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
#Override
public void onAnimationUpdate(ValueAnimator animation) {
mLastCenterX = (int) animation.getAnimatedValue();
setWillNotDraw(false);
postInvalidate();
}
};
private ValueAnimator.AnimatorUpdateListener mAnimatorListenerY = new ValueAnimator.AnimatorUpdateListener() {
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
#Override
public void onAnimationUpdate(ValueAnimator animation) {
mLastCenterY = (int) animation.getAnimatedValue();
setWillNotDraw(false);
postInvalidate();
}
};
private ValueAnimator mCenterAnimatorX;
private ValueAnimator mCenterAnimatorY;
private boolean canRender = false;
private OnAttachStateChangeListener mAttachListener = new OnAttachStateChangeListener() {
#Override
public void onViewAttachedToWindow(View v) {
canRender = true;
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
#Override
public void onViewDetachedFromWindow(View v) {
canRender = false;
removeOnAttachStateChangeListener(this);
}
};
private long mDuration = DEFAULT_DURATION;
private int mRadius = (int) DEFAULT_RADIUS;
private Interpolator mInterpolator = new LinearOutSlowInInterpolator();
private ValueAnimator mRadiusAnimator;
private ValueAnimator.AnimatorUpdateListener mRadiusAnimatorListener = new ValueAnimator.AnimatorUpdateListener() {
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
#Override
public void onAnimationUpdate(ValueAnimator animation) {
mRadius = (int) animation.getAnimatedValue();
}
};
private TextView mDescriptionText;
private Button mGotItButton;
private OnClickListener mExternalGotItButtonlistener;
private OnClickListener mGotItButtonClickListener = new OnClickListener() {
#Override
public void onClick(View v) {
setNextTarget();
if (mExternalGotItButtonlistener != null) {
mExternalGotItButtonlistener.onClick(v);
}
}
};
private Animator.AnimatorListener mAnimatorSetListener = new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
setNextTarget();
invalidate();
//mDescriptionText.layout(mTempRect.left, mTempRect.bottom + mTempRect.bottom, mDescriptionText. );
}
};
private Rect mTempRect;
private Paint mBackgroundPaint;
private Bitmap bitmap;
private Canvas temp;
private int mStatusBarHeight = 0;
public ShowCaseLayout(Context context) {
super(context);
setupLayout();
}
public ShowCaseLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setupLayout();
}
public ShowCaseLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setupLayout();
}
public void setTarget(View target, String hint) {
mTargetQueue.add(new Pair<>(hint, target));
}
#TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void setupLayout() {
mTargetQueue = new LinkedBlockingQueue<>();
setWillNotDraw(false);
mBackgroundPaint = new Paint();
int c = Color.argb(127, Color.red(Color.RED), Color.blue(Color.RED), Color.green(Color.RED));
mBackgroundPaint.setColor(c);
mEmptyPaint = new Paint();
mEmptyPaint.setColor(Color.TRANSPARENT);
mEmptyPaint.setStyle(Paint.Style.FILL);
mEmptyPaint.setAntiAlias(true);
mEmptyPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
if (!ViewCompat.isLaidOut(this))
addOnAttachStateChangeListener(mAttachListener);
else canRender = true;
mDescriptionText = new TextView(getContext());
mGotItButton = new Button(getContext());
mGotItButton.setText("GOT IT");
mGotItButton.setOnClickListener(mGotItButtonClickListener);
addView(mGotItButton, generateDefaultLayoutParams());
//ViewCompat.setAlpha(this, 0.5f);
}
#Override
protected LayoutParams generateDefaultLayoutParams() {
return new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!canRender) return;
temp.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mBackgroundPaint);
temp.drawCircle(mLastCenterX, mLastCenterY, mRadius, mEmptyPaint);
canvas.drawBitmap(bitmap, 0, 0, null);
}
#TargetApi(Build.VERSION_CODES.M)
private void animateCenterToNextTarget(View target) {
int[] locations = new int[2];
target.getLocationInWindow(locations);
int x = locations[0];
int y = locations[1];
mTempRect = new Rect(x, y, x + target.getWidth(), y + target.getHeight());
int centerX = mTempRect.centerX();
int centerY = mTempRect.centerY();
int targetRadius = Math.abs(mTempRect.right - mTempRect.left) / 2;
targetRadius += targetRadius * 0.05;
mCenterAnimatorX = ValueAnimator.ofInt(mLastCenterX, centerX).setDuration(mDuration);
mCenterAnimatorX.addUpdateListener(mAnimatorListenerX);
mCenterAnimatorY = ValueAnimator.ofInt(mLastCenterY, centerY).setDuration(mDuration);
mCenterAnimatorY.addUpdateListener(mAnimatorListenerY);
mRadiusAnimator = ValueAnimator.ofInt(mRadius, targetRadius);
mRadiusAnimator.addUpdateListener(mRadiusAnimatorListener);
playTogether(mCenterAnimatorY, mCenterAnimatorX, mRadiusAnimator);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
bitmap.eraseColor(Color.TRANSPARENT);
temp = new Canvas(bitmap);
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void playTogether(ValueAnimator... animators) {
AnimatorSet set = new AnimatorSet();
set.setInterpolator(mInterpolator);
set.setDuration(mDuration);
set.playTogether(animators);
set.addListener(mAnimatorSetListener);
set.start();
}
public void start(Activity activity) {
if (getParent() == null) {
attachLayoutToWindow(activity);
}
setNextTarget();
}
private void setNextTarget() {
Pair<String, View> pair = mTargetQueue.poll();
if (pair != null) {
if (pair.second != null)
animateCenterToNextTarget(pair.second);
mDescriptionText.setText(pair.first);
}
}
private void attachLayoutToWindow(Activity activity) {
FrameLayout rootLayout = (FrameLayout) activity.findViewById(android.R.id.content);
rootLayout.addView(this);
}
public void hideShowcaseLayout() {
}
public void setGotItButtonClickistener(OnClickListener mExternalGotItButtonlistener) {
this.mExternalGotItButtonlistener = mExternalGotItButtonlistener;
}
public TextView getDescriptionTextView() {
return mDescriptionText;
}
public void setDescriptionTextView(TextView textView) {
mDescriptionText = textView;
}
}
Please note that this code is incomplete and is under development, you should tweak it according your needs.
This layout will draw a circle around the View over its Rect.
Instead of drawing the circle you could drawRect to the Rect bounds of the target view or drawRoundRect if the View's Rect and background drawable Rect are complementary.
Drawing the line (drawLine()) should be from the target view:
startX = (rect.right - rect.left)/2;
startY = rect.bottom;
endX = startX;
endY = startY + arbitraryLineHeight;
if the endY is larger than the layout height you should be drawing it upwards rect.top - arbitraryLineHeight, otherwise you draw it as it is.
arbitraryLineHeight could be descriptionViewRect.top which makes it more dynamic, instead of using a constant value.
I am using Dave Morrissey's sub sampling scale Image View to display a high resolution map image. I want to add location markers at predefined coordinates on the map such that even when the image is zoomed or panned around, the markers stay put at the specified coordinates. How can I do this?
Extend the SubsamplingScaleImageView and override its onDraw() method
public class MapView extends SubsamplingScaleImageView {
private PointF sPin;
ArrayList<MapPin> mapPins;
ArrayList<DrawnPin> drawnPins;
Context context;
String tag = getClass().getSimpleName();
public MapView(Context context) {
this(context, null);
this.context = context;
}
public MapView(Context context, AttributeSet attr) {
super(context, attr);
this.context = context;
initialise();
}
public void setPins(ArrayList<MapPin> mapPins)
{
this.mapPins = mapPins;
initialise();
invalidate();
}
public PointF getPin() {
return sPin;
}
private void initialise() {
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Don't draw pin before image is ready so it doesn't move around during setup.
if (!isReady()) {
return;
}
drawnPins = new ArrayList<>();
Paint paint = new Paint();
paint.setAntiAlias(true);
float density = getResources().getDisplayMetrics().densityDpi;
for(int i=0;i<mapPins.size();i++)
{
MapPin mPin = mapPins.get(i);
Bitmap bmpPin = Utils.getBitmapFromAsset(context, mPin.getPinImgSrc());
float w = (density/420f) * bmpPin.getWidth();
float h = (density/420f) * bmpPin.getHeight();
bmpPin = Bitmap.createScaledBitmap(bmpPin, (int)w, (int)h, true);
PointF vPin = sourceToViewCoord(mPin.getPoint());
//in my case value of point are at center point of pin image, so we need to adjust it here
float vX = vPin.x - (bmpPin.getWidth()/2);
float vY = vPin.y - (bmpPin.getHeight()/2);
canvas.drawBitmap(bmpPin, vX, vY, paint);
//add added pin to an Array list to get touched pin
DrawnPin dPin = new DrawnPin();
dPin.setStartX(mPin.getX()-w/2);
dPin.setEndX(mPin.getX()+w/2);
dPin.setStartY(mPin.getY()-h/2);
dPin.setEndY(mPin.getY()+h/2);
dPin.setId(mPin.getId());
drawnPins.add(dPin);
}
}
public int getPinIdByPoint(PointF point)
{
for(int i=drawnPins.size()-1;i>=0;i--)
{
DrawnPin dPin = drawnPins.get(i);
if(point.x >= dPin.getStartX() && point.x<=dPin.getEndX())
{
if(point.y >= dPin.getStartY() && point.y<=dPin.getEndY())
{
return dPin.getId();
}
}
}
return -1; //negative no means no pin selected
}
class DrawnPin
{
float startX,startY,endX,endY;
int id;
public DrawnPin(float startX, float startY, float endX, float endY, int id)
{
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
this.id = id;
}
public DrawnPin()
{
//empty
}
public float getStartX()
{
return startX;
}
public void setStartX(float startX)
{
this.startX = startX;
}
public float getStartY()
{
return startY;
}
public void setStartY(float startY)
{
this.startY = startY;
}
public float getEndX()
{
return endX;
}
public void setEndX(float endX)
{
this.endX = endX;
}
public float getEndY()
{
return endY;
}
public void setEndY(float endY)
{
this.endY = endY;
}
public int getId()
{
return id;
}
public void setId(int id)
{
this.id = id;
}
}
My problem is that when I zoom into a higher level, the drawing filled area disappear.
it will be very follow up the topic:
Android MapView overlay disappearing when zooming in
The above link solve my problem to draw the line, but the filled area still stay the same problem.
I need for help to solve the filled area too.
my current code as following :
class MyOverlay extends Overlay {
private final List<GeoPoint> points;
private int oldX;
private int oldY;
public MyOverlay(List<GeoPoint> points) {
this.points = points;
}
#Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
super.draw(canvas, mapView, shadow);
int x1 = -1;
int y1 = -1;
int x2 = -1;
int y2 = -1;
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.GREEN);
paint.setStrokeWidth(5);
int size = points.size();
for (int i = 0; i <= size; i++) {
Point point = new Point();
mapView.getProjection().toPixels(points.get(i % size),
point);
x2 = point.x;
y2 = point.y;
if (i > 0) {
canvas.drawLine(x1, y1, x2, y2, paint);
}
x1 = x2;
y1 = y2;
}
//the following are drawing the filled area
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.FILL);
paint.setAlpha(60);
Projection projection = mapView.getProjection();
Point p1 = new Point();
Point p2 = new Point();
Point p3 = new Point();
projection.toPixels(gpoint1, p1);
projection.toPixels(gpoint2, p2);
projection.toPixels(gpoint3, p3);
Path path = new Path();
path.moveTo(p1.x, p1.y);
path.lineTo(p2.x, p2.y);
path.lineTo(p3.x, p3.y);
canvas.drawPath(path, paint);
}
}
You can get help from this code.
public abstract class BalloonItemizedOverlay<Item> extends
ItemizedOverlay<OverlayItem> {
private MapView mapView;
private BalloonOverlayView balloonView;
private View clickRegion;
private int viewOffset;
final MapController mc;
/**
* Create a new BalloonItemizedOverlay
*
* #param defaultMarker
* - A bounded Drawable to be drawn on the map for each item in
* the overlay.
* #param mapView
* - The view upon which the overlay items are to be drawn.
*/
public BalloonItemizedOverlay(Drawable defaultMarker, MapView mapView) {
super(defaultMarker);
this.mapView = mapView;
viewOffset = 0;
mc = mapView.getController();
}
#Override
public void draw(Canvas c, MapView m, boolean shadow) { // for disabling the
// shadow of overlay
super.draw(c, m, false);
}
/**
* Set the horizontal distance between the marker and the bottom of the
* information balloon. The default is 0 which works well for center bounded
* markers. If your marker is center-bottom bounded, call this before adding
* overlay items to ensure the balloon hovers exactly above the marker.
*
* #param pixels
* - The padding between the center point and the bottom of the
* information balloon.
*/
public void setBalloonBottomOffset(int pixels) {
viewOffset = pixels;
}
/**
* Override this method to handle a "tap" on a balloon. By default, does
* nothing and returns false.
*
* #param index
* - The index of the item whose balloon is tapped.
* #return true if you handled the tap, otherwise false.
*/
protected boolean onBalloonTap(int index) {
Globals.mapPinpointTap = true;
return false;
}
/*
* (non-Javadoc)
*
* #see com.google.android.maps.ItemizedOverlay#onTap(int)
*/
#Override
protected final boolean onTap(int index) {
boolean isRecycled;
final int thisIndex;
GeoPoint point;
Globals.mapPinpointTap = true;
thisIndex = index;
point = createItem(index).getPoint();
if (balloonView == null) {
balloonView = new BalloonOverlayView(mapView.getContext(),
viewOffset);
clickRegion = (View) balloonView
.findViewById(R.id.balloon_inner_layout);
isRecycled = false;
} else {
isRecycled = true;
}
balloonView.setVisibility(View.GONE);
List<Overlay> mapOverlays = mapView.getOverlays();
if (mapOverlays.size() > 1) {
hideOtherBalloons(mapOverlays);
}
balloonView.setData(createItem(index));
MapView.LayoutParams params = new MapView.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, point,
MapView.LayoutParams.BOTTOM_CENTER);
params.mode = MapView.LayoutParams.MODE_MAP;
setBalloonTouchListener(thisIndex);
balloonView.setVisibility(View.VISIBLE);
// /
/*
* String url = "http://maps.google.com/maps?q=" +
* (point.getLatitudeE6() / 1E6) + "," + (point.getLongitudeE6() / 1E6);
*/
// String url = "http://iceapp.coeus-solutions.de/api/map?location="
// + (point.getLatitudeE6() / 1E6) + ","
// + (point.getLongitudeE6() / 1E6);
// Intent browserIntent = new Intent(Intent.ACTION_VIEW,
// Uri.parse(url));
// balloonView.getContext().startActivity(browserIntent);
// /
if (isRecycled) {
balloonView.setLayoutParams(params);
} else {
mapView.addView(balloonView, params);
}
mc.animateTo(point);
return true;
}
/**
* Sets the visibility of this overlay's balloon view to GONE.
*/
private void hideBalloon() {
if (balloonView != null) {
balloonView.setVisibility(View.GONE);
}
}
/**
* Hides the balloon view for any other BalloonItemizedOverlay instances
* that might be present on the MapView.
*
* #param overlays
* - list of overlays (including this) on the MapView.
*/
private void hideOtherBalloons(List<Overlay> overlays) {
for (Overlay overlay : overlays) {
if (overlay instanceof BalloonItemizedOverlay<?> && overlay != this) {
((BalloonItemizedOverlay<?>) overlay).hideBalloon();
}
}
}
/**
* Sets the onTouchListener for the balloon being displayed, calling the
* overridden onBalloonTap if implemented.
*
* #param thisIndex
* - The index of the item whose balloon is tapped.
*/
private void setBalloonTouchListener(final int thisIndex) {
try {
#SuppressWarnings("unused")
Method m = this.getClass().getDeclaredMethod("onBalloonTap",
int.class);
Globals.mapPinpointTap = true;
clickRegion.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
View l = ((View) v.getParent())
.findViewById(R.id.balloon_main_layout);
Drawable d = l.getBackground();
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int[] states = { android.R.attr.state_pressed };
if (d.setState(states)) {
d.invalidateSelf();
}
return true;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
int newStates[] = {};
if (d.setState(newStates)) {
d.invalidateSelf();
}
// call overridden method
onBalloonTap(thisIndex);
return true;
} else {
return false;
}
}
});
} catch (SecurityException e) {
if (Globals.SHOW_LOGS)
Log.e("BalloonItemizedOverlay",
"setBalloonTouchListener reflection SecurityException");
return;
} catch (NoSuchMethodException e) {
// method not overridden - do nothing
return;
}
}
}
You should extend ItemizedOverlay instead of Overlay.
Implement it similar as described here "Part 2: Adding Overlay Items"
And add your draw method and other necessary stuff to it. Then it should work.
I have created custom drawable marker which uses canvas to draw
bounds. Everything works great excepts one thing: onItemSingleTapUp
not called when any marker on the screen taped.
Here is overlay creation code:
ItemizedIconOverlay<OverlayItem> groupsOverlay = new ItemizedIconOverlay<OverlayItem>(
new ArrayList<OverlayItem>(),
new OnItemGestureListener<OverlayItem>() {
#Override
public boolean onItemLongPress(int arg0, OverlayItem arg1) {
return false;
}
#Override
public boolean onItemSingleTapUp(int arg0, OverlayItem arg1) {
if(arg1 == null){
return false;
}
if(m_prevView != null){
m_mapView.removeView(m_prevView);
m_prevView = null;
}
View popUp = getLayoutInflater().inflate(R.layout.map_popup, m_mapView, false);
TextView tv = (TextView)popUp.findViewById(R.id.popupTextView);
tv.setText(arg1.getTitle());
MapView.LayoutParams mapParams = new MapView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
arg1.getPoint(),
MapView.LayoutParams.BOTTOM_CENTER, 0, 0);
m_mapView.addView(popUp, mapParams);
m_prevView = popUp;
return true;
}
}, new DefaultResourceProxyImpl(getApplicationContext()));
This is custom drawable marker:
package com.testapp.data;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
public class GroupMarkerDrawable extends Drawable {
private final static int DELTA_BOX = 4;
private final Paint m_paint;
private String m_text;
private double m_pxRadius;
public GroupMarkerDrawable(double pxRadius, String text) {
m_text = text;
m_paint = new Paint();
m_pxRadius = pxRadius;
m_paint.setAntiAlias(true);
}
#Override
public void draw(Canvas c) {
// Set the correct values in the Paint
m_paint.setARGB(190, 0, 0, 0);
m_paint.setStrokeWidth(2);
m_paint.setStyle(Style.STROKE);
m_paint.setTextAlign(Align.CENTER);
Rect bounds = new Rect();
m_paint.getTextBounds(m_text, 0, m_text.length(), bounds);
int centerX = getBounds().centerX();
int centerY = getBounds().centerY();
int w2 = bounds.width() / 2;
int h2 = bounds.height() / 2;
Rect rect = new Rect(centerX - w2 - DELTA_BOX, centerY - h2 -
DELTA_BOX, centerX + w2 + DELTA_BOX, centerY + h2 + DELTA_BOX);
// Draw it
c.drawCircle(centerX, centerY, (float) m_pxRadius, m_paint);
m_paint.setStyle(Style.FILL);
m_paint.setARGB(190, 0, 128, 0);
c.drawRect(rect, m_paint);
m_paint.setStyle(Style.STROKE);
m_paint.setARGB(190, 0, 0, 0);
c.drawRect(rect, m_paint);
c.drawText(m_text, centerX, centerY + h2, m_paint);
}
#Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
#Override
public void setAlpha(int arg0) {
}
#Override
public void setColorFilter(ColorFilter arg0) {
}
}
Same code using static drawable from resources, instead of
GroupMarkerDrawable, works.
I found how to solve this. Just need to return proper dimensions for Drawable. This requires overriding of two methods in GroupMarkerDrawable. FOr example like this:
#Override
public int getIntrinsicHeight() {
return m_pxRadius * 2;
};
#Override
public int getIntrinsicWidth() {
return m_pxRadius * 2;
};
An additional solution for a polygon.
The solutions of ydanila put me on the good track but I had to override hitTest method of ItemizedOverlayWithFocus class in order to get my polygon hit.
Drawable:
Drawable drawable = new Drawable() {
private int mIntrinsicHeight = 0;
private int mIntrinsicWidth = 0;
#Override
public void draw(Canvas canvas) {
// used to determine limit coordinates of the drawable
int yTop, yBottom, xLeft, xRight;
if (points != null && points.size() > 1) {
//we have to make a projection to convert from postions on the map in
//gradiant to a position on the view in pixels
final Projection pj = mapView.getProjection();
Path path = new Path();
Point centerMapPixelPoint = new Point();
Point tmpMapPixelPoint = new Point();
pj.toMapPixels(points.get(0), centerMapPixelPoint);
// init limit coordinates
xLeft = centerMapPixelPoint.x;
xRight = centerMapPixelPoint.x;
yTop = centerMapPixelPoint.y;
yBottom = centerMapPixelPoint.y;
path.moveTo(centerMapPixelPoint.x, centerMapPixelPoint.y);
for (int i = 1; i < points.size(); i++) {
pj.toMapPixels(points.get(i), tmpMapPixelPoint);
// update limit coordinates if necessary
if (xLeft > tmpMapPixelPoint.x) {
xLeft = tmpMapPixelPoint.x;
}
if (xRight < tmpMapPixelPoint.x) {
xRight = tmpMapPixelPoint.x;
}
if (yBottom < tmpMapPixelPoint.y) {
yBottom = tmpMapPixelPoint.y;
}
if (yTop > tmpMapPixelPoint.y) {
yTop = tmpMapPixelPoint.y;
}
path.lineTo(tmpMapPixelPoint.x, tmpMapPixelPoint.y);
}
// close polygon returning to first point
path.close();
canvas.drawPath(path, linePaint);
canvas.drawPath(path, innerPaint);
// calculate drawable height and width
mIntrinsicHeight = yTop -yBottom;
mIntrinsicWidth = xRight - xLeft;
}
}
#Override
public int getIntrinsicHeight() {
return mIntrinsicHeight;
};
#Override
public int getIntrinsicWidth() {
return mIntrinsicWidth;
};
};
Overlay:
public class MyItemizedIconOverlay<Item extends OverlayItem> extends ItemizedOverlayWithFocus<Item> {
#Override
protected boolean hitTest(Item item, Drawable marker, int hitX, int hitY) {
boolean hit = false;
Rect bounds = marker.getBounds();
if (hitX < bounds.right && hitX > bounds.left && hitY < bounds.top && hitY > bounds.bottom) {
hit = true;
} else {
hit = false;
}
return hit;
};