在鸿蒙上实现“热区”坐标和事件的捕捉

需求

  1. 捕获子 View 坐标,并具有拦截事件的能力。
  2. 捕获子 View 颜色值。

难点

  1. 捕获子 View 坐标,在 Android 中显而易见的思路是在父容器中拦截该事件。但是由于鸿蒙的Api不完善,暂时无法在父容器中拦截点击事件。

  2. 捕获子 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);
    }
}

跟一下流程,核心调用如下所示

image.png

通过 surface.getLastQueuedBuffer拿到了最后一次入队的Buffer,即最后一次绘制的数据,并拷贝给输出的Bitmap。

你可能感兴趣的:(在鸿蒙上实现“热区”坐标和事件的捕捉)