首页 > 代码库 > Android学习分享:执行某ViewGroup的动画时,子控件太多导致动画执行卡顿的问题
Android学习分享:执行某ViewGroup的动画时,子控件太多导致动画执行卡顿的问题
最近在项目中遇到一个问题,我有一个LinearLayout,里面装载了许多ImageView控件,ImageView控件显示着自己的图片,这个LinearLayout支持双指缩放,缩放采用ScaleAnimation来实现,但是但是在缩放过程中,屏幕十分卡顿,缩放效果根本没有跟上手指的缩放动作。后来在Google上查了一番,查到一个API,叫setAnimationDrawCacheEnabled(boolean enabled):
/** * Enables or disables the children‘s drawing cache during a layout animation. * By default, the drawing cache is enabled but this will prevent nested * layout animations from working. To nest animations, you must disable the * cache. * * @param enabled true to enable the animation cache, false otherwise * * @see #isAnimationCacheEnabled() * @see View#setDrawingCacheEnabled(boolean) */ public void setAnimationCacheEnabled(boolean enabled) { setBooleanFlag(FLAG_ANIMATION_CACHE, enabled); }
方法的注解我这里简单翻译一下:在执行一个Layout动画时开启或关闭子控件的绘制缓存。默认情况下,绘制缓存是开启的,但是这将阻止嵌套Layout动画的正常执行。对于嵌套动画,你必须禁用这个缓存。
先说drawing cache,绘制缓存的概念,Android为了提高View视图的绘制效率,提出了一个缓存的概念,其实就是一个Bitmap,用来存储View当前的绘制内容,在View的内容或者尺寸未发生改变时,这个缓存应该始终不被销毁,销毁了如果下次还用(开启了绘图缓存的前提下,API为setDrawingCacheEnabled(enabled),另外还可以设置绘图缓存Bitmap的质量,API为setDrawingCacheQuality(quality))就必须重建。
关于绘图缓存的相关介绍,可搜索这些相关API的介绍:
1)setDrawingCacheQuality(int quality)
2)setDrawingCacheEnabled(enabled)
3)setDrawingCacheBackgroundColor(color)
先看一段代码:
/** * <p>Forces the drawing cache to be built if the drawing cache is invalid.</p> * * <p>If you call {@link #buildDrawingCache()} manually without calling * {@link #setDrawingCacheEnabled(boolean) setDrawingCacheEnabled(true)}, you * should cleanup the cache by calling {@link #destroyDrawingCache()} afterwards.</p> * * <p>Note about auto scaling in compatibility mode: When auto scaling is not enabled, * this method will create a bitmap of the same size as this view. Because this bitmap * will be drawn scaled by the parent ViewGroup, the result on screen might show * scaling artifacts. To avoid such artifacts, you should call this method by setting * the auto scaling to true. Doing so, however, will generate a bitmap of a different * size than the view. This implies that your application must be able to handle this * size.</p> * * <p>You should avoid calling this method when hardware acceleration is enabled. If * you do not need the drawing cache bitmap, calling this method will increase memory * usage and cause the view to be rendered in software once, thus negatively impacting * performance.</p> * * @see #getDrawingCache() * @see #destroyDrawingCache() */ public void buildDrawingCache(boolean autoScale) { if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || (autoScale ? mDrawingCache == null : mUnscaledDrawingCache == null)) { mCachingFailed = false; int width = mRight - mLeft; int height = mBottom - mTop; final AttachInfo attachInfo = mAttachInfo; final boolean scalingRequired = attachInfo != null && attachInfo.mScalingRequired; if (autoScale && scalingRequired) { width = (int) ((width * attachInfo.mApplicationScale) + 0.5f); height = (int) ((height * attachInfo.mApplicationScale) + 0.5f); } final int drawingCacheBackgroundColor = mDrawingCacheBackgroundColor;
// 1.这里 final boolean opaque = drawingCacheBackgroundColor != 0 || isOpaque(); final boolean use32BitCache = attachInfo != null && attachInfo.mUse32BitDrawingCache; final long projectedBitmapSize = width * height * (opaque && !use32BitCache ? 2 : 4); final long drawingCacheSize = ViewConfiguration.get(mContext).getScaledMaximumDrawingCacheSize(); if (width <= 0 || height <= 0 || projectedBitmapSize > drawingCacheSize) { if (width > 0 && height > 0) { Log.w(VIEW_LOG_TAG, "View too large to fit into drawing cache, needs " + projectedBitmapSize + " bytes, only " + drawingCacheSize + " available"); } destroyDrawingCache(); mCachingFailed = true; return; } boolean clear = true; Bitmap bitmap = autoScale ? mDrawingCache : mUnscaledDrawingCache; if (bitmap == null || bitmap.getWidth() != width || bitmap.getHeight() != height) { Bitmap.Config quality;
// 2.这里 if (!opaque) { // Never pick ARGB_4444 because it looks awful // Keep the DRAWING_CACHE_QUALITY_LOW flag just in case switch (mViewFlags & DRAWING_CACHE_QUALITY_MASK) { case DRAWING_CACHE_QUALITY_AUTO: case DRAWING_CACHE_QUALITY_LOW: case DRAWING_CACHE_QUALITY_HIGH: default: quality = Bitmap.Config.ARGB_8888; break; } } else { // Optimization for translucent windows // If the window is translucent, use a 32 bits bitmap to benefit from memcpy() quality = use32BitCache ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; } // Try to cleanup memory if (bitmap != null) bitmap.recycle(); try { bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(), width, height, quality); bitmap.setDensity(getResources().getDisplayMetrics().densityDpi); if (autoScale) { mDrawingCache = bitmap; } else { mUnscaledDrawingCache = bitmap; } if (opaque && use32BitCache) bitmap.setHasAlpha(false); } catch (OutOfMemoryError e) { // If there is not enough memory to create the bitmap cache, just // ignore the issue as bitmap caches are not required to draw the // view hierarchy if (autoScale) { mDrawingCache = null; } else { mUnscaledDrawingCache = null; } mCachingFailed = true; return; } clear = drawingCacheBackgroundColor != 0; } Canvas canvas; if (attachInfo != null) { canvas = attachInfo.mCanvas; if (canvas == null) { canvas = new Canvas(); } canvas.setBitmap(bitmap); // Temporarily clobber the cached Canvas in case one of our children // is also using a drawing cache. Without this, the children would // steal the canvas by attaching their own bitmap to it and bad, bad // thing would happen (invisible views, corrupted drawings, etc.) attachInfo.mCanvas = null; } else { // This case should hopefully never or seldom happen canvas = new Canvas(bitmap); } if (clear) { bitmap.eraseColor(drawingCacheBackgroundColor); } computeScroll(); final int restoreCount = canvas.save(); if (autoScale && scalingRequired) { final float scale = attachInfo.mApplicationScale; canvas.scale(scale, scale); } canvas.translate(-mScrollX, -mScrollY); mPrivateFlags |= PFLAG_DRAWN; if (mAttachInfo == null || !mAttachInfo.mHardwareAccelerated || mLayerType != LAYER_TYPE_NONE) { mPrivateFlags |= PFLAG_DRAWING_CACHE_VALID; } // Fast path for layouts with no backgrounds if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas); if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().draw(canvas); } } else { draw(canvas); } canvas.restoreToCount(restoreCount); canvas.setBitmap(null); if (attachInfo != null) { // Restore the cached Canvas for our siblings attachInfo.mCanvas = canvas; } } }
两个标注了红色的地方,说明了这个mDrawingCacheBackgroundColor变量的作用,因此如果使用默认值,那么缓存Bitmap使用的是Bitmap.Config.ARGB_8888,比Bitmap.Config.RGB_565多占用了一半的内存,因此如果不想使用太大的内存,担心内存泄露,可以设置给mDrawingCacheBackgroundColor一个值,例如:
setDrawingCacheBackgroundColor(0xFF0C0C0C);
那么,那么,这个绘图缓存如何优化绘图速率,又怎么阻碍了Animation的执行?
先看两个方法:
1)ViewGroup -> dispatchDraw(Canvas canvas) 方法:
/** * {@inheritDoc} */ @Override protected void dispatchDraw(Canvas canvas) { final int count = mChildrenCount; final View[] children = mChildren; int flags = mGroupFlags; // 关键字:FLAG_RUN_ANIMATION,FLAG_ANIMATION_CACHE,cache,buildCache if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE; final boolean buildCache = !isHardwareAccelerated(); for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { final LayoutParams params = child.getLayoutParams(); attachLayoutAnimationParameters(child, params, i, count); bindLayoutAnimation(child); if (cache) { child.setDrawingCacheEnabled(true); if (buildCache) { child.buildDrawingCache(true); } } } } final LayoutAnimationController controller = mLayoutAnimationController; if (controller.willOverlap()) { mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE; } controller.start(); mGroupFlags &= ~FLAG_RUN_ANIMATION; mGroupFlags &= ~FLAG_ANIMATION_DONE; if (cache) { mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE; } if (mAnimationListener != null) { mAnimationListener.onAnimationStart(controller.getAnimation()); } } int saveCount = 0; final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; if (clipToPadding) { saveCount = canvas.save(); canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, mScrollX + mRight - mLeft - mPaddingRight, mScrollY + mBottom - mTop - mPaddingBottom); } // We will draw our child‘s animation, let‘s reset the flag mPrivateFlags &= ~PFLAG_DRAW_ANIMATION; mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED; boolean more = false; final long drawingTime = getDrawingTime(); if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) { for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } else { for (int i = 0; i < count; i++) { final View child = children[getChildDrawingOrder(count, i)]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } // Draw any disappearing views that have animations if (mDisappearingChildren != null) { final ArrayList<View> disappearingChildren = mDisappearingChildren; final int disappearingCount = disappearingChildren.size() - 1; // Go backwards -- we may delete as animations finish for (int i = disappearingCount; i >= 0; i--) { final View child = disappearingChildren.get(i); more |= drawChild(canvas, child, drawingTime); } } if (debugDraw()) { onDebugDraw(canvas); } if (clipToPadding) { canvas.restoreToCount(saveCount); } // mGroupFlags might have been updated by drawChild() flags = mGroupFlags; if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) { invalidate(true); } if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 && mLayoutAnimationController.isDone() && !more) { // We want to erase the drawing cache and notify the listener after the // next frame is drawn because one extra invalidate() is caused by // drawChild() after the animation is over mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER; final Runnable end = new Runnable() { public void run() { notifyAnimationListener(); } }; post(end); } }
从中可以看出这个FLAG_ANIMATION_CACHE的作用了,当然还和硬件加速扯上了关系,这里先不补充相关知识,想了解的可以度娘or谷歌。
附:对硬件加速带源码分析的比较好的一篇文章:
Android硬件加速绘制过程源码分析(一) Android硬件加速绘制过程源码分析(二)——DisplayList录制绘制操作 Android硬件加速绘制过程源码分析(三)——DisplayList的绘制过程 Android硬件加速绘制过程源码分析(四)——离屏硬件缓存HardwareLayer2)View -> draw(Canvas canvas, ViewGroup parent, long drawingTime) 方法;
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { boolean caching; …………… final int flags = parent.mGroupFlags; ………….. if ((flags & ViewGroup.FLAG_CHILDREN_DRAWN_WITH_CACHE) != 0 || (flags & ViewGroup.FLAG_ALWAYS_DRAWN_WITH_CACHE) != 0) { caching = true; // Auto-scaled apps are not hw-accelerated, no need to set scaling flag on DisplayList if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired; } else { caching = (layerType != LAYER_TYPE_NONE) || hardwareAccelerated; } ………….. if (caching) { if (!hardwareAccelerated) { if (layerType != LAYER_TYPE_NONE) { layerType = LAYER_TYPE_SOFTWARE; buildDrawingCache(true); } cache = getDrawingCache(true); } else { switch (layerType) { case LAYER_TYPE_SOFTWARE: if (useDisplayListProperties) { hasDisplayList = canHaveDisplayList(); } else { buildDrawingCache(true); cache = getDrawingCache(true); } break; case LAYER_TYPE_HARDWARE: if (useDisplayListProperties) { hasDisplayList = canHaveDisplayList(); } break; case LAYER_TYPE_NONE: // Delay getting the display list until animation-driven alpha values are // set up and possibly passed on to the view hasDisplayList = canHaveDisplayList(); break; } } } …………………..}
从上面的代码可以分析出来,如果不禁止绘图缓存,那么每次绘制子View时都要更新缓存并且将缓存画到画布中。这无疑是多了一步,画一个bitmap,animation需要不停的画所以也就多了很多操作,但是这个缓存不是说是对绘制视图的优化嘛,这个秘密就在View的invalidate中,当子View需要 invalidate时,事实上也是交给父布局去分发的。
/** * This is where the invalidate() work actually happens. A full invalidate() * causes the drawing cache to be invalidated, but this function can be called with * invalidateCache set to false to skip that invalidation step for cases that do not * need it (for example, a component that remains at the same dimensions with the same * content). * * @param invalidateCache Whether the drawing cache for this view should be invalidated as * well. This is usually true for a full invalidate, but may be set to false if the * View‘s contents or dimensions have not changed.
* 指示在视图刷新时,是否也要刷新绘图缓存,对于一个完全的刷新操作,比如视图内容发生了变化,
* 或者控件尺寸发生变化了,那么应该设置true,但是如果不是二者任何一个,则应该设置为false。 */ void invalidate(boolean invalidateCache) { if (skipInvalidate()) { return; } if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED || isOpaque() != mLastIsOpaque) { mLastIsOpaque = isOpaque(); mPrivateFlags &= ~PFLAG_DRAWN; mPrivateFlags |= PFLAG_DIRTY; if (invalidateCache) { mPrivateFlags |= PFLAG_INVALIDATED; mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; } final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; //noinspection PointlessBooleanExpression,ConstantConditions if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { if (p != null && ai != null && ai.mHardwareAccelerated) { // fast-track for GL-enabled applications; just invalidate the whole hierarchy // with a null dirty rect, which tells the ViewAncestor to redraw everything p.invalidateChild(this, null); return; } } if (p != null && ai != null) { final Rect r = ai.mTmpInvalRect; r.set(0, 0, mRight - mLeft, mBottom - mTop); // Don‘t call invalidate -- we don‘t want to internally scroll // our own bounds p.invalidateChild(this, r); } } }
接着,咱们再看一个ViewGroup的方法,setPersistentDrawingCache(int drawingCacheToKeep):
/** * Indicates what types of drawing caches should be kept in memory after * they have been created. * * @see #getPersistentDrawingCache() * @see #setAnimationCacheEnabled(boolean) * * @param drawingCacheToKeep one or a combination of {@link #PERSISTENT_NO_CACHE}, * {@link #PERSISTENT_ANIMATION_CACHE}, {@link #PERSISTENT_SCROLLING_CACHE} * and {@link #PERSISTENT_ALL_CACHES} */ public void setPersistentDrawingCache(int drawingCacheToKeep) { mPersistentDrawingCache = drawingCacheToKeep & PERSISTENT_ALL_CACHES; }
这个方法的作用,便是控制绘图缓存在被创建之后,什么时候使用。
方法的可用参数,系统提供了四个值:
1)PERSISTENT_ANIMATION_CACHE:动画前不可用,动画结束时可用,并保存此时的Cache。
2)PERSISTENT_SCROLLING_CACHE:滚动式不可用,滚动结束时可用,并保存此时的Cache。
3)PERSISTENT_ALL_CACHES:不管在什么时候,都是用缓存。
4)PERSISTENT_NO_CACHE:不适用缓存。
因此,你可以手动的控制AnimationDrawCache(在执行动画前禁用,执行完毕后启用)或者调用这个方法传入适用于相应场景的参数值就可以自动实现控制了。
大概的明白了,有木有!其实我理解的也不深入,都是在别人分析的基础上总结出来,希望对大家有用,也对自己有用。
Over!
Android学习分享:执行某ViewGroup的动画时,子控件太多导致动画执行卡顿的问题