实现自定义android柱状图控件!可显示气温!可相应点击事件的回调!

老规矩,先上运行效果图

实现自定义android柱状图控件!可显示气温!可相应点击事件的回调!_第1张图片


这是触发点击事件的运行图

实现自定义android柱状图控件!可显示气温!可相应点击事件的回调!_第2张图片


首先给大家介绍一些思路:

首先继承view控件,然后重点是要覆写draw方法实现自定义的绘制,最后实现一个手势监听器,在onTouch事件中进行监听,得到点击坐标后一一与每一个item的左右x坐标比对,看是否落入了该空间中,然后回调监听器即可。


接下来重点介绍一下draw方法:

首先上一张图给大家介绍一下需要知道哪些距离参数

实现自定义android柱状图控件!可显示气温!可相应点击事件的回调!_第3张图片

如图所示,这个柱状图有6个item,每个item的宽度就是2所示的宽度,即代码中的mItemBarWidth;1代表了一个item中的柱状图的宽度,即代码中的mBarWidth;0所示的就是单个item中除了柱状图宽度以外到两边的距离,两边的距离是相等的;代码中设定了一个mBeginXCoord变量,表示起始绘制的新坐标,即第一个0的宽度,这是为了累加方便,因为下一个柱状图的起始x坐标就是mBeginXCoord加上一个item的宽度而已;

所以在绘制之前需要把这些参数都计算好,由calculateFontHeight()方法,完成计算,我这里默认设定的是如果用户没有设定柱状图的宽度,则默认为item的一半宽度。


翠花,上代码

package com.example.rangebarchart;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Paint.FontMetrics;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.GestureDetector.OnGestureListener;

public class RangeBarChart extends View{

	/* 用户点击到了无效位置 */
	public static final int INVALID_POSITION = -1;
	
	/* 画笔 */
	private Paint mTextPaint = null;
	private Paint mBarPaint = null;
	
	/* 柱状图数据集 */
	private RangeBarDataSet mDataSet = null;
	
	/* 绘图颜色值 */
	private int mTextColor = Color.WHITE;
	private int mBarColor = Color.WHITE;
	
	/* 圆角矩形的弧度 */
	private int mRadius = 10;
	
	/* 条形图的宽度和高度 */
	private float mBarWidth = 0;
	private float mBarHeight = 0;
	
	/* 辅助计算柱宽,表示一个条目的宽度,包括柱子和空余部分 */
	private float mItemBarWidth = 0;
	
	/* 文字大小 **/
	private float mTextSize = 25;
	
	/* 表明重新设置了文字的参数,需要重新计算这块数据 */
	private boolean setTextSizeAgain = true;
	
	/* 起始横坐标,当用户未指定时,默认为半个空白条形宽度 */
	private float mBeginXCoord = 0;
	/* Max_Threshold_Temperature摄氏度位于的竖坐标,即传递过来的数据绘制最高温度应该的起始y坐标 */
	private float mBeginYCoord = 0;
	/* 当前字体高度 */
	private int mFontHeight = 0;
	
	/* 当绘制某一个String对象时,计算的水平或者竖直上的偏移量 */
	private float mPaintVerticalTextOffset = 0;
	private float mPaintHorizontalTextOffset = 0;
	
	/*
	 *	默认最高气温和最低气温默认分别是40和-40
	 *	在传递气温数据时,重新计算最高温和最低温的阈值
	 */
	private int Max_Threshold_Temperature = 40;
	private int Min_Threshold_Temperature = -40;
	private float Span_Threshold_Temperature = Max_Threshold_Temperature - Min_Threshold_Temperature;
	/*
	 *	表示图表可显示的极值温度与数据的极值温度的差值
	 *	比如当前最高温度为30度,但是图标最高可以画到31度,其实就是变相的paddingTop或者PaddingBottom值
	 */
	private static final int Differ_Threshold_Temperature = 1;
	
	/* 画布的宽高 */
	private int mWidth = 0;
	private int mHeight = 0;
	
	/* 绘制矩形时所需的三个坐标 */
	private float left;
	private float top;
	private float bottom;
	
	private GestureDetector mGestureDetector = null;
	private OnRangeBarItemClickListener mOnRangeBarItemClickListener = null;
	
	private Handler mHandler = null;
	
	public RangeBarChart(Context context) {
		this(context, null);
	}
	
	public RangeBarChart(Context context, AttributeSet attrs) {
		super(context, attrs);
		parseAttributes(context.obtainStyledAttributes(attrs, R.styleable.RangeBarChart));
		init(context);
	}
	
	public RangeBarChart(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		parseAttributes(context.obtainStyledAttributes(attrs, R.styleable.RangeBarChart));
		init(context);
	}
	
	private void parseAttributes(TypedArray a) {
		mTextSize = a.getDimension(R.styleable.RangeBarChart_textsize, 25);
		mBarWidth = (int)a.getDimension(R.styleable.RangeBarChart_barlength, 0);
		mRadius = (int)a.getDimension(R.styleable.RangeBarChart_barradius, 10);
		
		mTextColor = mBarColor = a.getColor(R.styleable.RangeBarChart_barcolor, Color.WHITE);
		a.recycle();
	}
	
	private void init( Context mContext ){
		mHandler = new UpdateUIHandler();
		
		mTextPaint = new Paint();
		mTextPaint.setAntiAlias(true);
		mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
		mTextPaint.setColor(mTextColor);
		mTextPaint.setTextSize(mTextSize);
		mTextPaint.setStyle(Paint.Style.FILL);
		
		mBarPaint = new Paint();
		mBarPaint.setAntiAlias(true);
		mBarPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
		mBarPaint.setColor(mBarColor);
		mBarPaint.setTextSize(mTextSize);
		mBarPaint.setStyle(Paint.Style.FILL);
		
		mDataSet = new RangeBarDataSet();
		
		mGestureDetector = new GestureDetector(mContext, new RangeBarChartOnGestureListener() );
	}
	
	@Override
	public void draw(Canvas canvas) {
		super.draw(canvas);
		
		/** 如果没有数据,则停止绘图 **/
		if( mDataSet.getSize() == 0 ){
			return;
		}
		
		if( setTextSizeAgain ){
			calculateFontHeight();
			setTextSizeAgain = !setTextSizeAgain;
		}
		
		/** 绘制表示第一天的矩形 **/
		left = mBeginXCoord;
		if( mDataSet.getIndexTopTemperature(0) < Max_Threshold_Temperature ){
			top = mBeginYCoord + ( Max_Threshold_Temperature - mDataSet.getIndexTopTemperature(0)) * mBarHeight  / Span_Threshold_Temperature;
		} else {
			top = mBeginYCoord;
		}
		if( mDataSet.getIndexLowTemperature(0) > Min_Threshold_Temperature ){
			bottom = mBeginYCoord + ( Max_Threshold_Temperature - mDataSet.getIndexLowTemperature(0)) * mBarHeight  / Span_Threshold_Temperature;
		} else {
			bottom = mBeginYCoord + mBarHeight;
		}
		RectF rectf = new RectF( left , top, left + mBarWidth , bottom);
		canvas.drawRoundRect(rectf, mRadius, mRadius, mBarPaint);
		mPaintHorizontalTextOffset = ( mBarWidth - mTextPaint.measureText(getText(true, 0)) ) / 2;
		mPaintVerticalTextOffset = ( mTextPaint.descent() - mTextPaint.ascent() ) / 2 - mTextPaint.descent();
		canvas.drawText( getText(true, 0), left + mPaintHorizontalTextOffset , top - mPaintVerticalTextOffset, mTextPaint);
		mPaintHorizontalTextOffset =  ( mBarWidth - mTextPaint.measureText(getText(false, 0)) ) / 2;
		canvas.drawText( getText(false, 0), left + mPaintHorizontalTextOffset, bottom + mFontHeight, mTextPaint);
		
		/** 绘制表示后面几天的矩形 **/
		for( int i = 1; i < mDataSet.getSize(); i++ ){
			left = mBeginXCoord + i * mItemBarWidth;
			
			if( mDataSet.getIndexTopTemperature(i) < Max_Threshold_Temperature ){
				top = mBeginYCoord + ( Max_Threshold_Temperature - mDataSet.getIndexTopTemperature(i)) * mBarHeight  / Span_Threshold_Temperature;
			} else {
				top = mBeginYCoord;
			}
			if( mDataSet.getIndexLowTemperature(i) > Min_Threshold_Temperature ){
				bottom = mBeginYCoord + ( Max_Threshold_Temperature - mDataSet.getIndexLowTemperature(i)) * mBarHeight  / Span_Threshold_Temperature;
			} else {
				bottom = mBeginYCoord + mBarHeight;
			};
			rectf=new RectF(  left, top, left + mBarWidth , bottom);
			canvas.drawRoundRect(rectf, mRadius, mRadius, mBarPaint);
			mPaintHorizontalTextOffset = ( mBarWidth - mTextPaint.measureText(getText(true, i)) ) / 2;
			canvas.drawText( getText(true, i), left + mPaintHorizontalTextOffset, top - mPaintVerticalTextOffset, mTextPaint);
			mPaintHorizontalTextOffset = ( mBarWidth - mTextPaint.measureText(getText(false, i)) ) / 2;
			canvas.drawText( getText(false, i), left + mPaintHorizontalTextOffset, bottom + mFontHeight, mTextPaint);
		}
	}
	
	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		mWidth = w;
		mHeight = h;
		setTextSizeAgain = true;
		super.onSizeChanged(w, h, oldw, oldh);
	}
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		if( mGestureDetector != null ){
			return mGestureDetector.onTouchEvent(event);
		}
		return true;
	}
	
	/**
	 *	重新计算坐标及参数
	 *	这里计算的条件是如果用户设置了柱宽,则使用用户设置的参数;
	 *	如果用户未设置,则采用等分控件宽度的方式
	 */
	private void calculateFontHeight(){
		
		mItemBarWidth = mWidth / mDataSet.getSize();
		if( mBarWidth <= 0 ){
			mBarWidth = mItemBarWidth / 2;
		}
		
		mBeginXCoord = ( mItemBarWidth - mBarWidth ) / 2;
		
		mTextPaint.setTextSize(mTextSize);
		
		FontMetrics fm = mTextPaint.getFontMetrics();
		mFontHeight = (int) Math.ceil(fm.descent - fm.ascent);
		
		mBarHeight = mHeight - mFontHeight * 2;
		mBeginYCoord = mFontHeight;
	}
	
	/**
	 *	取气温数据,并加上符号
	 * @param max	是否取高温数据
	 * @param index	待取数据的索引值
	 * @return
	 */
	private String getText( boolean max, int index ){
		if( max ){
			return mDataSet.getIndexTopTemperature(index)+ "°";
		} else {
			return mDataSet.getIndexLowTemperature(index)+ "°";
		}
	}
	
	/**
	 *	给柱状图控件填充数据并立即刷新UI
	 */
	public void setData( RangeBarDataSet mTemperatureData ){
		this.mDataSet = mTemperatureData;
		resetThresholdTemperature(mTemperatureData);
		updateUI();
	}
	
	/**
	 *	重置当前气温的最高温和最低温的阈值,及两者的差值
	 * @param mTemperatureData
	 */
	public void resetThresholdTemperature( RangeBarDataSet mTemperatureData ){
		
		int max = -100;
		int min = 100;
		
		for( int i = 0; i < mTemperatureData.getSize(); i++ ){
			/** 高温中的最高温 **/
			if( mTemperatureData.getIndexTopTemperature(i) > max ){
				max = mTemperatureData.getIndexTopTemperature(i);
			}
			/** 低温中的最低温 **/
			if( mTemperatureData.getIndexLowTemperature(i) < min ){
				min = mTemperatureData.getIndexLowTemperature(i);
			}
		}
		
		Max_Threshold_Temperature = max + Differ_Threshold_Temperature;
		Min_Threshold_Temperature = min - Differ_Threshold_Temperature;
		Span_Threshold_Temperature = Max_Threshold_Temperature - Min_Threshold_Temperature;
	}
	
	private void updateUI(){
		invalidate();
	}
	
	/**
	 *	手势监听器
	 * @author A Shuai
	 *
	 */
	private class RangeBarChartOnGestureListener implements OnGestureListener{

		@Override
		public boolean onDown(MotionEvent e) {
			return true;
		}

		@Override
		public void onShowPress(MotionEvent e) {  }

		@Override
		public boolean onSingleTapUp(MotionEvent e) {
			int position = identifyWhickItemClick(e.getX(), e.getY());
			if( position != INVALID_POSITION && mOnRangeBarItemClickListener != null ){
				mOnRangeBarItemClickListener.onItemClick(position);
			}
			return true;
		}

		@Override
		public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
			return false;
		}

		@Override
		public void onLongPress(MotionEvent e) {  }

		@Override
		public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
			return false;
		}
		
	}
	
	/**
	 *	根据点击的手势位置识别是第几个柱图被点击,这里并没有处理触摸点是否在柱状图上,只是简单的判断了一下触电是否在一个item的矩形内
	 * @param x
	 * @param y
	 * @return -1时表示点击的是无效位置
	 */
	private int identifyWhickItemClick( float x, float y ){
		float leftx = 0;
		float rightx = 0;
		for( int i = 0; i < mDataSet.getSize(); i++ ){
			leftx = i * mItemBarWidth;
			rightx = (i + 1) * mItemBarWidth;
			if( x < leftx ){
				break;
			}
			if( leftx <= x && x <= rightx ){
				return i;
			}
		}
		return INVALID_POSITION;
	}
	
	public OnRangeBarItemClickListener getOnRangeBarItemClickListener() {
		return mOnRangeBarItemClickListener;
	}

	public void setOnRangeBarItemClickListener(OnRangeBarItemClickListener mOnRangeBarItemClickListener) {
		this.mOnRangeBarItemClickListener = mOnRangeBarItemClickListener;
	}
	
	@SuppressLint("HandlerLeak")
	private class UpdateUIHandler extends Handler{

		@Override
		public void handleMessage(Message msg) {
			super.handleMessage(msg);
			invalidate();
		}
		
	}
	
}

这个控件思路还是相当简单,推荐大家看懂以后自己动手写下,这样更有助于理解,下一次我给大家更一篇完美的可拖动GridView视图控件(带拖动动画,删除动画等),主要也是为了帮助新手朋友少走弯路,更快的学好android。


好了,有疑问的朋友请在下面留言。


工程下载请点这里

你可能感兴趣的:(自定义视图控件,气温趋势,柱状图,自定义控件,完美可用,android)