在android中如何制作一個(gè)方向輪盤(pán)

先上效果圖

效果

原理很簡(jiǎn)單,其實(shí)就是一個(gè)自定義的view

通過(guò)觀察,很容易發(fā)現(xiàn),我們自己的輪盤(pán)就兩個(gè)view需要繪制,一個(gè)是外面的圓盤(pán),一個(gè)就隨手指移動(dòng)的滑塊;
外面的圓盤(pán)很好繪制,內(nèi)部的滑塊則需要采集手指的位置,根據(jù)手指的位置計(jì)算出滑塊在大圓內(nèi)的位置;
最后,我們做的UI不是單純做一個(gè)UI吧,肯定還是要用于實(shí)際應(yīng)用中去,所以要加一個(gè)通用性很好的回調(diào).

計(jì)算滑塊位置的原理:

  • 當(dāng)觸摸點(diǎn)在大圓與小圓的半徑差之內(nèi):
    那么滑塊的位置就是觸摸點(diǎn)的位置
  • 當(dāng)觸摸點(diǎn)在大圓與小圓的半徑差之外:
    已知大圓圓心坐標(biāo)(cx,cy),大圓半徑rout,小圓半徑rinside,觸摸點(diǎn)的坐標(biāo)(px,py)
    求小圓的圓心(ax,ay)?
原理

作為經(jīng)過(guò)九義的你我來(lái)說(shuō),這不就是一個(gè)簡(jiǎn)簡(jiǎn)單單的數(shù)學(xué)題嘛,很容易就求解出小圓的圓心位置了。
利用三角形相似:
\frac{ax-cx}{rout-rinside} = \frac{px-cx}{\sqrt{(px-cx)^2+(py-cy)^2}}
\frac{ay-cy}{rout-rinside} = \frac{py-cy}{\sqrt{(px-cx)^2+(py-cy)^2}}

通用性很好的接口:

滑塊在圓中的位置,可以很好的用一個(gè)二位向量來(lái)表示,也可以用兩個(gè)浮點(diǎn)的變量來(lái)表示;
xratio = \frac{ax-cx}{rout-rinside}
yratio = \frac{ay-cy}{rout-rinside}
這個(gè)接口就可以很好的表示了小圓在大圓的位置了,他們的取值范圍是[-1,1]

小技巧:

為了小圓能始終在脫手后回到終點(diǎn)位置,我們?cè)O(shè)計(jì)了一個(gè)動(dòng)畫(huà),當(dāng)然,實(shí)際情況中有一種情況是,你移動(dòng)到某個(gè)位置后,脫手后位置不能動(dòng),那你禁用這個(gè)動(dòng)畫(huà)即可。

代碼部分

tips:代碼部分的變量名與原理的變量名有出入

public class ControllerView extends View implements View.OnTouchListener {
  private Paint borderPaint = new Paint();//大圓的畫(huà)筆
  private Paint fingerPaint = new Paint();//小圓的畫(huà)筆
  private float radius = 160;//默認(rèn)大圓的半徑
  private float centerX = radius;//大圓中心點(diǎn)的位置cx
  private float centerY = radius;//大圓中心點(diǎn)的位置cy
  private float fingerX = centerX, fingerY = centerY;//小圓圓心的位置(ax,ay)
  private float lastX = fingerX, lastY = fingerY;//小圓自動(dòng)回歸中點(diǎn)動(dòng)畫(huà)中上一點(diǎn)的位置
  private float innerRadius = 30;//默認(rèn)小圓半徑
  private float radiusBorder = (radius - innerRadius);//大圓減去小圓的半徑
  private ValueAnimator positionAnimator;//自動(dòng)回中的動(dòng)畫(huà)
  private MoveListener moveListener;//移動(dòng)回調(diào)的接口

  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的測(cè)量
  //默認(rèn)wrapcontent,沒(méi)有做matchParent,指定大小的適配
  //view實(shí)際的大小是通過(guò)大圓半徑確定的
  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:
        //如果觸摸點(diǎn)在圓外
        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 { //如果觸摸點(diǎn)在圓內(nèi)
          changeFingerPosition(evx, evy);
        }
        positionAnimator.cancel();
        break;
      case MotionEvent.ACTION_UP:
        positionAnimator.setDuration(1000);
        positionAnimator.start();
        break;
    }
    return true;
  }

  /**
   * 改變位置的回調(diào)出來(lái)
   */
  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;
  }

  /**
    *回調(diào)事件的接口
    *
   **/
  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>

寫(xiě)在最后:

歡迎留言,討論~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

  • Quartz2D 簡(jiǎn)介 Quartz2D是二維(平面)的繪圖引擎(經(jīng)包裝的函數(shù)庫(kù),方便開(kāi)發(fā)者使用。也就是說(shuō)蘋(píng)果幫我...
    iOS_Cqlee閱讀 671評(píng)論 0 2
  • 一、前言 最近項(xiàng)目里面需要用到多個(gè)seekbar并且需要顯示不同的精度,于是參考了部分博客模仿華為的天氣刻度盤(pán)添加...
    IT魔幻師閱讀 9,983評(píng)論 3 83
  • 寫(xiě)作原因:Android進(jìn)階過(guò)程中有一個(gè)繞不開(kāi)的話(huà)題——自定義View。這一塊是安卓程序員更好地實(shí)現(xiàn)功能自主化必須...
    RoadToGeek閱讀 2,636評(píng)論 0 18
  • 1> 水印PPT簡(jiǎn)介 * 圖片水印作用:防止他人盜取圖片,加一些Logo,生成一張新的圖片。 * 怎么生成新的圖片...
    Hevin_Chen閱讀 292評(píng)論 0 0
  • 通過(guò)這個(gè)文章你可以學(xué)習(xí)到: 繪制圓形圖案 繪制直線(xiàn) 鎖屏不就是上面兩個(gè)+觸摸嗎?(<( ̄3 ̄)> ) 最終效果圖:...
    return_toLife閱讀 1,214評(píng)論 0 3

友情鏈接更多精彩內(nèi)容