SurfaceView开发2D游戏动画 暂停恢复后重启问题求解
问题根因
surfaceCreated会在每次Surface重建时触发(应用切后台再返回、横竖屏切换、锁屏解锁都会触发Surface销毁重建),原有逻辑在该回调中直接调用onStart(),会重新实例化所有Actor对象、重置元素坐标与透明度,直接把动画状态还原到初始值- 原有代码未区分首次初始化与Surface重建场景,也没有做动画状态留存,只要Surface重建就全量重置
- 附带隐式bug:Actor类
intersect方法缺失闭合括号、setVisibility未同步更新成员变量值、固定36ms帧延迟导致不同帧率设备动画速度不一致、直接吞掉渲染异常没有日志输出、自定义基础类命名为Object和JDK内置类冲突、线程优先级设置过低容易被系统抢占导致卡顿。
修复方案(纯SurfaceView实现,无额外技术栈依赖)
1. 拆分初始化逻辑,避免状态重置
新增初始化标记位,把原有onStart()逻辑拆为「全局仅执行一次的资源初始化」和「每次Surface创建执行的尺寸同步」两部分,从根源上避免Surface重建时重置动画状态。
修改AbstractPanel类对应代码:
// 新增初始化标记 private boolean isSceneInitialized = false; // 仅在首次启动时加载资源、初始化对象 private void initSceneResources() { if (isSceneInitialized) return; cmd = new Actor(0, 0); actor = new Actor(0, 0); actor2 = new Actor(0, 0); cmd.createFromAsset("cmd.png"); actor.createFromAsset("grandGL.png"); actor2.createFromAsset("presents.png"); cmd.setSpeed(1); cmd.setEnabled(true); isSceneInitialized = true; } // 每次Surface创建时调用,仅做视图尺寸同步 public void onSurfaceReady() { PositionedObject.syncView(this); // 仅首次初始化时设置初始位置、透明度 if (!isSceneInitialized) { initSceneResources(); actor.setGravity(Gravity.CENTER); actor2.alignTo(actor, Gravity.BOTTOM); actor.setVisibility(Actor.INVISIBLE); actor2.setVisibility(Actor.INVISIBLE); cmd.setPos(0,0); } } // 新增生命周期对接方法,在宿主Activity/Fragment的对应生命周期中调用 public void onPause() { stopGameLoop(); } public void onResume() { if (getHolder().getSurface().isValid()) { startGameLoop(); } }
修改surfaceCreated回调逻辑,去掉原有直接调用onStart()的逻辑,增加线程存活判断避免重复创建线程:
@Override public void surfaceCreated(SurfaceHolder holder) { if (thread2 == null || !thread2.isAlive()) { thread2 = new Thread(thread); thread2.setPriority(Thread.NORM_PRIORITY - 1); } startGameLoop(); if (!thread2.isAlive()) { thread2.start(); } onSurfaceReady(); }
2. 修复Actor类现有bug
补全缺失的语法问题,修正状态不同步bug:
// 补全intersect方法缺失的闭合括号 public boolean intersect(MovingObject mo) { return getRect().intersect(mo.getRect()); } // 修改setVisibility,同步更新成员变量 public void setVisibility(int visible) { this.visibility = visible; getPaint().setAlpha(visible); } // 新增支持时间系数的move重载,解决不同帧率设备速度不一致问题 public void move(int where, float timeScale) { if (!isEnabled()) return; int moveDistance = Math.round(speed * timeScale); switch (where) { case dirX: pos.x += moveDistance; break; case ndirX: pos.x -= moveDistance; break; case dirY: pos.y += moveDistance; break; case ndirY: pos.y -= moveDistance; break; case dirAll: move(dirX, timeScale); move(dirY, timeScale); break; case ndirAll: move(ndirX, timeScale); move(ndirY, timeScale); break; } } // 保留原有无参move方法兼容逻辑 public void move(int where) { move(where, 1f); }
3. 优化渲染线程帧率控制
替换原有固定36ms延迟的逻辑,改为基于时间差的帧率控制,保证不同设备动画速度一致,同时避免暂停恢复后时间跳变导致的位移异常:
public class BaseThread implements Runnable { private SurfaceHolder surHol; private boolean running; private long lastFrameTime = 0; private static final long TARGET_FRAME_DURATION = 16; // 目标60帧,每帧约16ms public BaseThread(SurfaceHolder holder) { surHol = holder; } private void delay(long l) { try { Thread.sleep(l); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public boolean isRunning() { return running; } public void setRunning(boolean run) { synchronized (this) { running = run; // 启动/恢复时重置帧时间,避免时间差计算错误 if (run) lastFrameTime = System.currentTimeMillis(); } } @Override public void run() { delay(800L); while (isRunning()) { Canvas canvas = null; long frameStartTime = System.currentTimeMillis(); long deltaTime = frameStartTime - lastFrameTime; float timeScale = deltaTime * 1f / TARGET_FRAME_DURATION; try { canvas = surHol.lockCanvas(); if (canvas != null) { synchronized (canvas) { onUpdate(timeScale); draw(canvas); } } } catch (Exception e) { e.printStackTrace(); // 保留异常日志便于排查 } finally { if (canvas != null) { surHol.unlockCanvasAndPost(canvas); } lastFrameTime = frameStartTime; long frameCost = System.currentTimeMillis() - frameStartTime; long waitTime = TARGET_FRAME_DURATION - frameCost; if (waitTime > 0) delay(waitTime); } } } }
4. 简化动画逻辑
拆分逻辑更新与纯绘制职责,替换原有十几个重复的透明度判断case,用线性插值计算淡入淡出效果,减少冗余代码:
// 逻辑更新,处理位移、透明度变化 public void onUpdate(float timeScale) { cmd.move(Actor.dirX, timeScale); int cmdX = cmd.getX(); // 淡入阶段:x从25到35,alpha从30线性升到255 if (cmdX >= 25 && cmdX <= 35) { int alpha = Math.min(255, 30 * (cmdX - 24)); actor.setVisibility(alpha); } // 显示第二个元素 if (cmdX >= 60 && actor2.getVisibility() != Actor.VISIBLE) { actor2.setVisibility(Actor.VISIBLE); } // 淡出阶段:x从130到146,alpha从210线性降到0 if (cmdX >= 130 && cmdX <= 146) { int alpha = Math.max(0, 210 - (cmdX - 130) * 12); actor.setVisibility(alpha); actor2.setVisibility(alpha); } } // 纯绘制逻辑,不处理状态变更 public void drawContent(Canvas canvas) { actor.draw(canvas); actor2.draw(canvas); cmd.draw(canvas); } @Override public void draw(Canvas canvas) { super.draw(canvas); canvas.drawColor(panelColor); drawContent(canvas); }
优化点汇总
- 拆分初始化逻辑后,应用暂停恢复、Surface重建时不会重置动画状态,会从暂停位置继续播放
- 替换重复的switch-case为线性插值计算,代码量减少60%,后续调整动画参数只需要修改系数即可
- 基于时间差的帧率控制,保证不同性能设备上动画速度一致,不会出现快慢差异
- 补全原有语法bug、状态同步bug,增加异常日志输出,减少隐式崩溃
- 优化线程创建与优先级逻辑,避免重复创建线程导致的内存泄漏、卡顿问题
- 建议将自定义基础类
Object重命名为BaseRenderObject,避免和JDK内置的java.lang.Object命名冲突,减少类型转换异常风险。
内容的提问来源于stack exchange,提问作者Danison Auza Nuñez
相关产品推荐
相关产品推荐

