Android 详解自定义View抽奖转盘

725 阅读3分钟

前言

由于自己经常使用四川移动掌上营业厅,经常在大转盘抽奖,每次都在使用这个抽奖,突然最近在练习自定义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()方法,其中要注意两个参数hOffsetvOffset两个值,分别代表水平方向距离和垂直方向距离,垂直方向偏移量我们便于适配大小,使用的是圆盘的半径的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…

总结

首先这个自定义抽奖转盘,网上有很多的例子,我这里再详细的讲一遍,首先是自己练习下,其次是记录下自己的过程。也参考了网上的资料,值得一提的是,不要觉得某个事情很简单你就不动手自己做,只有你自己亲自动手做了一遍,才能变成你自己的东西,程序猿更加是如此,拒绝眼高手低,看着谁都会,自己做的时候就各种懵逼。