Android主线程工作时动画问题:复杂布局渲染时dispatchDraw无法执行
兄弟,太懂你这种窘境了——复杂布局渲染把UI线程占得死死的,连dispatchDraw()里的动画都没机会跑,毕竟优化布局是后续计划,咱先搞个能快速生效的方案!
核心问题分析
你遇到的本质问题是:UI线程被复杂布局的渲染任务完全占用,而dispatchDraw()是UI线程的方法,ValueAnimator默认也在UI线程执行回调,主线程忙到连动画的更新事件都处理不了,自然动画就卡成静止状态了。
几个可行的临时解决方案
1. 用独立绘制载体跑动画(最推荐)
搞一个SurfaceView或者TextureView来承载你的动画,这俩的绘制是在单独的线程里,完全不会和复杂布局的渲染抢UI线程资源。
举个简单的SurfaceView动画示例:
public class AnimSurfaceView extends SurfaceView implements SurfaceHolder.Callback, ValueAnimator.AnimatorUpdateListener { private ValueAnimator animator; private float scaleValue = 1f; public AnimSurfaceView(Context context) { super(context); getHolder().addCallback(this); } @Override public void surfaceCreated(SurfaceHolder holder) { // 初始化动画 animator = ValueAnimator.ofFloat(1f, 2f); animator.setDuration(1000); animator.setRepeatCount(ValueAnimator.INFINITE); animator.setRepeatMode(ValueAnimator.REVERSE); animator.addUpdateListener(this); animator.start(); // 启动绘制线程 new DrawThread().start(); } @Override public void onAnimationUpdate(ValueAnimator animation) { scaleValue = (float) animation.getAnimatedValue(); // 通知重绘 getHolder().lockCanvas(); getHolder().unlockCanvasAndPost(null); // 触发绘制 } private class DrawThread extends Thread { @Override public void run() { while (animator.isRunning()) { Canvas canvas = getHolder().lockCanvas(); if (canvas != null) { // 在这里绘制你的动画内容,比如缩放的图形 canvas.save(); canvas.scale(scaleValue, scaleValue, getWidth()/2f, getHeight()/2f); canvas.drawColor(Color.parseColor("#FF4081")); canvas.restore(); getHolder().unlockCanvasAndPost(canvas); } try { Thread.sleep(16); // 模拟60fps } catch (InterruptedException e) { e.printStackTrace(); } } } } // 实现surfaceChanged和surfaceDestroyed的空方法 @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} @Override public void surfaceDestroyed(SurfaceHolder holder) { if (animator != null) animator.cancel(); } }
把这个SurfaceView加到你的布局最上层,就算下面的布局在疯狂渲染,它的动画也能流畅跑起来。
2. 给动画加个“轻量覆盖层”
如果你的动画只是加载提示类的,直接在布局最上层盖一个简单的View(比如半透明的加载圈),这个View本身没有复杂布局,绘制成本极低,不会被主线程的渲染任务阻塞。
示例代码:
// 拿到根布局(假设是FrameLayout) FrameLayout rootLayout = (FrameLayout) getWindow().getDecorView().findViewById(android.R.id.content); // 创建轻量动画View View loadingView = new View(this); loadingView.setBackgroundResource(R.drawable.your_loading_drawable); // 用逐帧动画或者Shape做的加载图 FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( dp2px(80), dp2px(80), Gravity.CENTER ); rootLayout.addView(loadingView, params); // 用ViewPropertyAnimator跑动画 loadingView.animate() .rotation(360f) .setDuration(1000) .setRepeatCount(ValueAnimator.INFINITE) .setInterpolator(new LinearInterpolator());
这种方案最省心,几行代码就能搞定,而且完全不影响下面的复杂布局渲染。
3. 异步加载复杂布局
如果愿意稍微改一下布局加载逻辑,用AsyncLayoutInflater(AndroidX提供)异步加载你的复杂布局,这样UI线程就不会被长时间阻塞,dispatchDraw()里的动画自然就能正常执行了。
示例代码:
// 异步加载复杂布局 new AsyncLayoutInflater(this).inflate(R.layout.your_super_complex_layout, rootLayout, (view, resid, parent) -> { // 布局加载完成后添加到父容器 rootLayout.addView(view); // 这里可以停止动画 stopLoadingAnimation(); }); // 在布局加载前启动动画 startLoadingAnimationInDispatchDraw();
这样UI线程在等待布局加载的过程中是空闲的,动画的更新回调就能正常触发了。
总结
优先选轻量覆盖层或者SurfaceView的方案,不用动现有布局的核心逻辑,快速解决问题;如果需要和布局深度联动,再考虑异步加载布局的方式。
内容的提问来源于stack exchange,提问作者Peri Hartman

