ATAK插件中JoystickView回调为NULL的技术求助
我正在为ATAK(Android Team Awareness Kit)开发插件,尝试实现虚拟摇杆功能。将参考实现的JoystickView.java移入项目后,摇杆渲染和操作均正常,但回调对象joystickCallback始终为NULL,无法触发onJoystickMoved()方法。
JoystickView与使用它的DropDownReceiver类在同一包下,DropDownReceiver已实现JoystickListener接口,Android Studio显示重写的onJoystickMoved()在JoystickView接口中有两处引用,但运行时joystickCallback始终为空,怀疑是上下文问题但无法定位。
相关代码
DropDownReceiver类
package com.myawesomeatakplugin; public class DropDownReceiver implements JoystickView.JoystickListener { // 其他代码 ... @Override public void onJoystickMoved(float xPercent, float yPercent, int source) { Log.e("CLASSTAG", "X percent: " + xPercent + " Y percent: " + yPercent); } // 其他代码 ... }
布局文件
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" android:theme="@style/ATAKTheme"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center_vertical" android:orientation="vertical"> <com.myawesomeatakplugin.JoystickView android:id="@+id/joystick" android:layout_width="200dp" android:layout_height="200dp" android:layout_marginTop="100dp"/> </LinearLayout> </layout>
JoystickView类
package com.myawesomeatakplugin; public class JoystickView extends SurfaceView implements SurfaceHolder.Callback, View.OnTouchListener { private float centerX; private float centerY; private float baseRadius; private float hatRadius; private JoystickListener joystickCallback; public JoystickView(Context context) { super(context); getHolder().addCallback(this); setOnTouchListener(this); if(context instanceof JoystickListener){ joystickCallback = (JoystickListener) context; } } public JoystickView(Context context, AttributeSet attrs) { super(context, attrs); getHolder().addCallback(this); setOnTouchListener(this); if(context instanceof JoystickListener){ joystickCallback = (JoystickListener) context; } } public JoystickView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); getHolder().addCallback(this); setOnTouchListener(this); if(context instanceof JoystickListener){ joystickCallback = (JoystickListener) context; } } public JoystickView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); getHolder().addCallback(this); setOnTouchListener(this); if(context instanceof JoystickListener){ joystickCallback = (JoystickListener) context; } } @Override public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) { setupDimensions(); drawJoystick(centerX, centerY); } @Override public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) { // TBD } @Override public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) { // TBD } private void setupDimensions(){ centerX = (float) getWidth() / 2; centerY = (float) getHeight() / 2; baseRadius = (float) Math.min(getWidth(), getHeight()) / 3; hatRadius = (float) Math.min(getWidth(), getHeight()) / 5; } private void drawJoystick(float newX, float newY){ if(getHolder().getSurface().isValid()){ Canvas myCanvas = this.getHolder().lockCanvas(); Paint colors = new Paint(); myCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); colors.setARGB(255, 50, 50, 50); myCanvas.drawCircle(centerX, centerY, baseRadius, colors); colors.setARGB(255, 0, 0, 255); myCanvas.drawCircle(newX, newY, hatRadius, colors); getHolder().unlockCanvasAndPost(myCanvas); } } @Override public boolean onTouch(View view, MotionEvent motionEvent) { if(view.equals(this)){ if(motionEvent.getAction() != MotionEvent.ACTION_UP){ float displacement = (float) Math.sqrt(Math.pow(motionEvent.getX() - centerX, 2) + Math.pow(motionEvent.getY() - centerY, 2)); if(displacement < baseRadius){ drawJoystick(motionEvent.getX(), motionEvent.getY()); // FOR SOME REASON, joystickCallback IS NULL HERE?!?!? if(joystickCallback != null) joystickCallback.onJoystickMoved((motionEvent.getX() - centerX)/baseRadius, (motionEvent.getY() - centerY)/baseRadius, getId()); } else { float ratio = baseRadius / displacement; float constrainedX = centerX + (motionEvent.getX() - centerX) * ratio; float constrainedY = centerY + (motionEvent.getY() - centerY) * ratio; drawJoystick(constrainedX, constrainedY); // FOR SOME REASON, joystickCallback IS NULL HERE?!?!? if(joystickCallback != null) joystickCallback.onJoystickMoved((constrainedX-centerX)/baseRadius, (constrainedY-centerY)/baseRadius, getId()); } } else { drawJoystick(centerX, centerY); } } return true; } public interface JoystickListener { void onJoystickMoved(float xPercent, float yPercent, int source); } }
问题原因
当前JoystickView的构造函数中,是通过判断传入的Context是否实现了JoystickListener来初始化回调对象,但DropDownReceiver并不是Context对象(它是ATAK插件的下拉接收器,属于BroadcastReceiver子类),而布局加载时传入的Context是Activity或Application,这些类并没有实现JoystickListener接口,因此joystickCallback始终为null。
解决方案
方案1:给JoystickView添加回调设置方法(推荐)
修改JoystickView,新增一个公开方法用于手动设置回调,摆脱对Context的依赖:
public class JoystickView extends SurfaceView implements SurfaceHolder.Callback, View.OnTouchListener { // ... 原有代码 ... // 添加回调设置方法 public void setJoystickListener(JoystickListener listener) { this.joystickCallback = listener; } // ... 原有代码 ... }
在DropDownReceiver中加载布局后,获取JoystickView实例并手动设置回调:
// 假设你通过LayoutInflater加载布局 View layoutView = inflater.inflate(R.layout.your_joystick_layout, null); JoystickView joystick = layoutView.findViewById(R.id.joystick); // DropDownReceiver已实现JoystickListener,直接传入this joystick.setJoystickListener(this);
方案2:调整布局加载的Context(不推荐,耦合性高)
如果坚持使用Context强转的方式,需要让加载布局的Context是实现了JoystickListener的类。但DropDownReceiver本身不是Context,因此需要将回调逻辑迁移到自定义Activity/Fragment中,并让该类实现JoystickListener,再用这个类的Context加载布局。不过这种方式不符合ATAK插件的架构设计,不推荐使用。
内容的提问来源于stack exchange,提问作者Brian

