fred-ye / summary

my blog
43 stars 9 forks source link

[Android]AchartEngine实现tooltip的功能 #30

Open fred-ye opened 10 years ago

fred-ye commented 10 years ago

在上一个项目中,我们采用了AChartEngine来画图,实现了一种类似于Web界面上的tooltip的功能。在做Web时实现这种功能就很简单了,像Highcharts这种专业的报表插件直接就内嵌有这种功能。但AChartEngine中默认是没有的,不过我们可以自己写一个。在我们的App中做的要是精细一些,此处就实现一个粗糙版本,但足以说明实现的思路。首先我们先看最终实现的效果如下: screen shot 2014-11-16 at 9 08 59 pm 横坐标中用来显示日期,纵坐标显示跑步的里程,点击折线上的某点时显示的是里程和对应消耗的热量。

Activity的实现

本例中的Activity实现就很简单了,主要是Mock一下数据。然后调用自己定的AchartEngine类来生成图表,最后将生成的图表添加到界面中去,让其显示出来。

public class TestAchartEngineActivity extends Activity {
    private LinearLayout llWraper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_achartengine);
        initView ();
    }

    private void initView() {
        llWraper = (LinearLayout)findViewById(R.id.ll_wraper);
        //模拟数据
        String[] times = new String [] {"11.11", "11.12", "11.13", "11.14", "11.15", "11.16", "11.17"};
        double[] mileages = new double [] {2.5, 3.2,4.3,5.8,4.6,3.9,3.3};
        double[] carolies = new double [] {3.5, 4.9,4.3,3.8,5.6,4.9,4.3};
        //生成图表。
        View view = new AChartLinear(mileages, carolies, times, llWraper).execute(this);
        llWraper.addView(view);
    }
}

AChartLinear 类的实现

public class AChartLinear {
    private XYMultipleSeriesRenderer renderer;
    private GraphicalView chartView;

    private String[] times;
    private double[] mileages;
    private double[] carolies;
    // 定义x轴上只会有7个刻度
    private double[] x = { 0, 1, 2, 3, 4, 5, 6 };

    private int[] margin = { 0, 0, 0, 0 };

    //tooltip图片的尺寸为74和51dp,因为我们是将tooltip图片(尺寸是111*76)放在hdpi下面。
    private int toolTipWidth = 74;
    private int toolTipHeight = 51;

    private PopupWindow popup = null;
    private View layout;
    private View viewParent;

    private void initPopupWindow(Context context) {
        if (popup != null) {
            popup.dismiss();
        }

        popup = new PopupWindow(layout, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    public AChartLinear(double[] mileages, double[] carolies, String[] times, View viewParent) {
        this.mileages = mileages;
        this.carolies = carolies;
        this.times = times;
        this.viewParent = viewParent;
    }

    public View execute(Context context) {

        renderer = getLinearRenderer(context);
        chartView = ChartFactory.getLineChartView(context, getlinearDataSet(x, mileages), renderer);

        initToolTip(context);
        return chartView;

    }

    private void initToolTip(Context context) {
        renderer.setClickEnabled(true);
        // 设置可点击的触控范围
        renderer.setSelectableBuffer(DensityUtil.dip2px(context, 20));

        final Context contextTemp = context;
        toolTipHeight = DensityUtil.dip2px(contextTemp, toolTipHeight);
        toolTipWidth = DensityUtil.dip2px(contextTemp, toolTipWidth);

        LayoutInflater inflater = LayoutInflater.from(contextTemp);
        layout = inflater.inflate(R.layout.home_tooltip, null);
        // 获取toolTip中两个TextView,这两个TextView的值会不断变化
        final TextView distanceTotal = (TextView) layout.findViewById(R.id.distance_total);
        final TextView calorieTotal = (TextView) layout.findViewById(R.id.calorie_total);

        chartView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                MotionEvent motionEvent = MotionEvent.obtain(event.getDownTime(), event.getEventTime(), MotionEvent.ACTION_DOWN, event.getX(), event.getY(),
                        event.getMetaState());
                chartView.onTouchEvent(motionEvent);
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                    SeriesSelection seriesSelection = ((GraphicalView) v).getCurrentSeriesAndPoint();
                    //当点击的位置是对应某一个点时,开始获取该点处的数据,并且弹出PopupWindow.
                    if (seriesSelection != null) {
                        distanceTotal.setText(seriesSelection.getValue() + "");
                        calorieTotal.setText((int) carolies[seriesSelection.getPointIndex()] + "");
                        Log.i("data", seriesSelection.getValue() + " " + carolies[seriesSelection.getPointIndex()]);
                        //以下代码是为了计算tooltip弹出的位置。
                        // 实际点击处的x,y坐标
                        double[] clickPoint = chartView.toRealPoint(0);

                        double xValue = seriesSelection.getXValue();// 基准点的x坐标
                        double yValue = seriesSelection.getValue();// 基准点的y坐标

                        double xPosition = event.getRawX() - event.getX() + margin[1] + ((event.getX() - margin[1]) * xValue / clickPoint[0]);
                        double yPosition = event.getRawY() - event.getY() + margin[0]
                                + ((event.getY() - margin[0]) * (renderer.getYAxisMax() - yValue) / (renderer.getYAxisMax() - clickPoint[1]));
                        int xOffset = (int) (xPosition - toolTipWidth / 2);
                        // 减去7个dip是为了让poupup和点之间的距离高一点。
                        int yOffset = (int) (yPosition - toolTipHeight - DensityUtil.dip2px(contextTemp, 7)); 
                        initPopupWindow(contextTemp);

                        popup.showAtLocation(viewParent, Gravity.NO_GRAVITY, xOffset, yOffset);

                    } else { //当点击的位置不是图表上折点的位置时,如果上一次点击弹出的popup还存在,就把它dismiss掉。
                        if (popup != null) {
                            popup.dismiss();
                        }
                    }
                }
                return true;
            }
        });
    }

    public XYMultipleSeriesRenderer getLinearRenderer(Context context) {
        XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
        // 以下四个margin分别代表了:top, left, bottom, right
        margin[0] = DensityUtil.dip2px(context, 10);
        margin[1] = DensityUtil.dip2px(context, 25);
        margin[2] = DensityUtil.dip2px(context, 15);
        margin[3] = DensityUtil.dip2px(context, 20);
        renderer.setMargins(margin);

        XYSeriesRenderer r = new XYSeriesRenderer();
        r.setLineWidth(DensityUtil.dip2px(context, 1.5f));
        r.setColor(Color.parseColor("#32b2cd")); // 设置折线的颜色

        r.setPointStyle(PointStyle.CIRCLE);//设置表中点的样式。
        r.setPointStrokeWidth(DensityUtil.dip2px(context, 2));

        renderer.addSeriesRenderer(r);

        renderer.setXLabels(0); // 设置不显示x轴上的原始刻度
        //自定义X轴上的刻度
        for (int i = 0; i < times.length; i++) {
            renderer.addXTextLabel(i, times[i]);
        }
        renderer.setXAxisMin(0);// x轴最小值
        renderer.setXAxisMax(6);
        renderer.setYLabels(7); // 定义Y轴的7个刻度

        renderer.setYAxisMin(0);// y轴最小值
        renderer.setYAxisMax(Math.ceil(getMaxY(mileages)));//Y轴的最大值由传入的参数决定

        renderer.setLabelsTextSize(DensityUtil.dip2px(context, 12));
        renderer.setShowTickMarks(false);
        renderer.setAxesColor(Color.parseColor("#3c3c3c")); // 设置X轴的颜色
        renderer.setXLabelsColor(Color.parseColor("#3c3c3c"));
        renderer.setYLabelsColor(0, Color.parseColor("#3c3c3c"));

        renderer.setYLabelsAlign(Align.RIGHT);
        renderer.setYLabelsVerticalPadding(-5);
        renderer.setYLabelsPadding(5);
        renderer.setShowGrid(true);// 显示网格
        renderer.setShowCustomTextGrid(true);
        renderer.setFitLegend(true);// 调整合适的位置
        renderer.setPanEnabled(false, false); // x,y轴不可拖动
        renderer.setGridColor(Color.parseColor("#D8D8D8")); // 网格
        renderer.setShowLegend(false); 
        renderer.setInScroll(true);
        renderer.setBackgroundColor(Color.TRANSPARENT); // 设置背景色透明
        renderer.setMarginsColor(Color.argb(0x00, 0xff, 0x00, 0x00));
        renderer.setApplyBackgroundColor(true); // 使背景色生效
        renderer.setPointSize(DensityUtil.dip2px(context, 5));//设置点的大小

        return renderer;
    }
    //生成画图表所需要的数据集合。
    private XYMultipleSeriesDataset getlinearDataSet(double[] x, double[] mileages) {
        XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset();
        XYSeries series = new XYSeries("value");
        int len = x.length;
        for (int i = 0; i < len; i++) {
            series.add(x[i], mileages[i]);
        }
        dataset.addSeries(series);
        return dataset;
    }
    //获取Y轴的最大值。
    private double getMaxY(double[] mileages) {
        int len = mileages.length;
        int i;
        double max = 1;
        for (i = 0; i < len; i++) {
            if (max < mileages[i]) {
                max = mileages[i];
            }
        }
        return max;
    }

    public PopupWindow getPopup() {
        return this.popup;
    }

}

此类的实现略显复杂,其思路是:我们调用的是该类中的execute方法来实现绘画图表的需求。该execute方法做了这么几件事:首先,构造render, reader中会定义图表的样式,坐标轴的刻度等。其次,构造数据集合,AchartEngine会利用这个数据集合来生成图表。再次,调用AchartEngine中的getLineChartView方法来生成图表。最后,完成tooltip的基本的配置,这个是由initToolTip方法来实现的。 图表的基本样式 图表的样式定义是由方法public XYMultipleSeriesRenderer getLinearRenderer(Context context) {决定的。都是对render属性的设置,没有太复杂的东西,代码中相应的地方也都有注释,此处就不说明。唯一需要注意一点的是,如果要自定义坐标轴上的文字,需要用setXLabels(0)这种方式,这个是针对X轴的设置,对Y轴的设置也是一样的。在本例中,我们的X轴显示的是日期,故采用如下实现:

    renderer.setXLabels(0); // 设置不显示x轴上的原始刻度
        //自定义X轴上的刻度
        for (int i = 0; i < times.length; i++) {
            renderer.addXTextLabel(i, times[i]);
        }

关于Y轴刻度分布的问题 由于Y轴的最大值是不固定的,所以Y轴的刻度分布就不太好弄。为此,我们会先计算传进来的Y轴的最大值,然后根据Y轴最大值来确定其刻度分布。

关于toolTip的渲染 这个是实现该功能的关键点,对应的是initToolTip方法。首先明确一点的是,当我们点击图表上折线的拐点时,会弹出tooTip,所以第一步,我们需要捕捉点击事件。 设计一个点击范围,也就是说当我在这个拐点方圆多少距离时的点击才算是一个有效的点击。代码如下:

renderer.setClickEnabled(true);
 // 设置可点击的触控范围
renderer.setSelectableBuffer(DensityUtil.dip2px(context, 20));

获取点击处的数据信息,该数据信息会显示在toolTip中。此处图表中拐点的数据表示的是distance而由于我们设计时calories的数据信息与distance是严格对应的,因此,只要知道该distance的下标,也就可以得到calories的数值了。

SeriesSelection seriesSelection = ((GraphicalView) v).getCurrentSeriesAndPoint();
//当点击的位置是对应某一个点时,开始获取该点处的数据,并且弹出PopupWindow.
if (seriesSelection != null) {
    distanceTotal.setText(seriesSelection.getValue() + "");
    calorieTotal.setText((int) carolies[seriesSelection.getPointIndex()] + "");
}

toolTip的位置坐标 最关键一点,便是toolTip的出现位置。便是这一段代码的工作了.

//以下代码是为了计算tooltip弹出的位置。
// 实际点击处的x,y坐标
double[] clickPoint = chartView.toRealPoint(0);

double xValue = seriesSelection.getXValue();// 基准点的x坐标
double yValue = seriesSelection.getValue();// 基准点的y坐标

double xPosition = event.getRawX() - event.getX() + margin[1] + ((event.getX() - margin[1]) * xValue / clickPoint[0]);
double yPosition = event.getRawY() - event.getY() + margin[0]
        + ((event.getY() - margin[0]) * (renderer.getYAxisMax() - yValue) / (renderer.getYAxisMax() - clickPoint[1]));
int xOffset = (int) (xPosition - toolTipWidth / 2);
// 减去7个dip是为了让poupup和点之间的距离高一点。
int yOffset = (int) (yPosition - toolTipHeight - DensityUtil.dip2px(contextTemp, 7)); 
initPopupWindow(contextTemp);
popup.showAtLocation(viewParent, Gravity.NO_GRAVITY, xOffset, yOffset);

由于AChartEngine中并没有API让我们获取点击的拐点在屏幕上的位置,但是我们可以采用曲线救国的策略。其思路是,通过AChartEngine我们可以知道我们点击处对应的x,y坐标。并且通过Android API 我们是可以知道当前点击的物理位置。有了这两点信息,我们就可以根据比例计算得出拐点的物理位置信息。有了拐点的物理位置信息,我们便可以定位toolTip。等我有时间再补一张图说明一下,就更清晰了。

暴露toolTip 代码中,我们设计了一个方法,向外暴露toolTip。主要是针对当我们点击图表的外部时,我们希望dismiss掉这个toolTip。如果不暴露出去,外面是不能操作的。

public PopupWindow getPopup() {
    return this.popup;
}

toolTip的布局

布局就很简单了,同样,那个背景就不上传了。这种背景在网上很容易找到的。本文中的背景图片尺寸是111*76。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/toast_tooltip"
    android:layout_width="74.0dip"
    android:layout_height="51.0dip"
    android:background="@drawable/home_bkg_tip_111x76"
    android:gravity="top"
    android:paddingTop="5.0dp" >

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_weight="1.0"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/distance_total"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:text="27.5"
            android:textSize="12sp" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:text="Km"
            android:textSize="10sp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1.0"
        android:gravity="right"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/calorie_total"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:text="100"
            android:textSize="12sp" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:text="Cal"
            android:textSize="10sp" />
    </LinearLayout>

</LinearLayout>