net.qiujuer.genius.ui.widget.GeniusBalloonMarker.java Source code

Java tutorial

Introduction

Here is the source code for net.qiujuer.genius.ui.widget.GeniusBalloonMarker.java

Source

/*
 * Copyright (C) 2014 Qiujuer <qiujuer@live.cn>
 * WebSite http://www.qiujuer.net
 * Created 02/25/2015
 * Changed 03/01/2015
 * Version 2.0.0
 * GeniusEditText
 *
 * 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 net.qiujuer.genius.ui.widget;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;

import net.qiujuer.genius.ui.R;
import net.qiujuer.genius.ui.drawable.BalloonMarkerDrawable;
import net.qiujuer.genius.ui.widget.compat.GeniusCompat;

/**
 * This is a BalloonMarker
 */
public class GeniusBalloonMarker extends ViewGroup implements BalloonMarkerDrawable.MarkerAnimationListener {
    private static final int PADDING_DP = 4;
    private static final int ELEVATION_DP = 8;
    private static final int SEPARATION_DP = 22;
    //The TextView to show the info
    private TextView mNumber;
    //The max width of this View
    private int mWidth;
    //some distance between the thumb and our bubble marker.
    //This will be added to our measured height
    private int mSeparation;
    BalloonMarkerDrawable mBalloonMarkerDrawable;

    public GeniusBalloonMarker(Context context) {
        this(context, null);
    }

    public GeniusBalloonMarker(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.GeniusBalloonMarkerStyle);
    }

    public GeniusBalloonMarker(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, "0");
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    public GeniusBalloonMarker(Context context, AttributeSet attrs, int defStyleAttr, String maxValue) {
        super(context, attrs, defStyleAttr);

        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        int padding = (int) (PADDING_DP * displayMetrics.density) * 2;
        mNumber = new TextView(context);
        //Add some padding to this textView so the bubble has some space to breath
        mNumber.setPadding(padding, 0, padding, 0);
        mNumber.setGravity(Gravity.CENTER);
        mNumber.setText(maxValue);
        mNumber.setMaxLines(1);
        mNumber.setSingleLine(true);
        GeniusCompat.setTextDirection(mNumber, TEXT_DIRECTION_LOCALE);
        mNumber.setVisibility(View.INVISIBLE);

        //add some padding for the elevation shadow not to be clipped
        //I'm sure there are better ways of doing this...
        setPadding(padding, padding, padding, padding);

        resetSizes(maxValue);

        mSeparation = (int) (SEPARATION_DP * displayMetrics.density);

        mBalloonMarkerDrawable = new BalloonMarkerDrawable(ColorStateList.valueOf(Color.TRANSPARENT), 0);
        mBalloonMarkerDrawable.setCallback(this);
        mBalloonMarkerDrawable.setMarkerListener(this);
        mBalloonMarkerDrawable.setExternalOffset(padding);

        GeniusCompat.setOutlineProvider(this, mBalloonMarkerDrawable);

        if (attrs != null) {

            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GeniusBalloonMarker,
                    R.attr.GeniusBalloonMarkerStyle, R.style.DefaultBalloonMarkerStyle);
            int textAppearanceId = a.getResourceId(R.styleable.GeniusBalloonMarker_g_markerTextAppearance,
                    R.style.DefaultBalloonMarkerTextAppearanceStyle);

            setTextAppearance(textAppearanceId);

            ColorStateList color = a.getColorStateList(R.styleable.GeniusBalloonMarker_g_markerBackgroundColor);
            setBackgroundColor(color);

            //Elevation for android 5+
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                float elevation = a.getDimension(R.styleable.GeniusBalloonMarker_g_markerElevation,
                        ELEVATION_DP * displayMetrics.density);
                ViewCompat.setElevation(this, elevation);
            }
            a.recycle();
        }
    }

    public void setTextAppearance(int resid) {
        mNumber.setTextAppearance(getContext(), resid);
    }

    public void setBackgroundColor(ColorStateList color) {
        mBalloonMarkerDrawable.setColorStateList(color);
    }

    public void setClosedSize(float closedSize) {
        mBalloonMarkerDrawable.setClosedStateSize(closedSize);
    }

    public void resetSizes(String maxValue) {
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
        //Account for negative numbers... is there any proper way of getting the biggest string between our range????
        mNumber.setText("-" + maxValue);
        //Do a first forced measure call for the TextView (with the biggest text content),
        //to calculate the max width and use always the same.
        //this avoids the TextView from shrinking and growing when the text content changes
        int wSpec = MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, MeasureSpec.AT_MOST);
        int hSpec = MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, MeasureSpec.AT_MOST);
        mNumber.measure(wSpec, hSpec);
        mWidth = Math.max(mNumber.getMeasuredWidth(), mNumber.getMeasuredHeight());
        removeView(mNumber);
        addView(mNumber, new FrameLayout.LayoutParams(mWidth, mWidth, Gravity.LEFT | Gravity.TOP));
    }

    @Override
    protected void dispatchDraw(@NonNull Canvas canvas) {
        mBalloonMarkerDrawable.draw(canvas);
        super.dispatchDraw(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthSize = mWidth + getPaddingLeft() + getPaddingRight();
        int heightSize = mWidth + getPaddingTop() + getPaddingBottom();
        //This diff is the basic calculation of the difference between
        //a square side size and its diagonal
        //this helps us account for the visual offset created by MarkerDrawable
        //when leaving one of the corners un-rounded
        int diff = (int) ((1.41f * mWidth) - mWidth) / 2;
        setMeasuredDimension(widthSize, heightSize + diff + mSeparation);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = getPaddingLeft();
        int top = getPaddingTop();
        int right = getWidth() - getPaddingRight();
        int bottom = getHeight() - getPaddingBottom();
        //the TetView is always layout at the top
        mNumber.layout(left, top, left + mWidth, top + mWidth);
        //the MarkerDrawable uses the whole view, it will adapt itself...
        // or it seems so...
        mBalloonMarkerDrawable.setBounds(left, top, right, bottom);
    }

    @Override
    protected boolean verifyDrawable(Drawable who) {
        return who == mBalloonMarkerDrawable || super.verifyDrawable(who);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        //HACK: Sometimes, the animateOpen() call is made before the View is attached
        //so the drawable cannot schedule itself to run the animation
        //I think we can call it here safely.
        //I've seen it happen in android 2.3.7
        animateOpen();
    }

    public void setValue(CharSequence value) {
        mNumber.setText(value);
    }

    public CharSequence getValue() {
        return mNumber.getText();
    }

    public void animateOpen() {
        mBalloonMarkerDrawable.stop();
        mBalloonMarkerDrawable.animateToPressed();
    }

    public void animateClose() {
        mBalloonMarkerDrawable.stop();
        ViewCompat.animate(mNumber).alpha(0f).setDuration(100).withEndAction(new Runnable() {
            @Override
            public void run() {
                //We use INVISIBLE instead of GONE to avoid a requestLayout
                mNumber.setVisibility(View.INVISIBLE);
                mBalloonMarkerDrawable.animateToNormal();
            }
        }).start();
    }

    @Override
    public void onOpeningComplete() {
        mNumber.setVisibility(View.VISIBLE);
        ViewCompat.animate(mNumber).alpha(1f).setDuration(100).start();
        if (getParent() instanceof BalloonMarkerDrawable.MarkerAnimationListener) {
            ((BalloonMarkerDrawable.MarkerAnimationListener) getParent()).onOpeningComplete();
        }
    }

    @Override
    public void onClosingComplete() {
        if (getParent() instanceof BalloonMarkerDrawable.MarkerAnimationListener) {
            ((BalloonMarkerDrawable.MarkerAnimationListener) getParent()).onClosingComplete();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mBalloonMarkerDrawable.stop();
    }

    public void setColors(int startColor, int endColor) {
        mBalloonMarkerDrawable.setColors(startColor, endColor);
    }
}