老规矩,先上运行效果图
这是触发点击事件的运行图
首先给大家介绍一些思路:
首先继承view控件,然后重点是要覆写draw方法实现自定义的绘制,最后实现一个手势监听器,在onTouch事件中进行监听,得到点击坐标后一一与每一个item的左右x坐标比对,看是否落入了该空间中,然后回调监听器即可。
接下来重点介绍一下draw方法:
首先上一张图给大家介绍一下需要知道哪些距离参数
如图所示,这个柱状图有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();
}
}
}
好了,有疑问的朋友请在下面留言。
工程下载请点这里