如何在Android中制作一个方向轮盘详解

先上效果图

D1CF7B89-03C7-F535-ED20-322C0A476BF4.gif

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

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

计算滑块位置的原理:
  • 当触摸点在大圆与小圆的半径差之内:

    那么滑块的位置就是触摸点的位置

  • 当触摸点在大圆与小圆的半径差之外:

    已知大圆圆心坐标(cx,cy),大圆半径rout,小圆半径rinside,触摸点的坐标(px,py)

    求小圆的圆心(ax,ay)?

EAEEB082-9265-2FFE-31B6-B6743A70DEFF.png

作为经过九义的你我来说,这不就是一个简简单单的数学题嘛,很容易就求解出小圆的圆心位置了。 利用三角形相似:

ax−cxrout−rinside=px−cx(px−cx)2+(py−cy)2\frac{ax-cx}{rout-rinside} = \frac{px-cx}{\sqrt{(px-cx)^2+(py-cy)^2}}rout−rinsideax−cx​=(px−cx)2+(py−cy)2​px−cx​

ay−cyrout−rinside=py−cy(px−cx)2+(py−cy)2\frac{ay-cy}{rout-rinside} = \frac{py-cy}{\sqrt{(px-cx)^2+(py-cy)^2}}rout−rinsideay−cy​=(px−cx)2+(py−cy)2​py−cy​

通用性很好的接口:

滑块在圆中的位置,可以很好的用一个二位向量来表示,也可以用两个浮点的变量来表示;

xratio=ax−cxrout−rinside xratio = \frac{ax-cx}{rout-rinside}xratio=rout−rinsideax−cx​

yratio=ay−cyrout−rinside yratio = \frac{ay-cy}{rout-rinside}yratio=rout−rinsideay−cy​

这个接口就可以很好的表示了小圆在大圆的位置了,他们的取值范围是[-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, );
  }

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

  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 == ) {
        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>
写在最后:

这个是一个智能小车的安卓控制端的一部分demo,如果您也对此项目感兴趣,欢迎留言,讨论~

收藏 (0)
评论列表
正在载入评论列表...
我是有底线的
为您推荐
    暂时没有数据