在android中如何制作一个方向轮盘

先上效果图

Screenrecorder-2021-09-13-09-55-26-155.gif

原理很简单,其实就是一个自定义的view

通过观察,很容易发现,我们自己的轮盘就两个view需要绘制,一个是外面的圆盘,一个就随手指移动的滑块; 外面的圆盘很好绘制,内部的滑块则需要采集手指的位置,根据手指的位置计算出滑块在大圆内的位置; 最后,我们做的UI不是单纯做一个UI吧,肯定还是要用于实际应用中去,所以要加一个通用性很好的回调.

计算滑块位置的原理:

  • 当触摸点在大圆与小圆的半径差之内:
    那么滑块的位置就是触摸点的位置
  • 当触摸点在大圆与小圆的半径差之外:
    已知大圆圆心坐标(cx,cy),大圆半径rout,小圆半径rinside,触摸点的坐标(px,py)
    求小圆的圆心(ax,ay)?

image.png

作为经过九义的你我来说,这不就是一个简简单单的数学题嘛,很容易就求解出小圆的圆心位置了。 利用三角形相似:
\frac{ax-cx}{rout-rinside} = \frac{px-cx}{\sqrt{(px-cx)^2+(py-cy)^2}}routrinsideaxcx=(pxcx)2+(pycy)2pxcx
\frac{ay-cy}{rout-rinside} = \frac{py-cy}{\sqrt{(px-cx)^2+(py-cy)^2}}routrinsideaycy=(pxcx)2+(pycy)2pycy

通用性很好的接口:

滑块在圆中的位置,可以很好的用一个二位向量来表示,也可以用两个浮点的变量来表示;
xratio = \frac{ax-cx}{rout-rinside}xratio=routrinsideaxcx
yratio = \frac{ay-cy}{rout-rinside}yratio=routrinsideaycy

这个接口就可以很好的表示了小圆在大圆的位置了,他们的取值范围是[-1,1]

小技巧:

为了小圆能始终在脱手后回到终点位置,我们设计了一个动画,当然,实际情况中有一种情况是,你移动到某个位置后,脱手后位置不能动,那你禁用这个动画即可。

代码部分

tips:代码部分的变量名与原理的变量名有出入

public class ControllerView extends View implements View.OnTouchListener {
private Paint borderPaint = new Paint();//大圆的画笔
private Paint fingerPaint = new Paint();//小圆的画笔
private float radius = 160;//默认大圆的半径
private float centerX = radius;//大圆中心点的位置cx
private float centerY = radius;//大圆中心点的位置cy
private float fingerX = centerX, fingerY = centerY;//小圆圆心的位置(ax,ay)
private float lastX = fingerX, lastY = fingerY;//小圆自动回归中点动画中上一点的位置
private float innerRadius = 30;//默认小圆半径
private float radiusBorder = (radius - innerRadius);//大圆减去小圆的半径
private ValueAnimator positionAnimator;//自动回中的动画
private MoveListener moveListener;//移动回调的接口

public ControllerView(Context context) {
super(context);
init(context, null, 0);
}

public ControllerView(Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}

public ControllerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}

//初始化
private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ControllerView);
int fingerColor = typedArray.getColor(R.styleable.ControllerView_fingerColor,
Color.parseColor("#3fffffff"));
int borderColor = typedArray.getColor(R.styleable.ControllerView_borderColor,
Color.GRAY);
radius = typedArray.getDimension(R.styleable.ControllerView_radius, 220);
innerRadius = typedArray.getDimension(R.styleable.ControllerView_fingerSize, innerRadius);
borderPaint.setColor(borderColor);
fingerPaint.setColor(fingerColor);
lastX = lastY = fingerX = fingerY = centerX = centerY = radius;
radiusBorder = radius - innerRadius;
typedArray.recycle();
}
setOnTouchListener(this);
positionAnimator = ValueAnimator.ofFloat(1);
positionAnimator.addUpdateListener(animation -> {
Float aFloat = (Float) animation.getAnimatedValue();
changeFingerPosition(lastX + (centerX - lastX) * aFloat, lastY + (centerY - lastY) * aFloat);
});
}

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(getActualSpec(widthMeasureSpec), getActualSpec(heightMeasureSpec));
}


//处理wrapcontent的测量
//默认wrapcontent,没有做matchParent,指定大小的适配
//view实际的大小是通过大圆半径确定的
public int getActualSpec(int spec) {
int mode = MeasureSpec.getMode(spec);
int len = MeasureSpec.getSize(spec);
switch (mode) {
case MeasureSpec.AT_MOST:
len = (int) (radius * 2);
break;
}
return MeasureSpec.makeMeasureSpec(len, mode);
}

//绘制
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(centerX, centerY, radius, borderPaint);
canvas.drawCircle(fingerX, fingerY, innerRadius, fingerPaint);
}

@Override public boolean onTouch(View v, MotionEvent event) {
float evx = event.getX(), evy = event.getY();
float deltaX = evx - centerX, deltaY = evy - centerY;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//圆外按压不生效
if (deltaX * deltaX + deltaY * deltaY > radius * radius) {
break;
}
case MotionEvent.ACTION_MOVE:
//如果触摸点在圆外
if (Math.abs(deltaX) > radiusBorder || Math.abs(deltaY) > radiusBorder) {
float distance = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
changeFingerPosition(centerX + (deltaX * radiusBorder / distance),
centerY + (deltaY * radiusBorder / distance));
} else { //如果触摸点在圆内
changeFingerPosition(evx, evy);
}
positionAnimator.cancel();
break;
case MotionEvent.ACTION_UP:
positionAnimator.setDuration(1000);
positionAnimator.start();
break;
}
return true;
}

/**
* 改变位置的回调出来
*/

private void changeFingerPosition(float fingerX, float fingerY) {
this.fingerX = fingerX;
this.fingerY = fingerY;
if (moveListener != null) {
float r = radius - innerRadius;
if (r == 0) {
invalidate();
return;
}
moveListener.move((fingerX - centerX) / r, (fingerY - centerY) / r);
}
invalidate();
}

@Override protected void finalize() throws Throwable {
super.finalize();
positionAnimator.removeAllListeners();
}

public void setMoveListener(
MoveListener moveListener) {
this.moveListener = moveListener;
}

/**
*回调事件的接口
*
**/

public interface MoveListener {
void move(float dx, float dy);
}
}

style.xml

<declare-styleable name="ControllerView">
<attr name="fingerColor" format="color" />
<attr name="borderColor" format="color" />
<attr name="fingerSize" format="dimension" />
<attr name="radius" format="dimension" />
</declare-styleable>

0 个评论

要回复文章请先登录注册