前言
之前在开发项目中,有一个功用是,规划一个虚拟摇杆,操作大疆无人机飞行,在完成过程中感觉比较训练自定义View的能力,在此记载一下,本文中摇杆代码从项目中抽取出来从头完成,如下是程序运行图:
功用分析
本次自定义View功用需求如下:
1.摇杆制作
自定义View制作摇杆大小圆,手指移动时只改动小圆方位,当手指接触点在大圆外时,小圆圆心在大圆边际上,而且制作一条蓝色弧线,制作度数为小圆圆心方位向两侧延伸45度(一般UI规划的时分,会给特定的圆弧形图片,假如显现图片就需求将图片移动到小圆圆心方位,之后依据手指接触点与大圆圆心夹角来旋转图片,目前没有找到类似的圆弧图片,后期看能不能找到类似的)。
2.摇杆移动数据回来
回来摇杆移动产生的数据,依据这些数据操控飞行图片移动。在这里我回来的是飞机图片x,y坐标应该改动的值。这个值详细怎么获得,在下面代码完成中讲解。
3.飞机图片移动
飞机图片移动相对简略,只需求在接收到摇杆数据的时分,修正飞机图片制作方位,偏重绘即可,需求留意的当地是摇杆移动飞机超出View边界该怎样处理。
代码完成
摇杆制作和摇杆移动数据回来,经过自定义的RockerView内完成,飞机图片移动,经过自定义的FlyView完成,上述功用在RockerView和FlyView代码完成里边介绍。
摇杆(RockerView)
咱们能够先从摇杆怎么制作开端。
首先从RockerView开头声明一些制作需求一些变量,比方画笔,圆心坐标,手指接触点坐标,圆半径等变量。
在init()办法内对画笔款式,颜色,View默认宽高级数据进行设置。
在onMeasure()办法内获取View的宽高方法,该办法简略能够概略为,宽高有详细值或许为match_parent。宽高设置为MeasureSpec.getSize()办法获取的数据,之后宽高值取两者中最小值,当宽高值在xml设置为wrap_content时,宽高取默认值,之后在办法末尾经过setMeasuredDimension()设置宽高。
在onLayout()办法内,对制作圆等图像用到的变量进行赋值,例如,大圆圆心xy值,小圆圆心xy值,大小圆半径,制作蓝色圆弧矩形,RockerView宽高级数据。
之后是onDraw()办法,在该办法内制作大小圆,蓝色圆弧等图画。只不过蓝色圆弧需求加上判别条件来操控是否制作。
手指接触时制作小圆方位改动,则需求重写onTouchEvent()办法,当手指按下或移动时,需求更新手指接触点坐标,并判别手指接触点是否超出大圆,超出大圆时,需求核算小圆圆心方位,而且还需求核算手指接触点与圆心连线和x正半轴构成的夹角。而且经过接口回来摇杆移动的数据,飞机图片依据这些数据来移动。
制作代码简略介绍如上,下面临View内一些需求留意当地进行介绍。假如看到完好代码,里边有一个自定义办法是initAngle(),该办法代码如下:
/** 核算夹角度数,并完成小圆圆心最多至大圆边上 */
private void initAngle() {
radian = Math.atan2((touchY - bigCenterY), (touchX - bigCenterX));
angle = (float) (radian * (180 / Math.PI));//规模-180-180
isBigCircleOut = false;
if (bigCenterX != -1 && bigCenterY != -1) {//大圆中心xy已赋值
double rxr = (double) Math.pow(touchX - bigCenterX, 2) + Math.pow(touchY - bigCenterY, 2);
distance = Math.sqrt(rxr);//手点击点间隔大圆圆心间隔
smallCenterX = touchX;
smallCenterY = touchY;
if (distance > bigRadius) {//间隔大于半圆半径时,固定小圆圆心在大圆边际上
smallCenterX = (int) (bigRadius / distance * (touchX - bigCenterX)) + bigCenterX;
smallCenterY = (int) (bigRadius / distance * (touchY - bigCenterY)) + bigCenterX;
isBigCircleOut = true;
}
}
}
这个办法用在onTouchEvent()办法的手指按下与移动事情中应用,这个办法前两行代码是核算手指接触点与圆心连线和x正半轴构成的夹角取值,夹角取值规模如下图所示。
代码先经过Math.atan2(y,x)办法获取手指接触点与圆心连线和x正半轴之间的弧度制,获取弧度后经过(float) (radian * (180 / Math.PI))获取对应的度数,这里特别留意下Math.atan2(y,x)办法是y值在前,x在后。
此外这个办法还核算了手指接触点与大圆圆心间隔,以及判别手指接触点是否在大圆外,以及在大圆外时,获取在大圆边际上的小圆圆心的xy值。
在核算小圆圆心的坐标需求了解一个当地是,view完成过程中运用的坐标系是屏幕坐标系,屏幕坐标系是以View左上角为原点,原点左边是x的正半轴,原点下面是y正半轴,屏幕坐标系和数学坐标系是不一样。小圆圆心坐标获取原理,是依据三角形的类似原理获取,小圆圆心的坐标获取原理如下图所示:
在上图中能够看到小圆y坐标的获取,小圆x坐标获取与y获取类似。能够直接把公式套进去。关于摇杆制作的内容,至此差不多完成了,下面来处理回来摇杆移动数据的功用。
回来摇杆移动数据是经过自定义接口完成的。在接触事情回来摇杆移动数据的事情有手指按下与移动。咱们代码能够写为下面的方法(下面代码是伪代码)。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//回来摇杆移动数据的办法
break;
case MotionEvent.ACTION_UP:
...
break;
}
postInvalidate();
return true;
}
假如依照上面代码写法咱们会发现,当咱们手指按下不动的时分或许手指按下移动一会后手指不动,是不会触发ACTION_MOVE事情的,不触发这个事情,咱们就无法回来摇杆移动的数据,从而无法操控飞机改动方位。效果图如下
解决这个问题,需求运用Handler和Runnable,在Runnable的run办法内,完成接口办法,并调用本身。getFlyOffset()是传递摇杆移动数据的办法,代码如下:
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (isStart){
getFlyOffset();
mHandler.postDelayed(this,drawTime);
}
}
};
之后在手指按下与点击事情里边,先判别Handler有没有开端,若isStart为true,则isStart改为false,并移除mRunnable,之后isStart改为true,推迟16ms执行mRunnable,当手指抬起时,若Handler状况为开端,则修正状况为false并移除mRunnable,这样就解决了手指按下不移动时,传递摇杆数据,相关代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
...
initAngle();
getFlyOffset();
if (isStart) {
isStart = false;
mHandler.removeCallbacks(mRunnable);
}
isStart = true;
mHandler.postDelayed(mRunnable,drawTime);
break;
case MotionEvent.ACTION_UP:
...
if (isStart) {
mHandler.removeCallbacks(mRunnable);//有问题
isStart = false;
}
break;
}
postInvalidate();
return true;
}
至此摇杆相关功用介绍完毕,RockerView完好代码如下:
public class RockerView extends View {
private final int VELOCITY = 40;//飞机速度
private Paint smallCirclePaint;//小圆画笔
private Paint bigCirclePaint;//大圆画笔
private Paint sideCirclePaint;//大圆边框画笔
private Paint arcPaint;//圆弧画布
private int smallCenterX = -1, smallCenterY = -1;//制作小圆圆心 x,y坐标
private int bigCenterX = -1,bigCenterY = -1;//制作大圆圆心 x,y坐标
private int touchX = -1, touchY = -1;//接触点 x,y坐标
private float bigRadiusProportion = 69F / 110F;//大圆半径占view一半宽度的比例 用于获取大圆半径
private float smallRadiusProportion = 4F / 11F;//小圆半径占view一半宽度的比例
private float bigRadius = -1;//大圆半径
private float smallRadius = -1;//小圆半径
private double distance = -1; //手指按压点与大圆圆心的间隔
private double radian = -1;//弧度
private float angle = -1;//度数 -180~180
private int viewHeight,viewWidth;
private int defaultViewHeight, defaultViewWidth;
private RectF arcRect = new RectF();//制作蓝色圆弧用到矩形
private int drawArcAngle = 90;//圆弧制作度数
private int arcOffsetAngle = -45;//圆弧偏移度数
private int drawTime = 16;//告知flyView重绘的时间间隔 这里是16ms一次
private boolean isBigCircleOut = false;//接触点在大圆外
private boolean isStart = false;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (isStart){
getFlyOffset();
mHandler.postDelayed(this,drawTime);
}
}
};
public RockerView(Context context) {
super(context);
init(context);
}
public RockerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public RockerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
defaultViewWidth = DensityUtil.dp2px(context,220);
defaultViewHeight = DensityUtil.dp2px(context,220);
bigCirclePaint = new Paint();
bigCirclePaint.setStyle(Paint.Style.FILL);
bigCirclePaint.setStrokeWidth(5);
bigCirclePaint.setColor(Color.parseColor("#1AFFFFFF"));
bigCirclePaint.setAntiAlias(true);
smallCirclePaint = new Paint();
smallCirclePaint.setStyle(Paint.Style.FILL);
smallCirclePaint.setStrokeWidth(5);
smallCirclePaint.setColor(Color.parseColor("#4DFFFFFF"));
smallCirclePaint.setAntiAlias(true);
sideCirclePaint = new Paint();
sideCirclePaint.setStyle(Paint.Style.STROKE);
sideCirclePaint.setStrokeWidth(DensityUtil.dp2px(context, 1));
sideCirclePaint.setColor(Color.parseColor("#33FFFFFF"));
sideCirclePaint.setAntiAlias(true);
arcPaint = new Paint();
arcPaint.setColor(Color.parseColor("#FF5DA9FF"));
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setStrokeWidth(5);
arcPaint.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取视图的宽高的测量方法
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width,height;
if (widthMode == MeasureSpec.EXACTLY){
width = widthSize;
}else {
width = defaultViewWidth;
}
if (heightMode == MeasureSpec.EXACTLY){
height = heightSize;
}else {
height = defaultViewHeight;
}
width = Math.min(width,height);
height = width;
//设置视图的宽度和高度
setMeasuredDimension(width,height);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
bigCenterX = getWidth() / 2;
bigCenterY = getHeight() / 2;
smallCenterX = bigCenterX;
smallCenterY = bigCenterY;
bigRadius = bigRadiusProportion * Math.min(bigCenterX, bigCenterY);
smallRadius = smallRadiusProportion * Math.min(bigCenterX, bigCenterY);
arcRect.set(bigCenterX-bigRadius,bigCenterY-bigRadius,bigCenterX+bigRadius,bigCenterY+bigRadius);
viewHeight = getHeight();
viewWidth = getWidth();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(bigCenterX, bigCenterY, bigRadius, bigCirclePaint);
canvas.drawCircle(smallCenterX, smallCenterY, smallRadius, smallCirclePaint);
canvas.drawCircle(bigCenterX, bigCenterY, bigRadius, sideCirclePaint);
if (isBigCircleOut) {
canvas.drawArc(arcRect,angle+arcOffsetAngle,drawArcAngle,false,arcPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
touchX = (int) event.getX();
touchY = (int) event.getY();
initAngle();
getFlyOffset();
if (isStart) {
isStart = false;
mHandler.removeCallbacks(mRunnable);
}
isStart = true;
mHandler.postDelayed(mRunnable,drawTime);
break;
case MotionEvent.ACTION_UP:
smallCenterX = bigCenterX;
smallCenterY = bigCenterY;
isBigCircleOut = false;
if (isStart) {
mHandler.removeCallbacks(mRunnable);//有问题
isStart = false;
}
break;
}
postInvalidate();
return true;
}
/** 核算夹角度数,并完成小圆圆心最多至大圆边上 */
private void initAngle() {
radian = Math.atan2((touchY - bigCenterY), (touchX - bigCenterX));
angle = (float) (radian * (180 / Math.PI));//规模-180-180
isBigCircleOut = false;
if (bigCenterX != -1 && bigCenterY != -1) {//大圆中心xy已赋值
double rxr = (double) Math.pow(touchX - bigCenterX, 2) + Math.pow(touchY - bigCenterY, 2);
distance = Math.sqrt(rxr);//手点击点间隔大圆圆心间隔
smallCenterX = touchX;
smallCenterY = touchY;
if (distance > bigRadius) {//间隔大于半圆半径时,固定小圆圆心在大圆边际上
smallCenterX = (int) (bigRadius / distance * (touchX - bigCenterX)) + bigCenterX;
smallCenterY = (int) (bigRadius / distance * (touchY - bigCenterY)) + bigCenterX;
isBigCircleOut = true;
}
}
}
/** 获取飞行偏移量 */
private void getFlyOffset() {
float x = (smallCenterX - bigCenterX) * 1.0f / viewWidth * VELOCITY;
float y = (smallCenterY - bigCenterY) * 1.0f / viewHeight * VELOCITY;
onRockerListener.getDate(this, x, y);
}
/**
* pX,pY为手指按点坐标减view的坐标
*/
public interface OnRockerListener {
public void getDate(RockerView rocker, final float pX, final float pY);
}
private OnRockerListener onRockerListener;
public void getDate(final OnRockerListener onRockerListener) {
this.onRockerListener = onRockerListener;
}
}
飞机(FlyView)
飞机图片移动相对简略,完成原理是在自定义View里边,经过改动制作图片办法(drawBitmap()办法)里的left,top值来模仿飞机移动。FlyView完成代码如下:
public class FlyView extends View {
private Paint mPaint;
private Bitmap mBitmap;
private int viewHeight, viewWidth;
private int imgHeight, imgWidth;
private int left, top;
public FlyView(Context context) {
super(context);
init(context);
}
public FlyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public FlyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
void init(Context context) {
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.fly);
imgHeight = mBitmap.getHeight();
imgWidth = mBitmap.getWidth();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewHeight = h;
viewWidth = w;
left = w / 2 - imgHeight / 2;
top = h / 2 - imgWidth / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, left, top, mPaint);
}
/** 移动图片 */
public void move(float x, float y) {
left += x;
top += y;
if (left < 0) {
left = 0;
}else if (left > viewWidth - imgWidth) {
left = viewWidth - imgWidth;
}
if (top < 0) {
top = 0;
} else if (top > viewHeight - imgHeight) {
top = viewHeight - imgHeight;
}
postInvalidate();
}
}
在Activity或许Fragment里边临View设置代码(kotlin)如下:
binding.viewRocker.getDate { _, pX, pY ->
binding.viewFly.move(pX, pY)
}
飞机图片如下:
总结
摇杆整体完成没有太复杂的逻辑,比较简单混的当地,或许是屏幕坐标系和数学坐标系能不能转过弯来。印象中好像能够经过Matrix将坐标改换,但一时间想不起来怎样完成,后面了解下Matrix相关内容。
关于虚拟摇杆完成有很多方法,我写的这个不是最优的方法,虚拟摇杆有些需求没有接触到,在代码完成中或许比较简略,小伙伴们看到文章不足的当地,能够留言告知我,一同学习交流下。