自定義屬性新建資源文件attrs
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ZhiMaScore">
<attr name="max_score" format="integer"/>
<attr name="min_score" format="integer"/>
<attr name="broken_line_color" format="color"/>
</declare-styleable>
</resources>
自定義view
package com.hsg.zhimascoredemo;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Joe on 2016/12/27.
*/
public class ZhiMaScoreView extends View {
private float viewWith;
private float viewHeight;
private float brokenLineWith = 0.5f;
private int brokenLineColor = 0xff02bbb7;
private int straightLineColor = 0xffe2e2e2;//0xffeaeaea
private int textNormalColor = 0xff7e7e7e;
private int maxScore = 700;
private int minScore = 650;
private int monthCount = 6;
private int selectMonth = 6;//選中的月份
private String[] monthText = new String[]{"6月", "7月", "8月", "9月", "10月", "11月"};
private int[] score = new int[]{660, 663, 669, 678, 682, 689};
private List<Point> scorePoints;
private int textSize = dipToPx(15);
private Paint brokenPaint;
private Paint straightPaint;
private Paint dottedPaint;
private Paint textPaint;
private Path brokenPath;
public ZhiMaScoreView(Context context)
{
super(context);
initConfig(context,null);
init();
}
public ZhiMaScoreView(Context context, AttributeSet attrs)
{
super(context, attrs);
initConfig(context,attrs);
init();
}
public ZhiMaScoreView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
initConfig(context,attrs);
init();
}
/**
* 初始化佈局配置
*
* @param context
* @param attrs
*/
private void initConfig(Context context, AttributeSet attrs)
{
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ZhiMaScore);
maxScore=a.getInt(R.styleable.ZhiMaScore_max_score,700);
minScore=a.getInt(R.styleable.ZhiMaScore_min_score,650);
brokenLineColor=a.getColor(R.styleable.ZhiMaScore_broken_line_color,brokenLineColor);
a.recycle();
}
private void init()
{
brokenPath = new Path();
brokenPaint = new Paint();
brokenPaint.setAntiAlias(true);
brokenPaint.setStyle(Paint.Style.STROKE);
brokenPaint.setStrokeWidth(dipToPx(brokenLineWith));
brokenPaint.setStrokeCap(Paint.Cap.ROUND);
straightPaint = new Paint();
straightPaint.setAntiAlias(true);
straightPaint.setStyle(Paint.Style.STROKE);
straightPaint.setStrokeWidth(brokenLineWith);
straightPaint.setColor((straightLineColor));
straightPaint.setStrokeCap(Paint.Cap.ROUND);
dottedPaint = new Paint();
dottedPaint.setAntiAlias(true);
dottedPaint.setStyle(Paint.Style.STROKE);
dottedPaint.setStrokeWidth(brokenLineWith);
dottedPaint.setColor((straightLineColor));
dottedPaint.setStrokeCap(Paint.Cap.ROUND);
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setColor((textNormalColor));
textPaint.setTextSize(dipToPx(15));
}
private void initData()
{
scorePoints = new ArrayList<>();
float maxScoreYCoordinate = viewHeight * 0.15f;
float minScoreYCoordinate = viewHeight * 0.4f;
Log.v("ScoreTrend", "initData: " + maxScoreYCoordinate);
float newWith = viewWith - (viewWith * 0.15f) * 2;//分隔線距離最左邊和最右邊的距離是0.15倍的viewWith
int coordinateX;
for(int i = 0; i < score.length; i++)
{
Log.v("ScoreTrend", "initData: " + score[i]);
Point point = new Point();
coordinateX = (int) (newWith * ((float) (i) / (monthCount - 1)) + (viewWith * 0.15f));
point.x = coordinateX;
if(score[i] > maxScore)
{
score[i] = maxScore;
}
else if(score[i] < minScore)
{
score[i] = minScore;
}
point.y = (int) (((float) (maxScore - score[i]) / (maxScore - minScore)) * (minScoreYCoordinate - maxScoreYCoordinate) + maxScoreYCoordinate);
scorePoints.add(point);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
viewWith = w;
viewHeight = h;
initData();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
drawDottedLine(canvas, viewWith * 0.15f, viewHeight * 0.15f, viewWith, viewHeight * 0.15f);
drawDottedLine(canvas, viewWith * 0.15f, viewHeight * 0.4f, viewWith, viewHeight * 0.4f);
drawText(canvas);
drawMonthLine(canvas);
drawBrokenLine(canvas);
drawPoint(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
this.getParent().requestDisallowInterceptTouchEvent(true);//一旦底層View收到touch的action後調用這個方法那麼父層View就不會再調用onInterceptTouchEvent了,也無法截獲以後的action
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
onActionUpEvent(event);
this.getParent().requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_CANCEL:
this.getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return true;
}
private void onActionUpEvent(MotionEvent event)
{
boolean isValidTouch = validateTouch(event.getX(), event.getY());
if(isValidTouch)
{
invalidate();
}
}
//是否是有效的觸摸範圍
private boolean validateTouch(float x, float y)
{
//曲線觸摸區域
for(int i = 0; i < scorePoints.size(); i++)
{
// dipToPx(8)乘以2爲了適當增大觸摸面積
if(x > (scorePoints.get(i).x - dipToPx(8) * 2) && x < (scorePoints.get(i).x + dipToPx(8) * 2))
{
if(y > (scorePoints.get(i).y - dipToPx(8) * 2) && y < (scorePoints.get(i).y + dipToPx(8) * 2))
{
selectMonth = i + 1;
return true;
}
}
}
//月份觸摸區域
//計算每個月份X座標的中心點
float monthTouchY = viewHeight * 0.7f - dipToPx(3);//減去dipToPx(3)增大觸摸面積
float newWith = viewWith - (viewWith * 0.15f) * 2;//分隔線距離最左邊和最右邊的距離是0.15倍的viewWith
float validTouchX[] = new float[monthText.length];
for(int i = 0; i < monthText.length; i++)
{
validTouchX[i] = newWith * ((float) (i) / (monthCount - 1)) + (viewWith * 0.15f);
}
if(y > monthTouchY)
{
for(int i = 0; i < validTouchX.length; i++)
{
Log.v("ScoreTrend", "validateTouch: validTouchX:" + validTouchX[i]);
if(x < validTouchX[i] + dipToPx(8) && x > validTouchX[i] - dipToPx(8))
{
Log.v("ScoreTrend", "validateTouch: " + (i + 1));
selectMonth = i + 1;
return true;
}
}
}
return false;
}
//繪製折線穿過的點
protected void drawPoint(Canvas canvas)
{
if(scorePoints == null)
{
return;
}
brokenPaint.setStrokeWidth(dipToPx(1));
for(int i = 0; i < scorePoints.size(); i++)
{
brokenPaint.setColor(brokenLineColor);
brokenPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(scorePoints.get(i).x, scorePoints.get(i).y, dipToPx(3), brokenPaint);
brokenPaint.setColor(Color.WHITE);
brokenPaint.setStyle(Paint.Style.FILL);
if(i == selectMonth - 1)
{
brokenPaint.setColor(0xffd0f3f2);
canvas.drawCircle(scorePoints.get(i).x, scorePoints.get(i).y, dipToPx(8f), brokenPaint);
brokenPaint.setColor(0xff81dddb);
canvas.drawCircle(scorePoints.get(i).x, scorePoints.get(i).y, dipToPx(5f), brokenPaint);
//繪製浮動文本背景框
drawFloatTextBackground(canvas, scorePoints.get(i).x, scorePoints.get(i).y - dipToPx(8f));
textPaint.setColor(0xffffffff);
//繪製浮動文字
canvas.drawText(String.valueOf(score[i]), scorePoints.get(i).x, scorePoints.get(i).y - dipToPx(5f) - textSize, textPaint);
}
brokenPaint.setColor(0xffffffff);
canvas.drawCircle(scorePoints.get(i).x, scorePoints.get(i).y, dipToPx(1.5f), brokenPaint);
brokenPaint.setStyle(Paint.Style.STROKE);
brokenPaint.setColor(brokenLineColor);
canvas.drawCircle(scorePoints.get(i).x, scorePoints.get(i).y, dipToPx(2.5f), brokenPaint);
}
}
//繪製月份的直線(包括刻度)
private void drawMonthLine(Canvas canvas)
{
straightPaint.setStrokeWidth(dipToPx(1));
canvas.drawLine(0, viewHeight * 0.7f, viewWith, viewHeight * 0.7f, straightPaint);
float newWith = viewWith - (viewWith * 0.15f) * 2;//分隔線距離最左邊和最右邊的距離是0.15倍的viewWith
float coordinateX;//分隔線X座標
for(int i = 0; i < monthCount; i++)
{
coordinateX = newWith * ((float) (i) / (monthCount - 1)) + (viewWith * 0.15f);
canvas.drawLine(coordinateX, viewHeight * 0.7f, coordinateX, viewHeight * 0.7f + dipToPx(4), straightPaint);
}
}
//繪製折線
private void drawBrokenLine(Canvas canvas)
{
brokenPath.reset();
brokenPaint.setColor(brokenLineColor);
brokenPaint.setStyle(Paint.Style.STROKE);
if(score.length == 0)
{
return;
}
Log.v("ScoreTrend", "drawBrokenLine: " + scorePoints.get(0));
brokenPath.moveTo(scorePoints.get(0).x, scorePoints.get(0).y);
for(int i = 0; i < scorePoints.size(); i++)
{
brokenPath.lineTo(scorePoints.get(i).x, scorePoints.get(i).y);
}
canvas.drawPath(brokenPath, brokenPaint);
}
//繪製文本
private void drawText(Canvas canvas)
{
textPaint.setTextSize(dipToPx(12));
textPaint.setColor(textNormalColor);
canvas.drawText(String.valueOf(maxScore), viewWith * 0.1f - dipToPx(10), viewHeight * 0.15f + textSize * 0.25f, textPaint);
canvas.drawText(String.valueOf(minScore), viewWith * 0.1f - dipToPx(10), viewHeight * 0.4f + textSize * 0.25f, textPaint);
textPaint.setColor(0xff7c7c7c);
float newWith = viewWith - (viewWith * 0.15f) * 2;//分隔線距離最左邊和最右邊的距離是0.15倍的viewWith
float coordinateX;//分隔線X座標
textPaint.setTextSize(dipToPx(12));
textPaint.setStyle(Paint.Style.FILL);
textPaint.setColor(textNormalColor);
textSize = (int) textPaint.getTextSize();
for(int i = 0; i < monthText.length; i++)
{
coordinateX = newWith * ((float) (i) / (monthCount - 1)) + (viewWith * 0.15f);
if(i == selectMonth - 1)
{
textPaint.setStyle(Paint.Style.STROKE);
textPaint.setColor(brokenLineColor);
RectF r2 = new RectF();
r2.left = coordinateX - textSize - dipToPx(4);
r2.top = viewHeight * 0.7f + dipToPx(4) + textSize / 2;
r2.right = coordinateX + textSize + dipToPx(4);
r2.bottom = viewHeight * 0.7f + dipToPx(4) + textSize + dipToPx(8);
canvas.drawRoundRect(r2, 10, 10, textPaint);
}
//繪製月份
canvas.drawText(monthText[i], coordinateX, viewHeight * 0.7f + dipToPx(4) + textSize + dipToPx(5), textPaint);
textPaint.setColor(textNormalColor);
}
}
//繪製顯示浮動文字的背景
private void drawFloatTextBackground(Canvas canvas, int x, int y)
{
brokenPath.reset();
brokenPaint.setColor(brokenLineColor);
brokenPaint.setStyle(Paint.Style.FILL);
//P1
Point point = new Point(x, y);
brokenPath.moveTo(point.x, point.y);
//P2
point.x = point.x + dipToPx(5);
point.y = point.y - dipToPx(5);
brokenPath.lineTo(point.x, point.y);
//P3
point.x = point.x + dipToPx(12);
brokenPath.lineTo(point.x, point.y);
//P4
point.y = point.y - dipToPx(17);
brokenPath.lineTo(point.x, point.y);
//P5
point.x = point.x - dipToPx(34);
brokenPath.lineTo(point.x, point.y);
//P6
point.y = point.y + dipToPx(17);
brokenPath.lineTo(point.x, point.y);
//P7
point.x = point.x + dipToPx(12);
brokenPath.lineTo(point.x, point.y);
//最後一個點連接到第一個點
brokenPath.lineTo(x, y);
canvas.drawPath(brokenPath, brokenPaint);
}
/**
* 畫虛線
*
* @param canvas 畫布
* @param startX 起始點X座標
* @param startY 起始點Y座標
* @param stopX 終點X座標
* @param stopY 終點Y座標
*/
private void drawDottedLine(Canvas canvas, float startX, float startY, float stopX, float stopY)
{
dottedPaint.setPathEffect(new DashPathEffect(new float[]{20, 10}, 4));
dottedPaint.setStrokeWidth(1);
// 實例化路徑
Path mPath = new Path();
mPath.reset();
// 定義路徑的起點
mPath.moveTo(startX, startY);
mPath.lineTo(stopX, stopY);
canvas.drawPath(mPath, dottedPaint);
}
public int[] getScore()
{
return score;
}
public void setScore(int[] score)
{
this.score = score;
initData();
}
public void setMaxScore(int maxScore)
{
this.maxScore = maxScore;
}
public void setMinScore(int minScore)
{
this.minScore = minScore;
}
/**
* dip 轉換成px
*
* @param dip
* @return
*/
private int dipToPx(float dip)
{
float density = getContext().getResources().getDisplayMetrics().density;
return (int) (dip * density + 0.5f * (dip >= 0 ? 1 : -1));
}
}
佈局裏面就可以用了
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.hsg.zhimascoredemo.MainActivity">
<com.hsg.zhimascoredemo.ZhiMaScoreView
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="#ffffff"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
效果圖