前言
最近在不断学习自定义控件,项目中有个旋转圆盘的需求,需要一个刻度一个刻度的走,网上没发现有符合要求的,遂自己实现一下,先看下效果(录完才发现没有触摸轨迹,,前面旋转的是手滑动的,大家懂哈~)
需求
1,圆盘可控制顺时针与逆时针旋转,圆盘中间需要设置图片
2,旋转需要按一个刻度一个刻度的走,具有跳跃性
3,需要提供顺时针与逆时针旋转的方法,以及可以禁止与开启旋转实现
首先当然是继承View啦,这里偷懒只定义了一个属性,用来设置圆中的图片
<resources>
<declare-styleable name="ControlableCircleView">
<attr name="circle_bg" format="reference"/>
</declare-styleable>
</resources>
控件中获取资源
private void initAttrs(AttributeSet attrs) {
TypedArray attribute = getContext().obtainStyledAttributes(attrs, R.styleable.ControlableCircleView);
Drawable bg = attribute.getDrawable(R.styleable.ControlableCircleView_circle_bg);
if (null != bg)
bg_bitmap = drawableToBitmap(bg);
attribute.recycle();
}
在onMeasure方法中初始化宽高以及指针长度、大圆半径和背景圆半径
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
//长短指针所在的圆半径
mClockRadio = Math.min(mWidth, mHeight) / 2 - mLongClockLine;
//背景图片圆半径
mBackRadio = mClockRadio - 20;
}
然后再onDraw中画刻度和图片,画刻度是通过旋转画布实现的,需要计算出每一格的角度
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!mHasInit) {
mClockPaint = new Paint();
mClockPaint.setAntiAlias(true);
mClockPaint.setStrokeWidth(3);
mClockPaint.setColor(mPaintColor);
mPreAngle = 360f / mClockNum;
mHasInit = true;
}
//画背景图片
drawBg(canvas);
canvas.rotate(touchRotate, mWidth / 2, mHeight / 2);
//画长短指针
for (int i = 0; i < mClockNum; i++) {
if (i % 2 == 0)
canvas.drawLine(mWidth / 2, mHeight / 2 - mClockRadio, mWidth / 2, mHeight / 2 - mClockRadio - mLongClockLine, mClockPaint);
else
canvas.drawLine(mWidth / 2, mHeight / 2 - mClockRadio, mWidth / 2, mHeight / 2 - mClockRadio - mLongClockLine / 2, mClockPaint);
canvas.rotate(mPreAngle, mWidth / 2, mHeight / 2);
}
}
这里是上面的drawBg方法,用来画圆形图片,通过画布交叉叠加的方式获得圆形bitmap,在头像显示中经常用到
/**
* 画背景图片
*
* @param canvas
*/
private void drawBg(Canvas canvas) {
if (null != bg_bitmap) {
Bitmap squareBitmap;
Bitmap scaledSrcBmp;
int bgWidth = bg_bitmap.getWidth();
int bgHeight = bg_bitmap.getHeight();
int scaleWidth = 0, scaleHeight = 0;
int x = 0, y = 0;
if (bgWidth > bgHeight) {
scaleWidth = scaleHeight = bgHeight;
x = (bgWidth - bgHeight) / 2;
y = 0;
squareBitmap = Bitmap.createBitmap(bg_bitmap, x, y, scaleWidth, scaleHeight);
} else if (bgWidth < bgHeight) {
scaleWidth = scaleHeight = bgWidth;
y = (bgHeight - bgWidth) / 2;
x = 0;
squareBitmap = Bitmap.createBitmap(bg_bitmap, x, y, scaleWidth, scaleHeight);
} else {
squareBitmap = bg_bitmap;
}
if (squareBitmap.getWidth() != mBackRadio * 2 || squareBitmap.getHeight() != mBackRadio * 2) {
scaledSrcBmp = Bitmap.createScaledBitmap(squareBitmap, (int) mBackRadio * 2,
(int) mBackRadio * 2, true);
} else {
scaledSrcBmp = squareBitmap;
}
Bitmap output = Bitmap.createBitmap(scaledSrcBmp.getWidth(),
scaledSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);
Canvas bgCanvas = new Canvas(output);
Rect rect = new Rect(0, 0, scaledSrcBmp.getWidth(), scaledSrcBmp.getHeight());
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
bgCanvas.drawARGB(0, 0, 0, 0);
bgCanvas.drawCircle(scaledSrcBmp.getWidth() / 2, scaledSrcBmp.getHeight() / 2, scaledSrcBmp.getWidth() / 2, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
bgCanvas.drawBitmap(scaledSrcBmp, rect, rect, paint);
//画圆形背景
final Rect rectSrc = new Rect(0, 0, output.getWidth(), output.getHeight());
final Rect rectDest = new Rect((int) (mWidth / 2 - mBackRadio), (int) (mHeight / 2 - mBackRadio),
(int) (mWidth / 2 + mBackRadio), (int) (mHeight / 2 + mBackRadio));
mBgPaint = new Paint();
canvas.drawBitmap(output, rectSrc, rectDest, mBgPaint);
}
}
对了,获取图片属性中使用了一个drawable转bitmap的方法,这里也贴一下
/**
* drawable转bitmap
*
* @param drawable
* @return
*/
public Bitmap drawableToBitmap(Drawable drawable) {
Bitmap bitmap = Bitmap.createBitmap(
drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(),
drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888
: Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
//canvas.setBitmap(bitmap);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
drawable.draw(canvas);
return bitmap;
}
然后就是处理触摸事件了,这里应该是最好玩的地方了,在这里才可以让它鲜活起来嘛。当手指触摸滑动后,计算手指落下点、圆心和滑动到的点三点的夹角,根据正负判断顺时针还是逆时针。注意这里有个坑,因为刻度值是长短相间的,当目前处于短刻度时,旋转一个刻度处于长刻度,显示的没问题,但是当再次旋转一个刻度时,你会发现圆盘没有变化,这是因为每次绘画刻度时都是从长刻度开始画的,所以旋转时需要增加(顺时针)或者减少(逆时针)一个刻度与两个刻度相间的方法来显示效果。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mForbitRotate)
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = event.getX();
downY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
movingX = event.getX();
movingY = event.getY();
//计算下落点、圆心、移动点三点间的夹角
float angle = calcAngle(downX, downY, mWidth / 2, mHeight / 2, movingX, movingY) / 18;
if (Math.abs(angle) >= mPreAngle) {
if (angle > 0) {
if (touchRotate / mPreAngle % 2 == 0)
touchRotate = mPreAngle;
else
touchRotate = 2 * mPreAngle;
} else {
if (touchRotate / mPreAngle % 2 == 0)
touchRotate = -mPreAngle;
else
touchRotate = -2 * mPreAngle;
}
//旋转监听
if (null != mOnRotateListener) {
if (angle > 0 && movingY < mHeight / 2 || angle < 0 && movingY > mHeight / 2)
mOnRotateListener.onRotateRight(1);
else
mOnRotateListener.onRotateLeft(1);
}
downX = movingX;
downY = movingY;
invalidate();
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onTouchEvent(event);
}
为了计算角度又重学了一遍三角函数,毕竟以前学的都还给老师了…
/**
* 计算三个点的夹角
*
* @param p1X
* @param p1Y
* @param centerX
* @param centerY
* @param p2X
* @param p2Y
* @return
*/
private float calcAngle(float p1X, float p1Y, float centerX, float centerY, float p2X, float p2Y) {
float dis1 = calcDisBetweenPoint(p1X, p1Y, centerX, centerY);
float sin1 = (centerX - p1X) / dis1;
float dis2 = calcDisBetweenPoint(centerX, centerY, p2X, p2Y);
float sin2 = (p2X - centerX) / dis2;
return (float) ((Math.asin(sin1) + Math.asin(sin2)) / 2 * Math.PI * 360);
}
/**
* 计算两个点中间的距离
*
* @param x1
* @param y1
* @param x2
* @param y2
* @return
*/
private float calcDisBetweenPoint(float x1, float y1, float x2, float y2) {
float disX = Math.abs(x1 - x2);
float disY = Math.abs(y1 - y2);
return (float) Math.sqrt(disX * disX + disY * disY);
}
你会发现上面添加了监听,没错,有监听才有存在的意义嘛,下面是一些方法和监听初始化
/**
* 向左旋转一格
*/
public void rotateLeft() {
if (!mForbitRotate) {
if (touchRotate / mPreAngle % 2 == 0)
touchRotate = -mPreAngle;
else
touchRotate = -2 * mPreAngle;
if (null != mOnRotateListener)
mOnRotateListener.onRotateLeft(1);
invalidate();
}
}
/**
* 向右旋转一格
*/
public void rotateRight() {
if (!mForbitRotate) {
if (touchRotate / mPreAngle % 2 == 0)
touchRotate = mPreAngle;
else
touchRotate = 2 * mPreAngle;
if (null != mOnRotateListener)
mOnRotateListener.onRotateRight(1);
invalidate();
}
}
/**
* 设置是否禁止旋转
*
* @param forbitRotate
*/
public void setForbitRotate(boolean forbitRotate) {
mForbitRotate = forbitRotate;
}
/**
* 获取控件是否禁止了旋转
*
* @return
*/
public boolean getForbitRotate() {
return mForbitRotate;
}
//设置滚动监听
private onRotateListener mOnRotateListener;
public interface onRotateListener {
void onRotateLeft(int num);
void onRotateRight(int num);
}
public void setOnRotate(onRotateListener onRotateListener) {
mOnRotateListener = onRotateListener;
}
到此就完事了,控件的一些变量都没有提供属性设置,都是很简单的步骤,大家有需求的可以自己添加一下,使之适用性更强。觉得有用就赞一下下啦