绘制曲线图有几个基本的元素:坐标轴,点,线,分清楚变化的和不变得,从而进行绘制,其实在智能电视TV开发---拍照+水印 中在拍照的图片上面绘制水印都是一样的原理,就是以一张图片为基准,取得srcBitmap的canvas,然后再使用canvas来把其他的内容绘制上去。网上也有几个开源的绘制图表的项目,但是具体到自己项目里面都不太适用。接下来我会用一个二手房房价行情的数据来绘制6个月内容的趋势图,水平绘制,宽比高长,左右可以拖动,适用于嵌入页面的,而不是占据整个屏幕。分部截图来各个部分的实现关键内容,最后贴出代码。
一、背景以及坐标轴的制作
先看一下效果图:
从上图中水平轴是月份,竖直轴包含两部分分别是销售金额以及销售的户数,对比这两幅图片,你可以看到左边部分是不动的,而底部的月份是可以滑动的,使用的是scroller来实现的。上面的内容基本上都是使用canvas来绘制上去的,所以要计算好位置。
/**
* 主要绘制可以滚动的,但是内容不变的
* 刻度
* 月份,年
* 水平的虚线
* 竖直的阴影矩形
* 当数据不为空的时候,绘制折线
*/
public void drawScrollStaticView(){
//清除屏幕
viewBgCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
float timesTop = scroll_bottom;
float scrollTimesTop = scroll_bottom;
float moneyTop = scroll_bottom;
float scrollMoneyTop = scroll_bottom;
int drawMonth = currentMonth;
int rightMarg = view_width - virtical_graduate_right;
float yearWidth = GetTextWH.getTextW("0000年", yearTextPaint);
//绘制水平月份刻度,总共countMonths个刻度
for (int i = 0; i < countMonths; i++) {
int temp = rightMarg - virtical_graduate_width;
virtical_graduate.setBounds(temp, scroll_bottom, rightMarg, 10+scroll_bottom);
virtical_graduate.draw(viewBgCanvas);
//绘制月份数字
viewBgCanvas.drawText(monthArrays[drawMonth], temp-monthTextW / 2, scroll_bottom + monthTextH+16, monthPaint);//绘制月份数字
//绘制月份的月
viewBgCanvas.drawText("月", temp - monthTextW / 4, scroll_bottom + monthTextH * 2 + 16, monthTextPaint);//绘制月
//绘制年
if (drawMonth == 0 && i < countMonths) {
viewBgCanvas.drawText(limitYears[(i / 12)], temp-yearWidth / 2, scroll_bottom + monthTextH * 3+16, yearTextPaint);
}
//绘制水平的虚线
int virtical_bottom = broken_divider_height;//虚线竖直高度
for (int j = 0; j < 3; j++) {
broken_divider.setBounds(temp, virtical_bottom, rightMarg, 2+virtical_bottom);
broken_divider.draw(viewBgCanvas);
virtical_bottom += broken_divider_height;
}
//绘制竖直的阴影矩形
if ((i & 0x1) == 0)
viewBgCanvas.drawRect(temp, 6, rightMarg, scroll_bottom, rectShaderPaint);
//当数据不为空的时候绘制数据折线图
if (modelInfos != null) {
if (i > actualMonths-1) {//当绘制的月数大约时间的月数的时候
timesTop = scroll_bottom;
moneyTop = scroll_bottom;
scrollTimesTop = scroll_bottom;
scrollMoneyTop = scroll_bottom;
drawBrokenLine(viewBgCanvas,"", timesTop, scrollTimesTop, "",moneyTop, scrollMoneyTop, temp);
}else if (i == actualMonths -1) {
timesTop = modelInfos[i].getSaleHouseCountTop();
moneyTop = modelInfos[i].getSaleMoneyTop();
scrollTimesTop = scroll_bottom;
scrollMoneyTop = scroll_bottom;
drawBrokenLine(viewBgCanvas,modelInfos[i].getSaleHouseCountText(), timesTop, scrollTimesTop, "¥"+modelInfos[i].getSaleMoney(), moneyTop, scrollMoneyTop, temp);
}else if (i < actualMonths-1) {
timesTop = modelInfos[i].getSaleHouseCountTop();
moneyTop = modelInfos[i].getSaleMoneyTop();
scrollTimesTop = modelInfos[i+1].getSaleHouseCountTop();
scrollMoneyTop = modelInfos[i+1].getSaleMoneyTop();
drawBrokenLine(viewBgCanvas, modelInfos[i].getSaleHouseCountText(), timesTop, scrollTimesTop, "¥"+modelInfos[i].getSaleMoney(), moneyTop, scrollMoneyTop, temp);
}
}
int drawMonth_1 = drawMonth -1;
if (drawMonth_1 < 0) {
drawMonth_1 = 11;
}
drawMonth = drawMonth_1;
rightMarg = temp;
}
viewLeftBgCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
drawScrollStaticLeftView(viewLeftBgCanvas);
}
/**
* 绘制左侧的销售金额和销售的房数
* @param canvas
*/
private void drawScrollStaticLeftView(Canvas canvas){
report_trend_abscissa_bg.setBounds(0, 0, virtical_graduate_width, scroll_height);
report_trend_abscissa_bg.draw(canvas);
float textH = GetTextWH.getTextH(leftTextPaint);
float textMoneyLeft = 4F * density;
int bottom_marge = broken_divider_height;
for (int i = 0; i < 3; ++i)
{
report_trend_abscissa_divider.setBounds(0, bottom_marge, virtical_graduate_width, bottom_marge + 2);
report_trend_abscissa_divider.draw(canvas);
canvas.drawText(limitMoneys[i], textMoneyLeft, bottom_marge + textH*3/2, leftTextPaint);
canvas.drawText(limitSalesTimes[i], textMoneyLeft, bottom_marge + textH * 5/2 + 6, leftTextPaint);
bottom_marge += broken_divider_height;
}
}
上面两段代码就是实现上面图片的关键部分。里面也解释的很详细
二、计算间距
在绘制文字的时候我们需要用到x,y而要合理的放置文字,我们还需要知道绘制文字的长度和高度,下面类就是获取文字长度和高度代码:
package com.jwzhangjie.smarttv_client.ui.trendview;
import android.graphics.Paint;
public class GetTextWH {
public static float getTextH(Paint paramPaint)
{
/**
* 为了说明,这里引用辅助线baseline=0,画文字的y坐标
top:文字的最顶端离baseline的距离(ascent的极限距离负数)
ascent:文字的上端离baseline的距离(负数)
descent:文字的下端离baseline的距离(正数)
bottom:文字最下端的距离(descent的极限距离正数)
lead:上行文字descent到下行ascent的距离
*/
Paint.FontMetrics localFontMetrics = paramPaint.getFontMetrics();
return (-(localFontMetrics.ascent + localFontMetrics.descent));
}
public static float getTextW(String paramString, Paint paramPaint)
{
return paramPaint.measureText(paramString);
}
}
/**
* 计算高度,宽度
*/
public void initMeage(){
layout_width = getWidth();//得到自己的宽度px
layout_height = getHeight();//得到自己的高度px
layout_width_34 = (int)(0.75F * layout_width);
virtical_graduate_width = dip2px(60);//一个虚拟刻度的高度
virtical_graduate_right = dip2px(20);//第一个虚拟刻度距离顶部的高度
int margeTop = Math.round(44.0F * density);//距离左边44dip
int margeBottom = Math.round(42.0F * density);//竖直的虚拟刻度线距离右边42dip
//6指的是竖直箭头也有宽度的,三角头右边一部分,整个箭头的一半是9px
virtical_graduate_bottom = layout_height - margeBottom - 6;
monthTextW = GetTextWH.getTextW("00", monthPaint);//计算出月份的数字的宽度
monthTextH = GetTextWH.getTextH(monthPaint);//绘制的月份*对齐刻度线
float monthTextW = GetTextWH.getTextW("月", monthTextPaint);
drawMonthTop = layout_height - (int)((monthTextW + margeBottom + monthTextW) / 2.0F);
drawYearTop = layout_height - (int)((virtical_graduate_bottom + GetTextWH.getTextW("2013年", yearTextPaint)) / 2F);
scroll_top = margeTop + 8;
scroll_bottom = virtical_graduate_bottom - 6;
scroll_height = scroll_bottom;
broken_divider_height = scroll_height / 4;//分为4份
viewBottomBgBitmap_left = margeBottom;
calculateActualWidth();
initViewBg();
initViewBottomBg();
}
三、将背景绘制在自定义的View上面
首先我们要覆写onDraw方法,如下:
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(viewBgBitmap, -view_width+layout_width-virtical_graduate_right, 0, viewBgPaint);
//绘制左边的部分,由于要固定在左边,所以x设置为getScrollX(),不然移动的
canvas.drawBitmap(viewLeftBgBitmap, getScrollX(), 0, viewBgPaint);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!isNotFirst) {//只需要执行一次就可以了
initMeage();
drawScrollStaticView();
isNotFirst = true;
}
}
四、触摸滑动
这部分首先要使用的就是onTouchEvent实现移动效果,计算移动的位置,松开手后回滚。
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastMotionX = x;
//动画还没有结束就按下了就结束动画
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = (int) (mLastMotionX - x);
mLastMotionX = x;
if (getScrollX() < layout_width_34 && getScrollX() > -(layout_width_34+scroll_width)) {
scrollBy(deltaX, 0);
}
invalidate();
break;
case MotionEvent.ACTION_UP:
if (getScrollX() > 0 || view_width <= layout_width) {
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
}else if (getScrollX() < -scroll_width) {
mScroller.startScroll(getScrollX(), 0, -getScrollX()-scroll_width, 0, 500);
}
invalidate();
break;
}
return true;
}
其实在case MotionEvent.ACTION_UP:里面还应该处理一种情况就是介于另外两种情况之间的,手在View上面快速滑动并松开手,刻度应该自己慢慢滑动到一端,现在先不实现,如果自己需要也可以添加,里面需要的方法就已经写了,直接调用就行。
五、更新数据
更新数据需要封装一个类,包含价格,售房的户数,以及保存距离顶端的位置等,代码如下:
package com.jwzhangjie.smarttv_client.ui.trendview;
import android.os.Parcel;
import android.os.Parcelable;
public class ModelInfo implements Parcelable{
@Override
public int describeContents() {
return 0;
}
public ModelInfo(){
}
private ModelInfo(Parcel source){
readFromParcel(source);
}
private double saleMoney;//销售金额
private int saleCount;//售房户数
private String saleMoneyText;//销售金额显示在布局的内容
private String saleHouseCountText;//销售楼房个数显示在布局的内容
private float saleMoneyTop;//销售金额显示在布局的顶部距离
private float saleHouseCountTop;//刷卡次数显示在布局顶部距离
private double maxMoney;//最大的销售金额
private int maxCount;//最大的售房户数
public void readFromParcel(Parcel source){
saleMoney = source.readDouble();
saleCount = source.readInt();
saleMoneyText = source.readString();
saleHouseCountText = source.readString();
saleMoneyTop = source.readFloat();
saleHouseCountTop = source.readFloat();
maxMoney = source.readDouble();
maxCount = source.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeDouble(saleMoney);
dest.writeInt(saleCount);
dest.writeString(saleMoneyText);
dest.writeString(saleHouseCountText);
dest.writeFloat(saleMoneyTop);
dest.writeFloat(saleHouseCountTop);
dest.writeDouble(maxMoney);
dest.writeInt(maxCount);
}
public static Creator<ModelInfo> CREATOR = new Creator<ModelInfo>() {
@Override
public ModelInfo createFromParcel(Parcel source) {
return new ModelInfo(source);
}
@Override
public ModelInfo[] newArray(int size) {
return new ModelInfo[size];
}
};
public double getSaleMoney() {
return saleMoney;
}
public void setSaleMoney(double saleMoney) {
this.saleMoney = saleMoney;
}
public int getSaleCount() {
return saleCount;
}
public void setSaleCount(int saleCount) {
this.saleCount = saleCount;
}
public String getSaleMoneyText() {
return saleMoneyText;
}
public void setSaleMoneyText(String saleMoneyText) {
this.saleMoneyText = saleMoneyText;
}
public String getSaleHouseCountText() {
return saleHouseCountText;
}
public void setSaleHouseCountText(String saleHouseCountText) {
this.saleHouseCountText = saleHouseCountText;
}
public float getSaleMoneyTop() {
return saleMoneyTop;
}
public void setSaleMoneyTop(float saleMoneyTop) {
this.saleMoneyTop = saleMoneyTop;
}
public float getSaleHouseCountTop() {
return saleHouseCountTop;
}
public void setSaleHouseCountTop(float saleHouseCountTop) {
this.saleHouseCountTop = saleHouseCountTop;
}
public double getMaxMoney() {
return maxMoney;
}
public void setMaxMoney(double maxMoney) {
this.maxMoney = maxMoney;
}
public int getMaxCount() {
return maxCount;
}
public void setMaxCount(int maxCount) {
this.maxCount = maxCount;
}
}
实现更新的方法如下:
/**
*更新数据
*将数据进行处理,计算每一个点距离布局的左边位置
*然后重新绘制刻度等图像,调用 drawScrollStaticView()
*@param maxMoney 最大的售房金额
*@param maxTimes 最大的售房户数
*/
public void updateData(ArrayList<ModelInfo> lists, double maxMoney, int maxTimes){
actualMonths = lists.size();
modelInfos = new ModelInfo[actualMonths];
//RangMaxMoney 返回刻度的最高金额
int RangMaxMoney = DataAccuracy.FomatMoneyString(limitMoneys, maxMoney);
//RangMaxTimes 返回刻度的最大户数
int RangMaxTimes = DataAccuracy.FomatTimesString(limitSalesTimes, maxTimes);
for (int i = 0; i < actualMonths; i++) {
ModelInfo modelInfo = lists.get(i);
float test = scroll_height - (float)(modelInfo.getSaleMoney() / RangMaxMoney * scroll_height);
float testTimes = scroll_height - (float)((double)modelInfo.getSaleCount() / RangMaxTimes * scroll_height);
modelInfo.setSaleMoneyTop(test);
modelInfo.setSaleHouseCountTop(testTimes);
modelInfos[i] = modelInfo;
}
drawScrollStaticView();
invalidate();
}
六、最终的实现效果和源码
6.1、主类,负责模拟数据,更新数据
package com.jwzhangjie.smarttv_client.ui.trendview;
import java.util.ArrayList;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import com.jwzhangjie.smarttv_client.R;
import com.jwzhangjie.smarttv_client.ui.base.BaseActivity;
public class Trend extends BaseActivity{
private MyTrendView trendView;
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_trend_view);
trendView = (MyTrendView)findViewById(R.id.trendView);
button = (Button)findViewById(android.R.id.button1);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
ArrayList<ModelInfo> modelInfos = new ArrayList<ModelInfo>();
ModelInfo modelInfo = new ModelInfo();
modelInfo.setMaxCount(13);
modelInfo.setMaxMoney(11200);
modelInfo.setSaleMoney(11200);
modelInfo.setSaleCount(13);
modelInfo.setSaleHouseCountText("11200元");
modelInfo.setSaleHouseCountText("13户");
modelInfos.add(modelInfo);
ModelInfo modelInfo1 = new ModelInfo();
modelInfo1.setSaleMoney(9000);
modelInfo1.setSaleCount(8);
modelInfo1.setSaleHouseCountText("9000元");
modelInfo1.setSaleHouseCountText("8户");
modelInfos.add(modelInfo1);
ModelInfo modelInfo2 = new ModelInfo();
modelInfo2.setSaleMoney(9000);
modelInfo2.setSaleCount(3);
modelInfo2.setSaleHouseCountText("9000元");
modelInfo2.setSaleHouseCountText("3户");
modelInfos.add(modelInfo2);
ModelInfo modelInfo3 = new ModelInfo();
modelInfo3.setSaleMoney(9200);
modelInfo3.setSaleCount(6);
modelInfo3.setSaleHouseCountText("9200元");
modelInfo3.setSaleHouseCountText("6户");
modelInfos.add(modelInfo3);
trendView.updateData(modelInfos, 11200, 13);
}
});
}
}
6.2、主类的布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="更新数据"
android:id="@android:id/button1"
/>
<include
layout="@layout/view_trend"
android:layout_width="300dip"
android:layout_height="200dip"
android:layout_centerInParent="true"
/>
</RelativeLayout>
6.3、趋势图的布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/trend_bg_repeat" >
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-2dip"
android:background="@drawable/report_trend_notch"
android:contentDescription="@string/app_name" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="42.0dip"
android:gravity="bottom" >
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/report_trend_timeline"
android:contentDescription="@string/app_name" />
</LinearLayout>
<com.jwzhangjie.smarttv_client.ui.trendview.MyTrendView
android:id="@+id/trendView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>
6.4、辅助类DataAccuracy.java
package com.jwzhangjie.smarttv_client.ui.trendview;
public class DataAccuracy {
private static int getMaxMoney(double paramDouble)
{
if (paramDouble < 4000.0D)
return 4000;
return (8000 * (int)Math.pow(2.0D, (int)b(paramDouble / 4000.0D)));
}
private static int getMaxTimes(int paramInt)
{
if (paramInt < 8)
return 8;
return (16 * (int)Math.pow(2.0D, (int)b(paramInt / 8)));
}
public static int FomatMoneyString(String[] paramArrayOfString, double paramDouble)
{
int i = getMaxMoney(paramDouble);
for (int j = 0; j < 3; ++j)
paramArrayOfString[j] = FormatDecimal.format(i * (3 - j) / 4);
return i;
}
public static int FomatTimesString(String[] paramArrayOfString, int paramInt)
{
int i = getMaxTimes(paramInt);
for (int j = 0; j < 3; ++j)
paramArrayOfString[j] = (i * (3 - j) / 4) + "次";
return i;
}
private static double b(double paramDouble)
{
return (Math.log(paramDouble) / Math.log(2.0D));
}
}
6.5、辅助类FormatDecimal.java
package com.jwzhangjie.smarttv_client.ui.trendview;
import java.text.DecimalFormat;
public class FormatDecimal {
private static DecimalFormat a = new DecimalFormat("#,###");
private static DecimalFormat b = new DecimalFormat("#.00");
private static DecimalFormat c = new DecimalFormat("#,##0.00");
private static DecimalFormat d = new DecimalFormat("0.0");
private static DecimalFormat e = new DecimalFormat("#0");
public static String format(double paramDouble)
{
return a.format(paramDouble);
}
public static String formatTwo(double params){
return b.format(params);
}
public static String formatThree(double params){
return c.format(params);
}
public static String formatFour(double params){
return d.format(params);
}
public static String formatFive(double params){
return e.format(params);
}
}
6.6、自定义的趋势图类
package com.jwzhangjie.smarttv_client.ui.trendview;
import java.util.ArrayList;
import java.util.Date;
import com.jwzhangjie.smarttv_client.R;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;
public class MyTrendView extends View {
private float density;
private float mLastMotionX;//记录上一次触摸的x的坐标
private boolean isNotFirst = false;//判断是否是第一次,如果是第一次则调用initMeage,绘制位图,这些是不变
// 图片资源
private Drawable virtical_graduate;// 刻度线
private Drawable broken_divider;//水平的虚线
private Drawable report_trend_abscissa_bg;//左边的渐进图片
private Drawable report_trend_abscissa_divider;//左边渐进部分的水平分割线
private int countMonths = 12;//总共需要绘制的月数这里默认设置12个月
private int actualMonths = 0;//传递的数据,总共的月数
private int currentMonth;//当前的月份
private int currentYear;//当前的年份
private int layout_width;//当前布局的宽度
private int layout_height;//当前布局的高度
private int layout_width_34;//当前布局的3/4
private int view_width;//实际View的宽度,要实现滚动
private int virtical_graduate_width;//一个虚拟刻度的宽度,这里设置60dip
private int virtical_graduate_right;//虚拟刻度距离右边顶部20dip
private int virtical_graduate_bottom;
private int scroll_width;//可以滚动的距离,实际宽度-布局本身宽度
private int scroll_height;
private int scroll_top;//可以滚动的阴影距离左边的距离
private int scroll_bottom;//可以滚动阴影距离布局右边的距离
private int broken_divider_height;//水平虚线之间的间距
private int viewBottomBgBitmap_left;//底部距离布局顶部的高度
private String[] monthArrays = { "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12" };
private String[] limitYears = new String[3];//今年,去年,前年
private String[] limitMoneys = new String[3];//存放售房的最高额,底部3000,2000,1000
private String[] limitSalesTimes = new String[3];//存放售房的个数,24,16,8
private float monthTextW;//文字的宽度
private float monthTextH;//计算出文字的高度/2,为了与刻度线对齐
private int drawMonthTop;//绘制月数字位于父布局上边的距离
private int drawYearTop;//绘制右边年的left位置
private int minimumFlingVelocitx; //滚动的最小速度
private int maxmumFlingVelocitx; //滚动的最大速度
private ModelInfo[] modelInfos = null;
/**
* 两个位图
* @viewBgBitmap 是用来加载刻度,月份,年,水平矩形,竖直虚线
* @viewBottomBgBitmap 来用绘制底部
*/
private Bitmap viewBgBitmap;
private Bitmap viewLeftBgBitmap;
/**
* Paint声明
*/
private Paint viewBgPaint;
private Paint rectShaderPaint;//竖直的矩形阴影
private Paint monthPaint;
private Paint monthTextPaint;
private Paint yearTextPaint;
private Paint leftTextPaint;//View左部渐进里面文本的Paint
private Paint moneyBrokenLine;//金额的折线Paint
private Paint moneyBrokenCircle;//金额的折线点的圈的Paint
private Paint timesBrokenLine;//售房户数的折线
private Paint timesBrokenCircle;//售房户数的折线的点的圈的Paint
private Paint moneyTextPaint;//绘制金额的文字
private Paint timesTextPaint;//绘制售房户数的文字
/**
* 与上面的Bitmap对应
*/
private Canvas viewBgCanvas;
private Canvas viewLeftBgCanvas;//绘制底部的视图
//滚动条,用来移动布局
private Scroller mScroller;
//设置滑动速度的
private VelocityTracker mVelocityTracker;
public MyTrendView(Context context) {
super(context);
initResource();
mScroller = new Scroller(context);
ViewConfiguration localViewConfiguration = ViewConfiguration.get(context);
minimumFlingVelocitx = localViewConfiguration.getScaledMinimumFlingVelocity();
maxmumFlingVelocitx = localViewConfiguration.getScaledMaximumFlingVelocity();
}
public MyTrendView(Context context, AttributeSet attrs) {
super(context, attrs);
initResource();
mScroller = new Scroller(context);
ViewConfiguration localViewConfiguration = ViewConfiguration.get(context);
minimumFlingVelocitx = localViewConfiguration.getScaledMinimumFlingVelocity();
maxmumFlingVelocitx = localViewConfiguration.getScaledMaximumFlingVelocity();
}
/**
* 加载资源,初始化一些变量
*/
@SuppressWarnings("deprecation")
public void initResource() {
Resources localResources = getResources();
density = localResources.getDisplayMetrics().density;
virtical_graduate = localResources.getDrawable(R.drawable.virtical_graduate);//刻度线
broken_divider = localResources.getDrawable(R.drawable.broken_divider);//绘制水平的虚线
report_trend_abscissa_bg = localResources.getDrawable(R.drawable.report_trend_abscissa_bg);
report_trend_abscissa_divider = localResources.getDrawable(R.drawable.report_trend_abscissa_divider);
viewBgPaint = new Paint();
rectShaderPaint = new Paint();
rectShaderPaint.setStyle(Paint.Style.FILL);
rectShaderPaint.setARGB(30, 26, 28, 33);
monthPaint = new Paint();
monthPaint.setAntiAlias(true);
monthPaint.setColor(getResources().getColor(R.color.white));
monthPaint.setTextAlign(Paint.Align.LEFT);
monthPaint.setTextSize(14.0F * this.density);
monthTextPaint = new Paint();
monthTextPaint.setAntiAlias(true);
monthTextPaint.setColor(getResources().getColor(R.color.white));
monthTextPaint.setTextAlign(Paint.Align.LEFT);
monthTextPaint.setTextSize(8.0F * this.density);
yearTextPaint = new Paint();
yearTextPaint.setAntiAlias(true);
yearTextPaint.setColor(getResources().getColor(R.color.white));
yearTextPaint.setTextAlign(Paint.Align.LEFT);
yearTextPaint.setTextSize(7.8F * density);
leftTextPaint = new Paint();
leftTextPaint.setAntiAlias(true);
leftTextPaint.setColor(-1);
leftTextPaint.setTextAlign(Paint.Align.LEFT);
leftTextPaint.setTextSize(7.0F * density);
moneyTextPaint = new Paint();
moneyTextPaint.setAntiAlias(true);
moneyTextPaint.setTextSize(8.0F * this.density);
moneyTextPaint.setStrokeWidth(5);
moneyTextPaint.setColor(getResources().getColor(R.color.white));
moneyTextPaint.setStyle(Paint.Style.FILL);
moneyBrokenLine = new Paint();
moneyBrokenLine.setAntiAlias(true);
moneyBrokenLine.setStrokeWidth(5);
moneyBrokenLine.setColor(getResources().getColor(R.color.orangered));
moneyBrokenLine.setStyle(Paint.Style.FILL);
moneyBrokenCircle = new Paint();
moneyBrokenCircle.setColor(getResources().getColor(R.color.orangered));
timesBrokenCircle = new Paint();
timesBrokenCircle.setAntiAlias(true);
timesBrokenCircle.setStyle(Paint.Style.FILL);
timesBrokenCircle.setColor(getResources().getColor(R.color.lime));
timesBrokenLine = new Paint();
timesBrokenLine.setAntiAlias(true);
timesBrokenLine.setStrokeWidth(5);
timesBrokenLine.setColor(getResources().getColor(R.color.lime));
timesTextPaint = new Paint();
timesTextPaint.setAntiAlias(true);
timesTextPaint.setStrokeWidth(5);
timesTextPaint.setTextSize(8.0F * this.density);
timesTextPaint.setColor(getResources().getColor(R.color.white));
timesTextPaint.setStyle(Paint.Style.FILL);
viewBgCanvas = new Canvas();
viewLeftBgCanvas = new Canvas();
Date localDate = new Date();
currentYear = (1900 + localDate.getYear());
currentMonth = localDate.getMonth();
limitYears[0] = this.currentYear + "年";
limitYears[1] = (-1 + this.currentYear) + "年";
limitYears[2] = (-2 + this.currentYear) + "年";
for (int i = 0; i < 3; i++) {
limitMoneys[i] = String.valueOf(1000 * (3 - i));
limitSalesTimes[i] = (2 * (3 - i)) + "户";
}
}
/**
* 计算高度,宽度
*/
public void initMeage(){
layout_width = getWidth();//得到自己的宽度px
layout_height = getHeight();//得到自己的高度px
layout_width_34 = (int)(0.75F * layout_width);
virtical_graduate_width = dip2px(60);//一个虚拟刻度的高度
virtical_graduate_right = dip2px(20);//第一个虚拟刻度距离顶部的高度
int margeTop = Math.round(44.0F * density);//距离左边44dip
int margeBottom = Math.round(42.0F * density);//竖直的虚拟刻度线距离右边42dip
//6指的是竖直箭头也有宽度的,三角头右边一部分,整个箭头的一半是9px
virtical_graduate_bottom = layout_height - margeBottom - 6;
monthTextW = GetTextWH.getTextW("00", monthPaint);//计算出月份的数字的宽度
monthTextH = GetTextWH.getTextH(monthPaint);//绘制的月份*对齐刻度线
float monthTextW = GetTextWH.getTextW("月", monthTextPaint);
drawMonthTop = layout_height - (int)((monthTextW + margeBottom + monthTextW) / 2.0F);
drawYearTop = layout_height - (int)((virtical_graduate_bottom + GetTextWH.getTextW("2013年", yearTextPaint)) / 2F);
scroll_top = margeTop + 8;
scroll_bottom = virtical_graduate_bottom - 6;
scroll_height = scroll_bottom;
broken_divider_height = scroll_height / 4;//分为4份
viewBottomBgBitmap_left = margeBottom;
calculateActualWidth();
initViewBg();
initViewBottomBg();
}
/**
*更新数据
*将数据进行处理,计算每一个点距离布局的左边位置
*然后重新绘制刻度等图像,调用 drawScrollStaticView()
*@param maxMoney 最大的售房金额
*@param maxTimes 最大的售房户数
*/
public void updateData(ArrayList<ModelInfo> lists, double maxMoney, int maxTimes){
actualMonths = lists.size();
modelInfos = new ModelInfo[actualMonths];
//RangMaxMoney 返回刻度的最高金额
int RangMaxMoney = DataAccuracy.FomatMoneyString(limitMoneys, maxMoney);
//RangMaxTimes 返回刻度的最大户数
int RangMaxTimes = DataAccuracy.FomatTimesString(limitSalesTimes, maxTimes);
for (int i = 0; i < actualMonths; i++) {
ModelInfo modelInfo = lists.get(i);
float test = scroll_height - (float)(modelInfo.getSaleMoney() / RangMaxMoney * scroll_height);
float testTimes = scroll_height - (float)((double)modelInfo.getSaleCount() / RangMaxTimes * scroll_height);
modelInfo.setSaleMoneyTop(test);
modelInfo.setSaleHouseCountTop(testTimes);
modelInfos[i] = modelInfo;
}
drawScrollStaticView();
invalidate();
}
/**
* 计算实际的view的高度
*/
public void calculateActualWidth(){
view_width = (1+countMonths) * virtical_graduate_width;
scroll_width = countMonths * virtical_graduate_width + virtical_graduate_right - layout_width;
}
/**
* 主要绘制可以滚动的,但是内容不变的
* 刻度
* 月份,年
* 水平的虚线
* 竖直的阴影矩形
* 当数据不为空的时候,绘制折线
*/
public void drawScrollStaticView(){
//清除屏幕
viewBgCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
float timesTop = scroll_bottom;
float scrollTimesTop = scroll_bottom;
float moneyTop = scroll_bottom;
float scrollMoneyTop = scroll_bottom;
int drawMonth = currentMonth;
int rightMarg = view_width - virtical_graduate_right;
float yearWidth = GetTextWH.getTextW("0000年", yearTextPaint);
//绘制水平月份刻度,总共countMonths个刻度
for (int i = 0; i < countMonths; i++) {
int temp = rightMarg - virtical_graduate_width;
virtical_graduate.setBounds(temp, scroll_bottom, rightMarg, 10+scroll_bottom);
virtical_graduate.draw(viewBgCanvas);
//绘制月份数字
viewBgCanvas.drawText(monthArrays[drawMonth], temp-monthTextW / 2, scroll_bottom + monthTextH+16, monthPaint);//绘制月份数字
//绘制月份的月
viewBgCanvas.drawText("月", temp - monthTextW / 4, scroll_bottom + monthTextH * 2 + 16, monthTextPaint);//绘制月
//绘制年
if (drawMonth == 0 && i < countMonths) {
viewBgCanvas.drawText(limitYears[(i / 12)], temp-yearWidth / 2, scroll_bottom + monthTextH * 3+16, yearTextPaint);
}
//绘制水平的虚线
int virtical_bottom = broken_divider_height;//虚线竖直高度
for (int j = 0; j < 3; j++) {
broken_divider.setBounds(temp, virtical_bottom, rightMarg, 2+virtical_bottom);
broken_divider.draw(viewBgCanvas);
virtical_bottom += broken_divider_height;
}
//绘制竖直的阴影矩形
if ((i & 0x1) == 0)
viewBgCanvas.drawRect(temp, 6, rightMarg, scroll_bottom, rectShaderPaint);
//当数据不为空的时候绘制数据折线图
if (modelInfos != null) {
if (i > actualMonths-1) {//当绘制的月数大约时间的月数的时候
timesTop = scroll_bottom;
moneyTop = scroll_bottom;
scrollTimesTop = scroll_bottom;
scrollMoneyTop = scroll_bottom;
drawBrokenLine(viewBgCanvas,"", timesTop, scrollTimesTop, "",moneyTop, scrollMoneyTop, temp);
}else if (i == actualMonths -1) {
timesTop = modelInfos[i].getSaleHouseCountTop();
moneyTop = modelInfos[i].getSaleMoneyTop();
scrollTimesTop = scroll_bottom;
scrollMoneyTop = scroll_bottom;
drawBrokenLine(viewBgCanvas,modelInfos[i].getSaleHouseCountText(), timesTop, scrollTimesTop, "¥"+modelInfos[i].getSaleMoney(), moneyTop, scrollMoneyTop, temp);
}else if (i < actualMonths-1) {
timesTop = modelInfos[i].getSaleHouseCountTop();
moneyTop = modelInfos[i].getSaleMoneyTop();
scrollTimesTop = modelInfos[i+1].getSaleHouseCountTop();
scrollMoneyTop = modelInfos[i+1].getSaleMoneyTop();
drawBrokenLine(viewBgCanvas, modelInfos[i].getSaleHouseCountText(), timesTop, scrollTimesTop, "¥"+modelInfos[i].getSaleMoney(), moneyTop, scrollMoneyTop, temp);
}
}
int drawMonth_1 = drawMonth -1;
if (drawMonth_1 < 0) {
drawMonth_1 = 11;
}
drawMonth = drawMonth_1;
rightMarg = temp;
}
viewLeftBgCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
drawScrollStaticLeftView(viewLeftBgCanvas);
}
private void drawBrokenLine(Canvas canvas, String times, float timesTop,
float scrollTimesTop, String money, float moneyTop,
float scrollMoneyTop, int rightMarg){
canvas.save();
canvas.drawCircle(rightMarg, timesTop, 6, timesBrokenLine);
canvas.drawLine(rightMarg, timesTop, rightMarg-virtical_graduate_width, scrollTimesTop, timesBrokenLine);
canvas.drawCircle(rightMarg, moneyTop, 6, moneyBrokenCircle);
canvas.drawLine(rightMarg, moneyTop, rightMarg-virtical_graduate_width, scrollMoneyTop, moneyBrokenLine);
canvas.drawText(times, rightMarg-15, timesTop, timesTextPaint);
canvas.drawText(money, rightMarg, moneyTop-5, moneyTextPaint);
}
/**
* 绘制左侧的销售金额和销售的房数
* @param canvas
*/
private void drawScrollStaticLeftView(Canvas canvas){
report_trend_abscissa_bg.setBounds(0, 0, virtical_graduate_width, scroll_height);
report_trend_abscissa_bg.draw(canvas);
float textH = GetTextWH.getTextH(leftTextPaint);
float textMoneyLeft = 4F * density;
int bottom_marge = broken_divider_height;
for (int i = 0; i < 3; ++i)
{
report_trend_abscissa_divider.setBounds(0, bottom_marge, virtical_graduate_width, bottom_marge + 2);
report_trend_abscissa_divider.draw(canvas);
canvas.drawText(limitMoneys[i], textMoneyLeft, bottom_marge + textH*3/2, leftTextPaint);
canvas.drawText(limitSalesTimes[i], textMoneyLeft, bottom_marge + textH * 5/2 + 6, leftTextPaint);
bottom_marge += broken_divider_height;
}
}
/**
* 初始化主体的位图,并绑定canvas
* 这样绘制在canvas上面图像映射到位图上面
*/
public void initViewBg(){
viewBgBitmap = Bitmap.createBitmap(view_width, layout_height, Bitmap.Config.ARGB_8888);
viewBgCanvas.setBitmap(viewBgBitmap);
}
/**
* 绘制左部位图,并绑定canvas
*/
public void initViewBottomBg(){
viewLeftBgBitmap = Bitmap.createBitmap(virtical_graduate_width, scroll_height, Bitmap.Config.ARGB_8888);
viewLeftBgCanvas.setBitmap(viewLeftBgBitmap);
}
private void obtainVelocityTracker(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
private void releaseVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(viewBgBitmap, -view_width+layout_width-virtical_graduate_right, 0, viewBgPaint);
//绘制左边的部分,由于要固定在左边,所以x设置为getScrollX(),不然移动的
canvas.drawBitmap(viewLeftBgBitmap, getScrollX(), 0, viewBgPaint);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!isNotFirst) {//只需要执行一次就可以了
initMeage();
drawScrollStaticView();
isNotFirst = true;
}
}
/**
* dip 与 px 换算
*
* @param dipValue
* @return
*/
public int dip2px(float dipValue) {
return (int) (dipValue / 1.5F * density + 0.5);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
}
super.computeScroll();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastMotionX = x;
//动画还没有结束就按下了就结束动画
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = (int) (mLastMotionX - x);
mLastMotionX = x;
if (getScrollX() < layout_width_34 && getScrollX() > -(layout_width_34+scroll_width)) {
scrollBy(deltaX, 0);
}
invalidate();
break;
case MotionEvent.ACTION_UP:
if (getScrollX() > 0 || view_width <= layout_width) {
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
}else if (getScrollX() < -scroll_width) {
mScroller.startScroll(getScrollX(), 0, -getScrollX()-scroll_width, 0, 500);
}
invalidate();
break;
}
return true;
}
public void myReleaseFling(int speed){
mScroller.fling(getScrollX(), 0, speed, 0, 0, scroll_width, 0, 0);
}
}
6.7、实现的效果:
左边显示内容,是动态变化的,根据你最大值来处理。