You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 22:33:24