摘要: 上次我们自定义了一个固定位置摇杆,此摇杆无法调整杆量,且位置固定,这次我们就来定义一个可以自由调整杆量和变换位置的虚拟摇杆。
下面就写一下需要实现此虚拟摇杆的步骤:
一丶初始化资源
我们需要美国手日本手的背景图片资源,滑动的点,对应的区域移动区域范围初始化,虚拟摇杆外围光影背景资源初始化。由于摇杆的左右手以及日本手和美国手的资源都不一样,所以通过外部传入的方式初始化。
/**
* @param bgId 背景图片资源id
* @param pointId 点的图片资源id
*/
public void initView(int bgId, int pointId) {
if (bgId > 0) {
bmpBg = BitmapFactory.decodeResource(context.getResources(), bgId);
if (null != bmpBg) {
circleWidth = bmpBg.getWidth();
}
}
if (pointId > 0) {
bmpPoint = BitmapFactory.decodeResource(context.getResources(), pointId);
}
//上下左右的滑动到对应区域的光线
leftLight = BitmapFactory.decodeResource(context.getResources(), R.mipmap.left_move_light);
rightLight = BitmapFactory.decodeResource(context.getResources(), R.mipmap.right_move_light);
topLight = BitmapFactory.decodeResource(context.getResources(), R.mipmap.top_move_light);
bottomLight = BitmapFactory.decodeResource(context.getResources(), R.mipmap.bottom_move_light);
ViewTreeObserver vto = getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
relayoutRc();
}
});
initRegion(context.getResources());
}
private void relayoutRc() {
viewH = getLayoutParams().height;
viewW = viewH;
circleWidth = bmpBg.getWidth();
scaling = (float) (viewW - DimensionUtil.dp2px(context, 12)) / circleWidth;
if (viewW > 0 && circleWidth > 0) {
scaling = (float) (viewW - DimensionUtil.dp2px(context, 12)) / circleWidth;
if (null != bmpBg) {
bmpBg = BitmapUtil.resizeBitmap(bmpBg, scaling);
}
if (null != bmpPoint) {
bmpPoint = BitmapUtil.resizeBitmap(bmpPoint, scaling);
}
if (null != bmpBg && null != bmpPoint) {
// Set circle center point
bgCenterX = (viewW / 2);
bgCenterY = (viewW / 2);
bgR = bmpBg.getWidth() / 2;
pointR = bmpPoint.getWidth() / 2;
bgR -= pointR;
bgHalfWidth = (int) ((Math.sin((45f / 180f) * Math.PI)) * bgR);
pointCenterX = POINT_CENTER_X = bgCenterX;
pointCenterY = POINT_CENTER_Y = bgCenterY;
requestLayout();
}
}
}
/**
* 初始化对应的多个区域
* @param resources 资源
*/
public void initRegion(Resources resources) {
float rockerWidth = resources.getDimensionPixelSize(R.dimen.rocker_width);
//四个扇形Path 描述四个扇形区域
float radius = rockerWidth / 2;
//计算45度扇形的起始点
float cos45 = (float) (Math.cos(45) * rockerWidth);
rectF = new RectF(0, 0,
rockerWidth, rockerWidth);
//四个方向按钮区域
left = new Path();
left.lineTo(-cos45, cos45);
left.addArc(rectF, 135, 90f);
left.lineTo(radius, radius);
top = new Path();
top.lineTo(-cos45, -cos45);
top.addArc(rectF, 225, 90f);
top.lineTo(radius, radius);
right = new Path();
right.lineTo(cos45, -cos45);
right.addArc(rectF, 315, 90f);
right.lineTo(radius, radius);
bottom = new Path();
bottom.lineTo(cos45, cos45);
bottom.addArc(rectF, 45, 90f);
bottom.lineTo(radius, radius);
//各个区域
regionLeft = new Region();
left.computeBounds(rectF, true);
regionLeft.setPath(left, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
regionTop = new Region();
top.computeBounds(rectF, true);
regionTop.setPath(top, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
regionRight = new Region();
right.computeBounds(rectF, true);
regionRight.setPath(right, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
regionBottom = new Region();
bottom.computeBounds(rectF, true);
regionBottom.setPath(bottom, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
//中间圆型区域
float innerRadiux = (float) (radius * 0.3);
rectFInner = new RectF(-innerRadiux, -innerRadiux,
innerRadiux, innerRadiux);
inner = new Path();
inner.addCircle(radius, radius, (float) (radius * 0.3), Path.Direction.CW);
regionInner = new Region();
inner.computeBounds(rectFInner, true);
regionInner.setPath(inner, new Region((int) rectFInner.left, (int) rectFInner.top, (int) rectFInner.right, (int) rectFInner.bottom));
}
二丶绘制虚拟摇杆
- 绘制背景,周围预留6dp要画位置的光影背景
// 画背景
if (null != bmpBg) {
canvas.drawBitmap(bmpBg, DimensionUtil.dp2px(context, 6), DimensionUtil.dp2px(context, 6), null);
}
- 绘制点,根据点的中心位置以及点的半径来准备绘制点的图形位置
//画点的位置
if (null != bmpPoint) {
canvas.drawBitmap(bmpPoint, (pointCenterX - pointR), (pointCenterY - pointR), null);
}
- 绘制光影背景,根据点的位置,来匹配点移动到了哪个区域,从而绘制哪个区域的光影背景。
//根据点的位置来画摇杆光影背景
matchingRegion(pointCenterX - pointR + bmpPoint.getWidth() / 2, pointCenterY - pointR + bmpPoint.getHeight() / 2);
switch (selectedPosition) {
case INNER://点位置在小圆内部,不进行绘制
break;
case LEFT://绘制左边光影背景
canvas.drawBitmap(leftLight, 0, (getHeight() - leftLight.getHeight()) / 2, paint);
break;
case TOP://绘制上边光影背景
canvas.drawBitmap(topLight, (getWidth() - topLight.getWidth()) / 2, 0, paint);
break;
case RIGHT://绘制右边光影背景
canvas.drawBitmap(rightLight, getWidth() - rightLight.getWidth(), (getHeight() - rightLight.getHeight()) / 2, paint);
break;
case BOTTOM://绘制底部光影背景
canvas.drawBitmap(bottomLight, (getWidth() - bottomLight.getWidth()) / 2, getWidth() - bottomLight.getHeight(), paint);
break;
default:
break;
}
三丶事件处理
主要是对其移动事件处理,根据手指滑动的位置动态绘制点的位置,并且需要对点的轨迹进行限制,现在点的轨迹有如下的两种方式限制。
- 点在背景圆的内部,做圆形的轨迹运动。
- 计算触摸的点和中心点的距离,如果点大于背景圆的半径,就计算角度,限制在对应角度的内圆上
- 没有触到内圆的时候,点的中心就是触摸点的位置。
private void showAroundCircle(int x, int y) {
// 当触屏区域不在圆形活动范围内
if (Math.sqrt(Math.pow((bgCenterX - x), 2) + Math.pow((bgCenterY - y), 2)) >= bgR) {
//得到摇杆与触屏点所形成的角度
double radian = getRadian(bgCenterX, bgCenterY, x, y);
//保证内部小圆运动的长度限制
//获取圆周运动的X坐标
pointCenterX = (int) ((bgR * Math.cos(radian)) + bgCenterX);
//获取圆周运动的Y坐标
pointCenterY = (int) ((bgR * Math.sin(radian)) + bgCenterY);
// 换算成角度值,跟X轴正半轴形成的角度, -180 ~ 180
angle = (float) (radian * 180 / Math.PI);
// 换算成跟Y轴正半轴之间的角度, -180 ~ 180
angle = (angle + 90);
if (angle > 180) {
angle = angle - 360;
}
} else {//如果小球中心点小于活动区域则随着用户触屏点移动即可
pointCenterX = x;
pointCenterY = y;
}
}
- 点在背景圆内做内切正方形为轨迹运动。
- 计算触摸点到中心圆的的x,y方向的差值的绝对值大于一半正方形边长需要限制其在正方形之内。
- 没有触到内切正方形的时候,点的中心就是触摸点的位置。
private void showAroundSquare(int x, int y) {
int xx = Math.abs(x - bgCenterX);
int yy = Math.abs(y - bgCenterY);
// 当触屏区域不在方形活动范围内
if (xx > bgHalfWidth || yy > bgHalfWidth) {
// 限制遥控点在方形范围内显示
if (xx > bgHalfWidth) {
int resX = xx - bgHalfWidth;
if (x > bgCenterX) {
pointCenterX = x - resX;
} else {
pointCenterX = x + resX;
}
} else {
pointCenterX = x;
}
if (yy > bgHalfWidth) {
int resY = yy - bgHalfWidth;
if (y > bgCenterY) {
pointCenterY = y - resY;
} else {
pointCenterY = y + resY;
}
} else {
pointCenterY = y;
}
//得到摇杆与触屏点所形成的角度
double radian = getRadian(bgCenterX, bgCenterY, x, y);
// 换算成角度值,跟X轴正半轴形成的角度, -180 ~ 180
angle = (float) (radian * 180 / Math.PI);
// 换算成跟Y轴正半轴之间的角度, -180 ~ 180
angle = (angle + 90);
if (angle > 180) {
angle = angle - 360;
}
} else {//如果小球中心点小于活动区域则随着用户触屏点移动即可
pointCenterX = x;
pointCenterY = y;
}
}
- 在事件处理方法onTouchEvent(MotionEvent event)方法对事件处理。(移动的时候限制点的轨迹,松手的时候复位。)
@Override
public boolean onTouchEvent(MotionEvent event) {
int x, y;
if (event.getAction() == MotionEvent.ACTION_DOWN) {
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
x = (int) event.getX();
y = (int) event.getY();
if (aroundCircle) {
showAroundCircle(x, y);
} else {
showAroundSquare(x, y);
}
} else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
resetValueWhenUp();
}
invalidate();
return true;
}
四丶事件回掉
在具体的运用的时候,我们需要根据点的位置,回掉具体的杆量值。
if (null != rockerViewInterface) {
// xVal, yVal: -1 ~ 1
float xVal = 0;
float yVal = 0;
if (aroundCircle) {
xVal = (float) (pointCenterX - POINT_CENTER_X) / bgR;
yVal = (float) (pointCenterY - POINT_CENTER_Y) / bgR;
} else {
xVal = (float) (pointCenterX - POINT_CENTER_X) / bgHalfWidth;
yVal = (float) (pointCenterY - POINT_CENTER_Y) / bgHalfWidth;
}
rockerViewInterface.onRockerChanged(this, xVal, yVal, angle);
}
五丶虚拟摇杆位置和移动范围限制
自定义一个容器,继承RelativeLayout,在事件处理拦截方法中去改变摇杆位置,以及限制虚拟摇杆移动范围。
- 在事件down中获取到虚拟摇杆的原始位置保存,并重置摇杆位置。由于摇杆可能会超出边界范围,获取到新的X,
Y位置需要判断此位置是否能完全显示虚拟摇杆,如果不能完整显示需要处理。 - 在up事件的时候,取down事件中保存的原始位置值复位摇杆位置。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int pointerCount = ev.getPointerCount();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
float x = ev.getX();
float y = ev.getY();
if (null != sv_rocker) {
if (rockerX < 0 || rockerY < 0) {
rockerX = sv_rocker.getX();//获取到摇杆的初始位置X
rockerY = sv_rocker.getY();//获取到摇杆的初始位置y
}
if (rockerW < 0 || rockerH < 0) {
rockerW = sv_rocker.getWidth();
rockerH = sv_rocker.getHeight();
}
if (rockerW > 0 && rockerH > 0) {//限制摇杆不允许超出屏幕范围,摇杆必须整个显示
float oneCenterL = (x - rockerW / 2);
float oneCenterT = (y - rockerH / 2);
if (x > (viewW - rockerW / 2)) {//限制摇杆右边x
oneCenterL = (viewW - rockerW);
}
if (oneCenterL<0){//限制摇杆左边x
oneCenterL=0;
}
sv_rocker.setX(oneCenterL);
if (y > (viewH - rockerH / 2)) {//限制摇杆下边y
oneCenterT = (viewH - rockerH);
}
if (oneCenterT<0){//限制摇杆上边y
oneCenterT=0;
}
sv_rocker.setY(oneCenterT);
}
}
break;
case MotionEvent.ACTION_POINTER_UP:
break;
case MotionEvent.ACTION_UP://摇杆中心复位
goOriginPosition();
break;
case MotionEvent.ACTION_CANCEL://摇杆中心复位
goOriginPosition();
break;
}
if(pointerCount >= 3){//防止三个手指点击,摇杆无法复位
return true;
}
return super.onInterceptTouchEvent(ev);
}
六丶使用方法
- 定义一个管理的类,传入对应的左右的摇杆范围View和虚拟摇杆View,根据美国手/日本手,左右摇杆初始化不同的图片资源,并设置不同的监听方法,来监听摇杆变化值。
/**
* 虚拟遥控飞行
*/
public class RockerFly {
private int rockerMode; //摇杆是什么手 0美国手 1日本手
private RockerRelativeLayout rrlThrottleLeft;
private RockerRelativeLayout rrlThrottleRight;
private RockerView rvThrottleLeft;
private RockerView rvThrottleRight;
public static float mi = 2f;
/**
* @param rrlThrottleLeft 左侧遥控范围
* @param rrlThrottleRight 右侧遥控范围
* @param rvThrottleLeft 左侧摇杆
* @param rvThrottleRight 右侧摇杆
*/
public RockerFly(
RockerRelativeLayout rrlThrottleLeft, RockerRelativeLayout rrlThrottleRight,
RockerView rvThrottleLeft, RockerView rvThrottleRight) {
rockerMode = SpSetGetUtils.getRockerMode();
this.rrlThrottleLeft = rrlThrottleLeft;
this.rrlThrottleRight = rrlThrottleRight;
this.rvThrottleLeft = rvThrottleLeft;
this.rvThrottleRight = rvThrottleRight;
initListener();
}
private void initListener() {
/**左侧摇杆显示/隐藏监听*/
rvThrottleLeft.setOnSensorLinstener(new RockerView.RockerViewListener() {
@Override
public void toggleLeftOrRight(boolean isShow) {
}
});
/**右侧摇杆显示/隐藏监听*/
rvThrottleRight.setOnSensorLinstener(new RockerView.RockerViewListener() {
@Override
public void toggleLeftOrRight(boolean isShow) {
}
});
}
public void initRockerFly() {
initRocker(rvThrottleLeft, rvThrottleRight);
}
//油门
private void initRocker(RockerView mSvRocker1, RockerView mSvRocker2) {
if (rockerMode == Constants.ROCKER_LEFT_THROTTLE) {//左手油门(美国手)
mSvRocker1.initView(R.mipmap.rocker_left_throttle_left,
R.mipmap.rocker_point);
mSvRocker1.setOnRockerChanged(new RockerViewInterface() {
@Override
public void onRockerChanged(View view, float x, float y, float angle) {
//xuanZhuan = x; // 左右方向 shangXia = y;// 上下升降
x = getX(x, angle);
y = -getY(y, angle);
}
});
mSvRocker2.initView(R.mipmap.rocker_left_throttle_right,
R.mipmap.rocker_point);
mSvRocker2.setOnRockerChanged(new RockerViewInterface() {
@Override
public void onRockerChanged(View view, float x, float y, float angle) {
//zuoYou = x;// 左右副翼 qianHou = y;// 上下油门
x = getXOrY(x);
y = -getXOrY(y);
}
});
} else if (rockerMode == Constants.ROCKER_RIGHT_THROTTLE) {//右手油门(日本手)
mSvRocker1.initView(R.mipmap.rocker_right_throttle_left,
R.mipmap.rocker_point);
mSvRocker1.setOnRockerChanged(new RockerViewInterface() {
@Override
public void onRockerChanged(View view, float x, float y, float angle) {
// xuanZhuan = x; // 左右方向 qianHou = y;// 上下升降
x = getXOrY(x);
y = -getXOrY(y);
}
});
mSvRocker2.initView(R.mipmap.rocker_right_throttle_right,
R.mipmap.rocker_point);
mSvRocker2.setOnRockerChanged(new RockerViewInterface() {
@Override
public void onRockerChanged(View view, float x, float y, float angle) {
//zuoYou = x;// 左右副翼 shangXia = y;// 上下油门
x = getX(x, angle);
y = -getY(y, angle);
}
});
}
}
public void dismissRockerFly() {
resetPoint();
if (null != rrlThrottleLeft) {
rrlThrottleLeft.resetlayout();
}
if (null != rrlThrottleRight) {
rrlThrottleRight.resetlayout();
}
}
public void resetPoint() {
if (null != rvThrottleLeft) {
rvThrottleLeft.reset();
}
if (null != rvThrottleRight) {
rvThrottleRight.reset();
}
}
}
- 在xml下布局摇杆,具体的布局代码如下所示。
<com.bird.rockerdome.rocker.RockerRelativeLayout
android:id="@+id/rrl_throttle_left"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.5">
<com.bird.rockerdome.rocker.RockerView
android:id="@+id/rv_throttle_left"
android:layout_width="@dimen/rocker_width"
android:layout_height="@dimen/rocker_width"
android:layout_centerInParent="true"
/>
</com.bird.rockerdome.rocker.RockerRelativeLayout>
<com.bird.rockerdome.rocker.RockerRelativeLayout
android:id="@+id/rrl_throttle_right"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.5">
<com.bird.rockerdome.rocker.RockerView
android:id="@+id/rv_throttle_right"
android:layout_width="@dimen/rocker_width"
android:layout_height="@dimen/rocker_width"
android:layout_centerInParent="true"
/>
</com.bird.rockerdome.rocker.RockerRelativeLayout>
- 在Activity中find具体的view,在onCreate() 周期中初始化管理类,并传入对于的值。
RockerFly mRockerFly;
private void initRocker() {
RockerRelativeLayout rrlThrottleLeft = findViewById(R.id.rrl_throttle_left);
RockerRelativeLayout rrlThrottleRight = findViewById(R.id.rrl_throttle_right);
RockerView rvThrottleLeft = findViewById(R.id.rv_throttle_left);
RockerView rvThrottleRight = findViewById(R.id.rv_throttle_right);
//摇杆飞行
mRockerFly = new RockerFly(rrlThrottleLeft, rrlThrottleRight,
rvThrottleLeft, rvThrottleRight);
mRockerFly.initRockerFly();
}
七丶结语
整个自定义可移动并能自由调整杆量大小的虚拟摇杆就已经完成了,整体来说就是需要掌握点的轨迹处理,以及在虚拟摇杆的父容器中进行事件拦截,根据初始按下的位置,改变虚拟摇杆位置,并对超出范围的摇杆部分限制,然界面可以一直显示一个完整的虚拟摇杆。
八丶下载链接
附上整个demo的下载链接(包括上一个的固定的虚拟摇杆)。