需求先看UI效果图吧
看到这肯定去找轮子,找了半天,没找到相似的,大部分搜到的都是点击外凸,而这个UI是内凸,其实外凸内凸区别还不小,没找到一样的,于是乎,和iOS说好了要不就放弃吧,然而第二天,人家夸夸夸撸完了,还是贝塞尔写的,当场晕死,我是真看不懂那公式,然后不甘心,拼命的改别人的轮子,就算不用贝塞尔,也得搞出来啊,于是乎,套两个圆环不就好了,于是,开始下手干活,下面贴出来iOS和Android的效果,左iOS右Android
虽然没有人家漂亮,但是基本也实现了,代码贴在下面了
辅助工具类GeomTool.class
import android.graphics.Point;
import android.graphics.RectF;
/**
* 与2D屏幕有关的计算,屏幕约定为X轴向右,Y轴向下,顺时针角度增加。
* Created by hxw on 2016/8/25.
*/
public class GeomTool {
/**
* 这个方法放在这里展示了原始的计算过程
*
* @see #calcCirclePoint
*/
@Deprecated
private static Point calcCirclePoint2(int angle, float radius, float cx, float cy,Point resultOut) {
if (resultOut == null) {
resultOut = new Point();
}
// 将angle控制在0-360,注意这里的angle是从X正轴顺时针增加。而sin,cos等的计算是X正轴开始逆时针增加
angle = clampAngle(angle);
double radians = angle / 180f * Math.PI;
double sin = Math.sin(radians);
double cos = Math.cos(radians);
double x = 0, y = 0;
if (angle == 0 || angle == 360) {
// sin:0 cos: 1
x = cx + radius;
y = cy;
} else if (angle > 0 && angle < 90) {
// sin:0~1 cos: 1~0
double dy = radius * sin;
double dx = radius * cos;
x = cx + dx;
y = cy + dy;
} else if (angle == 90) {
// sin:1 cos: 0
x = cx;
y = cy + radius;
} else if (angle > 90 && angle < 180) {
// sin:1~0 cos: 0~-1
double dy = radius * sin;
double dx = radius * cos;
x = cx + dx;
y = cy + dy;
} else if (angle == 180) {
// sin:0 cos: -1
x = cx - radius;
y = cy;
} else if (angle > 180 && angle < 270) {
// sin:0~-1 cos: -1~0
double dy = radius * sin;
double dx = radius * cos;
x = cx + dx;
y = cy + dy;
} else if (angle == 270) {
// sin:-1 cos: 0
x = cx;
y = cy - radius;
} else if (angle > 270 && angle < 360) {
// sin:-1~0 cos: 0~1
double dy = radius * sin;
double dx = radius * cos;
x = cx + dx;
y = cy + dy;
}
resultOut.set((int) x, (int) y);
return resultOut;
}
/**
* 计算指定角度、圆心、半径时,对应圆周上的点。
* @param angle 角度,0-360度,X正轴开始,顺时针增加。
* @param radius 圆的半径
* @param cx 圆心X
* @param cy 圆心Y
* @param resultOut 计算的结果(x, y) ,方便对象的重用。
* @return resultOut, or new Point if resultOut is null.
*/
public static Point calcCirclePoint(int angle, float radius, float cx, float cy, Point resultOut) {
if (resultOut == null) resultOut = new Point();
// 将angle控制在0-360,注意这里的angle是从X正轴顺时针增加。而sin,cos等的计算是X正轴开始逆时针增加
angle = clampAngle(angle);
double radians = angle / 180f * Math.PI;
double sin = Math.sin(radians);
double cos = Math.cos(radians);
double dy = radius * sin;
double dx = radius * cos;
double x = cx + dx;
double y = cy + dy;
resultOut.set((int) x, (int) y);
return resultOut;
}
/**
* 计算坐标(x, y)到圆心(cx, cy)形成的角度,角度从0-360,360度就是0度,顺时针增加
* (x轴向右,y轴向下)若2点重合返回-1;
*/
public static int calcAngle(float x, float y, float cx, float cy) {
double resultDegree = 0;
double vectorX = x - cx; // 点到圆心的X轴向量,X轴向右,向量为(0, vectorX)
double vectorY = cy - y; // 点到圆心的Y轴向量,Y轴向上,向量为(0, vectorY)
if (vectorX == 0 && vectorY == 0) {
// 重合?
return -1;
}
// 点落在X,Y轴的情况这里就排除
if (vectorX == 0) {
// 点击的点在Y轴上,Y不会为0的
if (vectorY > 0) {
resultDegree = 90;
} else {
resultDegree = 270;
}
} else if (vectorY == 0) {
// 点击的点在X轴上,X不会为0的
if (vectorX > 0) {
resultDegree = 0;
} else {
resultDegree = 180;
}
} else {
// 根据形成的正切值算角度
double tanXY = vectorY / vectorX;
double arc = Math.atan(tanXY);
// degree是正数,相当于正切在四个象限的角度的绝对值
double degree = Math.abs(arc / Math.PI * 180);
// 将degree换算为对应x正轴开始的0-360的角度
if (vectorY < 0 && vectorX > 0) {
// 右下 0-90
resultDegree = degree;
} else if (vectorY < 0 && vectorX < 0) {
// 左下 90-180
resultDegree = 180 - degree;
} else if (vectorY > 0 && vectorX < 0) {
// 左上 180-270
resultDegree = 180 + degree;
} else {
// 右上 270-360
resultDegree = 360 - degree;
}
}
return (int) resultDegree;
}
/**
* 计算指定区域中可放置的最大正方形区域。
* @param region 指定的区域
* @param squareRect 正方形区域,将在原区域中居中
* @return squareRect, or new RectF if squareRect is null.
*/
public static RectF calcMaxSquareRect(RectF region, RectF squareRect) {
if (squareRect == null) squareRect = new RectF();
if (region == null) return squareRect;
float w = region.width();
float h = region.height();
if (w == h) {
squareRect.set(region);
} else if (w > h) {
float padding = (w - h) / 2;
squareRect.set(region);
squareRect.inset(padding, 0);
} else { // (w < h)
float padding = (h - w) / 2;
squareRect.set(region);
squareRect.inset(0, padding);
}
return squareRect;
}
/**
* 将角度变换为0-360度。
* @param angle 原角度
* @return 0-360之间的等效角度
*/
public static int clampAngle(int angle) {
return ((angle % 360) + 360) % 360;
}
/**
* 返回给定值在区间[min, max]上的最近值。
*/
public static float clamp(float value, float min, float max) {
if (min > max) {
min = min + max;
max = min - max;
min = min - max;
}
if (value < min) {
return min;
} else if (value > max) {
return max;
}
return value;
}
/**
* 计算点(x1, y1)和(x2, y2)之间的距离。
*/
public static float calcDistance(float x1, float y1, float x2, float y2) {
return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
}
自定义View RingView.class
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Point;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import com.dq.demo.R;
import java.util.ArrayList;
import java.util.List;
public class RingView extends View {
private Context mContext;
private Paint mPaint;
private int mPaintWidth = 0; // 画笔的宽
private int topMargin = 30; // 上边距
private int leftMargin = 80; // 左边距
private Resources mRes;
private DisplayMetrics dm;
private int showRateSize = 12; // 展示文字的大小
private int circleCenterX = 96; // 圆心点X 要与外圆半径相等
private int circleCenterY = 96; // 圆心点Y 要与外圆半径相等
private int ringOuterRidus = 96; // 外圆的半径
private int ringPointRidus = 80; // 点所在圆的半径
private float rate = 0.4f; //点的外延距离 与 点所在圆半径的长度比率
private float extendLineWidth = 80; //点外延后 折的横线的长度
private RectF rectF; // 外圆所在的矩形
private RectF rectFPoint; // 点所在的矩形
private List<Integer> colorList;
private List<Float> rateList;
private boolean isShowRate;
private Float maxTotal = 0F;
private String lengthenLineColor = "#3A66AF";
private String lengthenTextColor = "#455B72";
public RingView(Context context) {
super(context, null);
}
public RingView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
initView();
}
public void setShow(List<Integer> colorList, List<Float> rateList) {
setShow(colorList, rateList, false);
}
public void setShow(List<Integer> colorList, List<Float> rateList, boolean isShowRate) {
this.colorList = colorList;
this.rateList = rateList;
this.isShowRate = isShowRate;
angles = new float[rateList.size()];
for (int i = 0; i < rateList.size(); i++) {
maxTotal += rateList.get(i);
}
for (int j = 0; j < rateList.size(); j++) {
angles[j] = (float) ((rateList.get(j) / maxTotal) * 360f);
}
}
private void initView() {
this.mRes = mContext.getResources();
this.mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
dm = new DisplayMetrics();
WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(dm);
int screenWidth = wm.getDefaultDisplay().getWidth();
leftMargin = (px2dip(screenWidth) - (2 * circleCenterX)) / 2;
mPaint.setColor(getResources().getColor(R.color.red));
mPaint.setStrokeWidth(dip2px(mPaintWidth));
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
rectF = new RectF(dip2px(mPaintWidth + leftMargin),
dip2px(mPaintWidth + topMargin),
dip2px(circleCenterX + ringOuterRidus + mPaintWidth * 2 + leftMargin),
dip2px(circleCenterY + ringOuterRidus + mPaintWidth * 2 + topMargin));
rectFPoint = new RectF(dip2px(mPaintWidth + leftMargin + (ringOuterRidus - ringPointRidus)),
dip2px(mPaintWidth + topMargin + (ringOuterRidus - ringPointRidus)),
dip2px(circleCenterX + ringPointRidus + mPaintWidth * 2 + leftMargin),
dip2px(circleCenterY + ringPointRidus + mPaintWidth * 2 + topMargin));
Log.e("矩形点:", dip2px(circleCenterX + ringOuterRidus + mPaintWidth * 2) + " --- " + dip2px(circleCenterY + ringOuterRidus + mPaintWidth * 2));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
pointList.clear();
if (colorList != null) {
for (int i = 0; i < colorList.size(); i++) {
mPaint.setColor(mRes.getColor(colorList.get(i)));
mPaint.setStyle(Paint.Style.FILL);
drawOuter(canvas, i);
}
}
if (colorList != null) {
for (int i = 0; i < colorList.size(); i++) {
drawInouter(canvas, i);
}
}
mPaint.setStyle(Paint.Style.FILL);
Paint paint1 = new Paint();
paint1.setColor(Color.parseColor("#A7A6FF"));
canvas.drawCircle(rectF.centerX(), rectF.centerY(), 150, paint1);
}
private float preRate;
private void drawArcCenterPoint(Canvas canvas, int position) {
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mRes.getColor(R.color.transparent));
mPaint.setStrokeWidth(dip2px(1));
canvas.drawArc(rectFPoint, preAngle, (endAngle) / 2, true, mPaint);
dealPoint(rectFPoint, preAngle, (endAngle) / 2, pointArcCenterList);
Point point = pointArcCenterList.get(position);
mPaint.setColor(Color.parseColor(lengthenLineColor));
if (position == mCurrentItem) {
/**
* 折线初始圆点
*/
canvas.drawCircle(point.x, point.y, dip2px(2), mPaint);
}
if (preRate / 2 + rateList.get(position) / 2 < 5) {
extendLineWidth += 40;
rate -= 0.05f;
} else {
extendLineWidth = 40;
rate = 0.4f;
}
// 外延画折线
float lineXPoint1 = (point.x - dip2px(leftMargin + ringOuterRidus)) * (1 + rate);
float lineYPoint1 = (point.y - dip2px(topMargin + ringOuterRidus)) * (1 + rate);
float[] floats = new float[8];
floats[0] = point.x;
floats[1] = point.y;
floats[2] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1;
floats[3] = dip2px(topMargin + ringOuterRidus) + lineYPoint1;
floats[4] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1;
floats[5] = dip2px(topMargin + ringOuterRidus) + lineYPoint1;
if (point.x >= dip2px(leftMargin + ringOuterRidus)) {
mPaint.setTextAlign(Paint.Align.LEFT);
floats[6] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1 + dip2px(extendLineWidth);
} else {
mPaint.setTextAlign(Paint.Align.RIGHT);
floats[6] = dip2px(leftMargin + ringOuterRidus) + lineXPoint1 - dip2px(extendLineWidth);
}
floats[7] = dip2px(topMargin + ringOuterRidus) + lineYPoint1;
mPaint.setColor(Color.parseColor(lengthenLineColor));
if (position == mCurrentItem) {
/**
* 折线线段
*/
canvas.drawLines(floats, mPaint);
}
mPaint.setTextSize(dip2px(showRateSize));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.parseColor(lengthenTextColor));
if (position == mCurrentItem) {
/**
* 文字渲染
*/
if (point.x >= dip2px(leftMargin + ringOuterRidus)) {
canvas.drawText(rateList.get(position) + "%", floats[6] - dip2px(extendLineWidth), floats[7] - dip2px(showRateSize) / 3, mPaint);
} else {
canvas.drawText(rateList.get(position) + "%", floats[6] + dip2px(extendLineWidth), floats[7] - dip2px(showRateSize) / 3, mPaint);
}
}
preRate = rateList.get(position);
}
List<Point> pointList = new ArrayList<>();
List<Point> pointArcCenterList = new ArrayList<>();
private void dealPoint(RectF rectF, float startAngle, float endAngle, List<Point> pointList) {
Path orbit = new Path();
//通过Path类画一个90度(180—270)的内切圆弧路径
orbit.addArc(rectF, startAngle, endAngle);
PathMeasure measure = new PathMeasure(orbit, false);
Log.e("路径的测量长度:", "" + measure.getLength());
float[] coords = new float[]{0f, 0f};
int divisor = 1;
measure.getPosTan(measure.getLength() / divisor, coords, null);
Log.e("coords:", "x轴:" + coords[0] + " -- y轴:" + coords[1]);
float x = coords[0];
float y = coords[1];
Point point = new Point(Math.round(x), Math.round(y));
pointList.add(point);
}
private void drawOuter(Canvas canvas, int position) {
if (rateList != null) {
endAngle = getAngle(rateList.get(position));
}
canvas.drawArc(rectF, preAngle, endAngle, true, mPaint);
if (isShowRate) {
/**
* 绘制折线外延
*/
drawArcCenterPoint(canvas, position);
}
preAngle = preAngle + endAngle;
}
/**
* 绘制内圆环
*/
private void drawInouter(Canvas canvas, int position) {
if (rateList != null) {
endAngle = getAngle(rateList.get(position));
}
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(getResources().getColor(R.color.white));
paint.setStyle(Paint.Style.FILL);
RectF rectF1 = new RectF(dip2px(mPaintWidth + 130), //左
dip2px(mPaintWidth + 60), //上
dip2px(circleCenterX + ringOuterRidus + mPaintWidth * 2 + 70), //右
dip2px(circleCenterY + ringOuterRidus + mPaintWidth * 2 + 0)); //下
if (mCurrentItem != position) {
canvas.drawArc(rectF1, preAngle, endAngle, true, paint);
}
preAngle = preAngle + endAngle;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int item = calcClickItem(event.getX(), event.getY());
if (item >= 0 && item < rateList.size()) {
setCurrentItem(item);
} else {
mCurrentItem = -1;
invalidate();
}
}
return super.onTouchEvent(event);
}
private int mCurrentItem = -1;
public float[] angles;
private void setCurrentItem(int item) {
Log.e(RingView.class.getSimpleName(), "Click To Position " + item);
if (mCurrentItem != item) {
mCurrentItem = item;
}
invalidate();
}
private int calcClickItem(float x, float y) {
if (rateList == null) return -1;
final float centerX = rectF.centerX();
final float centerY = rectF.centerY();
float outerRadius = rectF.width() / 2;
float innerRadius = 80;
// 计算点击的坐标(x, y)和圆中心点形成的角度,角度从0-360,顺时针增加
int clickedDegree = GeomTool.calcAngle(x, y, centerX, centerY);
double clickRadius = GeomTool.calcDistance(x, y, centerX, centerY);
if (clickRadius < innerRadius) {
// 点击发生在小圆内部,也就是点击到标题区域
// return -1;
} else if (clickRadius > outerRadius) {
// 点击发生在大圆环外
return -2;
}
// 计算出来的clickedDegree是整个View原始的,被点击item需要考虑startAngle。
int startAngle = -90;
int angleStart = startAngle;
for (int i = 0; i < angles.length; i++) {
int itemStart = (angleStart + 360) % 360;
float end = itemStart + angles[i];
if (end >= 360f) {
if (clickedDegree >= itemStart && clickedDegree < 360) return i;
if (clickedDegree >= 0 && clickedDegree < (end - 360)) return i;
} else {
if (clickedDegree >= itemStart && clickedDegree < end) {
return i;
}
}
angleStart += angles[i];
}
return -3;
}
private float preAngle = -90;
private float endAngle = -90;
/**
* @param percent 百分比
* @return
*/
private float getAngle(float percent) {
float a = 360f / maxTotal * percent;
return a;
}
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public int dip2px(float dpValue) {
return (int) (dpValue * dm.density + 0.5f);
}
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public int px2dip(float pxValue) {
return (int) (pxValue / dm.density + 0.5f);
}
}
xml中引入布局
<com.dq.demo.ui.view.RingView
android:id="@+id/ringView"
android:layout_marginTop="300dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
activity中使用
RingView ringView = (RingView) mView.findViewById(R.id.ringView);
// 添加的是颜色
List<Integer> colorList = new ArrayList<>();
colorList.add(R.color.color_ff3e60);
colorList.add(R.color.color_ffa200);
colorList.add(R.color.color_31cc64);
colorList.add(R.color.yellow_2);
colorList.add(R.color.grey_600);
colorList.add(R.color.text_top_1);
// 添加的是百分比
List<Float> rateList = new ArrayList<>();
rateList.add(10f);
rateList.add(15f);
rateList.add(25f);
rateList.add(40f);
rateList.add(30f);
rateList.add(28f);
ringView.setShow(colorList, rateList, true);
至此结束!!!
如有更好的方法,欢迎在评论区讨论。