最近APP大改版,在启动页加入了一个很炫的动画功能介绍。但是当运行在Android Q机子上的时候,出现了奔溃,奔溃信息如下所示:
2019-09-10 15:33:05.825 21734-21734/com.xtc.watch E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.xtc.watch, PID: 21734
java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAGS is allowed
at android.graphics.Canvas.checkValidSaveFlags(Canvas.java:442)
at android.graphics.Canvas.saveLayer(Canvas.java:519)
at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:222)
at com.airbnb.lottie.model.layer.CompositionLayer.drawLayer(CompositionLayer.java:100)
at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:188)
at com.airbnb.lottie.model.layer.CompositionLayer.drawLayer(CompositionLayer.java:100)
at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:188)
at com.airbnb.lottie.LottieDrawable.draw(LottieDrawable.java:311)
at android.widget.ImageView.onDraw(ImageView.java:1434)
at android.view.View.draw(View.java:21436)
at android.view.View.updateDisplayListIfDirty(View.java:20313)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
at android.view.View.updateDisplayListIfDirty(View.java:20273)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
at android.view.View.updateDisplayListIfDirty(View.java:20273)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
at android.view.View.updateDisplayListIfDirty(View.java:20273)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
at android.view.View.updateDisplayListIfDirty(View.java:20273)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
at android.view.View.updateDisplayListIfDirty(View.java:20273)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
at android.view.View.updateDisplayListIfDirty(View.java:20273)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4372)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4345)
at android.view.View.updateDisplayListIfDirty(View.java:20273)
at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:575)
at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:581)
at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:654)
at android.view.ViewRootImpl.draw(ViewRootImpl.java:3610)
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3418)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2755)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1721)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7598)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:966)
at android.view.Choreographer.doCallbacks(Choreographer.java:790)
at android.view.Choreographer.doFrame(Choreographer.java:725)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:951)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
问题所在是使用Lottie播放动画的时候出现的问题。
Lottie动画的github地址是: https://github.com/airbnb/lottie-android
错误日志部分信息如下,可以判断出应该是Lottie库出现了问题。
java.lang.IllegalArgumentException: Invalid Layer Save Flag - only ALL_SAVE_FLAGS is allowed
at android.graphics.Canvas.checkValidSaveFlags(Canvas.java:442)
at android.graphics.Canvas.saveLayer(Canvas.java:519)
at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:222)
at com.airbnb.lottie.model.layer.CompositionLayer.drawLayer(CompositionLayer.java:100)
at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:188)
at com.airbnb.lottie.model.layer.CompositionLayer.drawLayer(CompositionLayer.java:100)
at com.airbnb.lottie.model.layer.BaseLayer.draw(BaseLayer.java:188)
at com.airbnb.lottie.LottieDrawable.draw(LottieDrawable.java:311)
at android.widget.ImageView.onDraw(ImageView.java:1434)
至于具体错误原因请慢慢往下看。
我们进入 android.graphics.Canvas#checkValidSaveFlags
方法查看下
/**
* Restore everything when restore() is called (standard save flags).
* Note: for performance reasons, it is
* strongly recommended to pass this - the complete set of flags - to any
* call to saveLayer()
and saveLayerAlpha()
* variants.
*
*
Note: all methods that accept this flag
* have flagless versions that are equivalent to passing this flag.
*/
public static final int ALL_SAVE_FLAG = 0x1F;
private static void checkValidSaveFlags(int saveFlags) {
if (sCompatiblityVersion >= Build.VERSION_CODES.P
&& saveFlags != ALL_SAVE_FLAG) {
throw new IllegalArgumentException(
"Invalid Layer Save Flag - only ALL_SAVE_FLAGS is allowed");
}
}
这段代码检测的意思就是:当版本大于Build.VERSION_CODES.P(即SDK 28版本)的时候,并且saveFlags的值不为ALL_SAVE_FLAG的话,就抛出这个异常。
而 android.graphics.Canvas#checkValidSaveFlags
这个方法被saveLayer()方法和saveLayerAlpha()方法调用检测的。
/**
* This behaves the same as save(), but in addition it allocates and
* redirects drawing to an offscreen bitmap.
* Note: this method is very expensive,
* incurring more than double rendering cost for contained content. Avoid
* using this method, especially if the bounds provided are large. It is
* recommended to use a {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
* to apply an xfermode, color filter, or alpha, as it will perform much
* better than this method.
*
* All drawing calls are directed to a newly allocated offscreen bitmap.
* Only when the balancing call to restore() is made, is that offscreen
* buffer drawn back to the current target of the Canvas (either the
* screen, it's target Bitmap, or the previous layer).
*
* Attributes of the Paint - {@link Paint#getAlpha() alpha},
* {@link Paint#getXfermode() Xfermode}, and
* {@link Paint#getColorFilter() ColorFilter} are applied when the
* offscreen bitmap is drawn back when restore() is called.
*
* As of API Level API level {@value Build.VERSION_CODES#P} the only valid
* {@code saveFlags} is {@link #ALL_SAVE_FLAG}. All other flags are ignored.
*
* @deprecated Use {@link #saveLayer(RectF, Paint)} instead.
* @param bounds May be null. The maximum size the offscreen bitmap
* needs to be (in local coordinates)
* @param paint This is copied, and is applied to the offscreen when
* restore() is called.
* @param saveFlags see _SAVE_FLAG constants, generally {@link #ALL_SAVE_FLAG} is recommended
* for performance reasons.
* @return value to pass to restoreToCount() to balance this save()
*/
public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags) {
if (bounds == null) {
bounds = new RectF(getClipBounds());
}
checkValidSaveFlags(saveFlags);
return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint,
ALL_SAVE_FLAG);
}
/**
* Helper version of saveLayer() that takes 4 values rather than a RectF.
*
* As of API Level API level {@value Build.VERSION_CODES#P} the only valid
* {@code saveFlags} is {@link #ALL_SAVE_FLAG}. All other flags are ignored.
*
* @deprecated Use {@link #saveLayer(float, float, float, float, Paint)} instead.
*/
public int saveLayer(float left, float top, float right, float bottom, @Nullable Paint paint,
@Saveflags int saveFlags) {
checkValidSaveFlags(saveFlags);
return nSaveLayer(mNativeCanvasWrapper, left, top, right, bottom,
paint != null ? paint.getNativeInstance() : 0,
ALL_SAVE_FLAG);
}
/**
* This behaves the same as save(), but in addition it allocates and
* redirects drawing to an offscreen bitmap.
* Note: this method is very expensive,
* incurring more than double rendering cost for contained content. Avoid
* using this method, especially if the bounds provided are large. It is
* recommended to use a {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
* to apply an xfermode, color filter, or alpha, as it will perform much
* better than this method.
*
* All drawing calls are directed to a newly allocated offscreen bitmap.
* Only when the balancing call to restore() is made, is that offscreen
* buffer drawn back to the current target of the Canvas (either the
* screen, it's target Bitmap, or the previous layer).
*
* The {@code alpha} parameter is applied when the offscreen bitmap is
* drawn back when restore() is called.
*
* As of API Level API level {@value Build.VERSION_CODES#P} the only valid
* {@code saveFlags} is {@link #ALL_SAVE_FLAG}. All other flags are ignored.
*
* @deprecated Use {@link #saveLayerAlpha(RectF, int)} instead.
* @param bounds The maximum size the offscreen bitmap needs to be
* (in local coordinates)
* @param alpha The alpha to apply to the offscreen when it is
drawn during restore()
* @param saveFlags see _SAVE_FLAG constants, generally {@link #ALL_SAVE_FLAG} is recommended
* for performance reasons.
* @return value to pass to restoreToCount() to balance this call
*/
public int saveLayerAlpha(@Nullable RectF bounds, int alpha, @Saveflags int saveFlags) {
if (bounds == null) {
bounds = new RectF(getClipBounds());
}
checkValidSaveFlags(saveFlags);
return saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, alpha,
ALL_SAVE_FLAG);
}
/**
* Helper for saveLayerAlpha() that takes 4 values instead of a RectF.
*
* As of API Level API level {@value Build.VERSION_CODES#P} the only valid
* {@code saveFlags} is {@link #ALL_SAVE_FLAG}. All other flags are ignored.
*
* @deprecated Use {@link #saveLayerAlpha(float, float, float, float, int)} instead.
*/
public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha,
@Saveflags int saveFlags) {
checkValidSaveFlags(saveFlags);
alpha = Math.min(255, Math.max(0, alpha));
return nSaveLayerAlpha(mNativeCanvasWrapper, left, top, right, bottom,
alpha, ALL_SAVE_FLAG);
}
因此,不管是调用saveLayer()方法还是saveLayerAlpha()方法,如果targetSDK 已经达到28的话,saveFlags的值一定要设置为android.graphics.Canvas#ALL_SAVE_FLAG
在Lottie动画库里面,com.airbnb.lottie.model.layer.BaseLayer#draw() 方法中 有一段代码如下所示
if (hasMatteOnThisLayer()) {
L.beginSection("Layer#drawMatte");
L.beginSection("Layer#saveLayer");
canvas.saveLayer(rect, mattePaint, SAVE_FLAGS);
L.endSection("Layer#saveLayer");
clearCanvas(canvas);
//noinspection ConstantConditions
matteLayer.draw(canvas, parentMatrix, alpha);
L.beginSection("Layer#restoreLayer");
canvas.restore();
L.endSection("Layer#restoreLayer");
L.endSection("Layer#drawMatte");
}
因此可以看出来,就是这一行代码出现了问题。
canvas.saveLayer(rect, contentPaint, SAVE_FLAGS);
所以我们得去查一查,Lottie动画库是否有版本更新,是否修复了此问题。
去官网github查看 v2.5.4版本是2018年4月10日发布的。距离现在已经很久很久了。
最新版本是3.0.1 ,发布与2019年4月26日。
但是不能直接用最新版本,因为在ReadMe文档中有介绍,2.8.0以上版本只支持迁移到了androidX之后的工程。而我们项目虽然进行迁移了androidX操作,但是还在模块提测阶段,所以暂时不能使用最新版本。 等后面通过模块进入集成之后再升级。
v2.7.0 于 2018年9月24日发布,也是目前项目阶段能用的最高的版本了。就选这个版本试一试修复此bug没。
查看Lottie官网上的issue 也有人遇到同样的问题,并且issue被关闭了,应该是修复了。
https://github.com/airbnb/lottie-android/issues/747
https://github.com/airbnb/lottie-android/issues/768
官网上的pull合并代码请求查看下
https://github.com/airbnb/lottie-android/pull/748
https://github.com/airbnb/lottie-android/pull/824
所以应该是早就解决了此问题,正好我们项目刚好将targetSDK升级到了28,所以遇到此问题。下面我们升级到v2.7.0版本试一试。
升级之后,saveFlags已经变成了Canvas.ALL_SAVE_FLAG。所以没问题了!
if (hasMatteOnThisLayer()) {
L.beginSection("Layer#drawMatte");
L.beginSection("Layer#saveLayer");
saveLayerCompat(canvas, rect, mattePaint, false);
L.endSection("Layer#saveLayer");
clearCanvas(canvas);
//noinspection ConstantConditions
matteLayer.draw(canvas, parentMatrix, alpha);
L.beginSection("Layer#restoreLayer");
canvas.restore();
L.endSection("Layer#restoreLayer");
L.endSection("Layer#drawMatte");
}
这段代码调用了saveLayerCompat(canvas, rect, mattePaint, false);
方法,代码如下所示:
@SuppressLint("WrongConstant")
private void saveLayerCompat(Canvas canvas, RectF rect, Paint paint, boolean all) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// This method was deprecated in API level 26 and not recommented since 22, but its
// 2-parameter replacement is only available starting at API level 21.
canvas.saveLayer(rect, paint, all ? Canvas.ALL_SAVE_FLAG : SAVE_FLAGS);
} else {
canvas.saveLayer(rect, paint);
}
}
当SDK大于M(即23)的时候,走的是canvas.saveLayer(rect, paint);
而 android.graphics.Canvas#saveLayer(android.graphics.RectF, android.graphics.Paint) 源码如下
/**
* This behaves the same as save(), but in addition it allocates and
* redirects drawing to an offscreen rendering target.
* Note: this method is very expensive,
* incurring more than double rendering cost for contained content. Avoid
* using this method when possible and instead use a
* {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
* to apply an xfermode, color filter, or alpha, as it will perform much
* better than this method.
*
* All drawing calls are directed to a newly allocated offscreen rendering target.
* Only when the balancing call to restore() is made, is that offscreen
* buffer drawn back to the current target of the Canvas (which can potentially be a previous
* layer if these calls are nested).
*
* Attributes of the Paint - {@link Paint#getAlpha() alpha},
* {@link Paint#getXfermode() Xfermode}, and
* {@link Paint#getColorFilter() ColorFilter} are applied when the
* offscreen rendering target is drawn back when restore() is called.
*
* @param bounds May be null. The maximum size the offscreen render target
* needs to be (in local coordinates)
* @param paint This is copied, and is applied to the offscreen when
* restore() is called.
* @return value to pass to restoreToCount() to balance this save()
*/
public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) {
return saveLayer(bounds, paint, ALL_SAVE_FLAG);
}
传递的就是 android.graphics.Canvas#ALL_SAVE_FLAG ,因此该bug不复存在!
if (hasMatteOnThisLayer()) {
L.beginSection("Layer#drawMatte");
L.beginSection("Layer#saveLayer");
canvas.saveLayer(rect, mattePaint, SAVE_FLAGS);
L.endSection("Layer#saveLayer");
clearCanvas(canvas);
//noinspection ConstantConditions
matteLayer.draw(canvas, parentMatrix, alpha);
L.beginSection("Layer#restoreLayer");
canvas.restore();
L.endSection("Layer#restoreLayer");
L.endSection("Layer#drawMatte");
}
传的是 SAVE_FLAGS
不管是调用saveLayer()方法还是saveLayerAlpha()方法,如果targetSDK 已经达到28的话,saveFlags的值一定要设置为android.graphics.Canvas#ALL_SAVE_FLAG
作者:欧阳鹏 欢迎转载,与人分享是进步的源泉!
转载请保留原文地址:https://ouyangpeng.blog.csdn.net/article/details/100700954
☞ 本人QQ: 3024665621
☞ QQ交流群: 123133153
☞ github.com/ouyangpeng
☞ [email protected]