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

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

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


下面開始編碼
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布局時被設置成寬高不等的長方形,所以需要在onMeasure函數(shù)里進行處理。
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)畫外圓
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;
// 內圓半徑
int radiusInner = getWidth() / 5;
// 圓心坐標(cx,cy)
float cx = getWidth() / 2;
float cy = getHeight() / 2;
if (null == mPaintForCircle) {
mPaintForCircle = new Paint();
}
mPaintForCircle.setAntiAlias(true);
mPaintForCircle.setStyle(Paint.Style.FILL);
// 畫外圓
mPaintForCircle.setColor(Color.argb(0x7f, 0x11, 0x11, 0x11));
canvas.drawCircle(cx, cy, radiusOuter, mPaintForCircle);
// TODO 畫內圓
}
}
4)畫內圓
內圓是運動的,它的位置與用戶的手指觸摸坐標有關,按照效果,用戶可以觸摸的范圍是包裹HandleView的ViewGroup(FrameLayout之類的),這里先寫個接口用于獲取手指觸摸坐標。
public class HandleView extends View {
private HandleReaction mHandleReaction;
public void setHandleReaction(HandleReaction handleReaction) {
mHandleReaction = handleReaction;
}
public interface HandleReaction {
/**
* 獲取用戶觸摸坐標
* @return
*/
float[] getTouchPosition();
}
// ...
}

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


由公式可得出,分母(開平方根那個數(shù)的值)是cx2和cy2都需要的公用的值,命名為ratio,計算代碼如下。
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)獲取觸摸坐標
建立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)坐標修正
表面上,上面內圓圓心計算代碼是正確的,但實際上,由于我們的HandleView通過接口從它的父布局那里拿到了觸摸坐標與HandleView內部坐標的參考坐標系不是同一個,他們相差一個HandleView相對于它父布局的getLeft與getTop的偏移,參照圖Android-View-Size,所以需要對計算代碼進行修正,如下:
// 經過修正后的內圓圓心坐標代碼
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源碼