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

来源:互联网 发布:美萍收银软件 编辑:程序博客网 时间:2024/05/17 07:13

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

这里写图片描述

一、绘制

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


原创粉丝点击