|
|
@@ -6,11 +6,14 @@ import android.graphics.Bitmap;
|
|
|
import android.graphics.Canvas;
|
|
|
import android.graphics.Color;
|
|
|
import android.graphics.Matrix;
|
|
|
+import android.graphics.Paint;
|
|
|
import android.graphics.Picture;
|
|
|
import android.graphics.PointF;
|
|
|
+import android.graphics.Rect;
|
|
|
import android.graphics.SurfaceTexture;
|
|
|
import android.os.Looper;
|
|
|
import android.util.AttributeSet;
|
|
|
+import android.view.Choreographer;
|
|
|
import android.view.MotionEvent;
|
|
|
import android.view.TextureView;
|
|
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
|
|
@@ -29,7 +32,9 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
private SurfaceTexture surface;
|
|
|
private boolean isMapLoadFinish = false;
|
|
|
|
|
|
- /** 用 COWList 避免 refresh() 遍历时被并发修改 */
|
|
|
+ /**
|
|
|
+ * 用 COWList 避免 refresh() 遍历时被并发修改
|
|
|
+ */
|
|
|
private final CopyOnWriteArrayList<MapBaseLayer> layers = new CopyOnWriteArrayList<>();
|
|
|
private MapLayer mapLayer;
|
|
|
|
|
|
@@ -62,6 +67,103 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
private Picture framePicture; // 每帧录制的世界坐标内容
|
|
|
private int frameW = 0, frameH = 0; // 录制尺寸(通常等于地图宽高)
|
|
|
|
|
|
+ // ===== 线程 & 合帧控制 =====
|
|
|
+ private android.os.HandlerThread renderThread;
|
|
|
+ private android.os.Handler renderHandler;
|
|
|
+ private final java.util.concurrent.atomic.AtomicBoolean frameDirty = new java.util.concurrent.atomic.AtomicBoolean(false);
|
|
|
+ private final java.util.concurrent.atomic.AtomicBoolean drawScheduled = new java.util.concurrent.atomic.AtomicBoolean(false);
|
|
|
+
|
|
|
+ // 拷贝当前状态用于渲染,避免锁 UI 线程
|
|
|
+ private final Object stateLock = new Object();
|
|
|
+ private final Matrix drawMatrix = new Matrix();
|
|
|
+ private float drawZoom = 1.0f;
|
|
|
+ private float drawRotateDeg = 0.0f;
|
|
|
+
|
|
|
+ // 目标帧率(可调)
|
|
|
+ private static final long FRAME_INTERVAL_MS = 16; // ~60fps
|
|
|
+
|
|
|
+ // 过滤采样,减少缩放“抖光”
|
|
|
+ private final Paint composePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.FILTER_BITMAP_FLAG);
|
|
|
+
|
|
|
+ private Bitmap frontBmp, backBmp;
|
|
|
+ private Canvas backCanvas;
|
|
|
+ private int worldW = 0, worldH = 0;
|
|
|
+ private final Object worldLock = new Object();
|
|
|
+ private volatile boolean worldDirty = true; // 地图或图层内容变更时置 true
|
|
|
+
|
|
|
+ // ==== 交互优先:手势锁 + 延后一次相机操作 ====
|
|
|
+ private volatile boolean userGestureActive = false;
|
|
|
+ private final Object cameraOpLock = new Object();
|
|
|
+ private Runnable pendingCameraOp = null; // 只保留“最后一次”
|
|
|
+ private android.animation.ValueAnimator cameraAnimator; // 如果你有平移动画,按需取消
|
|
|
+
|
|
|
+ private volatile boolean renderEnabled = true;
|
|
|
+
|
|
|
+ private final Runnable renderRunnable = new Runnable() {
|
|
|
+ @Override public void run() {
|
|
|
+ try {
|
|
|
+ if (!renderEnabled) return;
|
|
|
+ drawScheduled.set(false);
|
|
|
+
|
|
|
+ if (!frameDirty.getAndSet(false)) return;
|
|
|
+ if (surface == null || !isMapLoadFinish) return;
|
|
|
+
|
|
|
+ // 复制当前状态(避免 UI 线程锁)
|
|
|
+ final Matrix m = new Matrix();
|
|
|
+ final float zoom, deg;
|
|
|
+ synchronized (stateLock) {
|
|
|
+ m.set(drawMatrix);
|
|
|
+ zoom = drawZoom;
|
|
|
+ deg = drawRotateDeg;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1) 世界缓冲(offscreen 双缓冲)
|
|
|
+ final int w = Math.max(1, (int) getMapWidth());
|
|
|
+ final int h = Math.max(1, (int) getMapHeight());
|
|
|
+ ensureWorldBuffers(w, h);
|
|
|
+
|
|
|
+ if (worldDirty) {
|
|
|
+ synchronized (worldLock) {
|
|
|
+ // 清透明,避免残影
|
|
|
+ backCanvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR);
|
|
|
+
|
|
|
+ // ★ 关键:传恒等矩阵;Layer 内禁止再 concat 外部矩阵
|
|
|
+ for (MapBaseLayer layer : layers) {
|
|
|
+ if (layer.isVisible) layer.draw(backCanvas, IDENTITY, zoom, deg);
|
|
|
+ }
|
|
|
+ // 原子交换 front/back
|
|
|
+ Bitmap tmp = frontBmp; frontBmp = backBmp; backBmp = tmp;
|
|
|
+ backCanvas.setBitmap(backBmp);
|
|
|
+ worldDirty = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2) 合成到屏幕(只做一次 concat)
|
|
|
+ Canvas screen = lockCanvas();
|
|
|
+ if (screen != null) {
|
|
|
+ try {
|
|
|
+ screen.drawColor(backgroundColor);
|
|
|
+ screen.save();
|
|
|
+ screen.concat(m);
|
|
|
+ // FILTER/DITHER 让缩放更稳,不闪像素点
|
|
|
+ screen.drawBitmap(frontBmp, null, new Rect(0, 0, worldW, worldH), composePaint);
|
|
|
+ screen.restore();
|
|
|
+ } finally {
|
|
|
+ unlockCanvasAndPost(screen);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3) 如果期间又有新的 refresh(),排到下一帧(≈60fps)
|
|
|
+ if (frameDirty.get()) {
|
|
|
+ if (renderHandler != null) {
|
|
|
+ renderHandler.postDelayed(this, FRAME_INTERVAL_MS);
|
|
|
+ drawScheduled.set(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Throwable ignore) { /* 保住渲染线程 */ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
public MapView(Context context) {
|
|
|
this(context, null);
|
|
|
}
|
|
|
@@ -84,6 +186,10 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
setSurfaceTextureListener(this);
|
|
|
setOpaque(true);
|
|
|
setClickable(true);
|
|
|
+
|
|
|
+ renderThread = new android.os.HandlerThread("MapRenderThread", android.os.Process.THREAD_PRIORITY_DISPLAY);
|
|
|
+ renderThread.start();
|
|
|
+ renderHandler = new android.os.Handler(renderThread.getLooper());
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -93,16 +199,94 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
- public void onSurfaceTextureSizeChanged(SurfaceTexture st, int width, int height) { }
|
|
|
+ public void onSurfaceTextureSizeChanged(SurfaceTexture st, int width, int height) {
|
|
|
+ }
|
|
|
|
|
|
@Override
|
|
|
public boolean onSurfaceTextureDestroyed(SurfaceTexture st) {
|
|
|
surface = null;
|
|
|
+ if (renderThread != null) {
|
|
|
+ renderThread.quitSafely();
|
|
|
+ renderThread = null;
|
|
|
+ renderHandler = null;
|
|
|
+ }
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
- public void onSurfaceTextureUpdated(SurfaceTexture st) { }
|
|
|
+ public void onSurfaceTextureUpdated(SurfaceTexture st) {
|
|
|
+ }
|
|
|
+
|
|
|
+ private final Choreographer.FrameCallback vsyncCallback = frameTimeNanos -> {
|
|
|
+ if (!renderEnabled || renderHandler == null) { drawScheduled.set(false); return; }
|
|
|
+ renderHandler.post(renderRunnable);
|
|
|
+ drawScheduled.set(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 外部可调用:Fragment 可见/不可见时切换
|
|
|
+ public void setRenderEnabled(boolean enabled) {
|
|
|
+ if (renderEnabled == enabled) return;
|
|
|
+ renderEnabled = enabled;
|
|
|
+ notifyLayersVisible(enabled); // ✅ 通知子层
|
|
|
+
|
|
|
+ if (!enabled) {
|
|
|
+ // 关机:停止一切排队的渲染
|
|
|
+ drawScheduled.set(false);
|
|
|
+ frameDirty.set(false);
|
|
|
+ if (renderHandler != null) renderHandler.removeCallbacks(renderRunnable);
|
|
|
+ } else {
|
|
|
+ // 开机:若有待渲染帧,下一次 vsync 再画
|
|
|
+ if (frameDirty.get()) Choreographer.getInstance().postFrameCallback(vsyncCallback);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public interface PausableLayer {
|
|
|
+ void onHostVisibilityChanged(boolean visible);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void notifyLayersVisible(boolean visible) {
|
|
|
+ for (MapBaseLayer layer : layers) {
|
|
|
+ if (layer instanceof PausableLayer) {
|
|
|
+ ((PausableLayer) layer).onHostVisibilityChanged(visible);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尺寸变化或首次构建时调用
|
|
|
+ private void ensureWorldBuffers(int w, int h) {
|
|
|
+ if (w <= 0 || h <= 0) return;
|
|
|
+ if (frontBmp != null && frontBmp.getWidth() == w && frontBmp.getHeight() == h) return;
|
|
|
+ // 回收旧的
|
|
|
+ if (frontBmp != null) frontBmp.recycle();
|
|
|
+ if (backBmp != null) backBmp.recycle();
|
|
|
+
|
|
|
+ frontBmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
|
|
+ backBmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
|
|
+ backCanvas = new Canvas(backBmp);
|
|
|
+ worldW = w;
|
|
|
+ worldH = h;
|
|
|
+ worldDirty = true; // 新缓冲,需要重绘世界
|
|
|
+ }
|
|
|
+
|
|
|
+ private void setGestureActive(boolean active) {
|
|
|
+ userGestureActive = active;
|
|
|
+ if (!active) flushPendingCameraOp();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void runOrDeferCameraOp(Runnable op) {
|
|
|
+ if (userGestureActive) {
|
|
|
+ synchronized (cameraOpLock) { pendingCameraOp = op; }
|
|
|
+ } else {
|
|
|
+ // 放到主线程执行,避免线程问题
|
|
|
+ post(op);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void flushPendingCameraOp() {
|
|
|
+ Runnable r;
|
|
|
+ synchronized (cameraOpLock) { r = pendingCameraOp; pendingCameraOp = null; }
|
|
|
+ if (r != null) post(r);
|
|
|
+ }
|
|
|
|
|
|
public void loadMap(Bitmap bitmap) {
|
|
|
Bitmap safe = ensureSafeBitmapSize(bitmap);
|
|
|
@@ -157,56 +341,14 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
public void refresh() {
|
|
|
if (surface == null || !isMapLoadFinish) return;
|
|
|
|
|
|
- // 1) 确定世界坐标系尺寸(优先用地图尺寸)
|
|
|
- int worldW = Math.max(1, (int) getMapWidth());
|
|
|
- int worldH = Math.max(1, (int) getMapHeight());
|
|
|
- if (worldW <= 1 || worldH <= 1) {
|
|
|
- // 没有地图时退化回旧路径(直接按 currentMatrix 画)
|
|
|
- Canvas fallback = lockCanvas();
|
|
|
- if (fallback != null) {
|
|
|
- try {
|
|
|
- fallback.drawColor(backgroundColor);
|
|
|
- for (MapBaseLayer layer : layers) {
|
|
|
- if (layer.isVisible) {
|
|
|
- layer.draw(fallback, currentMatrix, currentZoom, currentRotateDegrees);
|
|
|
- }
|
|
|
- }
|
|
|
- } finally {
|
|
|
- unlockCanvasAndPost(fallback);
|
|
|
- }
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 2) 录制一帧:世界坐标系下(不带任何矩阵变换)
|
|
|
- if (framePicture == null || frameW != worldW || frameH != worldH) {
|
|
|
- framePicture = new Picture();
|
|
|
- frameW = worldW;
|
|
|
- frameH = worldH;
|
|
|
- }
|
|
|
- Canvas rec = framePicture.beginRecording(frameW, frameH);
|
|
|
- // 背景:交给主画布处理(保持透明),如果你想要统一底色,也可以这里 rec.drawColor(backgroundColor);
|
|
|
- rec.drawColor(Color.TRANSPARENT);
|
|
|
- for (MapBaseLayer layer : layers) {
|
|
|
- if (layer.isVisible) {
|
|
|
- // 关键:传入恒等矩阵,让各层在“世界坐标系”里绘制
|
|
|
- layer.draw(rec, IDENTITY, currentZoom, currentRotateDegrees);
|
|
|
- }
|
|
|
+ frameDirty.set(true);
|
|
|
+ synchronized (stateLock) {
|
|
|
+ drawMatrix.set(currentMatrix);
|
|
|
+ drawZoom = currentZoom;
|
|
|
+ drawRotateDeg = currentRotateDegrees;
|
|
|
}
|
|
|
- framePicture.endRecording();
|
|
|
-
|
|
|
- // 3) 把录制好的内容一次性按 currentMatrix 贴到屏幕
|
|
|
- Canvas canvas = lockCanvas();
|
|
|
- if (canvas != null) {
|
|
|
- try {
|
|
|
- canvas.drawColor(backgroundColor);
|
|
|
- canvas.save();
|
|
|
- canvas.concat(currentMatrix); // 整体缩放/平移/旋转应用在这里
|
|
|
- framePicture.draw(canvas); // (0,0) → (worldW, worldH)
|
|
|
- canvas.restore();
|
|
|
- } finally {
|
|
|
- unlockCanvasAndPost(canvas);
|
|
|
- }
|
|
|
+ if (renderEnabled && drawScheduled.compareAndSet(false, true)) {
|
|
|
+ Choreographer.getInstance().postFrameCallback(vsyncCallback);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -216,6 +358,8 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
int action = event.getAction() & MotionEvent.ACTION_MASK;
|
|
|
switch (action) {
|
|
|
case MotionEvent.ACTION_DOWN:
|
|
|
+ setGestureActive(true);
|
|
|
+ if (cameraAnimator != null && cameraAnimator.isRunning()) cameraAnimator.cancel();
|
|
|
saveMatrix.set(currentMatrix);
|
|
|
startTouch.set(event.getX(), event.getY());
|
|
|
lastMove.set(event.getX(), event.getY());
|
|
|
@@ -225,6 +369,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
}
|
|
|
break;
|
|
|
case MotionEvent.ACTION_POINTER_DOWN:
|
|
|
+ setGestureActive(true);
|
|
|
if (event.getPointerCount() == 2) {
|
|
|
saveMatrix.set(currentMatrix);
|
|
|
saveZoom = currentZoom;
|
|
|
@@ -235,6 +380,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
}
|
|
|
break;
|
|
|
case MotionEvent.ACTION_UP:
|
|
|
+ setGestureActive(false); // 这里会自动 flush 最后一次相机请求
|
|
|
if (withFloorPlan(event.getX(), event.getY())) {
|
|
|
for (MapBaseLayer layer : layers) {
|
|
|
layer.onTouch(event);
|
|
|
@@ -275,7 +421,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
}
|
|
|
|
|
|
public float[] mapXYToScreenXY(float x, float y) {
|
|
|
- float[] pts = { x, y };
|
|
|
+ float[] pts = {x, y};
|
|
|
currentMatrix.mapPoints(pts); // map -> screen(与 onDraw 完全一致)
|
|
|
return pts;
|
|
|
}
|
|
|
@@ -292,7 +438,9 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
return isMapLoadFinish;
|
|
|
}
|
|
|
|
|
|
- /** 在主线程添加图层;避免并发改动引发 CME */
|
|
|
+ /**
|
|
|
+ * 在主线程添加图层;避免并发改动引发 CME
|
|
|
+ */
|
|
|
public void addLayer(final MapBaseLayer layer) {
|
|
|
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
|
post(() -> addLayer(layer));
|
|
|
@@ -306,7 +454,9 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** 在主线程移除图层 */
|
|
|
+ /**
|
|
|
+ * 在主线程移除图层
|
|
|
+ */
|
|
|
public void removeLayer(final MapBaseLayer layer) {
|
|
|
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
|
post(() -> removeLayer(layer));
|
|
|
@@ -319,7 +469,9 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** 清空所有图层(主线程) */
|
|
|
+ /**
|
|
|
+ * 清空所有图层(主线程)
|
|
|
+ */
|
|
|
public void clearLayers() {
|
|
|
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
|
post(this::clearLayers);
|
|
|
@@ -349,6 +501,15 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
refresh();
|
|
|
}
|
|
|
|
|
|
+ public void markWorldDirty() {
|
|
|
+ worldDirty = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void refreshWorld() {
|
|
|
+ worldDirty = true;
|
|
|
+ refresh(); // 仍然走你的 vsync 合帧
|
|
|
+ }
|
|
|
+
|
|
|
public float getCurrentRotateDegrees() {
|
|
|
return currentRotateDegrees;
|
|
|
}
|
|
|
@@ -434,12 +595,16 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
return mapLayer != null ? mapLayer.getImage().getHeight() : 0f;
|
|
|
}
|
|
|
|
|
|
- /** 瞬间把地图上的 (x, y) 点移动到屏幕中心。 */
|
|
|
+ /**
|
|
|
+ * 瞬间把地图上的 (x, y) 点移动到屏幕中心。
|
|
|
+ */
|
|
|
public void centerOnPoint(float x, float y) {
|
|
|
mapCenterWithPoint(x, y);
|
|
|
}
|
|
|
|
|
|
- /** 瞬间中心并缩放到 zoom(zoom 需在 min~max 之间) */
|
|
|
+ /**
|
|
|
+ * 瞬间中心并缩放到 zoom(zoom 需在 min~max 之间)
|
|
|
+ */
|
|
|
public void centerAndZoom(float x, float y, float zoom) {
|
|
|
setCurrentZoom(zoom);
|
|
|
mapCenterWithPoint(x, y);
|
|
|
@@ -471,7 +636,7 @@ public class MapView extends TextureView implements TextureView.SurfaceTextureLi
|
|
|
float[] pts = {x, y};
|
|
|
currentMatrix.mapPoints(pts);
|
|
|
|
|
|
- float dx = getWidth() / 2f - pts[0];
|
|
|
+ float dx = getWidth() / 2f - pts[0];
|
|
|
float dy = getHeight() / 2f - pts[1];
|
|
|
currentMatrix.postTranslate(dx, dy);
|
|
|
|