自定义View:用Canvas实现转盘View

前两天道长的同学让道长帮忙做个控件,道长看到效果图和需求感觉挺有意思,然后就答应下来了。下面和小伙伴们分享一下制作过程。
效果图和需求就不给小伙伴们看了,需求大概就是转盘可以来回拖动,点击有标志显示。然后给小伙伴们看一下完成后的效果图(道长终于会弄动态图了,哈哈哈…):

这里写图片描述

一、绘制

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就停止绘制。小伙伴们也可以自己设置。

这篇博客暂时就到这里了,希望这篇博客能够为小伙伴们提供一些帮助。如果有更好的优化或者改进希望小伙伴们可以告知道长。源码贴在下面。

源码下载

CoronaDemo


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值