其實(shí)雷達(dá)圖這個(gè)view嘛,繪制起來真的不難,網(wǎng)上也有很多優(yōu)秀的view和教程称簿,主要知識(shí)點(diǎn)就是繪制正N邊形的一個(gè)過程朴爬,也就是對(duì)Path類使用即寒,下面在這里簡(jiǎn)單記錄一下自己的編寫過程和思路,成品效果如下:

首先簡(jiǎn)單的分析一下,繪制這樣一個(gè)雷達(dá)圖大致需要3步:
- 繪制所有的正N邊形
- 繪制中心點(diǎn)到各頂點(diǎn)的連線
- 繪制數(shù)據(jù)區(qū)域N邊形
1母赵、繪制所有的正N邊形
這個(gè)雷達(dá)網(wǎng)由半徑遞減的多個(gè)正N邊形組成逸爵,至于具體繪制幾個(gè),應(yīng)該設(shè)置一個(gè)參數(shù)mLayer
以供隨時(shí)調(diào)整凹嘲,這里暫定默認(rèn)值為5师倔。
- 第一步 找到原點(diǎn)坐標(biāo),這個(gè)好辦周蹭,直接找view的中心點(diǎn)即可
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mPointCenter = new PointF(w / 2, h / 2);
}
- 第二步 求出最外層N邊形外接圓半徑趋艘,也就是原點(diǎn)至最外層正N邊形頂點(diǎn)的連線距離,這個(gè)也好辦凶朗,因?yàn)関iew的寬高設(shè)置的并不一定相等瓷胧,所以取view的寬高中小的一個(gè)。但并不能直接將此值作為半徑棚愤,還需要為頂點(diǎn)描述文字預(yù)留空間搓萧,同時(shí)我們也希望頂點(diǎn)描述文字和頂點(diǎn)之間有一定的間距,所以最終半徑的值是在此基礎(chǔ)上再減去頂點(diǎn)描述文字的寬度和間距宛畦。
private void calcRadius() {
if (mVertexText == null || mVertexText.size() == 0) {
mRadius = Math.min(mPointCenter.x, mPointCenter.y)
- mVertexTextOffset;
} else {
String maxText = Collections.max(mVertexText,
new Comparator<String>() {
@Override
public int compare(String lhs, String rhs) {
return lhs.length() - rhs.length();
}
});
float maxTextWidth = mVertexTextPaint.measureText(maxText);
if (mVertexTextOffset == 0) {
Paint.FontMetrics fontMetrics = mVertexTextPaint
.getFontMetrics();
float textHeight = fontMetrics.descent - fontMetrics.ascent;
mVertexTextOffset = (int) Math.sqrt(Math.pow(maxTextWidth, 2)
+ Math.pow(textHeight, 2)) / 2;
if (mVertexTextOffset < dp2px(15)) {
mVertexTextOffset = dp2px(15);
}
}
mRadius = Math.min(mPointCenter.x, mPointCenter.y)
- (maxTextWidth + mVertexTextOffset);
}
}
- 第三步 繪制所有正N邊形瘸洛,就需要得出正N邊形所有頂點(diǎn)的坐標(biāo),因?yàn)槲覀円呀?jīng)有了N邊形外接圓半徑的值次和,根據(jù)每個(gè)頂點(diǎn)相對(duì)于原點(diǎn)的圓心角度數(shù)反肋,就可以通過三角函數(shù)求出頂點(diǎn)x、y的值踏施,所需公式如下:
x = sin(a) × r y = cos(a) × r a為角石蔗、r為半徑
這里有個(gè)小問題需要注意下,在java中Math類的三角函數(shù)接收的參數(shù)并不角度读规,而是弧度抓督,所以需要用2 * Math.PI
表示360°
mAngle = 2 * Math.PI / mVertexCount;
for (int i = mLayer; i >= 1; i--) {
float radius = mRadius / mLayer * i;
Path p = new Path();
for (int j = 1; j <= mVertexCount; j++) {
float x = (float) (mPointCenter.x + Math.sin(mAngle * j) * radius);
float y = (float) (mPointCenter.y + Math.cos(mAngle * j) * radius);
if (j == 1) {
p.moveTo(x, y);
} else {
p.lineTo(x, y);
}
}
p.close();
canvas.drawPath(p, mLayerPaint);
}
繪制的時(shí)候,我們可以給加點(diǎn)特技什么的束亏,比如每層多邊形的畫筆設(shè)置不同的顏色铃在,效果如下:
或者不繪制多邊形,將雷達(dá)網(wǎng)直接繪制成圓形碍遍,當(dāng)然定铜,繪制圓形就簡(jiǎn)單多了,不需要算頂點(diǎn)的坐標(biāo)怕敬,一句話就搞定
canvas.drawCircle(mPointCenter.x, mPointCenter.y, radius, mLayerPaint);
效果如下:
2揣炕、繪制中心點(diǎn)到各頂點(diǎn)的連線
有了上面的基礎(chǔ),繪制這個(gè)連線就簡(jiǎn)單多了东跪,這里依然使用Path
來做連線
for (int i = 1; i <= mVertexCount; i++) {
Path p = new Path();
p.moveTo(mPointCenter.x, mPointCenter.y);
float x = (float) (mPointCenter.x + Math.sin(mAngle * i) * mRadius);
float y = (float) (mPointCenter.y + Math.cos(mAngle * i) * mRadius);
p.lineTo(x, y);
canvas.drawPath(p, mRadarLinePaint);
}
同時(shí)還可以把頂點(diǎn)描述文字加上去
for (int i = 1; i <= mVertexCount; i++) {
float x = (float) (mPointCenter.x + Math.sin(mAngle * i) * (mRadius + mVertexTextOffset));
float y = (float) (mPointCenter.y + Math.cos(mAngle * i) * (mRadius + mVertexTextOffset));
String text = mVertexText.get(i - 1);
float textWidth = mVertexTextPaint.measureText(text);
Paint.FontMetrics fontMetrics = mVertexTextPaint.getFontMetrics();
float textHeight = fontMetrics.descent - fontMetrics.ascent;
canvas.drawText(text, x - textWidth / 2, y + textHeight / 4, mVertexTextPaint);
}
效果如下:
3畸陡、繪制數(shù)據(jù)區(qū)域N邊形
數(shù)據(jù)區(qū)域繪制也是使用Path
類鹰溜,方法和繪制雷達(dá)網(wǎng)的N邊形一樣,只是每次半徑的數(shù)值是根據(jù)數(shù)據(jù)的值不斷變化的為了能方便的添加多組數(shù)據(jù)先來定義雷達(dá)圖的數(shù)據(jù)類
public class RadarData {
private String mLabel;
private List<Float> mValue;
private int mColor;
private List<String> mValueText;
private int mVauleTextColor;
private int mValueTextSize;
private boolean mValueTextEnable;
}
和添加數(shù)據(jù)的方法
public void addData(RadarData data) {
mRadarData.add(data);
initData(data);
animeValue(2000);
}
然后是數(shù)據(jù)區(qū)域內(nèi)容的繪制丁恭,根據(jù)數(shù)據(jù)值占最大值的比例求出半徑
List<Float> values = radarData.getValue();
Path p = new Path();
for (int j = 1; j <= values.size(); j++) {
float value = values.get(j - 1);
double percent = value / mMaxValue;
float x = (float) (mPointCenter.x + Math.sin(mAngle * j + mRotateAngle) * mRadius * percent);
float y = (float) (mPointCenter.y + Math.cos(mAngle * j + mRotateAngle) * mRadius * percent);
if (j == 1) {
p.moveTo(x, y);
} else {
p.lineTo(x, y);
}
}
p.close();
mValuePaint.setAlpha(255);
mValuePaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(p, mValuePaint);
mValuePaint.setStyle(Paint.Style.FILL);
mValuePaint.setAlpha(150);
canvas.drawPath(p, mValuePaint);
效果圖就不貼了曹动,和本文第一張動(dòng)圖一樣,至此整個(gè)雷達(dá)圖就繪制出來了
好了牲览,接下來我們給雷達(dá)圖添加手勢(shì)旋轉(zhuǎn)的功能墓陈,轉(zhuǎn)起來
旋轉(zhuǎn)也不難,不過有個(gè)前提第献,旋轉(zhuǎn)的時(shí)候頂點(diǎn)描述文字雖然也跟著旋轉(zhuǎn)贡必,但其排列方向不能變,任何時(shí)候都要保證是水平排列的庸毫,如果只是簡(jiǎn)單的使用view的setRotation方法來進(jìn)行旋轉(zhuǎn)操作仔拟,就無(wú)法保證文字永遠(yuǎn)是水平排列的,所以我們需要對(duì)各頂點(diǎn)的坐標(biāo)進(jìn)行操作飒赃,使其跟隨手指觸摸移動(dòng)距離整體移動(dòng)理逊,然后不斷的對(duì)整個(gè)視圖進(jìn)行重繪以實(shí)現(xiàn)旋轉(zhuǎn)效果
首先重寫onTouchEvent方法并使用GestureDetector管理觸摸手勢(shì)
public RadarView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDetector = new GestureDetectorCompat(mContext, new GestureListener());
mDetector.setIsLongpressEnabled(false);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mRotationEnable) return super.onTouchEvent(event);
return mDetector.onTouchEvent(event);
}
既然要處理手指觸摸移動(dòng),那我們重寫GestureListener的onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
方法
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
double rotate = mRotateAngle;
double dis = RotateUtil.getRotateAngle(new PointF(e2.getX() - distanceX, e2.getY() - distanceY)
, new PointF(e2.getX(), e2.getY()), mPointCenter);
rotate += dis;
handleRotate(rotate);
return super.onScroll(e1, e2, distanceX, distanceY);
}
這里思路是這樣的盒揉,我們?cè)趏nScroll里計(jì)算本次手指移動(dòng)前后總共移動(dòng)了多少度的角
請(qǐng)看下圖,假設(shè)移動(dòng)前手指在A點(diǎn)兑徘,移動(dòng)后在B點(diǎn)刚盈,目的是根據(jù)移動(dòng)的距離計(jì)算出角a的度數(shù)
計(jì)算出角a的度數(shù)后,就可以重繪view挂脑,還記得我們?cè)谥袄L制正N邊形的時(shí)候各頂點(diǎn)的坐標(biāo)都是使用三角函數(shù)計(jì)算出來的么藕漱,我們只需要在計(jì)算各頂點(diǎn)的時(shí)候,將三角函數(shù)當(dāng)前的角度加上這個(gè)角a崭闲,這樣整個(gè)雷達(dá)圖就旋轉(zhuǎn)了a度肋联,只要手指不斷的移動(dòng),view就會(huì)不斷的旋轉(zhuǎn)刁俭,比如我們挑之前繪制時(shí)第二步繪制中心點(diǎn)到各頂點(diǎn)的連線來改造下
for (int i = 1; i <= mMaxVertex; i++) {
Path p = new Path();
p.moveTo(mPointCenter.x, mPointCenter.y);
float x = (float) (mPointCenter.x + Math.sin(mAngle * i + mRotateAngle) * mRadius);
float y = (float) (mPointCenter.y + Math.cos(mAngle * i + mRotateAngle) * mRadius);
p.lineTo(x, y);
canvas.drawPath(p, mRadarLinePaint);
}
其他只要需要計(jì)算頂點(diǎn)的坐標(biāo)的地方都和這個(gè)同樣道理
那么我們?cè)趺从?jì)算出這么一個(gè)移動(dòng)的角度呢橄仍,我這里寫了一個(gè)RotateUtil類專門來處理
public class RotateUtil {
public static final double CIRCLE_ANGLE = 2 * Math.PI;
protected static double getRotateAngle(PointF p1, PointF p2, PointF mPointCenter) {
int q1 = getQuadrant(p1, mPointCenter);
int q2 = getQuadrant(p2, mPointCenter);
double angle1 = getAngle(p1, mPointCenter);
double angle2 = getAngle(p2, mPointCenter);
if (q1 == q2) {
return angle1 - angle2;
} else {
return 0;
}
}
//得到一個(gè)坐標(biāo)點(diǎn)相對(duì)于原點(diǎn)的圓心角度數(shù)
public static double getAngle(PointF p, PointF mPointCenter) {
float x = p.x - mPointCenter.x;
float y = mPointCenter.y - p.y;
double angle = Math.atan(y / x);
return getNormalizedAngle(angle);
}
//根據(jù)一個(gè)坐標(biāo)點(diǎn)判斷其所在象限
public static int getQuadrant(PointF p, PointF mPointCenter) {
float x = p.x;
float y = p.y;
if (x > mPointCenter.x) {
if (y > mPointCenter.y) {
return 4;
} else if (y < mPointCenter.y) {
return 1;
}
} else if (x < mPointCenter.x) {
if (y > mPointCenter.y) {
return 3;
} else if (y < mPointCenter.y) {
return 2;
}
}
return -1;
}
public static double getNormalizedAngle(double angle) {
while (angle < 0)
angle += CIRCLE_ANGLE;
return angle % CIRCLE_ANGLE;
}
}
其實(shí)邏輯也很簡(jiǎn)單,只需要分別得到A點(diǎn)和B點(diǎn)相對(duì)于原點(diǎn)的圓心角度數(shù)然后相減即可牍戚,那么如果根據(jù)一個(gè)坐標(biāo)點(diǎn)得到角呢侮繁,這里就需要用到反三角函數(shù)了
a = arctan(tan(a)) = arctan(y/x)
這里也有一點(diǎn)需要注意,通過反正切計(jì)算出來角度(其實(shí)是弧度)后如孝,還需要判斷其所在的象限宪哩,我們知道象限角的函數(shù)值是有負(fù)值的,所以如果兩個(gè)角如果不在同一象限第晰,就不能讓其相減
可以看到已經(jīng)可以跟隨手指移動(dòng)進(jìn)行旋轉(zhuǎn)了锁孟,但是仔細(xì)觀察會(huì)發(fā)現(xiàn)一個(gè)問題彬祖,就是旋轉(zhuǎn)的太僵硬了,沒有慣性品抽,這個(gè)好辦储笑,我們可以根據(jù)滑動(dòng)的加速度制造這么一個(gè)fling效果,讓手指滑動(dòng)停止后繼續(xù)旋轉(zhuǎn)一段距離
如何辦到呢桑包,這就需要Scroller出場(chǎng)了南蓬,使用Scroller的fling
方法,讓它根據(jù)速度為我們計(jì)算這段距離和時(shí)間哑了,至于速度怎么獲得赘方?,重寫GestureListener中onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
即可
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (Math.abs(velocityX) > Math.abs(velocityY)) {
mFlingPoint = e2.getX();
mScroller.fling((int) e2.getX(), 0, (int) velocityX, 0, (int) (-mPerimeter + e2.getX()), (int) (mPerimeter + e2.getX()), 0, 0);
} else if (Math.abs(velocityY) > Math.abs(velocityX)) {
mFlingPoint = e2.getY();
mScroller.fling(0, (int) e2.getY(), 0, (int) velocityY, 0, 0, (int) (-mPerimeter + e2.getY()), (int) (mPerimeter + e2.getY()));
}
invalidate();
return super.onFling(e1, e2, velocityX, velocityY);
}
fling的min和max的值使用最外層N邊形外接圓的周長(zhǎng)來做限制弱左,當(dāng)然這可以按照自己的想法隨意制訂窄陡,想轉(zhuǎn)的距離再長(zhǎng)點(diǎn)加大這個(gè)值的范圍就行了
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
int max = Math.max(Math.abs(x), Math.abs(y));
double rotateDis = RotateUtil.CIRCLE_ANGLE * (Math.abs(max - mFlingPoint) / mPerimeter);
double rotate = mRotateAngle;
if (mRotateOrientation > 0) {
rotate += rotateDis;
} else if (mRotateOrientation < 0) {
rotate -= rotateDis;
}
handleRotate(rotate);
mFlingPoint = max;
invalidate();
}
}
computeScroll里,按照滑動(dòng)距離相對(duì)于外接圓周長(zhǎng)的占比求出旋轉(zhuǎn)的角度拆火,重繪view即可
可以看到已經(jīng)能比較順滑的旋轉(zhuǎn)了
最后跳夭,我們?cè)俳o數(shù)據(jù)區(qū)加一個(gè)動(dòng)畫效果,直接用屬性動(dòng)畫就好们镜,比較簡(jiǎn)單沒什么可說的币叹,直接上代碼吧
public void animeValue(int duration){
for (int i = 0; i < mRadarData.size(); i++) {
RadarData data = mRadarData.get(i);
ValueAnimator anime = ValueAnimator.ofFloat(0, 1f);
final List<Float> values = data.getValue();
final List<Float> values2 = new ArrayList<>(values);
anime.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float percent = Float.parseFloat(animation.getAnimatedValue().toString());
for (int i = 0; i < values.size(); i++) {
values.set(i, values2.get(i) * percent);
}
invalidate();
}
});
anime.setDuration(duration).start();
}
}
就先寫這么一個(gè)動(dòng)畫吧,以后想到別的了再慢慢加進(jìn)去
github:https://github.com/qstumn/RadarView
感謝:http://blog.csdn.net/crazy__chen/article/details/50163693