需求
- 捕获子 View 坐标,并具有拦截事件的能力。
- 捕获子 View 颜色值。
难点
捕获子 View 坐标,在 Android 中显而易见的思路是在父容器中拦截该事件。但是由于鸿蒙的Api不完善,暂时无法在父容器中拦截点击事件。
捕获子 View 颜色值,在 Android 中显而易见的思路是设法获取该 View 的 Bitmap,再根据坐标获取该点的颜色值。但是很遗憾,鸿蒙依旧没有 Api 去获取 View 的截图。
实现
捕获子 View 的坐标
既然无法在父容器中拦截,那么不妨丢一个跟子 View A 大小一样的 View B 盖在上面,那么事件必然会先经过 View B。因此,在获取到 View A 的宽高之后,动态添加一把 View B,再到 View B 的 onTouchEvent 中拦截事件。
捕获子 View 颜色值
如 浅析鸿蒙原理 一文所述 ,鸿蒙中的界面是由 SurfaceView 过渡到鸿蒙自己的绘制体系的,因此可以尝试通过获取 SurfaceView 的截图,再通过对坐标的换算,即可获取到坐标点的颜色值。
下面重点阐述下获取 SurfaceView 截图的步骤和原理。
常见的获取 View 截图的方式如下所示
获取 View 截图
public static Bitmap getViewBitmapNoBg(View view) {
view.setDrawingCacheEnabled(true);
view.buildDrawingCache(true);
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
// clear drawing cache
view.setDrawingCacheEnabled(false);
return bitmap;
}
获取 ViewGroup 截图
public static Bitmap getViewGroupBitmapNoBg(ViewGroup viewGroup) {
// 创建对应大小的bitmap(重点)
Bitmap bitmap = Bitmap.createBitmap(viewGroup.getWidth(), viewGroup.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
viewGroup.draw(canvas);
return bitmap;
}
但通过 SurfaceView#getDrawingCache 获取到的却是一块黑色。跟一下 View#getDrawingCache 的流程,核心的调用逻辑在 View#buildDrawingCacheImpl 中。
public void buildDrawingCache(boolean autoScale) {
if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || (autoScale ?
mDrawingCache == null : mUnscaledDrawingCache == null)) {
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW,
"buildDrawingCache/SW Layer for " + getClass().getSimpleName());
}
try {
buildDrawingCacheImpl(autoScale);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
}
/**
* private, internal implementation of buildDrawingCache, used to enable tracing
*/
private void buildDrawingCacheImpl(boolean autoScale) {
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;
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, getClass().getSimpleName() + " not displayed because it is"
+ " too large to fit into a software layer (or 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;
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 {
//重点1:创建了Bitmap
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();
}
//重点二:将 Bitmap 关联到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);
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
} else {
//重点三: 手动调用View#draw方法,开始绘制流程
draw(canvas);
}
canvas.restoreToCount(restoreCount);
canvas.setBitmap(null);
if (attachInfo != null) {
// Restore the cached Canvas for our siblings
attachInfo.mCanvas = canvas;
}
}
由上述分析可知 View#getDrawingCache 实际就是手动调了一把 View#draw,将绘制结果丢到自己创建的Bitmap 中。但是 SurfaceView#onDraw 中只是绘制一个黑色背景,其真正的绘制逻辑一般是通过如下方式:
public void surfaceCreated(SurfaceHolder holder) {
mIsRunning = true;
new Thread(this).start();
}
@Override
public void run() {
long start = System.currentTimeMillis();
while (mIsRunning) {
draw();
}
}
private void draw() {
mCanvas = mHolder.lockCanvas();
if (mCanvas != null) {
try {
mCanvas = mHolder.lockCanvas();//向BufferQueue申请图形缓冲区GraphicBuffer
// 绘制
canvas.drawLine(0,100,400,100,mPaint);
} catch (Exception e) {
} finally {
if (mCanvas != null)
mHolder.unlockCanvasAndPost(mCanvas);//通知SurfaceFlinger进行图层合成
}
}
}
经过一番搜索,发现 Android 有提供如下Api 去获取 SurfaceView 或 Window 的截图。
public final class PixelCopy {
public static final int ERROR_DESTINATION_INVALID = 5;
public static final int ERROR_SOURCE_INVALID = 4;
public static final int ERROR_SOURCE_NO_DATA = 3;
public static final int ERROR_TIMEOUT = 2;
public static final int ERROR_UNKNOWN = 1;
public static final int SUCCESS = 0;
PixelCopy() {
throw new RuntimeException("Stub!");
}
public static void request(@RecentlyNonNull SurfaceView source, @RecentlyNonNull Bitmap dest, @RecentlyNonNull PixelCopy.OnPixelCopyFinishedListener listener, @RecentlyNonNull Handler listenerThread) {
throw new RuntimeException("Stub!");
}
public static void request(@RecentlyNonNull SurfaceView source, @RecentlyNullable Rect srcRect, @RecentlyNonNull Bitmap dest, @RecentlyNonNull PixelCopy.OnPixelCopyFinishedListener listener, @RecentlyNonNull Handler listenerThread) {
throw new RuntimeException("Stub!");
}
public static void request(@RecentlyNonNull Surface source, @RecentlyNonNull Bitmap dest, @RecentlyNonNull PixelCopy.OnPixelCopyFinishedListener listener, @RecentlyNonNull Handler listenerThread) {
throw new RuntimeException("Stub!");
}
public static void request(@RecentlyNonNull Surface source, @RecentlyNullable Rect srcRect, @RecentlyNonNull Bitmap dest, @RecentlyNonNull PixelCopy.OnPixelCopyFinishedListener listener, @RecentlyNonNull Handler listenerThread) {
throw new RuntimeException("Stub!");
}
public static void request(@RecentlyNonNull Window source, @RecentlyNonNull Bitmap dest, @RecentlyNonNull PixelCopy.OnPixelCopyFinishedListener listener, @RecentlyNonNull Handler listenerThread) {
throw new RuntimeException("Stub!");
}
public static void request(@RecentlyNonNull Window source, @RecentlyNullable Rect srcRect, @RecentlyNonNull Bitmap dest, @RecentlyNonNull PixelCopy.OnPixelCopyFinishedListener listener, @RecentlyNonNull Handler listenerThread) {
throw new RuntimeException("Stub!");
}
public interface OnPixelCopyFinishedListener {
void onPixelCopyFinished(int var1);
}
}
跟一下流程,核心调用如下所示
通过 surface.getLastQueuedBuffer拿到了最后一次入队的Buffer,即最后一次绘制的数据,并拷贝给输出的Bitmap。