Android Open Source - Polaris Map Callout View






From Project

Back to project page Polaris.

License

The source code is released under:

Apache License

If you think the Android project Polaris listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright (C) 2012 Cyril Mottier (http://www.cyrilmottier.com)
 */*from w  w w  .  j a va 2 s  . co  m*/
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.cyrilmottier.polaris;

import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.OverlayItem;

/**
 * @author Cyril Mottier
 */
public class MapCalloutView extends ViewGroup {

    /**
     * @author Cyril Mottier
     */
    public interface OnDoubleTapListener {
        /**
         * Called when a view has been double tapped.
         * 
         * @param v The view that was double tapped.
         */
        void onDoubleTap(View v);
    }

    public static final int ANCHOR_MODE_FIXED = 1;
    public static final int ANCHOR_MODE_VARIABLE = 2;

    private final Point mTempPoint = new Point();
    private final Rect mTempRect1 = new Rect();
    private final Rect mTempRect2 = new Rect();

    private final GestureListener mGestureListener = new GestureListener();

    private LinearLayout mCallout;

    private TextView mTitle;
    private TextView mSubtitle;
    private View mDisclosure;
    private FrameLayout mContentContainer;
    private View mContent;

    private View mLeftAccessory;
    private View mRightAccessory;
    private View mCustomView;

    private boolean mIsDisclosureEnabled;
    private GestureDetector mGestureDetector;
    private OnClickListener mOnClickListener;
    private OnDoubleTapListener mOnDoubleTapListener;

    private int mInset;
    private int mSpacing;
    private int mBottomOffset;
    private int mAnchorMode = ANCHOR_MODE_VARIABLE;

    private boolean mNeedRelayout;
    private MapCalloutDrawable mMapCalloutDrawable;

    public MapCalloutView(Context context) {
        super(context);
        init(context);
    }

    public MapCalloutView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public MapCalloutView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    @SuppressWarnings("deprecation")
    private void init(Context context) {
        LayoutInflater.from(context).inflate(R.layout.polaris__map_callout_view_merge, this);

        mInset = getResources().getDimensionPixelSize(R.dimen.polaris__spacing_large);
        mSpacing = getResources().getDimensionPixelSize(R.dimen.polaris__spacing_normal);

        mGestureDetector = new GestureDetector(getContext(), mGestureListener);
        mGestureDetector.setOnDoubleTapListener(mGestureListener);

        mMapCalloutDrawable = new MapCalloutDrawable(context);

        mCallout = (LinearLayout) findViewById(R.id.polaris__callout);
        mCallout.setOnTouchListener(mOnTouchListener);
        mCallout.setBackgroundDrawable(mMapCalloutDrawable);

        mTitle = (TextView) findViewById(R.id.polaris__title);
        mSubtitle = (TextView) findViewById(R.id.polaris__subtitle);
        mDisclosure = findViewById(R.id.polaris__disclosure);
        mContentContainer = (FrameLayout) findViewById(R.id.polaris__content_container);
        mContent = findViewById(R.id.polaris__content);
    }

    @Override
    public void setBackground(Drawable background) {
        oopsBackgroundModified();
    }

    @Override
    public void setBackgroundColor(int color) {
        oopsBackgroundModified();
    }

    @Override
    public void setBackgroundResource(int resid) {
        oopsBackgroundModified();
    }

    @Override
    public void setBackgroundDrawable(Drawable background) {
        oopsBackgroundModified();
    }

    @Override
    public void setOnClickListener(OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        mOnClickListener = l;
    }

    @Override
    public boolean isClickable() {
        return mCallout.isClickable();
    }

    @Override
    public void setClickable(boolean clickable) {
        mCallout.setClickable(clickable);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        final int extraPadding = mInset;
        final int widthCoeff = mAnchorMode == ANCHOR_MODE_VARIABLE ? 2 : 1;
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec) - 2 * extraPadding;
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec) - 2 * extraPadding;

        mCallout.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.getMode(widthMeasureSpec)),
                MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.getMode(heightMeasureSpec)));

        setMeasuredDimension(mCallout.getMeasuredWidth() * widthCoeff, mCallout.getMeasuredHeight() + mBottomOffset);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        switch (mAnchorMode) {
            case ANCHOR_MODE_VARIABLE:
                layoutVariable(changed, l, t, r, b);
                break;

            case ANCHOR_MODE_FIXED:
            default:
                layoutFixed(changed, l, t, r, b);
                break;
        }
    }

    private void layoutFixed(boolean changed, int l, int t, int r, int b) {
        mCallout.layout(0, 0, mCallout.getMeasuredWidth(), mCallout.getMeasuredHeight());
    }

    private void layoutVariable(boolean changed, int l, int t, int r, int b) {
        if (mNeedRelayout) {
            if (!(getParent() instanceof MapView)) {
                throw new IllegalStateException(MapCalloutView.class.getSimpleName() + " can only be used in MapView");
            }

            final MapView mapView = (MapView) getParent();
            final MapCalloutDrawable drawable = mMapCalloutDrawable;

            final Rect mapViewDrawingRect = mTempRect1;
            mapView.getDrawingRect(mapViewDrawingRect);

            final Rect selfDrawingRect = mTempRect2;
            getDrawingRect(selfDrawingRect);
            mapView.offsetDescendantRectToMyCoords(this, selfDrawingRect);

            int anchorX = selfDrawingRect.centerX();

            int calloutX = (int) ((mapViewDrawingRect.right - mapViewDrawingRect.left - mCallout.getMeasuredWidth()) / 2.0f + 0.5f);

            // What's the farthest to the left and right that we could point to,
            // given our background image constraints?
            int minX = calloutX + drawable.getLeftMargin();
            int maxX = calloutX + mCallout.getMeasuredWidth() - drawable.getRightMargin();

            // we may need to scoot over to the left or right to point at the
            // correct spot
            int adjustX = 0;
            if (anchorX < minX) {
                adjustX = anchorX - minX;
            }
            if (anchorX > maxX) {
                adjustX = anchorX - maxX;
            }

            calloutX = calloutX + adjustX;

            //@formatter:off
            selfDrawingRect.set(
                    calloutX,
                    t,
                    calloutX + mCallout.getMeasuredWidth(),
                    t + mCallout.getMeasuredHeight());
            //@formatter:on

            mapViewDrawingRect.inset(mInset, mInset);
            offsetToContainRect(selfDrawingRect, mapViewDrawingRect, mTempPoint);

            selfDrawingRect.offset(-l, -t);

            drawable.setAnchorOffset(anchorX - calloutX);

            //@formatter:off
            mCallout.layout(
                    selfDrawingRect.left, 
                    selfDrawingRect.top,
                    selfDrawingRect.right,
                    selfDrawingRect.bottom);
            //@formatter:on           

            if (mTempPoint.x != 0 || mTempPoint.y != 0) {
                MapViewUtils.smoothScrollBy(mapView, mTempPoint.x, mTempPoint.y);
            }

            mNeedRelayout = false;
        } else {
            mCallout.layout(mTempRect2.left, mTempRect2.top, mTempRect2.right, mTempRect2.bottom);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mNeedRelayout = true;
    }

    public void show(MapView mapView, GeoPoint point, boolean animated) {
        final int index = mapView.indexOfChild(this);
        if (index == -1) {
            mapView.addView(this, new MapView.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, point,
                    MapView.LayoutParams.BOTTOM_CENTER));
        }

        bringToFront();

        final MapView.LayoutParams params = (MapView.LayoutParams) getLayoutParams();
        params.width = LayoutParams.WRAP_CONTENT;
        params.height = LayoutParams.WRAP_CONTENT;
        params.mode = MapView.LayoutParams.MODE_MAP;
        params.point = point;

        mNeedRelayout = true;
        if (animated) {
            final Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.polaris__grow_fade_in_from_bottom);
            startAnimation(animation);
        }

        setVisibility(View.VISIBLE);
    }

    public void dismiss(boolean animated) {
        setVisibility(View.GONE);
        if (animated) {
            final Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.polaris__shrink_fade_out_to_bottom);
            startAnimation(animation);
        }
    }

    /**
     * Set a new listener to listen to double tap events.
     * 
     * @param l The listener to set
     */
    public void setOnDoubleTapListener(OnDoubleTapListener l) {
        mOnDoubleTapListener = l;
    }

    /**
     * Set the title of the {@link MapCalloutView}. The {@link MapCalloutView}
     * automatically manages empty (null or zero-length) title.
     * 
     * @param title The title to apply to this {@link MapCalloutView}
     */
    public void setTitle(CharSequence title) {
        if (!TextUtils.isEmpty(title)) {
            mTitle.setText(title);
            mTitle.setVisibility(View.VISIBLE);
        } else {
            mTitle.setVisibility(View.GONE);
        }
    }

    /**
     * Set the subtitle of the {@link MapCalloutView}. The
     * {@link MapCalloutView} automatically manages empty (null or zero-length)
     * subtitle.
     * 
     * @param subtitle The subtitle to apply to this {@link MapCalloutView}
     */
    public void setSubtitle(CharSequence subtitle) {
        if (!TextUtils.isEmpty(subtitle)) {
            mSubtitle.setText(subtitle);
            mSubtitle.setVisibility(View.VISIBLE);
        } else {
            mSubtitle.setVisibility(View.GONE);
        }
    }

    /**
     * Sets the view's data from a given overlay item. Overlay's title and
     * snippet are respectively used as the new title and subtitle.
     * 
     * @param item - The overlay item containing the relevant view's data (title
     *            and snippet).
     */
    public void setData(OverlayItem item) {
        setTitle(item == null ? null : item.getTitle());
        setSubtitle(item == null ? null : item.getSnippet());
    }

    /**
     * Indicates whether the disclosure indicator is enabled or not. Please note
     * an enabled disclosure indicator doesn't mean it is visible. Indeed, it
     * may be enabled but invisible if a non-null right accessory view has been
     * set.
     * 
     * @return true is the disclosure indicator is enabled. false otherwise
     * @see #setDisclosureEnabled(boolean)
     */
    public boolean isDisclosureEnabled() {
        return mIsDisclosureEnabled;
    }

    /**
     * Enable or disable the disclosure indicator. Put simple, the disclosure
     * indicator should always be visible when the {@link MapCalloutView} is
     * clickable i.e. when an action such as "opening" a details screen is done
     * on click.
     * 
     * @param enabled Whether the disclosure indicator is enabled or not
     */
    public void setDisclosureEnabled(boolean enabled) {
        if (mIsDisclosureEnabled != enabled) {
            mIsDisclosureEnabled = enabled;
            if (enabled && mRightAccessory == null) {
                mDisclosure.setVisibility(View.VISIBLE);
            } else {
                mDisclosure.setVisibility(View.GONE);
            }
        }
    }

    /**
     * Return the right accessory view.
     * 
     * @return The right accessory view.
     */
    public View getRightAccessoryView() {
        return mRightAccessory;
    }

    /**
     * Set a new right accessory view to this {@link MapCalloutView}. The newly
     * added view will remove the previous one. Setting
     * {@link android.view.ViewGroup.LayoutParams} to the given right accessory
     * is not necessary as they will be automatically set to (
     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT},
     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}). Please note it
     * is not possible to use the right accessory in addition to the disclosure
     * indicator. A non-null right accessory automatically replaces the
     * disclosure indicator.
     * 
     * @param rightAccessoryView The new right accessory view.
     */
    public void setRightAccessoryView(View rightAccessoryView) {
        if (rightAccessoryView != null && rightAccessoryView.getParent() != null) {
            throw new IllegalArgumentException("The given view is already attached to a parent");
        }

        if (mRightAccessory != rightAccessoryView) {
            // Remove the previous custom view
            if (mRightAccessory != null) {
                mCallout.removeView(mRightAccessory);
            }

            mRightAccessory = rightAccessoryView;

            // Add the new custom view
            if (rightAccessoryView != null) {
                mDisclosure.setVisibility(View.GONE);

                final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                params.gravity = Gravity.CENTER_VERTICAL;
                params.weight = 0;
                params.leftMargin = mSpacing;

                mCallout.addView(rightAccessoryView, params);
            } else {
                if (mIsDisclosureEnabled) {
                    mDisclosure.setVisibility(View.VISIBLE);
                } else {
                    mDisclosure.setVisibility(View.GONE);
                }
            }
        }
    }

    /**
     * Return the left accessory view.
     * 
     * @return The left accessory view.
     */
    public View getLeftAccessoryView() {
        return mLeftAccessory;
    }

    /**
     * Set a new left accessory view to this {@link MapCalloutView}. The newly
     * added view will remove the previous one. Setting
     * {@link android.view.ViewGroup.LayoutParams} to the given left accessory
     * is not necessary as they will be automatically set to (
     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT},
     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}).
     * 
     * @param leftAccessoryView The new left accessory view.
     */
    public void setLeftAccessoryView(View leftAccessoryView) {
        if (leftAccessoryView != null && leftAccessoryView.getParent() != null) {
            throw new IllegalArgumentException("The given view is already attached to a parent");
        }

        if (mLeftAccessory != leftAccessoryView) {
            // Remove the previous custom view
            if (mLeftAccessory != null) {
                mCallout.removeView(mLeftAccessory);
            }

            mLeftAccessory = leftAccessoryView;

            // Add the new custom view
            if (leftAccessoryView != null) {

                final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                params.gravity = Gravity.CENTER_VERTICAL;
                params.weight = 0;
                params.rightMargin = mSpacing;

                mCallout.addView(leftAccessoryView, 0, params);
            }
        }
    }

    /**
     * Return the custom view.
     * 
     * @return The custom view or null if no custom view has been set
     */
    public View getCustomView() {
        return mCustomView;
    }

    /**
     * Set a new custom view to this {@link MapCalloutView}. The newly added
     * view will remove the previous one. Custom views are usually used to
     * completely manage the content of the {@link MapCalloutView}. Setting
     * {@link android.view.ViewGroup.LayoutParams} to the given left accessory
     * is not necessary as they will be automatically set to (
     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT},
     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}).
     * 
     * @param customView The new custom view.
     */
    public void setCustomView(View customView) {
        if (customView != null && customView.getParent() != null) {
            throw new IllegalArgumentException("The given view is already attached to a parent");
        }

        if (mCustomView != customView) {
            // Remove the previous custom view
            if (mCustomView != null) {
                mContentContainer.removeView(mCustomView);
            }

            mCustomView = customView;

            // Add the new custom view
            if (customView != null) {
                mContent.setVisibility(View.GONE);
                mContentContainer.addView(customView, new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
            } else {
                mContent.setVisibility(View.VISIBLE);
            }
        }
    }

    /**
     * Indicates whether this {@link MapCalloutView} has some displayable
     * content. The result of this method is used as a hint to know whether or
     * not the callout should be displayed once an {@link Annotation} has been
     * applied to it.
     * 
     * @return true if this callout has some displayable content, false
     *         otherwise.
     */
    public boolean hasDisplayableContent() {
        if (mCustomView != null) {
            return true;
        }
        if (mLeftAccessory != null || mRightAccessory != null) {
            return true;
        }
        if (mTitle != null && mTitle.getVisibility() == View.VISIBLE) {
            return true;
        }
        if (mSubtitle != null && mSubtitle.getVisibility() == View.VISIBLE) {
            return true;
        }
        return false;
    }

    /**
     * @deprecated use {@link #getBottomOffset()} instead
     */
    @Deprecated
    public int getMarkerHeight() {
        return mBottomOffset;
    }

    /**
     * @deprecated use {@link #setBottomOffset(int)} instead
     */
    @Deprecated
    public void setMarkerHeight(int markerHeight) {
        setBottomOffset(markerHeight);
    }

    /**
     * Return the dimension in pixels the callout will be offset from the
     * bottom.
     * 
     * @return The bottom offset in pixels
     */
    public int getBottomOffset() {
        return mBottomOffset;
    }

    /**
     * Define the dimension in pixels the callout will be offset from the
     * bottom. This is usually used to offset the callout from the height of the
     * marker so that it appears on top of it.
     * 
     * @return The bottom offset (in pixels) the callout should be drawn
     *         regarding its normal Y-origin.
     */
    public void setBottomOffset(int bottomOffset) {
        if (bottomOffset < 0) {
            bottomOffset = 0;
        }
        if (mBottomOffset != bottomOffset) {
            mBottomOffset = bottomOffset;
            requestLayout();
            invalidate();
        }
    }

    public int getAnchorMode() {
        return mAnchorMode;
    }

    public void setAnchorMode(int anchorMode) {
        switch (anchorMode) {
            case ANCHOR_MODE_FIXED:
            case ANCHOR_MODE_VARIABLE:
                break;
            default:
                anchorMode = ANCHOR_MODE_FIXED;
                break;
        }
        if (mAnchorMode != anchorMode) {
            mAnchorMode = anchorMode;
            switch (anchorMode) {
                case ANCHOR_MODE_VARIABLE:
                    mNeedRelayout = true;
                    break;
                case ANCHOR_MODE_FIXED:
                default:
                    mMapCalloutDrawable.setAnchorOffset(MapCalloutDrawable.ANCHOR_POSITION_CENTER);
                    break;
            }
            requestLayout();
            invalidate();
        }
    }

    private void offsetToContainRect(Rect innerRect, Rect outerRect, Point outPoint) {

        final int offsetLeft = Math.min(0, innerRect.left - outerRect.left);
        final int offsetTop = Math.min(0, innerRect.top - outerRect.top);
        final int offsetRight = Math.max(0, innerRect.right - outerRect.right);
        final int offsetBottom = Math.max(0, innerRect.bottom - outerRect.bottom);

        //@formatter:off
        outPoint.set(
                offsetLeft != 0 ? offsetLeft : offsetRight,
                offsetTop != 0 ? offsetTop : offsetBottom);
        //@formatter:on
    }

    private void oopsBackgroundModified() {
        throw new UnsupportedOperationException("The background of a " + MapCalloutView.class.getSimpleName() + " cannot be changed");
    }

    private final OnTouchListener mOnTouchListener = new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            // We want to consume MotionEvent even if the view is not clickable
            // to prevent the user clicking on hidden widgets.
            if (!isClickable()) {
                return true;
            }

            if (mGestureDetector.onTouchEvent(event)) {
                return true;
            }

            // HACK Cyril: GestureDetector never callbacks onSingleTapConfirmed
            // when the click is done in done after a long press (which we don't
            // want to handle).
            if (event.getAction() == MotionEvent.ACTION_UP) {
                mGestureListener.onUp(event);
            }

            return false;
        }
    };

    private class GestureListener extends SimpleOnGestureListener {

        private final Rect mRect = new Rect();
        private boolean mHasLongPressed;

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            if (mOnClickListener != null) {
                mOnClickListener.onClick(MapCalloutView.this);
            }
            return false;
        }

        public void onUp(MotionEvent e) {
            getDrawingRect(mRect);
            if (mRect.contains((int) e.getX(), (int) e.getY()) && mHasLongPressed) {
                onSingleTapConfirmed(e);
            }
            mHasLongPressed = false;
        }

        public void onLongPress(MotionEvent e) {
            mHasLongPressed = true;
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            if (mOnDoubleTapListener != null) {
                mOnDoubleTapListener.onDoubleTap(MapCalloutView.this);
            }
            return true;
        }
    }
}




Java Source Code List

com.cyrilmottier.android.polarissample.MainActivity.java
com.cyrilmottier.android.polarissample.util.Config.java
com.cyrilmottier.polaris.Annotation.java
com.cyrilmottier.polaris.Build.java
com.cyrilmottier.polaris.CoordinateRegion.java
com.cyrilmottier.polaris.MapCalloutDrawable.java
com.cyrilmottier.polaris.MapCalloutView.java
com.cyrilmottier.polaris.MapViewUtils.java
com.cyrilmottier.polaris.PolarisMapView.java
com.cyrilmottier.polaris.internal.AnnotationsOverlay.java
com.cyrilmottier.polaris.internal.Config.java
com.cyrilmottier.polaris.internal.OverlayContainer.java