前言
由于自己经常使用四川移动掌上营业厅,经常在大转盘抽奖,每次都在使用这个抽奖,突然最近在练习自定义View,而且以前也没有做过这个自定义抽奖转盘,所以就当是练练手,然而百度上随便一搜索就是一大把,各种各样的解决方案。虽然网上的解决方案很多,但是我觉得即使再简单我们也要自己撸一变才能变成自己的东西,因此有了此文。
国际惯例先看运行效果,怕你们跑了:
分析实现步骤:
从上面的效果图我们可以简单的分析下,总的我们拆解成四部分:
-
背景层就是上图边缘的红色部分
-
中间旋转的圆盘部分
-
文字
-
小图标
1.背景层部分实现:
背景层部分的话,有两种实现方式:
(1)画在最底层,相当于背景的形式;假如你在边缘有设计其他的样式,只需要在第二步在边缘设置合适的padding即可
(2)画在最上层,UI切图成一个圆环的形式;这里同样要设置一个合适的padding来满足需求。
2.中间旋转的盘块
Canvas中有个5个参数的方法叫:
drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
@NonNull Paint paint)
-
参数一 oval:圆弧所在的椭圆对象。(所在的椭圆或者圆要跟oval内切)
-
参数二 startAngle:圆弧的起始角度。
-
参数三 sweepAngle:圆弧的角度。
-
参数四 useCenter:是否显示半径连线,true表示显示圆弧与圆心的半径连线,false表示不显示;为false那么就只是一段圆弧,不会和中心店连接起来。
-
参数五 paint:绘制时所使用的画笔。
只需要设置好起始角度和圆弧的角度,paint的颜色即可绘制出我们需要的圆盘。
3.文字
从效果图可以看出,我们的文字不在一条直线上,而是带有圆弧。因此这里就不能使用我们常用的drawText()方法,而是通过传入Path:
drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset,
float vOffset, @NonNull Paint paint)
-
参数一 text:需要画的文字
-
参数二 path:我们需要画的文字的路径。这里怎么获得我们path的路径呢?
addArc(RectF oval, float startAngle, float sweepAngle)
这个是Path的方法,添加一段圆弧路径。
- 参数三 paint:画笔
4.小图标
小图标是比较麻烦的,我们这里是将每个图标都canvas的方法:
drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst,
@Nullable Paint paint)
-
参数一 bitmap:我们需要显示的图片
-
参数二 Rect:对图片进行裁剪,如果传入null,则表示显示整个图片
-
参数三 RectF:在canvas画布中我们需要显示的大小,RectF比图片大则图片放大,RectF比图片小则图片缩小
-
参数四 Paint:我们的画笔,画bitmap的时候传入null即可。
实践步骤
1.绘制背景
@Override
protected void onDraw(Canvas canvas) {
//1.绘制背景
canvas.drawBitmap(mBgBitmap, null, new RectF(mPadding / 2, mPadding / 2
, getMeasuredWidth() - mPadding / 2, getMeasuredHeight() - mPadding / 2), null);
}
这里为了简单方便,直接使用一个背景图片来画,参数mPadding是我们在XML布局中设置padding值。 效果:
2.画圆盘的盘块:
@Override
protected void onDraw(Canvas canvas) {
//1.绘制背景
canvas.drawBitmap(mBgBitmap, null, new RectF(mPadding / 2, mPadding / 2
, getMeasuredWidth() - mPadding / 2, getMeasuredHeight() - mPadding / 2), null);
//2.绘制盘块
int tempAngle = (int) mStartAngle;
float sweepAngle = 360 / mItems;
for (int i = 0; i < mItems; i++) {
//1.绘制盘块背景
mArcPaint.setColor(ContextCompat.getColor(getContext(), colors[i]));
canvas.drawArc(mRange, tempAngle, sweepAngle, true, mArcPaint);
tempAngle += sweepAngle;
}
}
mStartAngle是一个起始画圆盘的角度,360/盘块数 就是要的画一个盘块的角度,tempAngle 每个盘块的起始角度。
运行效果:
3.绘制文字
@Override
protected void onDraw(Canvas canvas) {
//1.绘制背景
canvas.drawBitmap(mBgBitmap, null, new RectF(mPadding / 2, mPadding / 2
, getMeasuredWidth() - mPadding / 2, getMeasuredHeight() - mPadding / 2), null);
//2.绘制盘块
int tempAngle = (int) mStartAngle;
float sweepAngle = 360 / mItems;
for (int i = 0; i < mItems; i++) {
//1.绘制盘块背景
mArcPaint.setColor(ContextCompat.getColor(getContext(), colors[i]));
canvas.drawArc(mRange, tempAngle, sweepAngle, true, mArcPaint);
//2.绘制盘块的文字
Path path = new Path();
path.addArc(mRange, tempAngle, sweepAngle);
//通过水平偏移量使得文字居中 水平偏移量=弧度/2 - 文字宽度/2
float textWidth = mTextPaint.measureText(prizeName[i]);
float hOffset = (float) (mRadius * Math.PI / mItems / 2 - textWidth / 2);
//垂直偏移量 = 半径/6
float vOffset = mRadius / 2 / 6;
canvas.drawTextOnPath(prizeName[i], path, hOffset, vOffset, mTextPaint);
tempAngle += sweepAngle;
}
这里画出弧形的文字,前面说了主要是通过Path来获得一个弧形的路径,然后通过drawTextOnPath()方法,其中要注意两个参数hOffset,vOffset两个值,分别代表水平方向距离和垂直方向距离,垂直方向偏移量我们便于适配大小,使用的是圆盘的半径的1/6,水平方向偏移量=圆弧/2-文字宽度/2
运行效果:
4.绘制图片
这里我们先来解释下如何将图片放置到我们想要的位置,假设这里我讲图片放在每个盘块的中心的位置,也就是半径/2的位置。简单解释一下,我们中心点的坐标是知道的假设是(centerX,centerY),中心点到我们放置的盘块的中心的距离知= 半径/2 ,角度=tempAngle画盘块的起始角度+弧度sweepAngle/2,这三个参数有了之后,我们直接通过cos、sin可以分别得出每小图标中心点的坐标。
@Override
protected void onDraw(Canvas canvas) {
//1.绘制背景
canvas.drawBitmap(mBgBitmap, null, new RectF(mPadding / 2, mPadding / 2
, getMeasuredWidth() - mPadding / 2, getMeasuredHeight() - mPadding / 2), null);
//2.绘制盘块
int tempAngle = (int) mStartAngle;
float sweepAngle = 360 / mItems;
for (int i = 0; i < mItems; i++) {
//1.绘制盘块背景
mArcPaint.setColor(ContextCompat.getColor(getContext(), colors[i]));
canvas.drawArc(mRange, tempAngle, sweepAngle, true, mArcPaint);
//2.绘制盘块的文字
Path path = new Path();
path.addArc(mRange, tempAngle, sweepAngle);
//通过水平偏移量使得文字居中 水平偏移量=弧度/2 - 文字宽度/2
float textWidth = mTextPaint.measureText(prizeName[i]);
float hOffset = (float) (mRadius * Math.PI / mItems / 2 - textWidth / 2);
//垂直偏移量 = 半径/6
float vOffset = mRadius / 2 / 6;
canvas.drawTextOnPath(prizeName[i], path, hOffset, vOffset, mTextPaint);
//3.绘制盘块上面的IMG
//约束下图片的宽度
int imgWidth = mRadius / 8;
//获取弧度
float angle = (float) Math.toRadians(tempAngle + sweepAngle / 2);
//将图片移动到圆弧中心位置
float x = (float) (mCenter + mRadius / 2 / 2 * Math.cos(angle));
float y = (float) (mCenter + mRadius / 2 / 2 * Math.sin(angle));
//确认绘制的矩形
RectF rectF = new RectF(x - imgWidth / 2, y - imgWidth / 2, x + imgWidth / 2, y + imgWidth / 2);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), imgs[i]);
canvas.drawBitmap(bitmap, null, rectF, null);
tempAngle += sweepAngle;
}
这里我们固定了一个图片的宽、高为直径/8,便于适配。这里同样是使用到了RectF,只要计算到四个边的距离可以了。
运行效果:
哈哈,到这里我们可以看到,我们抽奖转盘已经-完成了。中间在添加一个指示器,我们的圆盘就大功告成了。
额,不对,好像还要旋转的哇?
不知道大家有没有看出来这里的图片都是竖着放置的,如果有需求是要将我们的图片和我们的盘块的方向一致,这里简单提示下是需要同Matrix来旋转角度,使得我们的图片旋转
核心代码:
//旋转绘制的图片
ArrayList<Bitmap> bitmaps = new ArrayList<>();
for (int j = 0; j < imgs.length; j++) {
//获取bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), imgs[j]);
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Matrix matrix = new Matrix();
//设置缩放值
matrix.postScale(1f, 1f);
//旋转的角度
matrix.postRotate(sweepAngle * j);
//获取旋转后的bitmap
Bitmap rotateBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
//将旋转过的图片保存到列表中
bitmaps.add(rotateBitmap);
}
旋转的角度就是我们每个盘块的角度
5.圆盘旋转
常用的方式有两种:
-
一种是一直重新绘制圆盘,改变起始角度。
-
另一种是通过属性动画,也推荐的方式,简单,方便。
这里我就使用一种不常用的方式,就是一直重新绘制:
@Override
protected void onDraw(Canvas canvas) {
//1.绘制背景
canvas.drawBitmap(mBgBitmap, null, new RectF(mPadding / 2, mPadding / 2
, getMeasuredWidth() - mPadding / 2, getMeasuredHeight() - mPadding / 2), null);
//2.绘制盘块
int tempAngle = (int) mStartAngle;
float sweepAngle = 360 / mItems;
for (int i = 0; i < mItems; i++) {
//1.绘制盘块背景
mArcPaint.setColor(ContextCompat.getColor(getContext(), colors[i]));
canvas.drawArc(mRange, tempAngle, sweepAngle, true, mArcPaint);
//2.绘制盘块的文字
Path path = new Path();
path.addArc(mRange, tempAngle, sweepAngle);
//通过水平偏移量使得文字居中 水平偏移量=弧度/2 - 文字宽度/2
float textWidth = mTextPaint.measureText(prizeName[i]);
float hOffset = (float) (mRadius * Math.PI / mItems / 2 - textWidth / 2);
//垂直偏移量 = 半径/6
float vOffset = mRadius / 2 / 6;
canvas.drawTextOnPath(prizeName[i], path, hOffset, vOffset, mTextPaint);
//3.绘制盘块上面的IMG
//约束下图片的宽度
int imgWidth = mRadius / 8;
//获取弧度
float angle = (float) Math.toRadians(tempAngle + sweepAngle / 2);
//将图片移动到圆弧中心位置
float x = (float) (mCenter + mRadius / 2 / 2 * Math.cos(angle));
float y = (float) (mCenter + mRadius / 2 / 2 * Math.sin(angle));
//确认绘制的矩形
RectF rectF = new RectF(x - imgWidth / 2, y - imgWidth / 2, x + imgWidth / 2, y + imgWidth / 2);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), imgs[i]);
canvas.drawBitmap(bitmap, null, rectF, null);
tempAngle += sweepAngle;
}
if (start) {
mStartAngle += mSpeed;
//16ms之后刷新界面
mHandler.postDelayed(new MyRunnable(), 16);
mSpeed -= 1;
if (mSpeed < 10) {
mSpeed -= 0.5;
}
if (mSpeed < 3) {
mSpeed -= 0.1;
}
if (mSpeed < 0) {
mSpeed = 0;
start = false;
}
}
}
通过一个标志位:start 是否点击了开始抽奖,如果点击了,我们就刷新一下界面,同时start=true,通过handle来发送消息再重新绘制界面;这里我们每绘制一次,就将我们的mSeep-1,当mSeep小于10的时候每次-0.5,小于3的时候-0.1,这样就达到了慢慢减速的效果,知道mSeep=0的时候就停止转动了,start=false,就停止转动了。
这样我们的自定View抽奖转盘就大功告成了。
最终的效果:
最后附上Demo地址:github.com/scorpioLt/L…
总结
首先这个自定义抽奖转盘,网上有很多的例子,我这里再详细的讲一遍,首先是自己练习下,其次是记录下自己的过程。也参考了网上的资料,值得一提的是,不要觉得某个事情很简单你就不动手自己做,只有你自己亲自动手做了一遍,才能变成你自己的东西,程序猿更加是如此,拒绝眼高手低,看着谁都会,自己做的时候就各种懵逼。