自定义一个简单的轮盘

1,903 阅读7分钟
原文链接: zhuanlan.zhihu.com

前一段时间,在百度贴吧里面看见一张图片,就例如我上面的主题图,是自定义一个这样的控件,于是就抽时间写了一个,也写了好长时间了,本着互相学习的态度,今晚把我写的代码分享出来,还请各位android 大牛指正。当然,也只能当个demo,如果要在实际开发中用,肯定还需要完善。(不知道为什么,AS录的屏的MP4文件不能转为gif,于是就只能自己拍了,好丑。)

那现在来谈谈我这个View的实现。自定义View,肯定是要继承View类,但是我让view 转起来,还是继承View嘛?android为我们提供了更好的SurfaceView。关于继承SurfaceView,前辈大神门已经为我们写好了一套标准模板,我们直接拿来用就好。代码如下,

public class SurfaceViewTempalte extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mSurfaceHolder;
private Canvas mCanvas;
//绘制线程
    private Thread t;
//是否关闭
    private boolean isRunning;

public SurfaceViewTempalte(Context context) {
this(context, null);
    }

public SurfaceViewTempalte(Context context, AttributeSet attrs) {
super(context, attrs);
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
//可以获取焦点
        setFocusable(true);
        setFocusableInTouchMode(true);
//设置常量
        setKeepScreenOn(true);
    }

@Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
isRunning = true;
t = new Thread(this);
t.start();
    }

@Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {

    }

@Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
isRunning = false;
    }

@Override
    public void run() {
while (isRunning) {
            draw();
        }
    }

/**
     * 绘制
     */
    private void draw() {
try {
            //获取画布
mCanvas = mSurfaceHolder.lockCanvas();
if (mCanvas != null) {

            }
        } catch (Exception e) {
e.printStackTrace();
        } finally {
if (mCanvas != null) {
//释放
                mSurfaceHolder.unlockCanvasAndPost(mCanvas);
            }
        }

    }
}

有了模板,那我们直接套用就好了啊,下面是我定义的成员变量,以及做的一些初始化的操作,之所以把这些罗列出来,是方便大家看后面的代码。因为是demo,一些颜色资源属性我就直接赋值了。

//holder
private SurfaceHolder mSurfaceHolder;
//画布
private Canvas mCanvas;
//绘制线程
private Thread t;
//是否关闭
private boolean isRunning;
//盘块的直径
private int mRadius;
//绘制盘块的画笔
private Paint mArcPaint;
//绘制文本的画笔
private Paint mTextPaint;
//绘制内圆的画笔
private Paint mInsidePaint;
//盘快的范围
private RectF mRangs;
//内圆的范围
private RectF mmInsideRangs;
//中心位子
private int mCenter;
//文字大小
private float mTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics());
//起始角度
private volatile float mStartAngle = 0;
//盘块的滚动速度
private double mSpeed = 0;
//部分占比
private Float[] mScale = new Float[]{0.3f, 0.25f, 0.2f, 0.15f, 0.1f};
//item数量
private int mItemSize;
//颜色资源
private Integer[] mColors = new Integer[]{0xFFFFC300, 0xFFF17E01, 0xFFFF0000, 0xFF00FF00, 0xFF0000FF};
//回调接口
private ItemChangedListener mItemChangedListener;

我们在构造函数以及surfaceCreated()方法中做一些初始化,代码批注都很详细,我就不一一解释了。

public ProgressPan(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
//可以获取焦点
    setFocusable(true);
    setFocusableInTouchMode(true);
//设置常量
    setKeepScreenOn(true);
mItemSize = mScale.length;
}


@Override
public void surfaceCreated(SurfaceHolder holder) {
isRunning = true;
//初始化绘制线程
    t = new Thread(this);
t.start();
//初始化盘块画笔
    mArcPaint = new Paint();
mArcPaint.setAntiAlias(true);
mArcPaint.setDither(true);
//初始化内圆画笔
    mInsidePaint = new Paint();
mInsidePaint.setAntiAlias(true);
mInsidePaint.setDither(true);
mInsidePaint.setColor(0xffffffff);
//初始化文字画笔
    mTextPaint = new Paint();
mTextPaint.setColor(0xff000000);
mTextPaint.setTextSize(mTextSize);
//初始化盘块范围
    mRangs = new RectF(0, 0, mRadius, mRadius);
//初始化内圆盘块范围
    mmInsideRangs = new RectF(mRadius / 4, mRadius / 4, mRadius * 3 / 4, mRadius * 3 / 4);
}

基本的初始化工作完成了,但是圆环半径还没有定义,那怎么办,关于自定义View的测量,咱们重写一下onMeasure()方法。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//因为是个圆,所以获取的宽高中较小的尺寸
int width = Math.min(getMeasuredWidth(), getMeasuredHeight());
//直径
mRadius = width - 50;
//中心点
mCenter = width / 2;
//设置宽高
    setMeasuredDimension(width, width);
}

那现在,我们是不是可以开始绘制了,在绘制前,首先说一下View上面的点,任何一个平面图形,我们都可以定义一个坐标系,那我们view的坐标系怎么定义,是以我们这个空间的左上角我(0,0)点,然后向右向下分别为X轴与Y轴,这样,我们便可以根据我们上面测量的View 的尺寸,把我们这个View 上面的点的坐标都计算出来,例如,我们控件的中心点就是(mCenter ,mCenter)。明白了这个,我们现在画起来。代码我不一次性都咱贴出来,咱们慢慢来,注重过程。还说一点,关于绘制圆环的的起始位置,是以控件中心点正右边为起始位置,顺时针绘制的。

   //绘制控件背景颜色       
 mCanvas.drawColor(0xFFFFFFFF);
//绘制刻度盘
 float tmpAngle = 0;//这一过程中,将之申明为成员变量,不然每次赋初值,图案就覆盖了
float sweepAngle = 360 / mItemSize;

for (int i = 0; i < mItemSize; i++) {
//绘制圆环
  mArcPaint.setColor(mColors[i]);
mRangs = new RectF(mCenter - mRadius / 2, mCenter - mRadius / 2 + 20, mCenter + mRadius / 2, mCenter + mRadius / 2 + 20);
mCanvas.drawArc(mRangs, tmpAngle, sweepAngle, true, mArcPaint);              
//画内圆
 mCanvas.drawCircle(mCenter, mCenter, mRadius / 4, mInsidePaint);
//起始角度加加
  tmpAngle += sweepAngle;
}         

这样,我们便把圆环绘制出来了,而且是颜色各异的扇形圆环。绘制方法的给个参数就不一一讲解了,一时(想不起来的的请看API或则源码。

但是我们要让圆环动起来, 该怎么做,是不是只要改变一下起始的位置就行了,float tmpAngle = mStartAngle;然后我们滑动一点,改变一点mStartAngle的值,是不是就让圆环动起来了,现在我们重写onTouchEvent,达到动态改变mStartAngle的值。关于onTouchEvent方法,我就不解释太多了,但是,我们的圆环可以顺时针,可以逆时针,我们怎样做了?咱们让顺时针旋转mStartAngle++,逆时针是mStartAngle--,就达到了这个目的,关于判断旋转方法,咱么可以借助数学知识,计算斜率(见代码)。如果这不是蛮懂得话,那数学就是体育老师教的咯,玩笑话。其实作为一个程序员来说,数学还是蛮重要。

private float mDownX;
private float mDownY;
private float newX;
private float newY;
@Override
public boolean onTouchEvent(MotionEvent event) {
//移动前的值
    switch (event.getAction()) {
case MotionEvent.ACTION_UP:
            break;
case MotionEvent.ACTION_DOWN:
            mDownX = event.getX();
mDownY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
            newX = event.getX();
newY = event.getY();
float k1 = (mDownY - mCenter) / (mDownX - mCenter);
float k2 = (newY - mCenter) / (newX - mCenter);
if (k1 < k2) {  //顺时针
                mStartAngle = mStartAngle + 3;
            } else {//逆时针
                mStartAngle = mStartAngle - 3;
            }
mDownX = event.getX();
mDownY = event.getY();
flag = index;
break;
    }
return true;
}

现在,圆环也动起来了,但是,旋转到下方的位置,扇形就凸起来了,这个怎么实现,其实这个有一个技巧,咱们找一个相对点,而我找的这个相对点的位置就是每部分圆环的正中心线的顶点,当这个点的坐标位置满足一定条件时(xmind进入x1与x2中间的区域),就可以判断圆环进入了对应区域,在这个相对点的变化过程中,运用到了三角函数的周期性,所以说数学很重要。


private void draw() {
try {
mCanvas = mSurfaceHolder.lockCanvas();
if (mCanvas != null) {
mCanvas.drawColor(0xFFFFFFFF);
//绘制刻度盘
            float tmpAngle = mStartAngle;
float sweepAngle = 360 / mItemSize;
            //进入的x1值 离开的x2值(相对与圆心)
            float x1 = (float) (mCenter * Math.cos(Math.PI * (90 - sweepAngle / 2) / 180));
float x2 = (float) (mCenter * Math.cos(Math.PI * (90 + sweepAngle / 2) / 180));
for (int i = 0; i < mItemSize; i++) {
//绘制圆环
                mArcPaint.setColor(mColors[i]);
//圆环的的中点角度
                float middleAngle = sweepAngle / 2 + mStartAngle + i * sweepAngle;
//中边的x 与 y 值(相对与圆心)
                float xMid = (float) (mCenter * Math.cos(Math.PI * middleAngle / 180));
float yMid = (float) (mCenter * Math.sin(Math.PI * middleAngle / 180));
                //绘制凸出来的圆环               
              if (yMid > 0 && x2 <= xMid && x1 > xMid) {
mRangs = new RectF(mCenter - mRadius / 2, mCenter - mRadius / 2 + 20, mCenter + mRadius / 2, mCenter + mRadius / 2 + 20);
mCanvas.drawArc(mRangs, tmpAngle, sweepAngle, true, mArcPaint);
//覆盖突出的地方
                    mRangs = new RectF(mCenter - mRadius / 4 - 3, mCenter - mRadius / 4 + 20, mCenter + mRadius / 4 + 3, mCenter + mRadius / 4 + 20);
mCanvas.drawArc(mRangs, tmpAngle, sweepAngle, true, mInsidePaint);
//画内圆
                    mCanvas.drawCircle(mCenter, mCenter, mRadius / 4, mInsidePaint);
                //绘制其他圆环
                } else {
mRangs = new RectF(mCenter - mRadius / 2, mCenter - mRadius / 2, mCenter + mRadius / 2, mCenter + mRadius / 2);
mCanvas.drawArc(mRangs, tmpAngle, sweepAngle, true, mArcPaint);
//画内圆
                    mCanvas.drawCircle(mCenter, mCenter, mRadius / 4, mInsidePaint);
                }
//绘文字
                float textWidth = mTextPaint.measureText(mScale[i] * 100 + "%");
mCanvas.drawText(mScale[i] * 100 + "%", mCenter - textWidth / 2, mCenter, mTextPaint);
//起始角度加加
                tmpAngle += sweepAngle;
            }
        }
    } catch (Exception e) {
e.printStackTrace();
    } finally {
if (mCanvas != null) {
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
        }
    }
}

代码到这儿,其实就差不多了,但是我们还想要一个旋转选中的回调,怎么写,思想跟上面的一样,还是借组中间点,只不过现在我们的xnind需要每一次计算而已。


private int getIndex() {
//每一个分的角度
    float sweepAngle = 360 / mItemSize;
    //进入的x1值 离开的x2值(相对与圆心)
    float x1 = (float) (mCenter * Math.cos(Math.PI * (90 - sweepAngle / 2) / 180));
float x2 = (float) (mCenter * Math.cos(Math.PI * (90 + sweepAngle / 2) / 180));
for (int i = 0; i < mItemSize; i++) {
float middleAngle = sweepAngle / 2 + mStartAngle + i * sweepAngle;
float xMid = (float) (mCenter * Math.cos(Math.PI * middleAngle / 180));
float yMid = (float) (mCenter * Math.sin(Math.PI * middleAngle / 180));
if (yMid > 0 && x2 <= xMid && x1 > xMid) {
return i;
        }
    }
return -1;
}

然后我们把回调的加上:

case MotionEvent.ACTION_MOVE:
   。。。
if (k1 < k2) {  //顺时针
      。。
    } else {//逆时针
     。。 
    }
int index = getIndex();
    if (mItemChangedListener != null && index != -1 && index != flag) {
mItemChangedListener.onTtemChanged(index);
    }
mDownX = event.getX();
  。。
break;

这个控件基本就这样了,还有许多需要完善的地方,写的不好的地方,请大家指正,android自定义View这部分确实是个难点,我自己也好多不懂,也想求抱大腿,奈何没有大腿让我抱。