智能电视TV开发---曲线图绘制

 绘制曲线图有几个基本的元素:坐标轴,点,线,分清楚变化的和不变得,从而进行绘制,其实在智能电视TV开发---拍照+水印   中在拍照的图片上面绘制水印都是一样的原理,就是以一张图片为基准,取得srcBitmap的canvas,然后再使用canvas来把其他的内容绘制上去。网上也有几个开源的绘制图表的项目,但是具体到自己项目里面都不太适用。接下来我会用一个二手房房价行情的数据来绘制6个月内容的趋势图,水平绘制,宽比高长,左右可以拖动,适用于嵌入页面的,而不是占据整个屏幕。分部截图来各个部分的实现关键内容,最后贴出代码。

      一、背景以及坐标轴的制作

      先看一下效果图:

智能电视TV开发---曲线图绘制智能电视TV开发---曲线图绘制

从上图中水平轴是月份,竖直轴包含两部分分别是销售金额以及销售的户数,对比这两幅图片,你可以看到左边部分是不动的,而底部的月份是可以滑动的,使用的是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、实现的效果:

智能电视TV开发---曲线图绘制智能电视TV开发---曲线图绘制

智能电视TV开发---曲线图绘制

智能电视TV开发---曲线图绘制


左边显示内容,是动态变化的,根据你最大值来处理。









智能电视TV开发---曲线图绘制

上一篇:真心不想再神戳戳的用Guava写FP了


下一篇:小白dp uva 10154 - Weights and Measures (贪心+dp )