Let’s start with the renderings

The principle is very simple, in fact, is a custom view

Through observation, it is easy to find that our own roulette wheel only has two views to draw, one is the outer disk, one is pointing to the moving slider; The outer disk is very easy to draw, the inner slider needs to collect the position of the finger, according to the position of the finger to calculate the position of the slider in the large circle; Finally, the UI we’re making is not just a UI, it’s definitely going to be used in the real world, so we’re going to add a callback that’s universal.

Principle of calculating slider position:

  • When the touch point is within the radius difference between the large circle and the small circle: then the position of the slider is the position of the touch point
  • When the touch point is outside the radius difference between the large circle and the small circle:

    We know the coordinates of the center of the large circle (cx,cy), the radius of the large circle rout, the radius of the small circle rinside, and the coordinates of the touch point (px,py).

    What is the center of the small circle (Ax,ay)?

As for you and me, it is a simple math problem. It is easy to solve the center position of the small circle. Using triangle similarity: Ax – cxrout – rinside = cx (cx) px – px – 2 + (py – cy) 2 \ frac {ax – cx} {rout – rinside} = \ frac {px – cx} {\ SQRT {(px – cx) ^ 2 + (py – cy) ^ 2}} rout – rinsideax – cx = (cx) px – 2 + (py – cy) 2 px – cx Ay – cyrout – rinside = py – cy (px cx) – 2 + (py – cy) 2 \ frac {ay – cy} {rout – rinside} = \ frac {p y – cy} {\ SQRT {(px – cx) ^ 2 + (py – cy) ^ 2}} rout – rinsideay – cy = (cx) px – 2 + (py – cy) 2 py – cy

Interface with good versatility:

The position of the slider in the circle can be well represented by a two-digit vector, or by two floating point variables; 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

This interface is a good representation of the position of the smaller circle in the larger circle. Their values range from [-1,1].

Tip:

In order for the circle to always return to the end position after release, we designed an animation. Of course, in reality, there is a situation where you move to a certain position and the position cannot move after release, then you can disable the animation.

Code section

Tips: Variable names in the code section differ from those in the principle

public class ControllerView extends View implements View.OnTouchListener {
  private Paint borderPaint = new Paint();// Big round brush
  private Paint fingerPaint = new Paint();// Small round brushes
  private float radius = 160;// The radius of the large circle is default
  private float centerX = radius;// The center of the circle is cx
  private float centerY = radius;// The center of the circle is cy
  private float fingerX = centerX, fingerY = centerY;// The center of the circle (ax,ay)
  private float lastX = fingerX, lastY = fingerY;// The circle automatically returns to the position of the previous point in the midpoint animation
  private float innerRadius = 30;// Default small circle radius
  private float radiusBorder = (radius - innerRadius);// The radius of the larger circle minus the smaller circle
  private ValueAnimator positionAnimator;// Automatic callback animation
  private MoveListener moveListener;// Move the interface for the callback

  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);
  }

  / / initialization
  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));
  }


  // Process the wrapcontent measurement
  // Default wrapContent, no matchParent, specifies the size of the match
  // The actual size of the view is determined by the radius of the circle
  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);
  }

  / / to draw
  @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:
        // Circular external press does not work
        if (deltaX * deltaX + deltaY * deltaY > radius * radius) {
          break;
        }
      case MotionEvent.ACTION_MOVE:
        // If the touch point is outside the circle
        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 { // If the touch point is inside the circle
          changeFingerPosition(evx, evy);
        }
        positionAnimator.cancel();
        break;
      case MotionEvent.ACTION_UP:
        positionAnimator.setDuration(1000);
        positionAnimator.start();
        break;
    }
    return true;
  }

  /** * Change the position of the callback out */
  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(a) throws Throwable {
    super.finalize();
    positionAnimator.removeAllListeners();
  }

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

  /** * callback event interface ** */
  public interface MoveListener {
    void move(float dx, float dy); }}Copy the code

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>
Copy the code

Write at the end:

This is a smart car android control terminal part of the demo, if you are interested in this project, welcome to leave a message, discussion ~