android 获取当前画布,Android硬件位图填坑之获取硬件画布
前言
Hardware Bitmap(硬件位圖)是Android8.0加入的新功能,通過設置Bitmap的config為Bitmap.Config.HARDWARE,創建所謂的Hardware Bitmap,它不同與其他Config的Bitmap,Hardware Bitmap對應的像素數據是存儲在顯存中,并對圖片僅在屏幕上繪制的場景做了優化;
硬件位圖的介紹參考Glide文檔
何如使用Hardware Bitmap
創建Hardware Bitmap
眾所周知,Bitmap的創建一般是調用BitmapFactory這個工廠類來實現,由于Hardware Bitmap需要配置Bitmap.Config.HARDWARE屬性,一個基本的獲取用Hardware Bitmap的寫法如下:
val options = BitmapFactory.Options()
options.inPreferredConfig = Bitmap.Config.HARDWARE
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.dog, options)
復制代碼
主要是需要設置BitmapFactory.Options的inPreferredConfig為Bitmap.Config.HARDWARE;
針對HARDWARE情況BitmapFactory的提示
如果設置了inPreferredConfig = Bitmap.Config.HARDWARE,千萬不要設置options.inMutable = true,這樣會引起報錯,因為Hardware Bitmap是不可變的,也不能被利用;另外inBitmap屬性也沒有必要設置,因為硬件位圖不需要當前進程的緩存復用,如果設置inBitmap可能會替換掉之前設置的inPreferredConfig屬性;
使用Hardware Bitmap
通過上一步的創建,我們獲得Bitmap對象,首先我們可以通過bitmap.getConfig()獲取到當前Bitmap是不是Hardware,其次,大多數情況下,我們是把Bitmap設置給ImageView控件;
imageView.setImageBitmap(bitmap)
復制代碼
一行代碼搞定imageView沒錯,這行代碼一般情況下是沒有問題的,那么問題在哪里?
首先,硬件位圖只支持GPU的繪制,言外之意是這個ImageView必須在開啟硬件加速的Activity中,而且當前這個ImageView不能設置軟件層 (software layer type);
開啟硬件加速的代碼
//application級別開啟硬件加速
>
//activity級別開啟硬件加速
復制代碼在View 上使用software layer type
ImageView imageView = …
imageView.setImageBitmap(hardwareBitmap);
imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
復制代碼
如果我們滿足硬件加速和不設置software layer type這兩個條件,在正真使用中還有坑,其中最大的也最頻繁發生的就是通過Canvas來改變Bitmap的形狀或者其他的轉換;
拿圓形圖片做例子
假設我們需要顯示圓形圖片,一般解決方案有兩種:通過自定義控件處理和通過Glide等工具類直接剪裁Bitmap;當Bitmap剪裁遇到HARDWARE就是問題的開始;
通過自定義控件比如CircleImageView的方案,onDraw()方法如下:
@Override
protected void onDraw(Canvas canvas) {
if (mDisableCircularTransformation) {
super.onDraw(canvas);
return;
}
if (mBitmap == null) {
return;
}
if (mCircleBackgroundColor != Color.TRANSPARENT) {
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint);
}
canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
if (mBorderWidth > 0) {
canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
}
}
復制代碼
這是CircleImageView重新onDraw()方法,通過自定義控件實現剪切圓角,在設置硬件位圖Bitmap時,一般都沒有問題;
通過類似Glide等直接處理Bitmap的方式剪裁圓形圖片,基本代碼如下:
Canvas canvas = new Canvas(resultBitmap);
// Draw a circle
canvas.drawCircle(radius, radius, radius, CIRCLE_CROP_SHAPE_PAINT);
// Draw the bitmap in the circle
canvas.drawBitmap(inBitmap, null, destRect, CIRCLE_CROP_BITMAP_PAINT);
clear(canvas);
復制代碼
通過類似工具類的形式直接對Bitmap進行修改,執行到canvas.drawBitmap就會報異常,異常信息是java.lang.IllegalStateException: Software rendering doesn't support hardware bitmaps;
如何避免報異常
我大致想了這么兩個方案:
方案一:所有關于剪切Bitmap的操作都改成自定義控件,在自定義控件的onDraw中實現;
方案二:尋找一種方案,解決掉自己創建的Canvas不報異常,這樣就能繼續用工具類來處理Bitmap;
方案一技術實現比較簡單,把項目中所用用到處理Bitmap的邏輯都換成自定義控件,但是可能涉及到很多處代碼的修改,是一個功夫活;
方案二實施起來有點障礙,因為除了通過new Canvas(Bitmap)獲取畫布,還能通過什么方式能拿到Canvas,對了還有SurfaceView也是可以拿到Canvas,但是SurfaceView不支持硬件加速,所以直接就Pass了,想實現方案二我認為得弄清自定義控件onDraw()方法中Canvas從何而來;
分析Canvas流程
View.onDraw()中的Canvas從何而來
我們知道,View的繪制流程是從ViewRootImpl.performTraversals()這個方法開始
performTraversals()
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
//調動performDraw()
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();
} else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).endChangingAnimations();
}
mPendingTransitions.clear();
}
}
復制代碼
performTraversals()方法調用performDraw(),然后performDraw()方法中又調用draw(fullRedrawNeeded),大部門繪制的邏輯都是在draw(fullRedrawNeeded)方法中;
draw(fullRedrawNeeded)
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
//省略代碼
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
if (mAttachInfo.mThreadedRenderer != null &&
!mAttachInfo.mThreadedRenderer.isEnabled() &&
mAttachInfo.mThreadedRenderer.isRequested()) {
//省略代碼
//drawSoftware
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
復制代碼
從draw(fullRedrawNeeded)方法可以看到,如果支持硬件加速,調用mAttachInfo.mThreadedRenderer.draw()方法,否則調用drawSoftware()方法,繪制的基本流程從這里分叉;
drawSoftware如何獲得Canvas
drawSoftware()
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
final Canvas canvas;
try {
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
final int bottom = dirty.bottom;
canvas = mSurface.lockCanvas(dirty);
Surface.lockCanvas()
//noinspection ConstantConditions
if (left != dirty.left || top != dirty.top || right != dirty.right
|| bottom != dirty.bottom) {
attachInfo.mIgnoreDirtyState = true;
}
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
handleOutOfResourcesException(e);
return false;
} catch (IllegalArgumentException e) {
mLayoutRequested = true;
return false;
}
//省略代碼
}
復制代碼
從drawSoftware()方法可以知道,軟件繪制的流程是從Surface.lockCanvas()獲得Canvas對象;
View體系硬件加速Canvas創建過程
ThreadedRenderer.draw()
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
attachInfo.mIgnoreDirtyState = true;
final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
choreographer.mFrameInfo.markDrawStart();
//調用updateRootDisplayList更新DisplayList
updateRootDisplayList(view, callbacks);
}
復制代碼
ThreadedRenderer.updateRootDisplayList()
private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Record View#draw()");
updateViewTreeDisplayList(view);
if (mRootNodeNeedsUpdate || !mRootNode.isValid()) {
//通過RootNode.start創建DisplayListCanvas
DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight);
try {
final int saveCount = canvas.save();
canvas.translate(mInsetLeft, mInsetTop);
callbacks.onPreDraw(canvas);
canvas.insertReorderBarrier();
canvas.drawRenderNode(view.updateDisplayListIfDirty());
canvas.insertInorderBarrier();
callbacks.onPostDraw(canvas);
canvas.restoreToCount(saveCount);
mRootNodeNeedsUpdate = false;
} finally {
//最終調用end方法
mRootNode.end(canvas);
}
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
復制代碼
View.updateDisplayListIfDirty()
public RenderNode updateDisplayListIfDirty() {
final RenderNode renderNode = mRenderNode;
//省略代碼
int layerType = getLayerType();
//創建DisplayListCanvas
final DisplayListCanvas canvas = renderNode.start(width, height);
canvas.setHighContrastText(mAttachInfo.mHighContrastText);
try {
//判斷layerType
if (layerType == LAYER_TYPE_SOFTWARE) {
buildDrawingCache(true);
Bitmap cache = getDrawingCache(true);
if (cache != null) {
canvas.drawBitmap(cache, 0, 0, mLayerPaint);
}
} else {
computeScroll();
canvas.translate(-mScrollX, -mScrollY);
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);//dispatchDraw
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
if (debugDraw()) {
debugDrawFocus(canvas);
}
} else {
//調用draw()方法
draw(canvas);
}
}
} finally {
renderNode.end(canvas);
setDisplayListProperties(renderNode);
}
} else {
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
}
return renderNode;
}
復制代碼
從上面基本流程可以看出,硬件加速下Canvas的創建是調用RenderNode.create()方法,每個View都有自己的RenderNode,RenderNode的創建是在View的構造方法中;
View構造方法
public View(Context context) {
mContext = context;
mResources = context != null ? context.getResources() : null;
mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED | FOCUSABLE_AUTO;
//省略
mRenderNode = RenderNode.create(getClass().getName(), this);
//省略
}
復制代碼
RenderNode通過調用靜態方法create得到RenderNode對象,我們繼續看RenderNode.create()方法
RenderNode.create()
/**
* @param name The name of the RenderNode, used for debugging purpose. May be null.
* @return A new RenderNode.
*/
public static RenderNode create(String name, @Nullable View owningView) {
return new RenderNode(name, owningView);
}
復制代碼
create()方法有兩個參數,第一個name,第二個是owningView,而且是可以為空的,從注釋上來看,name只是為了調試用,而且owningView可以為空,我們可以用反射去創建一個簡單的RenderNode;
嘗試創建一個Canvas
回顧一下,寫出一個簡單的創建一個硬件加速Canvas的代碼:
第一行,創建RenderNode
RenderNode node = RenderNode.create("helloworld", null);
第二行,創建DisplayListCanvas
final DisplayListCanvas canvas = node.start(bitmapWidth, bitmapHeight);
第三行,執行canvas的操作
canvas.xxx();
第四行,執行node.end()方法
node.end(canvas);
復制代碼
一個簡單的DisplayListCanvas創建流程在腦海中浮現出來,但是還有個問題,我們執行完canvas的繪制操作之后,生成的產物Bitmap從哪里得到,我們回顧和ViewRootImpl打交道的硬件加速繪制相關的類是ThreadedRenderer,我們剛才看了這個類的draw()方法和updateRootDisplayList()方法,很有意思,它還有一個這個靜態的方法createHardwareBitmap(RenderNode node, int width, int height);
ThreadedRenderer.createHardwareBitmap()
public static Bitmap createHardwareBitmap(RenderNode node, int width, int height) {
return nCreateHardwareBitmap(node.getNativeDisplayList(), width, height);
}
復制代碼
該方法根據傳入的RenderNode創建一個硬件加速的Bitmap并返回,要求傳入的這個node必須是根root,在這里,一個完整的獲取替換Canvas的流程應該是這樣;
第一行,創建RenderNode
RenderNode node = RenderNode.create("helloworld", null);
第二行,創建DisplayListCanvas
final DisplayListCanvas canvas = node.start(width, height);
第三行,執行canvas的操作
canvas.xxx();
第四行,執行node.end()方法
node.end(canvas);
第五行,調用createHardwareBitmap生成Bitmap
bitmap = ThreadedRenderer.createHardwareBitmap(node,width,height)
復制代碼
基于上面的偽代碼分析,我寫了一個避免反射調優化版的Hardware Canvas,基本調用如下:
//創建HardwareCanvasManager
val hardwareCanvasManager = HardwareCanvasManager()
try {
//獲取canvas
val canvas = hardwareCanvasManager.createCanvas(size, size)
//畫圓形or其他繪制
canvas.drawCircle(radius, radius, radius, CIRCLE_CROP_SHAPE_PAINT);
//畫原圖,通過畫筆設置SRC_IN屬性
canvas.drawBitmap(inBitmap, null, destRect, CIRCLE_CROP_BITMAP_PAINT);
//得到bitmap
val buildBitmap = hardwareCanvasManager.buildBitmap()
//將bitmap設置給ImageView
iv.setImageBitmap(buildBitmap)
} finally {
//清理工作
hardwareCanvasManager.clean()
}
復制代碼
Github傳送門
總結
這篇水文主要是分析View繪制下Canvas的創建流程,關于硬件加速的更詳細的介紹,推薦大家看這篇文章www.jianshu.com/p/40f660e17…。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的android 获取当前画布,Android硬件位图填坑之获取硬件画布的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 因与下属发生不当关系!麦当劳前CEO退回
- 下一篇: 小米12 Ultra配置曝光:6.6英寸