1.整体效果
静态显示效果:
动态显示效果
静态显示效果
2.效果实现分析
从效果图中我们可以知道,要实现上面的效果。首先要绘画出圆形背景(这步比较简单),第二步是绘画出各个选择区域和分割线,第三步是绘画出中心的图片,第四部是要让各个选择区域可以跟随手指的滑动进行转动已经实现类似与点击的效果。其中的难点和重心也就是第二步,而且注意到每个选择区域中包含有小图片和文字信息,还有选择区域之间的分割线。博主这边的做法就是将小图标,文字,分割线已经每个小区域的背景通过Canvas绘制在一个Bitmap上,当旋转的时候采用canvas.rotate(tmpAngle, mCenter, mCenter);方法进行图片的旋转,最后将各个选择区域通过Canvas绘制在一起。
3.具体实现步骤
1.测量控件的大小,让整个控件呈现为方形。
/**
* 设置控件为正方形
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = Math.min(getMeasuredWidth(), getMeasuredHeight());
// 中心点
mCenter = width / 2;
mDrawWidth = width;
//屏幕的宽度
setMeasuredDimension(width, width);
init();
}
在init()中实现对一些数据的初始化操作。
private void init() {
if (!hasMeasured) {
//得到各个比例下的图片大小
mBackColorWidth = getDrawWidth(mBackColorWidth);
mRangeWidth = getDrawWidth(mRangeWidth);
mCheckBitmapWidth = getDrawWidth(mCheckBitmapWidth);
mLitterBitWidth = getDrawWidth(mLitterBitWidth);
mTextSize = TypedValue.applyDimension(
mTextSize / 3, getResources().getDisplayMetrics());
hasMeasured = true;
}
// 初始化绘制圆弧的画笔
mArcPaint = new Paint();
mArcPaint.setAntiAlias(true);
mArcPaint.setDither(true);
// 初始化绘制文字的画笔
mTextPaint = new TextPaint();
mTextPaint.setColor(0xff222222);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setAntiAlias(true);
mTextPaint.setDither(true);
//圆形背景的画笔
mBackColorPaint = new Paint();
mBackColorPaint.setColor(0xffae6112);
mBackColorPaint.setAntiAlias(true);
mBackColorPaint.setDither(true);
//未选中的分割线的画笔
mLinePaint = new Paint();
mLinePaint.setAntiAlias(true);
mLinePaint.setDither(true);
mLinePaint.setStrokeWidth(dip2px(1f));
mLinePaint.setStyle(Paint.Style.FILL);
mLinePaint.setColor(0xffff9406);
// 内圈画盘
mRange = new RectF(mCenter - mRangeWidth / 2, mCenter - mRangeWidth / 2, mCenter + mRangeWidth / 2, mCenter + mRangeWidth / 2);
//中心图片绘制区域
mCheckBitmapRect = new RectF(mCenter - mCheckBitmapWidth / 2, mCenter - mCheckBitmapWidth / 2, mCenter + mCheckBitmapWidth / 2, mCenter + mCheckBitmapWidth / 2);
}
2.确定圆形背景的大小和绘制圆形背景(这边根据采取的大小是1080p下的780px)。为了保证在控件任意大小下都能适配,需要根据控件的大小对宽度进行计算。
/**
* 当前切图的比例都是在1080p下进行的,所以这边的比例就是1080的
*
* mDrawWidth 表示为控件的宽度
**/
private float getDrawWidth(float width) {
return width * mDrawWidth / 1080f;
}
//绘制背景图
mCanvas.drawCircle(mCenter, mCenter, mBackColorWidth / 2, mBackColorPaint);
同样的,中心图片绘制方式是类似的,这里采用的方式是通过RectF去控制绘制区域
//绘制中心图片
mCanvas.drawBitmap(mCheckBitmap, null, mCheckBitmapRect, null);
3.实现各个选择区域(重点部分),从效果图中我们可以知道,每个选择区域对应两种状态(选中和非选中状态)。那么在绘制的时候也要根据这两种状态来区别背景颜色。还要考虑的是每次更新状态的时候,有些区域只需要更新角度,有些区域需要更新背景颜色,那么需要将这些区域保存起来,避免重复生成,减少刷新的时间。区域的旋转采用的是canvas的rotate方法进行旋转。
/**
* 对每个选项进行绘制
**/
private Bitmap getDrawItemBitmap(float tmpAngle, float sweepAngle, int position) {
//是否需要重新绘制
boolean needToNew = false;
//根据状态判断是否需要重新绘制
if (checkPosition == position && bitInfos.get(position).info.bitmapType == TYPE_UNCHECKED) {//这次选中,上次没选中的要更新
needToNew = true;
bitInfos.get(position).info.bitmapType = TYPE_CHECKED;
} else if (checkPosition != position && bitInfos.get(position).info.bitmapType == TYPE_CHECKED) {//这次没选中,上次选中的要更新
needToNew = true;
bitInfos.get(position).info.bitmapType = TYPE_UNCHECKED;
}
if (bitInfos.get(position).info.itemBitmap == null || needToNew) {
//选择背景颜色
if (checkPosition == position) {
mArcPaint.setColor(mColors[1]);
} else {
mArcPaint.setColor(mColors[0]);
}
//绘制每一个小块
bitInfos.get(position).info.itemBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas itemCanvas = new Canvas(bitInfos.get(position).info.itemBitmap);
//根据角度进行同步旋转
itemCanvas.rotate(tmpAngle, mCenter, mCenter);
//绘制背景颜色,从最上边开始画
itemCanvas.drawArc(mRange, -sweepAngle / 2 - 90, sweepAngle, true, mArcPaint);
//绘制小图片和文本,因为一起画好画点
drawIconAndText(position, itemCanvas);
//绘制分割线,这里保证没一个小块都有一条分割线,分割线的位置是在最右侧
drawCanvasLine(itemCanvas);
} else {
Canvas itemCanvas = new Canvas(bitInfos.get(position).info.itemBitmap);
//根据角度进行同步旋转
itemCanvas.rotate(tmpAngle, mCenter, mCenter);
}
return bitInfos.get(position).info.itemBitmap;
}
在drawIconAndText方法中可以看到,对小图片的绘制约束采用的Rect来确定位置和大小,其他图片的区域在半径的1/4处,而文字的采用的是StaticLayout,具体StaticLayout的用法可以自行google。
/**
* 绘制小图片和文字
*
* @param i
*/
private void drawIconAndText(int i, Canvas canvas) {
//根据的标注,比例为115/730
float rt = mLitterBitWidth / mRangeWidth;
//计算绘画区域的直径
int mRadius = (int) (mRange.right - mRange.left);
int imgWidth = (int) (mRadius * rt);
//获取中心点坐标
int x = mCenter;
//这边让图片从四分之一出开始画
int y = (int) (mCenter - mRadius / 2 + (float) mRadius / 2 * 1 / 4f);
//确定小图片的区域
Rect rect = new Rect(x - imgWidth / 2, y - imgWidth / 2, x + imgWidth
/ 2, y + imgWidth / 2);
//将图片画上去
canvas.drawBitmap(bitInfos.get(i).bitmap, null, rect, null);
//绘制文本
if (!TextUtils.isEmpty(bitInfos.get(i).text)) {
//最大字数限制为8个字
if (bitInfos.get(i).text.length() > 8) {
bitInfos.get(i).text = bitInfos.get(i).text.substring(0, 8);
}
StaticLayout textLayout = new StaticLayout(bitInfos.get(i).text, mTextPaint, imgWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0, false);
canvas.translate(mCenter, rect.bottom + dip2px(2));
textLayout.draw(canvas);
//画完之后移动回来
canvas.translate(-mCenter, -(rect.bottom + dip2px(2)));
}
}
drawCanvasLine方法是绘制分割线,当然,当选择区域只为1的时候是没有分割线的(实际使用中基本上不会有这种情况出现),分割线的位置在选择区域的最右侧。
/**
* 绘制分割线
**/
private void drawCanvasLine(Canvas canvas) {
if (mItemCount == 1) {
return;
}
//计算终点角度,这边从最上边为0度开始计算,则角度在0-180之间,给0.5f的偏移
float range = 360f / mItemCount / 2 - 0.5f;
//计算半径的长度
float radio = (mRange.right - mRange.left) / 2;
//计算终点坐标
float x = 0;
float y = 0;
if (range < 90) {
x = (float) (radio * Math.sin(Math.toRadians(range)));
y = (float) (radio * Math.cos(Math.toRadians(range)));
x += mCenter;
y = mCenter - y;
} else {
x = (float) (radio * Math.sin(Math.toRadians(180 - range)));
y = (float) (radio * Math.cos(Math.toRadians(180 - range)));
x += mCenter;
y += mCenter;
}
canvas.drawLine(mCenter, mCenter, x, y, mLinePaint);
}
最后将每个选择区域进行通过canvas绘制在一起,实现整体的圆盘效果
//绘制前景图片,这里包含的是图片信息和文字信息,还有背景圆弧背景展示
private Bitmap getFontBitmap() {
fontBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(fontBitmap);
//根据角度进行同步旋转
canvas.rotate(mStartAngle, mCenter, mCenter);
float tmpAngle = 0;
float sweepAngle = (float) (360 / mItemCount);
for (int i = 0; i < mItemCount; i++) {
//这边可以得到新的bitmap
canvas.drawBitmap(getDrawItemBitmap(tmpAngle, sweepAngle, i), 0, 0, null);
tmpAngle += sweepAngle;
}
return fontBitmap;
}
4.上面的步骤已经实现了静态效果的呈现,但是还无法进行圆盘旋转以及点击选中的效果。要让圆盘进行滚动起来,只需要改变绘制圆盘的canvas的角度就可以了,即改变canvas.rotate(mStartAngle, mCenter, mCenter)中的mStartAngle的值(这里用mStartAngle来存储转动的总量)。
@Override
public boolean onTouchEvent(MotionEvent event) {
//判断按点是否在圆内
if (!mRange.contains(event.getX(), event.getY())) {
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchX = event.getX();
touchY = event.getY();
//按下时的时间
nowClick = System.currentTimeMillis();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
float moveY = event.getY();
//得到旋转的角度
float arc = getRoundArc(touchX, touchY, moveX, moveY);
//重新赋值
touchX = moveX;
touchY = moveY;
//起始角度变化下,然后进行重新绘制
mStartAngle += arc;
onDrawInvalidate();
break;
case MotionEvent.ACTION_UP:
if (System.currentTimeMillis() - nowClick <= timeClick) {//此时为点击事件
//落点的角度加上偏移量基于初始的点的位置
checkPosition = calInExactArea(getRoundArc(event.getX(), event.getY()) - mStartAngle);
onDrawInvalidate();
if (mOnWheelCheckListener != null) {
mOnWheelCheckListener.onCheck(checkPosition);
}
}
break;
}
return true;
}
点击事件的判断只需要ACTION_DOWN和ACTION_UP的时间差是否在一定的时间内完成(这里给的是500毫秒)
在calInExactArea方法中进行落点区域的判断,可以根据ACTION_UP时点的坐标,圆心的坐标(mCenter,mcenter),以及0度角延长线上的坐标(mCenter,0)。通过三点的坐标计算ACTION_UP时点相对应角度。计算方式为:先用两点间距离公式求出三角形的三边a,b,c,再用余弦定理cosC=(a2+b2-c^2)/2ab求角C。然后根据角C和mStartAngle(总旋转的角度)来计算落点的区域。
//根据落点计算角度
private float getRoundArc(float upX, float upY) {
float arc = 0;
//首先计算三边的长度
float a = (float) Math.sqrt(Math.pow(mCenter - mCenter, 2) + Math.pow(0 - mCenter, 2));
float b = (float) Math.sqrt(Math.pow(upX - mCenter, 2) + Math.pow(upY - mCenter, 2));
float c = (float) Math.sqrt(Math.pow(upX - mCenter, 2) + Math.pow(upY - 0, 2));
//判断是否为三角形
if (a + b > c) {//两边之和大于第三边为三角形
/**
* 接下来计算角度
*
* acos((a2+b2-c2)/2ab)
*
* **/
arc = (float) (Math.acos((Math.pow(a, 2) + Math.pow(b, 2) - Math.pow(c, 2)) / (2 * a * b)) * 180 / Math.PI);
//判断是大于左边还是右边,也就是180以内还是以外
if (upX < mCenter) {//此时是180以外的
arc = 360 - arc;
}
} else {//上下边界的问题
if (upX == mCenter) {
if (upY < mCenter) {
arc = 0;
} else {
arc = 180;
}
}
}
return arc;
}
/**
* 根据当前旋转的mStartAngle计算当前滚动到的区域
*
* @param startAngle
*/
public int calInExactArea(float startAngle) {
float size = 360f / mItemCount;
// 确保rotate是正的,且在0-360度之间
float rotate = (startAngle % 360.0f + 360.0f) % 360.0f;
for (int i = 0; i < mItemCount; i++) {
// 每个的中奖范围
if (i == 0) {
if ((rotate > 360 - size / 2) || (rotate < size / 2)) {
return i;
}
} else {
float from = size * (i - 1) + size / 2;
float to = from + size;
if ((rotate > from) && (rotate < to)) {
return i;
}
}
}
return -1;
}
滑动的时候旋转角度的实现,计算方法与计算落点角度的方法是一样的,根据上次的落点坐标,圆心的坐标,本次的落点的坐标,,计算旋转的角度c,然后 mStartAngle += arc;根据 mStartAngle去进行旋转,这边要注意的是反余弦的值的范围是0-180,所以这边需要注意旋转的区域。
//根据三点的坐标计算旋转的角度
private float getRoundArc(float startX, float startY, float endX, float endY) {
float arc = 0;
//首先计算三边的长度
float a = (float) Math.sqrt(Math.pow(startX - mCenter, 2) + Math.pow(startY - mCenter, 2));
float b = (float) Math.sqrt(Math.pow(endX - mCenter, 2) + Math.pow(endY - mCenter, 2));
float c = (float) Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2));
//判断是否为三角形
if (a + b > c) {//两边之和大于第三边为三角形
/**
* 接下来计算角度
*
* acos((a2+b2-c2)/2ab)
*
* **/
arc = (float) (Math.acos((Math.pow(a, 2) + Math.pow(b, 2) - Math.pow(c, 2)) / (2 * a * b)) * 180 / Math.PI);
if (startX <= mCenter && endX >= mCenter && startY < mCenter && endY < mCenter) {//上边顺时针越界,不管他
} else if (startX >= mCenter && endX <= mCenter && startY < mCenter && endY < mCenter) {//上边逆时针越界
arc = -arc;
} else if (startX <= mCenter && endX >= mCenter && startY > mCenter && endY > mCenter) {//下边逆时针越界
arc = -arc;
} else if (startX <= mCenter && endX >= mCenter && startY < mCenter && endY < mCenter) {//下边顺时针越界,不管他
} else if (endX >= mCenter && startX >= mCenter) {//这个时候表示在右半区
if (startY > endY) {
arc = -arc;
}
} else if (endX < mCenter && startX < mCenter) {//此时在左半区
if (startY < endY) {
arc = -arc;
}
}
}
if (Math.abs(arc) >= 0 && Math.abs(arc) <= 180) {//主要解决nan的问题
return arc;
} else {
return 0;
}
}