前两天道长的同学让道长帮忙做个控件,道长看到效果图和需求感觉挺有意思,然后就答应下来了。下面和小伙伴们分享一下制作过程。
效果图和需求就不给小伙伴们看了,需求大概就是转盘可以来回拖动,点击有标志显示。然后给小伙伴们看一下完成后的效果图(道长终于会弄动态图了,哈哈哈…):
一、绘制
1.测量空间宽高
在onMeasure()中测量宽高并且设置宽高为宽高的最小值,代码如下:
/**
* 设置控件为正方形
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = Math.min(getMeasuredWidth(), getMeasuredHeight());
// 获取圆形的直径
mRadius = width - mPadding - mBackgroundPadding;
// 中心点
mCenter = width / 2;
setMeasuredDimension(width, width);
}
2.绘制控件背景以及圆盘背景
小伙伴们应该都知道使用Canvas绘制View的话要从底层开始,不然的话会遮挡底层的展示。
/**
* 绘制控件背景以及圆盘背景
*/
private void drawBackground() {
mCanvas.drawBitmap(mBgBitmap, null, new Rect(0, 0, getMeasuredWidth(), getMeasuredWidth()), null);
// 圆盘背景颜色设置
mPaint.setColor(0x50000000);
mCanvas.drawCircle(mCenter, mCenter, mCenter - mPadding, mPaint);
}
3.绘制扇形以及扇形上的文字、背景
运用for循环绘制每一个扇形上的文字以及背景,代码如下:
float tmpAngle = mStartAngle;
float sweepAngle = (float) (360 / mItemCount);
// 绘制扇形以及扇形上的文字、背景
for (int i = 0; i < mItemCount; i++) {
// 绘制扇形
drawFanShaped(tmpAngle, sweepAngle, i);
// 绘制扇形上的文本
drawFanText(tmpAngle, sweepAngle, mStrs[i]);
// drawFanText(tmpAngle, sweepAngle, "i:" + i + ":::" + (i * 60 + offset));
// 移动到下一区域
tmpAngle += sweepAngle;
}
1)绘制扇形区域
- 计算偏置量
首先计算出来每次拖动圆盘的偏置量,即转动角度,代码如下:
// 计算偏置量
float turnAngle = tmpAngle % 360;
if (i == 0) {
offset = turnAngle;
}
我们看一下效果图:
- 矫正偏置量
计算出来每次拖动圆盘的偏置量后进行矫正,矫正完成后我们就可以知道扇形转动到的角度,代码如下:
// 矫正偏置量
setRight = i * sweepAngle + offset;
if (i * sweepAngle + offset > 360) {
setRight = i * sweepAngle + offset - 360;
} else if (i * sweepAngle + offset < 0) {
setRight = i * sweepAngle + offset + 360;
}
我们再看一下矫正后的效果图:
- 设置扇形的绘制区域
我们这里要考虑到当扇形转动到0度边界的点击情况,代码如下:
// 设置扇形区域边界
if (setRight > 300) {
if ((finalClickAngle >= setRight && finalClickAngle < 360) || (finalClickAngle >= 0 && finalClickAngle < setRight - 300)) {
setClickZone();
clickZone = i;
drawSingle(setRight, sweepAngle);
} else {
setDefaultZone();
}
} else {
if (finalClickAngle >= setRight && finalClickAngle < setRight + sweepAngle) {
setClickZone();
clickZone = i;
drawSingle(setRight, sweepAngle);
} else {
setDefaultZone();
}
}
/**
* 默认扇形区域边界
*/
private void setDefaultZone() {
mRange = new RectF(mPadding + mBackgroundPadding, mPadding + mBackgroundPadding, mRadius, mRadius);
}
/**
* 设置点击扇形区域边界
*/
private void setClickZone() {
mRange = new RectF(mPadding + mScalePadding, mPadding + mScalePadding,
mRadius - mScalePadding + mBackgroundPadding, mRadius - mScalePadding + mBackgroundPadding);
}
- 图片渲染绘制扇形图片
由于扇形的背景是由图片绘制,所以我们这里要用到图片渲染,代码如下:
setFanPaintShader(i);
mCanvas.drawArc(mRange, tmpAngle, sweepAngle, true, mBitmapPaint);
/**
* 设置扇形渲染对象
*
* @param i
*/
private void setFanPaintShader(int i) {
// 创建Bitmap渲染对象
mBitmapShader = new BitmapShader(mImgsBitmap[i], Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
float scale = 1.0f;
// 比较bitmap宽和高,获得较小值
int bSize = Math.min(mImgsBitmap[i].getWidth(), mImgsBitmap[i].getHeight());
scale = mRadius * 1.0f / bSize;
// shader的变换矩阵,用于放大或者缩小
mMatrix.setScale(scale, scale);
// 设置变换矩阵
mBitmapShader.setLocalMatrix(mMatrix);
// 设置shader
mBitmapPaint.setShader(mBitmapShader);
}
- 绘制扇形上的文字
我们看到扇形上的文字居中而且成弧形,通过计算偏移量,以及矫正让文字居中,代码如下:
/**
* 绘制扇形上的文本
*
* @param startAngle
* @param sweepAngle
* @param string
*/
private void drawFanText(float startAngle, float sweepAngle, String string) {
Path path = new Path();
path.addArc(mRange, startAngle, sweepAngle);
float textWidth = mTextPaint.measureText(string);
// 利用水平偏移让文字居中
float hOffset = (float) (mRadius * Math.PI / mItemCount / 2 - textWidth / 2);// 水平偏移
float vOffset = mRadius / 2 / 6;// 垂直偏移
mCanvas.drawTextOnPath(string, path, hOffset, vOffset, mTextPaint);
}
1)绘制中心遮挡区域
- 绘制中心遮挡区域的背景
中心遮挡区域的背景可以看到是有透视效果,其实也是通过和控件背景同一个图片渲染,代码如下:
/**
* 绘制中心遮挡圆背景
*/
private void drawCenterBg() {
setCirclePaintShader();
mCanvas.drawCircle(mCenter, mCenter, mCenter / 2, mCircleBitmapPaint);
}
/**
* 设置中心遮挡圆渲染对象
*/
private void setCirclePaintShader() {
// 创建Bitmap渲染对象
mCircleBitmapShader = new BitmapShader(mBgBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
float scale = 1.0f;
// 比较bitmap宽和高,获得较小值
int bSize = Math.min(mBgBitmap.getWidth(), mBgBitmap.getHeight());
scale = mRadius * 1.0f / bSize;
// shader的变换矩阵,用于放大或者缩小
mCircleMatrix.setScale(scale, scale);
// 设置变换矩阵
mCircleBitmapShader.setLocalMatrix(mCircleMatrix);
// 设置shader
mCircleBitmapPaint.setShader(mCircleBitmapShader);
}
- 绘制中心遮挡的文字
中心遮挡的文字的设置比较简单,但是要计算文字的偏置量,通过矫正让文字居中,代码如下:
/**
* 绘制中心遮挡的文字
*
* @param str
*/
private void drawCenterText(String str) {
float textWidth = mTextPaint.measureText(str);
// 利用偏移让文字居中
float hOffset = textWidth / 2;// 水平偏移
float vOffset = mTextSize / 4;// 垂直偏移
mCanvas.drawText(str, mCenter - hOffset, mCenter + vOffset, mTextPaint);
}
到了这里,整个空间的绘制已经完成,下面看一下转盘的拖动以及点击。效果图如下:
二、动作
1.转盘的拖动以及点击
这里我们先看一下onTouchEvent()中关于点击事件的处理,代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
start();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
// 每次转动圆盘都要去掉点中区域
finalClickAngle = -1;
isClick = false;
break;
case MotionEvent.ACTION_MOVE:
// 圆心的下方
if (downY - mCenter >= 0) {
distanceX = -(event.getX() - downX);
} else {// 圆心的上方
distanceX = event.getX() - downX;
}
// 圆心的右方
if (downX - mCenter >= 0) {
distanceY = event.getY() - downY;
} else {// 圆心的左方
distanceY = -(event.getY() - downY);
}
// 圆盘转动的距离
if (Math.abs(distanceY) - Math.abs(distanceX) >= 0) {
distance = distanceY;
} else {
distance = distanceX;
}
// 每隔30px采集一次定位点
if (Math.abs(distance) >= 30) {
downX = event.getX();
downY = event.getY();
}
// 圆盘移动误差矫正
float moveDistance = disTemp - Math.abs(distance);
if (moveDistance < 5 && moveDistance >= 0) {
distance = 0;
} else {
disTemp = Math.abs(distance);
}
// 圆盘转动状态设置
if (Math.abs(distance) < 5) {
distance = 0;
turnState = true;
} else {
turnState = false;
}
// 点击误差矫正
if (Math.abs(distance) > 5) {
isClick = true;
}
break;
case MotionEvent.ACTION_UP:
// 每项角度大小
float angle = (float) (360 / mItemCount);
// 角度 = Math.atan((dpPoint.y-dpCenter.y) / (dpPoint.x-dpCenter.x)) / π(3.14) * 180
double clickAngle = Math.atan((downY - mCenter) / (downX - mCenter)) / Math.PI * 180;
// 点击区域
int zone = (int) (clickAngle / angle);
float overflow = (float) (clickAngle % angle);
// 点击角度的矫正
// 圆心的下方
if (downY - mCenter >= 0) {
if (overflow >= 0) {
finalClickAngle = (float) clickAngle;
} else {
finalClickAngle = (float) clickAngle + 180;
}
} else {// 圆心的上方
if (overflow >= 0) {
finalClickAngle = (float) clickAngle + 180;
} else {
finalClickAngle = (float) clickAngle + 360;
}
}
if (isClick == false) {
// 调用回调接口
clickZone();
} else {
// 每次转动圆盘都要去掉点中区域
finalClickAngle = -1;
}
turnState = true;
break;
}
return true;
}
- 回调接口调用
由于流程控制先点击后绘制,所以在视觉上就会存在延时。所以回调接口调用要延时100ms,代码如下:
/**
* 回调点击区域
*/
private void clickZone() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (distance == 0) {
mViewOnClickListener.onClicked(clickZone);
}
}
}, 100);
}
- 点击区域标志的绘制
由于点击区域标志显示在每个扇形区域的中间,所以就要通过角度计算出标志的坐标。代码如下:
/**
* 绘制点击区域的标志
*
* @param angle
* @param sweepAngle
*/
private void drawSingle(float angle, float sweepAngle) {
mPaint.setColor(Color.BLUE);
// 计算标志的坐标
// positionX = Math.sin(Math.PI*角度/180) * R positionY = Math.cos(Math.PI*角度/180) * R
float positionX = (float) (Math.cos(Math.PI * (angle + sweepAngle / 2) / 180) * mCenter);
float positionY = (float) (Math.sin(Math.PI * (angle + sweepAngle / 2) / 180) * mCenter);
mCanvas.drawCircle(mCenter + positionX, mCenter + positionY, 20, mPaint);
}
2.不断绘制
由于不断绘制View属于耗时操作,所以我们要开启一个线程,在子线程中不断绘制。代码如下:
/**
* 开启线程
*/
public void start() {
threadState = true;
stopTime = 0;
if (thread != null && thread.isAlive()) {
if (DEBUG) {
Log.e("yushan", "start: thread is alive");
}
} else {
thread = new Thread(new Runnable() {
@Override
public void run() {
// 不断的进行绘制
while (threadState) {
long start = System.currentTimeMillis();
draw();
long end = System.currentTimeMillis();
long pieTime = end - start;
stopTime += pieTime;
try {
if (pieTime < 50) {
Thread.sleep(50 - pieTime);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3秒不操作就休眠
if (stopTime >= 3000) {
stop();
}
}
if (DEBUG) {
Log.i("yushan", "run: thread stopping");
}
}
});
thread.start();
}
}
/**
* 关闭线程
*/
public void stop() {
if (threadState) {
threadState = false;
}
}
三、优化
1.矫正
在刚才的代码中已经贴过了,就不一一贴了,就是一些转盘转动矫正,点击矫正什么的(主要是道长有些懒,不想贴了)。
2.休眠
不停地绘制View非常耗电,而且占用大量手机内存,容易造成内存溢出。所以道长设置每过3s,只要不操作View,View就停止绘制。小伙伴们也可以自己设置。
这篇博客暂时就到这里了,希望这篇博客能够为小伙伴们提供一些帮助。如果有更好的优化或者改进希望小伙伴们可以告知道长。源码贴在下面。