摘要: 上次我们自定义了一个固定位置摇杆,此摇杆无法调整杆量,且位置固定,这次我们就来定义一个可以自由调整杆量和变换位置的虚拟摇杆。

android 虚拟接入点 安卓虚拟移动_android 虚拟接入点


下面就写一下需要实现此虚拟摇杆的步骤:

一丶初始化资源

我们需要美国手日本手的背景图片资源,滑动的点,对应的区域移动区域范围初始化,虚拟摇杆外围光影背景资源初始化。由于摇杆的左右手以及日本手和美国手的资源都不一样,所以通过外部传入的方式初始化。

/**
     * @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));

    }

二丶绘制虚拟摇杆

  1. 绘制背景,周围预留6dp要画位置的光影背景
// 画背景
       if (null != bmpBg) {
          canvas.drawBitmap(bmpBg, DimensionUtil.dp2px(context, 6), DimensionUtil.dp2px(context, 6), null);
       }
  1. 绘制点,根据点的中心位置以及点的半径来准备绘制点的图形位置
//画点的位置
  if (null != bmpPoint) {
     canvas.drawBitmap(bmpPoint, (pointCenterX - pointR), (pointCenterY - pointR), null);
  }
  1. 绘制光影背景,根据点的位置,来匹配点移动到了哪个区域,从而绘制哪个区域的光影背景。
//根据点的位置来画摇杆光影背景
 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;
            }

三丶事件处理
主要是对其移动事件处理,根据手指滑动的位置动态绘制点的位置,并且需要对点的轨迹进行限制,现在点的轨迹有如下的两种方式限制。

  1. 点在背景圆的内部,做圆形的轨迹运动。
  • 计算触摸的点和中心点的距离,如果点大于背景圆的半径,就计算角度,限制在对应角度的内圆上
  • 没有触到内圆的时候,点的中心就是触摸点的位置。
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;
        }
    }
  1. 点在背景圆内做内切正方形为轨迹运动。
  • 计算触摸点到中心圆的的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;
        }
    }
  1. 在事件处理方法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的下载链接(包括上一个的固定的虚拟摇杆)。