需求先看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*/@Deprecatedprivate 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: 1x = cx + radius;y = cy;} else if (angle > 0 && angle < 90) {// sin:0~1 cos: 1~0double dy = radius * sin;double dx = radius * cos;x = cx + dx;y = cy + dy;} else if (angle == 90) {// sin:1 cos: 0x = cx;y = cy + radius;} else if (angle > 90 && angle < 180) {// sin:1~0 cos: 0~-1double dy = radius * sin;double dx = radius * cos;x = cx + dx;y = cy + dy;} else if (angle == 180) {// sin:0 cos: -1x = cx - radius;y = cy;} else if (angle > 180 && angle < 270) {// sin:0~-1 cos: -1~0double dy = radius * sin;double dx = radius * cos;x = cx + dx;y = cy + dy;} else if (angle == 270) {// sin:-1 cos: 0x = cx;y = cy - radius;} else if (angle > 270 && angle < 360) {// sin:-1~0 cos: 0~1double 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-90resultDegree = degree;} else if (vectorY < 0 && vectorX < 0) {// 左下 90-180resultDegree = 180 - degree;} else if (vectorY > 0 && vectorX < 0) {// 左上 180-270resultDegree = 180 + degree;} else {// 右上 270-360resultDegree = 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));}@Overrideprotected 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;}@Overridepublic 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.RingViewandroid: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);
至此结束!!!
如有更好的方法,欢迎在评论区讨论。