Android自定義View系列之《貪吃蛇大作戰(zhàn)》方向操作鍵效果實(shí)現(xiàn)

前段時(shí)間很火的一款貪吃蛇游戲,可玩性很高,幾點(diǎn)規(guī)則改造就將傳統(tǒng)的貪吃蛇改活了,當(dāng)時(shí)我拿過(guò)13000多分,還嘚瑟了很久。今天來(lái)個(gè)教程10分鐘實(shí)現(xiàn)它。。。額,不是,實(shí)現(xiàn)它的方向操作按鈕效果,看下圖左下角的那兩個(gè)同心圓。

貪吃蛇大作戰(zhàn)

用戶手指觸碰屏幕任意位置,內(nèi)圓就往用戶手指那個(gè)方向移動(dòng)至外圓邊界內(nèi)切,實(shí)現(xiàn)后效果圖如下所示。

效果圖

先看兩張圖,分別是Android坐標(biāo)系與Android View尺寸函數(shù)的含義,其中,Android坐標(biāo)系往右x軸遞增,往下y軸遞增,不多說(shuō)。

Android坐標(biāo)系
Android-View-Size

下面開(kāi)始編碼

1)創(chuàng)建HandleView類,繼承自View

/**
 * 貪吃蛇大作戰(zhàn)方向控制按鈕效果
 */
public class HandleView extends View {

  public HandleView(Context context) {
    this(context, null);
  }

  public HandleView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }

  public HandleView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

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

  @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
  }

  @Override protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // TODO
  }
}

上述代碼可以作為幾乎所有自定義View的初始代碼模板。

方向操作按鈕等寬等高,我們不想它在xml布局時(shí)被設(shè)置成寬高不等的長(zhǎng)方形,所以需要在onMeasure函數(shù)里進(jìn)行處理。

2)重載onMeasure

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(getDefaultSize2(getSuggestedMinimumWidth(), widthMeasureSpec),
  getDefaultSize2(getSuggestedMinimumHeight(), heightMeasureSpec));
  int childWidthSize = getMeasuredWidth();
  widthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
  heightMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
 * Compare to: {@link android.view.View#getDefaultSize(int, int)}
 * If mode is AT_MOST, return the child size instead of the parent size
 * (unless it is too big).
 */
private static int getDefaultSize2(int size, int measureSpec) {
  int result = size;
  int specMode = MeasureSpec.getMode(measureSpec);
  int specSize = MeasureSpec.getSize(measureSpec);
  
  switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
      result = size;
      break;
    case MeasureSpec.AT_MOST:
      result = Math.min(size, specSize);
      break;
    case MeasureSpec.EXACTLY:
      result = specSize;
      break;
  }
  return result;
}

3)畫(huà)外圓

public class HandleView extends View {
  private Paint mPaintForCircle;
  // ...
  @Override protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // 背景透明
    canvas.drawColor(Color.TRANSPARENT);
    // 外圓半徑
    int radiusOuter = getWidth() / 2;
    // 內(nèi)圓半徑
    int radiusInner = getWidth() / 5;

    // 圓心坐標(biāo)(cx,cy)
    float cx = getWidth() / 2;
    float cy = getHeight() / 2;

    if (null == mPaintForCircle) {
      mPaintForCircle = new Paint();
    }
    mPaintForCircle.setAntiAlias(true);
    mPaintForCircle.setStyle(Paint.Style.FILL);

    // 畫(huà)外圓
    mPaintForCircle.setColor(Color.argb(0x7f, 0x11, 0x11, 0x11));
    canvas.drawCircle(cx, cy, radiusOuter, mPaintForCircle);
    // TODO 畫(huà)內(nèi)圓
  }
}

4)畫(huà)內(nèi)圓

內(nèi)圓是運(yùn)動(dòng)的,它的位置與用戶的手指觸摸坐標(biāo)有關(guān),按照效果,用戶可以觸摸的范圍是包裹HandleView的ViewGroup(FrameLayout之類的),這里先寫(xiě)個(gè)接口用于獲取手指觸摸坐標(biāo)。

public class HandleView extends View {

  private HandleReaction mHandleReaction;

  public void setHandleReaction(HandleReaction handleReaction) {
    mHandleReaction = handleReaction;
  }

  public interface HandleReaction {
    /**
     * 獲取用戶觸摸坐標(biāo)
     * @return
     */
    float[] getTouchPosition();
  }
  
  // ...
}
示意圖

內(nèi)圓半徑固定,位置由圓心的坐標(biāo)決定,所以關(guān)鍵是得出內(nèi)圓的圓心坐標(biāo)隨用戶手指的觸摸坐標(biāo)的變化而變化的函數(shù)關(guān)系,讓我們建立方程式:(用工具畫(huà)太花時(shí)間,將就手畫(huà),見(jiàn)諒!P.S.好像回到中學(xué)有木有)

建立方程組
結(jié)果

由公式可得出,分母(開(kāi)平方根那個(gè)數(shù)的值)是cx2和cy2都需要的公用的值,命名為ratio,計(jì)算代碼如下。

float[] touchPosition = mHandleReaction.getTouchPosition();
double ratio = (radiusOuter - radiusInner) / 
  Math.sqrt(
    Math.pow(touchPosition[0] - cx, 2) + 
    Math.pow(touchPosition[1] - cy, 2));
float cx2 = (float) (ratio * (touchPosition[0] - cx) + cx);
float cy2 = (float) (ratio * (touchPosition[1] - cy) + cy);

mPaintForCircle.setColor(Color.argb(0xff, 0x11, 0x11, 0x11));
canvas.drawCircle(cx2, cy2, radiusInner, mPaintForCircle);

5)獲取觸摸坐標(biāo)
建立MainActivity,布局文件就不給出了,文末附帶源碼地址。

public class MainActivity extends AppCompatActivity
    implements HandleView.HandleReaction, View.OnTouchListener {

  private float[] mTouchPosition = null;
  private HandleView mHandleView;

  @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    FrameLayout frameLayout = (FrameLayout) findViewById(R.id.frameLayout);
    frameLayout.setOnTouchListener(this);
    mHandleView = (HandleView) findViewById(R.id.handleView);
    mHandleView.setHandleReaction(this);
  }

  @Override public boolean onTouch(View view, MotionEvent motionEvent) {
    switch (motionEvent.getAction()) {
      case MotionEvent.ACTION_DOWN:
      case MotionEvent.ACTION_MOVE: {
        mTouchPosition = new float[2];
        mTouchPosition[0] = motionEvent.getX();
        mTouchPosition[1] = motionEvent.getY();
        mHandleView.invalidate();
        return true;
      }
      case MotionEvent.ACTION_UP: {
        mTouchPosition = null;
        mHandleView.invalidate();
        return true;
      }
    }
    return false;
  }

  @Override public float[] getTouchPosition() {
    return mTouchPosition;
  }
}

6)坐標(biāo)修正
表面上,上面內(nèi)圓圓心計(jì)算代碼是正確的,但實(shí)際上,由于我們的HandleView通過(guò)接口從它的父布局那里拿到了觸摸坐標(biāo)與HandleView內(nèi)部坐標(biāo)的參考坐標(biāo)系不是同一個(gè),他們相差一個(gè)HandleView相對(duì)于它父布局的getLeft與getTop的偏移,參照?qǐng)DAndroid-View-Size,所以需要對(duì)計(jì)算代碼進(jìn)行修正,如下:

// 經(jīng)過(guò)修正后的內(nèi)圓圓心坐標(biāo)代碼
double ratio = (radiusOuter - radiusInner) / 
  Math.sqrt(
    Math.pow(touchPosition[0] - cx - getLeft(), 2) + 
    Math.pow(touchPosition[1] - cy - getTop(), 2));
float cx2 = (float) (ratio * (touchPosition[0] - cx - getLeft()) + cx);
float cy2 = (float) (ratio * (touchPosition[1] - cy - getTop()) + cy);

最后附上源碼地址
GitHub源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容