代码
import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import java.util.ArrayList; import java.util.List; import androidx.annotation.ColorRes; import androidx.annotation.Nullable; /** * 心率图 */ public class HeartRateView extends View { private int mDefaultWith = 800; //最小宽度 private int mDefaultHeight = 300;//最小高度 private int mWidth; private int mHeight; private Paint mLinePaint;//线条画笔 private float mLineWidth;//线的粗细 private int mLineColor;//线的颜色 private Paint mFillPaint;//下边填充画笔 private Paint mTextPaint;//文本画笔 private int mTextColor;//文本颜色 private int mTextSize;//字体大小 private int mMaxYNum;//Y轴最大值 private int mMaxXNum;//X轴最大值 分钟 private int mTextPadding;//文本与线的距离 private int mItemYNum;//竖向几个 private int mItemXNum;//横向几个 private long mStartTime = -1; private List<HeartRateView.HeartData> dataList; // private int mCacheVolume = 500;//缓存数据量 Paint.FontMetrics mTextPaintFm; private Bitmap mWaveLineBitmap;//折线绘制图片 private int mOffsetTime;//向右偏移2s private Canvas mCanvasWaveLine; //图片绘制工具 private Path mWaveLinePath;//折线路径 private Rect mWaveLineBitmapSrc; private RectF mWaveLineBitmapDst; private float mWaveLineBitmapWidth; private int mWaveLineBitmapHeight; private Path mFillPath;//填充路径 private List mRemoveList = new ArrayList(); private float mScreenPercentage = 0.6f;//取值0.1f~1f 占屏比 public HeartRateView(Context context) { this(context, null); } public HeartRateView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public HeartRateView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public HeartRateView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs); } public <T extends HeartRateView.HeartData> void setDataList(List<T> dataList) { this.dataList = (List<HeartData>) dataList; } public <T extends HeartRateView.HeartData> void addData(T data) { if (dataList == null) { throw new NullPointerException(); } dataList.add(data); } //初始化 private void init(Context context, AttributeSet attrs) { TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.HeartRateView); mLineWidth = array.getDimensionPixelSize(R.styleable.HeartRateView_hrv_line_width, 3); mLineColor = array.getColor(R.styleable.HeartRateView_hrv_line_color, Color.GREEN); mTextColor = array.getColor(R.styleable.HeartRateView_hrv_text_color, Color.WHITE); mTextSize = array.getDimensionPixelSize(R.styleable.HeartRateView_hrv_text_size, 20); mMaxYNum = array.getInt(R.styleable.HeartRateView_hrv_max_y_size, 200); mMaxXNum = array.getInt(R.styleable.HeartRateView_hrv_max_x_size, 60); mTextPadding = array.getDimensionPixelSize(R.styleable.HeartRateView_hrv_text_padding, 10); mItemYNum = array.getInt(R.styleable.HeartRateView_hrv_item_y_num, 5); mItemXNum = array.getInt(R.styleable.HeartRateView_hrv_item_x_num, 12); mOffsetTime = array.getInt(R.styleable.HeartRateView_hrv_offset_time, 2); mScreenPercentage = array.getFloat(R.styleable.HeartRateView_hrv_screen_percentage, 0.6f); if (mScreenPercentage > 1f || mScreenPercentage <= 0f) { mScreenPercentage = 1f; } array.recycle(); //线 mLinePaint = new Paint(); mLinePaint.setColor(mLineColor); mLinePaint.setAntiAlias(true); mLinePaint.setStrokeWidth(mLineWidth); mLinePaint.setStyle(Paint.Style.STROKE); mLinePaint.setStrokeCap(Paint.Cap.ROUND); mLinePaint.setStrokeJoin(Paint.Join.ROUND); mLinePaint.setPathEffect(new CornerPathEffect(10)); //填充 mFillPaint = new Paint(); mFillPaint.setColor(mLineColor); mFillPaint.setAlpha(30); mFillPaint.setStrokeWidth(mLineWidth); mFillPaint.setAntiAlias(true); mFillPaint.setStyle(Paint.Style.FILL); //文本 mTextPaint = new Paint(); mTextPaint.setColor(mTextColor); mTextPaint.setTextSize(mTextSize); mTextPaint.setStrokeWidth(0.5f); mTextPaint.setAntiAlias(true); mTextPaint.setTextAlign(Paint.Align.LEFT); mTextPaint.setStyle(Paint.Style.FILL); // mStartTime = System.currentTimeMillis();//开始时间 mTextPaintFm = mTextPaint.getFontMetrics(); mWaveLinePath = new Path(); mWaveLineBitmapSrc = new Rect(); mWaveLineBitmapDst = new RectF(); mFillPath = new Path(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); float YTextMaxWith = mTextPaint.measureText(mMaxYNum + ""); float XTextMaxWith = mTextPaint.measureText(mMaxXNum + ""); float mLeft = YTextMaxWith + getPaddingLeft() + mTextPadding;//折线绘制左坐标 float mRight = (XTextMaxWith / 2f) + mWidth - getPaddingRight();//折线绘制右坐标 float mTop = getPaddingTop();//折线绘制上坐标 float mBottom = mHeight - XTextMaxWith - getPaddingBottom() - mTextPadding;//折线绘制上坐标 float itemHeight = (mBottom - mTop - 10) / mItemYNum; //每个item像素高度 //绘制横线和文本 for (int i = 0; i < mItemYNum + 1; i++) { //绘制线和文本 float mYLineY = mBottom - (i * itemHeight);//线的位置 int num = mMaxYNum / mItemYNum * i; drawYLineText(canvas, mLeft, mRight, mYLineY, num); } if (mStartTime != -1) { float XItemWith = (mRight - mLeft) / mItemXNum;//横向间隔像素 int mTimeInterval = mMaxXNum / mItemXNum; //横向时间间隔 long nowTime = System.currentTimeMillis(); double times = (nowTime - mStartTime) / 1000d / mTimeInterval;//需要绘制的个数 //绘制X动态文本 for (long i = 0; i < times; i++) { // float mStatX = mRight - (mRight - mLeft) * (nowTime - mStartTime) / (mMaxXNum * 1000f); float mStatX = mRight - ((mRight - mLeft) * (1f - mScreenPercentage)) - (mRight - mLeft) * (nowTime - mStartTime) / (mMaxXNum * 1000f); float TextX = mStatX + i * XItemWith; if (TextX > mLeft + mTextPadding) { drawXText(canvas, TextX, mBottom, i * mTimeInterval); } } //绘制折线 drawWaveLine(canvas, mLeft, mTop, mRight, mBottom, itemHeight, nowTime); postInvalidateDelayed(20); } } /** * 绘制数据折线 * * @param canvas */ private void drawWaveLine(Canvas canvas, float mLeft, float mTop, float mRight, float mBottom, float itemHeight, long nowTime) { if (dataList != null && dataList.size() > 0) { mRemoveList.clear(); float onePixXTime = mMaxXNum * 1000f / (mRight - mLeft);//一个像素代表的值 float onePixYValue = (mMaxYNum * 1000f / mItemYNum) / itemHeight; mWaveLinePath.reset(); mFillPath.reset(); mCanvasWaveLine.save(); mCanvasWaveLine.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); int minIndex = -1; //绘制波浪线 float mPreX = 0; float mPreY = 0; for (int index = 0; index < dataList.size(); index++) { HeartRateView.HeartData item = dataList.get(index); if (item.isValid()) { float IndexX = mWaveLineBitmapWidth - ((nowTime - mStartTime) - (item.getDataTime() - mStartTime)) / onePixXTime; float IndexY = mWaveLineBitmapHeight - item.getDataNum() * 1000f / onePixYValue; // mCanvasWaveLine.drawText(item.getDataNum()+"",IndexX,IndexY-10,mTextPaint); if (IndexX < 0) { mRemoveList.add(item); minIndex = -1; continue; } if (minIndex == -1) { minIndex = index; //移动到这里 mFillPath.moveTo(IndexX, mWaveLineBitmapHeight); mWaveLinePath.moveTo(IndexX, IndexY); mFillPath.lineTo(IndexX, IndexY); mPreX = IndexX; mPreY = IndexY; } else { //链接到这里 // mWaveLinePath.quadTo(mPreX, mPreY, IndexX, IndexY); mWaveLinePath.lineTo(IndexX, IndexY); mFillPath.lineTo(IndexX, IndexY); mPreX = IndexX; mPreY = IndexY; } } else { if (minIndex != -1) { minIndex = -1; mFillPath.lineTo(mPreX, mWaveLineBitmapHeight); mFillPath.close(); } } } mCanvasWaveLine.drawPath(mWaveLinePath, mLinePaint); mFillPath.lineTo(mWaveLineBitmapWidth, mWaveLineBitmapHeight); mFillPath.close(); mCanvasWaveLine.drawPath(mFillPath, mFillPaint); int bitmapLeft = (int) (mWaveLineBitmapWidth / 2 - (mRight - mLeft) / 2); int bitmapRight = (int) (bitmapLeft + (mRight - mLeft)); int bitmapBottom = (int) mWaveLineBitmapHeight; int bitmapTop = (int) (bitmapBottom - (mBottom - mTop)); // mWaveLineBitmapSrc.set(bitmapLeft, bitmapTop, bitmapRight, bitmapBottom);//图片 mWaveLineBitmapSrc.set((int) (bitmapLeft + (bitmapRight - bitmapLeft) * (1f - mScreenPercentage)), bitmapTop, bitmapRight, bitmapBottom);//图片 // mWaveLineBitmapDst.set(mLeft, mTop, mRight, mBottom);//位置 mWaveLineBitmapDst.set(mLeft, mTop, (mRight - (mRight - mLeft) * (1f - mScreenPercentage)), mBottom);//位置 canvas.drawBitmap(mWaveLineBitmap, mWaveLineBitmapSrc, mWaveLineBitmapDst, mLinePaint); mCanvasWaveLine.restore(); dataList.removeAll(mRemoveList); } } /** * 绘制 X 文本 * * @param canvas * @param TextX 文本中心x坐标 * @param mBottom 最底部横线Y高度 * @param num 绘制文本 */ private void drawXText(Canvas canvas, float TextX, float mBottom, long num) { float textLeft = TextX - mTextPaint.measureText(num + ""); float baseLineY = mBottom + mTextPadding - mTextPaintFm.top; canvas.drawText(num + "", textLeft, baseLineY, mTextPaint); } /** * 绘制Y * * @param canvas * @param mLeft 线条最左侧 * @param mRight 线条最右侧 * @param mYLineY 线条高度 * @param num 文本内容 */ private void drawYLineText(Canvas canvas, float mLeft, float mRight, float mYLineY, int num) { canvas.drawLine(mLeft, mYLineY, mRight, mYLineY, mTextPaint); String text = num + ""; float baseLineX = mLeft - mTextPadding - mTextPaint.measureText(text); float baseLineY = mYLineY + (mTextPaintFm.bottom - mTextPaintFm.top) / 2 - mTextPaintFm.bottom; canvas.drawText(text, baseLineX, baseLineY, mTextPaint); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); mWidth = getWidth(); mHeight = getHeight(); RemeasureBitMap(); } /** * 重新测量图片大小 */ private void RemeasureBitMap() { float XTextMaxWith = mTextPaint.measureText(mMaxXNum + ""); float YTextMaxWith = mTextPaint.measureText(mMaxYNum + ""); float mLeft = getPaddingLeft() + YTextMaxWith + mTextPadding;//折线绘制左坐标 float mRight = mWidth - getPaddingRight() + (XTextMaxWith / 2);//折线绘制右坐标 float offsetX = mOffsetTime * (mRight - mLeft) / 60;//偏移两秒 mWaveLineBitmap = Bitmap.createBitmap((int) ((mRight - mLeft) + (2 * offsetX)), mHeight, Bitmap.Config.ARGB_8888); mWaveLineBitmapWidth = mWaveLineBitmap.getWidth(); mWaveLineBitmapHeight = mWaveLineBitmap.getHeight(); mCanvasWaveLine = new Canvas(mWaveLineBitmap); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST) { widthSize = mDefaultWith; } if (heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST) { heightSize = mDefaultHeight; } setMeasuredDimension(widthSize, heightSize); } public static abstract class HeartData { private long dataTime; public abstract boolean isValid();//有效数据 public abstract int getDataNum();//获取数量 public long getDataTime() { return dataTime; } public void setDataTime(long dataTime) { this.dataTime = dataTime; } } public void startMeasure() { mStartTime = System.currentTimeMillis();//开始时间 invalidate(); } public void stopMeasure() { mStartTime = -1;//开始时间 invalidate(); } public void setLineWidth(float mLineWidth) { this.mLineWidth = mLineWidth; } public void setLineColor(@ColorRes int mLineColor) { this.mLineColor = mLineColor; } public void setTextColor(@ColorRes int mTextColor) { this.mTextColor = mTextColor; } public void setTextSize(int mTextSize) { this.mTextSize = mTextSize; requestLayout(); } public void setMaxYNum(int mMaxYNum) { this.mMaxYNum = mMaxYNum; } public void setMaxXNum(int mMaxXNum) { this.mMaxXNum = mMaxXNum; } public void setTextPadding(int mTextPadding) { this.mTextPadding = mTextPadding; requestLayout(); } public void setItemYNum(int mItemYNum) { this.mItemYNum = mItemYNum; } public void setItemXNum(int mItemXNum) { this.mItemXNum = mItemXNum; } public void setOffsetTime(int mOffsetTime) { this.mOffsetTime = mOffsetTime; requestLayout(); } public void setScreenPercentage(float mScreenPercentage) { if (mScreenPercentage > 1f || mScreenPercentage <= 0f) { mScreenPercentage = 1f; } this.mScreenPercentage = mScreenPercentage; } }
资源
<declare-styleable name="HeartRateView"> <attr name="hrv_line_width" format="dimension" /> <attr name="hrv_line_color" format="color" /> <attr name="hrv_text_color" format="color" /> <attr name="hrv_text_size" format="dimension" /> <attr name="hrv_max_y_size" format="integer" /> <attr name="hrv_max_x_size" format="integer" /> <attr name="hrv_text_padding" format="dimension" /> <attr name="hrv_item_y_num" format="integer" /> <attr name="hrv_item_x_num" format="integer" /> <attr name="hrv_offset_time" format="integer" /> <attr name="hrv_screen_percentage" format="float" /> </declare-styleable>