Canvas路径重叠异常求助:射线动画残留多余线条
问题排查:射线动画重叠残留线条
我正在开发一个从屏幕左上角发射三条射线的动画,射线需展开并擦除屏幕整个“上方”区域。目前已取得进展,但两条射线出现异常重叠,仿佛互相挤压,残留一条不必要的线条,恳请帮忙排查问题原因。
当前效果说明
动画运行时,两条射线的擦除区域出现异常重叠,中间残留一条未被正确擦除的线条,呈现出互相挤压的视觉效果。
自定义View代码
class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : View(context, attrs, defStyleAttr) { companion object { const val ADDITIONAL_NUM_VALUE_HOLDER = "additional_num" const val TEXT_TOP_PADDING_VALUE_HOLDER = "text_top_padding" } private var rayPaint: Paint private var textPaint: TextPaint private var rayPath: Path = Path() private var additionalNum = 1f private var shouldAnimateRays = false private var textX = 20f private var textY = 0f init { rayPaint = Paint().apply { style = Paint.Style.FILL strokeWidth = 4f color = -0x1 xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) isAntiAlias = true } textPaint = TextPaint().apply { style = Paint.Style.FILL color = Color.BLUE textSize = 60f isAntiAlias = true } } override fun dispatchDraw(canvas: Canvas) { val bitmap = Bitmap.createBitmap(canvas.width, canvas.height, Bitmap.Config.ARGB_8888) bitmap.eraseColor(Color.WHITE) val additionalCanvas = Canvas(bitmap) additionalCanvas.drawColor(Color.GREEN) val someText = "Hello world" val textLayout = StaticLayout.Builder .obtain(someText, 0, someText.length, textPaint, additionalCanvas.width) .setAlignment(Layout.Alignment.ALIGN_NORMAL) .setLineSpacing(0.0f, 1.0f) .setIncludePad(true) .build() additionalCanvas.save() additionalCanvas.translate(textX, textY) textLayout.draw(additionalCanvas) additionalCanvas.restore() if (shouldAnimateRays) { val x = canvas.width.toFloat() val y = canvas.height.toFloat() // first ray rayPath.moveTo(0f, 0f) rayPath.lineTo(x, (y * 0.5f) - additionalNum) rayPath.lineTo(x, (y * 0.5f) + additionalNum) rayPath.lineTo(0f, 0f) // second ray rayPath.moveTo(0f, 0f) rayPath.lineTo(x, (y * 0.7f) - additionalNum) rayPath.lineTo(x, (y * 0.7f) + additionalNum) rayPath.lineTo(0f, 0f) // third ray rayPath.moveTo(0f, 0f) rayPath.lineTo((x * 0.7f) - additionalNum, y) rayPath.lineTo((x * 0.7f) + additionalNum, y) rayPath.lineTo(0f, 0f) additionalCanvas.drawPath(rayPath, rayPaint) } canvas.drawBitmap(bitmap, 0f, 0f, null) super.dispatchDraw(canvas) } fun animateShapes() { val textValuesHolder = PropertyValuesHolder.ofFloat(TEXT_TOP_PADDING_VALUE_HOLDER, -100f, 20f) val valuesHolder = PropertyValuesHolder.ofFloat(ADDITIONAL_NUM_VALUE_HOLDER, 1f, 800f) val animator = ValueAnimator().apply { setValues(valuesHolder) duration = 2_000 interpolator = AccelerateDecelerateInterpolator() addUpdateListener { val addNum = it.getAnimatedValue(ADDITIONAL_NUM_VALUE_HOLDER) as Float additionalNum = addNum invalidate() } } val textAnimator = ValueAnimator().apply { setValues(textValuesHolder) duration = 2_000 interpolator = AccelerateDecelerateInterpolator() addUpdateListener { val paddingTop = it.getAnimatedValue(TEXT_TOP_PADDING_VALUE_HOLDER) as Float textY = paddingTop invalidate() } doOnEnd { shouldAnimateRays = true animator.start() } } textAnimator.start() } }
Activity代码
@AndroidEntryPoint class CustomActivity : AppCompatActivity() { private lateinit var binding: ActivityCustomBinding protected var rootView: ViewGroup? = null private set override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityCustomBinding.inflate(layoutInflater) setContentView(binding.root) rootView = findViewById(android.R.id.content) binding.customView.animateShapes() } }
布局文件
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorWhite"> <ImageView android:id="@+id/imageView" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleX="0.8" android:scaleY="0.8" android:src="@drawable/common_google_signin_btn_icon_dark_normal" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> <com.app.android.view.CustomView android:id="@+id/customView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
问题原因及解决方法
核心问题
- 路径未重置:
rayPath作为成员变量,每次dispatchDraw绘制射线时未清空旧路径,导致动画更新时新路径叠加在旧路径之上,最终出现重叠挤压效果。 - Xfermode兼容问题:
PorterDuff.Mode.CLEAR在硬件加速模式下可能出现渲染异常,导致擦除效果不符合预期。
修复步骤
1. 重置路径避免叠加
在绘制射线前调用rayPath.reset()清空旧路径,同时用close()替代手动lineTo(0f,0f),路径闭合更规范:
if (shouldAnimateRays) { val x = canvas.width.toFloat() val y = canvas.height.toFloat() // 重置路径,清除之前的绘制内容 rayPath.reset() // first ray rayPath.moveTo(0f, 0f) rayPath.lineTo(x, (y * 0.5f) - additionalNum) rayPath.lineTo(x, (y * 0.5f) + additionalNum) rayPath.close() // second ray rayPath.moveTo(0f, 0f) rayPath.lineTo(x, (y * 0.7f) - additionalNum) rayPath.lineTo(x, (y * 0.7f) + additionalNum) rayPath.close() // third ray rayPath.moveTo(0f, 0f) rayPath.lineTo((x * 0.7f) - additionalNum, y) rayPath.lineTo((x * 0.7f) + additionalNum, y) rayPath.close() additionalCanvas.drawPath(rayPath, rayPaint) }
2. 关闭硬件加速解决Xfermode异常
在CustomView的初始化代码中添加硬件加速关闭逻辑:
init { // 关闭硬件加速,解决CLEAR模式渲染异常 setLayerType(LAYER_TYPE_SOFTWARE, null) rayPaint = Paint().apply { // 原有画笔配置 } textPaint = TextPaint().apply { // 原有画笔配置 } }
3. 可选:使用局部Path对象
每次绘制时创建新的局部Path变量,彻底避免成员变量路径累加问题:
if (shouldAnimateRays) { val x = canvas.width.toFloat() val y = canvas.height.toFloat() val rayPath = Path() // 绘制第一条射线 rayPath.moveTo(0f, 0f) rayPath.lineTo(x, (y * 0.5f) - additionalNum) rayPath.lineTo(x, (y * 0.5f) + additionalNum) rayPath.close() additionalCanvas.drawPath(rayPath, rayPaint) // 重置局部路径绘制第二条射线 rayPath.reset() rayPath.moveTo(0f, 0f) rayPath.lineTo(x, (y * 0.7f) - additionalNum) rayPath.lineTo(x, (y * 0.7f) + additionalNum) rayPath.close() additionalCanvas.drawPath(rayPath, rayPaint) // 重置局部路径绘制第三条射线 rayPath.reset() rayPath.moveTo(0f, 0f) rayPath.lineTo((x * 0.7f) - additionalNum, y) rayPath.lineTo((x * 0.7f) + additionalNum, y) rayPath.close() additionalCanvas.drawPath(rayPath, rayPaint) }
内容的提问来源于stack exchange,提问作者memeprincess
相关产品推荐
相关产品推荐

