安卓Canvas梳理

Android中的canvas介绍 3、绘制的东西4、绘制的画笔Paint(颜色和样式)Canvas对象的获取方式有三种:第一种我们通过重写View.onDraw方法,View中的Canvas对象会被当做参数传递过来,我们操作这个Canvas,效果会直接反应在View中。第二种就是当你想自己创建一个Canvas对象。从上面的基本要素可以明白,一个Canvas对象一定是结合了一个Bitmap对象的。所以一定要为一个Canvas对象设置一个Bitmap对象。[java] view plain copy//得到一个Bitmap对象,当然也可以使用 阅读详情

安卓中Canvas是用来绘制图像的常用api。Canvas在java,hwui和skia中都有定义。

java

BaseCanvas

frameworks/base/graphics/java/android/graphics/BaseCanvas.java

/**
 * This class is a base class for Canvas's drawing operations. Any modifications here
 * should be accompanied by a similar modification to {@link BaseRecordingCanvas}.
 *
 * The purpose of this class is to minimize the cost of deciding between regular JNI
 * and @FastNative JNI to just the virtual call that Canvas already has.
 *
 * @hide
 */
public abstract class BaseCanvas {
    /**
     * Should only be assigned in constructors (or setBitmap if software canvas),
     * freed by NativeAllocation.
     * @hide
     */
    @UnsupportedAppUsage
    protected long mNativeCanvasWrapper;
    ...
    // ---------------------------------------------------------------------------
    // Drawing methods
    // These are also implemented in RecordingCanvas so that we can
    // selectively apply on them
    // Everything below here is copy/pasted from Canvas.java
    // The JNI registration is handled by android_graphics_Canvas.cpp
    // ---------------------------------------------------------------------------

    public void drawArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean useCenter, @NonNull Paint paint) {
        throwIfHasHwFeaturesInSwMode(paint);
        nDrawArc(mNativeCanvasWrapper, left, top, right, bottom, startAngle, sweepAngle,
                useCenter, paint.getNativeInstance());
    }
    ...
}

BaseCanvas主要有一个C++对象mNativeCanvasWrapper,其draw方法都直接调用了C++对象的对应draw方法。

Canvas

frameworks/base/graphics/java/android/graphics/Canvas.java

/**
 * The Canvas class holds the "draw" calls. To draw something, you need
 * 4 basic components: A Bitmap to hold the pixels, a Canvas to host
 * the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect,
 * Path, text, Bitmap), and a paint (to describe the colors and styles for the
 * drawing).
 *
 * <div class="special reference">
 * <h3>Developer Guides</h3>
 * <p>For more information about how to use Canvas, read the
 * <a href="{@docRoot}guide/topics/graphics/2d-graphics.html">
 * Canvas and Drawables</a> developer guide.</p></div>
 */
public class Canvas extends BaseCanvas {
    ...
    // may be null
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 117521088)
    private Bitmap mBitmap;
    ...
    /**
     * Construct a canvas with the specified bitmap to draw into. The bitmap
     * must be mutable.
     *
     * <p>The initial target density of the canvas is the same as the given
     * bitmap's density.
     *
     * @param bitmap Specifies a mutable bitmap for the canvas to draw into.
     */
    public Canvas(@NonNull Bitmap bitmap) {
        if (!bitmap.isMutable()) {
            throw new IllegalStateException("Immutable bitmap passed to Canvas constructor");
        }
        throwIfCannotDraw(bitmap);
        bitmap.setGainmap(null);
        mNativeCanvasWrapper = nInitRaster(bitmap.getNativeInstance());
        mFinalizer = NoImagePreloadHolder.sRegistry.registerNativeAllocation(
                this, mNativeCanvasWrapper);
        mBitmap = bitmap;
        mDensity = bitmap.mDensity;
    }
    ...
    // ---------------- Draw Methods -------------------

    /**
     * <p>
     * Draw the specified arc, which will be scaled to fit inside the specified oval.
     * </p>
     * <p>
     * If the start angle is negative or >= 360, the start angle is treated as start angle modulo
     * 360.
     * </p>
     * <p>
     * If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs
     * slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is
     * negative, the sweep angle is treated as sweep angle modulo 360
     * </p>
     * <p>
     * The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0
     * degrees (3 o'clock on a watch.)
     * </p>
     *
     * @param oval The bounds of oval used to define the shape and size of the arc
     * @param startAngle Starting angle (in degrees) where the arc begins
     * @param sweepAngle Sweep angle (in degrees) measured clockwise
     * @param useCenter If true, include the center of the oval in the arc, and close it if it is
     *            being stroked. This will draw a wedge
     * @param paint The paint used to draw the arc
     */
    public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
            @NonNull Paint paint) {
        super.drawArc(oval, startAngle, sweepAngle, useCenter, paint);
    }

Canvas继承了BaseCanvas,Canvas还有一个bitmap,Canvas主要是用来在bitmap上进行绘制,其draw方法直接调用了super的draw方法。

BaseRecordingCanvas

frameworks/base/graphics/java/android/graphics/BaseRecordingCanvas.java

/**
 * This class is a base class for canvases that defer drawing operations, so all
 * the draw operations can be marked @FastNative. It contains a re-implementation of
 * all the methods in {@link BaseCanvas}.
 *
 * @hide
 */
public class BaseRecordingCanvas extends Canvas {

    public BaseRecordingCanvas(long nativeCanvas) {
        super(nativeCanvas);
    }

    @Override
    public final void drawArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean useCenter, @NonNull Paint paint) {
        nDrawArc(mNativeCanvasWrapper, left, top, right, bottom, startAngle, sweepAngle,
                useCenter, paint.getNativeInstance());
    }
    ...
}

BaseRecordingCanvas继承了Canvas,所有的draw方法直接调用了C++对象的draw方法。

RecordingCanvas

frameworks/base/graphics/java/android/graphics/RecordingCanvas.java

/**
 * A Canvas implementation that records view system drawing operations for deferred rendering.
 * This is used in combination with RenderNode. This class keeps a list of all the Paint and
 * Bitmap objects that it draws, preventing the backing memory of Bitmaps from being released while
 * the RecordingCanvas is still holding a native reference to the memory.
 *
 * This is obtained by calling {@link RenderNode#beginRecording()} and is valid until the matching
 * {@link RenderNode#endRecording()} is called. It must not be retained beyond that as it is
 * internally reused.
 */
public final class RecordingCanvas extends BaseRecordingCanvas {
    ...
    /**
     * TODO: Temporarily exposed for RenderNodeAnimator(Set)
     * @hide */
    public RenderNode mNode;
    private int mWidth;
    private int mHeight;

    /*package*/
    static RecordingCanvas obtain(@NonNull RenderNode node, int width, int height) {
        if (node == null) throw new IllegalArgumentException("node cannot be null");
        RecordingCanvas canvas = sPool.acquire();
        if (canvas == null) {
            canvas = new RecordingCanvas(node, width, height);
        } else {
            nResetDisplayListCanvas(canvas.mNativeCanvasWrapper, node.mNativeRenderNode,
                    width, height);
        }
        canvas.mNode = node;
        canvas.mWidth = width;
        canvas.mHeight = height;
        return canvas;
    }

    /*package*/
    void recycle() {
        mNode = null;
        sPool.release(this);
    }

    /*package*/
    void finishRecording(RenderNode node) {
        nFinishRecording(mNativeCanvasWrapper, node.mNativeRenderNode);
    }

    ///////////////////////////////////////////////////////////////////////////
    // Constructors
    ///////////////////////////////////////////////////////////////////////////

    private RecordingCanvas(@NonNull RenderNode node, int width, int height) {
        super(nCreateDisplayListCanvas(node.mNativeRenderNode, width, height));
        mDensity = 0; // disable bitmap density scaling
    }
    ...
    ///////////////////////////////////////////////////////////////////////////
    // Drawing
    ///////////////////////////////////////////////////////////////////////////

    /**
     * Draws a circle
     *
     * @param cx
     * @param cy
     * @param radius
     * @param paint
     *
     * @hide
     */
    public void drawCircle(CanvasProperty<Float> cx, CanvasProperty<Float> cy,
            CanvasProperty<Float> radius, CanvasProperty<Paint> paint) {
        nDrawCircle(mNativeCanvasWrapper, cx.getNativeContainer(), cy.getNativeContainer(),
                radius.getNativeContainer(), paint.getNativeContainer());
    }
    ...
}

RecordingCanvas继承BaseRecordingCanvas,主要是用来给RenderNode录制绘制操作用的。

在View的绘制流程中,会调用每个View的updateDisplayListIfDirty方法:(frameworks/base/core/java/android/view/View.java)

    /**
     * Gets the RenderNode for the view, and updates its DisplayList (if needed and supported)
     * @hide
     */
    @NonNull
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public RenderNode updateDisplayListIfDirty() {
        final RenderNode renderNode = mRenderNode;
        ...

        if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
                || !renderNode.hasDisplayList()
                || (mRecreateDisplayList)) {
            ...
            final RecordingCanvas canvas = renderNode.beginRecording(width, height);

            try {
                if (layerType == LAYER_TYPE_SOFTWARE) {
                    ...
                } else {
                    ...
                    // Fast path for layouts with no backgrounds
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        dispatchDraw(canvas);
                        ...
                    } else {
                        draw(canvas);
                    }
                }
            } finally {
                renderNode.endRecording();
                setDisplayListProperties(renderNode);
            }
        } else {
            ...
        }
        return renderNode;
    }

首先通过renderNode.beginRecording方法获取一个RecordingCanvas,然后调用View.draw方法使用这个canvas录制绘制命令,最后调用renderNode.endRecording完成绘制命令录制。

hwui

Canvas

frameworks/base/libs/hwui/hwui/Canvas.h

class ANDROID_API Canvas {
public:
    ...
    virtual void drawRoundRect(uirenderer::CanvasPropertyPrimitive* left,
                               uirenderer::CanvasPropertyPrimitive* top,
                               uirenderer::CanvasPropertyPrimitive* right,
                               uirenderer::CanvasPropertyPrimitive* bottom,
                               uirenderer::CanvasPropertyPrimitive* rx,
                               uirenderer::CanvasPropertyPrimitive* ry,
                               uirenderer::CanvasPropertyPaint* paint) = 0;
    ...
}

Canvas是一个抽象类,定义了许多draw方法,供子类去实现。

SkiaCanvas

frameworks/base/libs/hwui/SkiaCanvas.h

// Holds an SkCanvas reference plus additional native data.
class SkiaCanvas : public Canvas {
public:
    ...
    virtual void drawColor(int color, SkBlendMode mode) override;
    ...
private:
    ...
    std::unique_ptr<SkCanvas> mCanvasOwned;  // Might own a canvas we allocated.
    SkCanvas* mCanvas;                       // We do NOT own this canvas, it must survive us
                                             // unless it is the same as mCanvasOwned.get().
    ...
};

SkiaCanvas继承了Canvas,SkiaCanvas包装了一个SkCanvas,所有的draw方法都直接调用了这个SkCanvas的draw方法,例如drawColor的实现:(frameworks/base/libs/hwui/SkiaCanvas.cpp)

void SkiaCanvas::drawColor(int color, SkBlendMode mode) {
    mCanvas->drawColor(color, mode);
}

在java的Canvas构造器中使用nInitRaster方法创建C++的Canvas对象:(frameworks/base/libs/hwui/jni/android_graphics_Canvas.cpp)

// Native wrapper constructor used by Canvas(Bitmap)
static jlong initRaster(JNIEnv* env, jobject, jlong bitmapHandle) {
    SkBitmap bitmap;
    if (bitmapHandle != 0) {
        bitmap::toBitmap(bitmapHandle).getSkBitmap(&bitmap);
    }
    return reinterpret_cast<jlong>(Canvas::create_canvas(bitmap));
}

这里调用了Canvas的create_canvas方法:(frameworks/base/libs/hwui/SkiaCanvas.cpp)

Canvas* Canvas::create_canvas(const SkBitmap& bitmap) {
    return new SkiaCanvas(bitmap);
}

实际上就是创建了一个C++的SkiaCanvas对象。

SkiaRecordingCanvas

frameworks/base/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h

/**
 * A SkiaCanvas implementation that records drawing operations for deferred rendering backed by a
 * SkLiteRecorder and a SkiaDisplayList.
 */
class SkiaRecordingCanvas : public SkiaCanvas {
public:
    explicit SkiaRecordingCanvas(uirenderer::RenderNode* renderNode, int width, int height) {
        initDisplayList(renderNode, width, height);
    }
    ...
    virtual void drawRoundRect(uirenderer::CanvasPropertyPrimitive* left,
                               uirenderer::CanvasPropertyPrimitive* top,
                               uirenderer::CanvasPropertyPrimitive* right,
                               uirenderer::CanvasPropertyPrimitive* bottom,
                               uirenderer::CanvasPropertyPrimitive* rx,
                               uirenderer::CanvasPropertyPrimitive* ry,
                               uirenderer::CanvasPropertyPaint* paint) override;
    ...
private:
    RecordingCanvas mRecorder;
    std::unique_ptr<SkiaDisplayList> mDisplayList;
    ...
    /**
     *  A new SkiaDisplayList is created or recycled if available.
     *
     *  @param renderNode is optional and used to recycle an old display list.
     *  @param width used to calculate recording bounds.
     *  @param height used to calculate recording bounds.
     */
    void initDisplayList(uirenderer::RenderNode* renderNode, int width, int height);
    ...
};

SkiaRecordingCanvas继承SkiaCanvas,存有RecordingCanvas和SkiaDisplayList。

SkiaRecordingCanvas主要是用过initDisplayList方法初始化:(frameworks/base/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp)

void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, int width,
                                          int height) {
    mCurrentBarrier = nullptr;
    LOG_FATAL_IF(mDisplayList.get() != nullptr);

    if (renderNode) {
        mDisplayList = renderNode->detachAvailableList();
    }
    if (!mDisplayList) {
        mDisplayList.reset(new SkiaDisplayList());
    }

    mDisplayList->attachRecorder(&mRecorder, SkIRect::MakeWH(width, height));
    SkiaCanvas::reset(&mRecorder);
    mDisplayList->setHasHolePunches(false);
}

这里调用了SkiaCanvas的reset方法:(frameworks/base/libs/hwui/SkiaCanvas.cpp)

void SkiaCanvas::reset(SkCanvas* skiaCanvas) {
    if (mCanvas != skiaCanvas) {
        mCanvas = skiaCanvas;
        mCanvasOwned.reset();
    }
    mSaveStack.reset(nullptr);
}

将RecordingCanvas设置到了内部包装的SkCanvas上面。

SkiaRecordingCanvas还实现了一些draw方法,例如SkiaRecordingCanvas的drawCircle方法:(frameworks/base/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp)

void SkiaRecordingCanvas::drawCircle(uirenderer::CanvasPropertyPrimitive* x,
                                     uirenderer::CanvasPropertyPrimitive* y,
                                     uirenderer::CanvasPropertyPrimitive* radius,
                                     uirenderer::CanvasPropertyPaint* paint) {
    drawDrawable(mDisplayList->allocateDrawable<AnimatedCircle>(x, y, radius, paint));
}

调用了SkiaCanvas的drawDrawable方法:(frameworks/base/libs/hwui/SkiaCanvas.h)

// Holds an SkCanvas reference plus additional native data.
class SkiaCanvas : public Canvas {
    ...
    void drawDrawable(SkDrawable* drawable) { mCanvas->drawDrawable(drawable); }
    ...
}

最终调用了内部包装的SkCanvas的drawDrawable方法。

在java的RecordingCanvas的构造器中使用nCreateDisplayListCanvas方法创建C++的Canvas对象:(frameworks/base/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp)

static jlong android_view_DisplayListCanvas_createDisplayListCanvas(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr,
        jint width, jint height) {
    RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
    return reinterpret_cast<jlong>(Canvas::create_recording_canvas(width, height, renderNode));
}

调用了Canvas的reate_recording_canvas方法:(frameworks/base/libs/hwui/hwui/Canvas.cpp)

Canvas* Canvas::create_recording_canvas(int width, int height, uirenderer::RenderNode* renderNode) {
    return new uirenderer::skiapipeline::SkiaRecordingCanvas(renderNode, width, height);
}

实际上就是创建了一个SkiaRecordingCanvas对象。

RecordingCanvas

frameworks/base/libs/hwui/RecordingCanvas.h

class RecordingCanvas final : public SkCanvasVirtualEnforcer<SkNoDrawCanvas> {
public:
    RecordingCanvas();
    void reset(DisplayListData*, const SkIRect& bounds);
    ...
    void onDrawRect(const SkRect&, const SkPaint&) override;
    ...
private:
    ...
    DisplayListData* fDL;
    ...
};

RecordingCanvas里面主要是有一个DisplayListData,这个DisplayListData在reset方法里面赋值:(frameworks/base/libs/hwui/RecordingCanvas.cpp)

void RecordingCanvas::reset(DisplayListData* dl, const SkIRect& bounds) {
    this->resetCanvas(bounds.right(), bounds.bottom());
    fDL = dl;
    mClipMayBeComplex = false;
    mSaveCount = mComplexSaveCount = 0;
}

调用位置是在SkiaRecordingCanvas::initDisplayList中调用了SkiaDisplayList的attachRecorder方法:(frameworks/base/libs/hwui/RecordingCanvas.cpp)

class SkiaDisplayList {
    ...
    void attachRecorder(RecordingCanvas* recorder, const SkIRect& bounds) {
        recorder->reset(&mDisplayList, bounds);
    }
    ...
}

所以SkiaRecordingCanvas的RecordingCanvas的DisplayListData被设置成了指向SkiaRecordingCanvas的SkiaDisplayList的DisplayListData。也就是:

SkiaRecordingCanvas.mRecorder.fDL = &SkiaRecordingCanvas.mDisplayList.mDisplayList

SkiaRecordingCanvas.mCanvas = &SkiaRecordingCanvas.mRecorder

RecordingCanvas还重写了onDraw方法,例如RecordingCanvas的onDrawRect方法:(frameworks/base/libs/hwui/RecordingCanvas.cpp)

void RecordingCanvas::onDrawRect(const SkRect& rect, const SkPaint& paint) {
    fDL->drawRect(rect, paint);
}

直接调用了其DisplayListData的drawRect方法去录制。

DisplayListData

frameworks/base/libs/hwui/RecordingCanvas.h

class DisplayListData final {
public:
    ...
    void draw(SkCanvas* canvas) const;
    ...
private:
    ...
    void drawRect(const SkRect&, const SkPaint&);
    ...
    template <typename T, typename... Args>
    void* push(size_t, Args&&...);

    template <typename Fn, typename... Args>
    void map(const Fn[], Args...) const;

    AutoTMalloc<uint8_t> fBytes;
    size_t fUsed = 0;
    size_t fReserved = 0;

    bool mHasText : 1;
    bool mHasFill : 1;
};

DisplayListData的录制操作例如drawRect方法:(frameworks/base/libs/hwui/RecordingCanvas.cpp)

void DisplayListData::drawRect(const SkRect& rect, const SkPaint& paint) {
    this->push<DrawRect>(0, rect, paint);
}

调用了DisplayListData的push方法:(frameworks/base/libs/hwui/RecordingCanvas.cpp)

template <typename T, typename... Args>
void* DisplayListData::push(size_t pod, Args&&... args) {
    size_t skip = SkAlignPtr(sizeof(T) + pod);
    LOG_FATAL_IF(skip >= (1 << 24));
    if (fUsed + skip > fReserved) {
        static_assert(is_power_of_two(SKLITEDL_PAGE),
                      "This math needs updating for non-pow2.");
        // Next greater multiple of SKLITEDL_PAGE.
        fReserved = (fUsed + skip + SKLITEDL_PAGE) & ~(SKLITEDL_PAGE - 1);
        fBytes.realloc(fReserved);
        LOG_ALWAYS_FATAL_IF(fBytes.get() == nullptr, "realloc(%zd) failed", fReserved);
    }
    LOG_FATAL_IF((fUsed + skip) > fReserved);
    auto op = (T*)(fBytes.get() + fUsed);
    fUsed += skip;
    new (op) T{std::forward<Args>(args)...};
    op->type = (uint32_t)T::kType;
    op->skip = skip;

    // check if this is a fill op or not, in case we need to avoid messing with it with force invert
    if constexpr (!std::is_same_v<T, DrawTextBlob>) {
        if (hasPaintWithFill(args...)) {
            mHasFill = true;
        }
    }

    return op + 1;
}

这里使用了C++的placement new方式,在fBytes的最后构建并添加了类型为T的结构,此时T是DrawRect:(frameworks/base/libs/hwui/RecordingCanvas.cpp)

struct Op {
    uint32_t type : 8;
    uint32_t skip : 24;
};

struct DrawRect final : Op {
    static const auto kType = Type::DrawRect;
    DrawRect(const SkRect& rect, const SkPaint& paint) : rect(rect), paint(paint) {}
    SkRect rect;
    SkPaint paint;
    void draw(SkCanvas* c, const SkMatrix&) const { c->drawRect(rect, paint); }
};

DrawRect继承Op,并保存了这次操作所需的数据SkRect和SkPaint。这样就实现了绘制操作保存到了DisplayListData的fBytes中。

最后实际绘制之前录制的操作的时候,会调用DisplayListData的draw方法:(frameworks/base/libs/hwui/RecordingCanvas.cpp)

void DisplayListData::draw(SkCanvas* canvas) const {
    SkAutoCanvasRestore acr(canvas, false);
    this->map(draw_fns, canvas, canvas->getTotalMatrix());
}

调用了DisplayListData的map方法:(frameworks/base/libs/hwui/RecordingCanvas.cpp)

template <typename Fn, typename... Args>
inline void DisplayListData::map(const Fn fns[], Args... args) const {
    auto end = fBytes.get() + fUsed;
    for (const uint8_t* ptr = fBytes.get(); ptr < end;) {
        auto op = (const Op*)ptr;
        auto type = op->type;
        auto skip = op->skip;
        if (auto fn = fns[type]) {  // We replace no-op functions with nullptrs
            fn(op, args...);        // to avoid the overhead of a pointless call.
        }
        ptr += skip;
    }
}

这里遍历fBytes,对其中的每个op调用fn函数,fn函数是作为参数传进来的draw_fns的下标为type的元素。draw_fns定义:(frameworks/base/libs/hwui/RecordingCanvas.cpp)

// All ops implement draw().
#define X(T)                                                    \
    [](const void* op, SkCanvas* c, const SkMatrix& original) { \
        ((const T*)op)->draw(c, original);                      \
    },
static const draw_fn draw_fns[] = {
#include "DisplayListOps.in"
};
#undef X

其中DisplayListOps.in的内容:(frameworks/base/libs/hwui/DisplayListOps.in)

X(Save)
X(Restore)
X(SaveLayer)
X(SaveBehind)
X(Concat)
X(SetMatrix)
X(Scale)
X(Translate)
X(ClipPath)
X(ClipRect)
X(ClipRRect)
X(ClipRegion)
X(ClipShader)
X(ResetClip)
X(DrawPaint)
X(DrawBehind)
X(DrawPath)
X(DrawRect)
X(DrawRegion)
X(DrawOval)
X(DrawArc)
X(DrawRRect)
X(DrawDRRect)
X(DrawAnnotation)
X(DrawDrawable)
X(DrawPicture)
X(DrawImage)
X(DrawImageRect)
X(DrawImageLattice)
X(DrawTextBlob)
X(DrawPatch)
X(DrawPoints)
X(DrawVertices)
X(DrawAtlas)
X(DrawShadowRec)
X(DrawVectorDrawable)
X(DrawRippleDrawable)
X(DrawWebView)
X(DrawSkMesh)
X(DrawMesh)

这个X是写在draw_fns之前的宏定义,实际上就是一个lambda函数,函数里面调用了每种Op的draw方法。就这样实现了将录制好了的操作绘制到传进来的SkCanvas里面。

skia

SkCanvas

external/skia/include/core/SkCanvas.h

/** \class SkCanvas
    SkCanvas provides an interface for drawing, and how the drawing is clipped and transformed.
    SkCanvas contains a stack of SkMatrix and clip values.

    SkCanvas and SkPaint together provide the state to draw into SkSurface or SkDevice.
    Each SkCanvas draw call transforms the geometry of the object by the concatenation of all
    SkMatrix values in the stack. The transformed geometry is clipped by the intersection
    of all of clip values in the stack. The SkCanvas draw calls use SkPaint to supply drawing
    state such as color, SkTypeface, text size, stroke width, SkShader and so on.

    To draw to a pixel-based destination, create raster surface or GPU surface.
    Request SkCanvas from SkSurface to obtain the interface to draw.
    SkCanvas generated by raster surface draws to memory visible to the CPU.
    SkCanvas generated by GPU surface uses Vulkan or OpenGL to draw to the GPU.

    To draw to a document, obtain SkCanvas from SVG canvas, document PDF, or SkPictureRecorder.
    SkDocument based SkCanvas and other SkCanvas subclasses reference SkDevice describing the
    destination.

    SkCanvas can be constructed to draw to SkBitmap without first creating raster surface.
    This approach may be deprecated in the future.
*/
class SK_API SkCanvas {
public:
    ...
    /** Draws SkRect rect using clip, SkMatrix, and SkPaint paint.
        In paint: SkPaint::Style determines if rectangle is stroked or filled;
        if stroked, SkPaint stroke width describes the line thickness, and
        SkPaint::Join draws the corners rounded or square.

        @param rect   rectangle to draw
        @param paint  stroke or fill, blend, color, and so on, used to draw

        example: https://fiddle.skia.org/c/@Canvas_drawRect
    */
    void drawRect(const SkRect& rect, const SkPaint& paint);
    ...
protected:
    ...
    // NOTE: If you are adding a new onDraw virtual to SkCanvas, PLEASE add an override to
    // SkCanvasVirtualEnforcer (in SkCanvasVirtualEnforcer.h). This ensures that subclasses using
    // that mechanism  will be required to implement the new function.
    virtual void onDrawRect(const SkRect& rect, const SkPaint& paint);
    ...
};

SkCanvas有很多draw方法,例如SkCanvas的drawRect方法:(external/skia/src/core/SkCanvas.cpp)

void SkCanvas::drawRect(const SkRect& r, const SkPaint& paint) {
    TRACE_EVENT0("skia", TRACE_FUNC);
    // To avoid redundant logic in our culling code and various backends, we always sort rects
    // before passing them along.
    this->onDrawRect(r.makeSorted(), paint);
}

直接调用了SkCanvas的onDrawRect方法:(external/skia/src/core/SkCanvas.cpp)

void SkCanvas::onDrawRect(const SkRect& r, const SkPaint& paint) {
    SkASSERT(r.isSorted());
    if (this->internalQuickReject(r, paint)) {
        return;
    }

    auto layer = this->aboutToDraw(paint, &r, PredrawFlags::kCheckForOverwrite);
    if (layer) {
        this->topDevice()->drawRect(r, layer->paint());
    }
}

调用了SkDevice的drawRect方法,drawRect方法的内容取决于继承SkDevice的类的具体实现,将在其它文章中分析。

SkCanvasVirtualEnforcer

external/skia/include/core/SkCanvasVirtualEnforcer.h

// If you would ordinarily want to inherit from Base (eg SkCanvas, SkNWayCanvas), instead
// inherit from SkCanvasVirtualEnforcer<Base>, which will make the build fail if you forget
// to override one of SkCanvas' key virtual hooks.
template <typename Base>
class SkCanvasVirtualEnforcer : public Base {
public:
    using Base::Base;

protected:
    void onDrawPaint(const SkPaint& paint) override = 0;
    void onDrawBehind(const SkPaint&) override {} // make zero after android updates
    void onDrawRect(const SkRect& rect, const SkPaint& paint) override = 0;
    void onDrawRRect(const SkRRect& rrect, const SkPaint& paint) override = 0;
    void onDrawDRRect(const SkRRect& outer, const SkRRect& inner,
                      const SkPaint& paint) override = 0;
    void onDrawOval(const SkRect& rect, const SkPaint& paint) override = 0;
    void onDrawArc(const SkRect& rect, SkScalar startAngle, SkScalar sweepAngle, bool useCenter,
                   const SkPaint& paint) override = 0;
    void onDrawPath(const SkPath& path, const SkPaint& paint) override = 0;
    void onDrawRegion(const SkRegion& region, const SkPaint& paint) override = 0;

    void onDrawTextBlob(const SkTextBlob* blob, SkScalar x, SkScalar y,
                        const SkPaint& paint) override = 0;

    void onDrawPatch(const SkPoint cubics[12], const SkColor colors[4],
                     const SkPoint texCoords[4], SkBlendMode mode,
                     const SkPaint& paint) override = 0;
    void onDrawPoints(SkCanvas::PointMode mode, size_t count, const SkPoint pts[],
                      const SkPaint& paint) override = 0;

#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK
    // This is under active development for Chrome and not used in Android. Hold off on adding
    // implementations in Android's SkCanvas subclasses until this stabilizes.
    void onDrawEdgeAAQuad(const SkRect& rect, const SkPoint clip[4],
            SkCanvas::QuadAAFlags aaFlags, const SkColor4f& color, SkBlendMode mode) override {}
#else
    void onDrawEdgeAAQuad(const SkRect& rect, const SkPoint clip[4],
            SkCanvas::QuadAAFlags aaFlags, const SkColor4f& color, SkBlendMode mode) override = 0;
#endif

    void onDrawAnnotation(const SkRect& rect, const char key[], SkData* value) override = 0;
    void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override = 0;

    void onDrawDrawable(SkDrawable* drawable, const SkMatrix* matrix) override = 0;
    void onDrawPicture(const SkPicture* picture, const SkMatrix* matrix,
                       const SkPaint* paint) override = 0;
};

SkCanvasVirtualEnforcer是一个抽象基类,将所有关键的onDraw方法都重写成抽象的了,作用是如果继承类少实现了某个方法就会导致编译不通过,从而确保继承类不会漏实现某个onDraw方法。

SkNoDrawCanvas

external/skia/include/utils/SkNoDrawCanvas.h

// SkNoDrawCanvas is a helper for SkCanvas subclasses which do not need to
// actually rasterize (e.g., analysis of the draw calls).
//
// It provides the following simplifications:
//
//   * not backed by any device/pixels
//   * conservative clipping (clipping calls only use rectangles)
//
class SK_API SkNoDrawCanvas : public SkCanvasVirtualEnforcer<SkCanvas> {
public:
    SkNoDrawCanvas(int width, int height);
    SkNoDrawCanvas(const SkIRect&);

    // Optimization to reset state to be the same as after construction.
    void resetCanvas(int w, int h)        { this->resetForNextPicture(SkIRect::MakeWH(w, h)); }
    void resetCanvas(const SkIRect& rect) { this->resetForNextPicture(rect); }

protected:
    SaveLayerStrategy getSaveLayerStrategy(const SaveLayerRec& rec) override;
    bool onDoSaveBehind(const SkRect*) override;

    // No-op overrides for aborting rasterization earlier than SkNullBlitter.
    void onDrawAnnotation(const SkRect&, const char[], SkData*) override {}
    void onDrawDRRect(const SkRRect&, const SkRRect&, const SkPaint&) override {}
    void onDrawDrawable(SkDrawable*, const SkMatrix*) override {}
    void onDrawTextBlob(const SkTextBlob*, SkScalar, SkScalar, const SkPaint&) override {}
    void onDrawPatch(const SkPoint[12], const SkColor[4], const SkPoint[4], SkBlendMode,
                     const SkPaint&) override {}

    void onDrawPaint(const SkPaint&) override {}
    void onDrawBehind(const SkPaint&) override {}
    void onDrawPoints(PointMode, size_t, const SkPoint[], const SkPaint&) override {}
    void onDrawRect(const SkRect&, const SkPaint&) override {}
    void onDrawRegion(const SkRegion&, const SkPaint&) override {}
    void onDrawOval(const SkRect&, const SkPaint&) override {}
    void onDrawArc(const SkRect&, SkScalar, SkScalar, bool, const SkPaint&) override {}
    void onDrawRRect(const SkRRect&, const SkPaint&) override {}
    void onDrawPath(const SkPath&, const SkPaint&) override {}

    void onDrawImage2(const SkImage*, SkScalar, SkScalar, const SkSamplingOptions&,
                      const SkPaint*) override {}
    void onDrawImageRect2(const SkImage*, const SkRect&, const SkRect&, const SkSamplingOptions&,
                          const SkPaint*, SrcRectConstraint) override {}
    void onDrawImageLattice2(const SkImage*, const Lattice&, const SkRect&, SkFilterMode,
                             const SkPaint*) override {}
    void onDrawAtlas2(const SkImage*, const SkRSXform[], const SkRect[], const SkColor[], int,
                  SkBlendMode, const SkSamplingOptions&, const SkRect*, const SkPaint*) override {}

    void onDrawVerticesObject(const SkVertices*, SkBlendMode, const SkPaint&) override {}
    void onDrawShadowRec(const SkPath&, const SkDrawShadowRec&) override {}
    void onDrawPicture(const SkPicture*, const SkMatrix*, const SkPaint*) override {}

    void onDrawEdgeAAQuad(const SkRect&, const SkPoint[4], QuadAAFlags, const SkColor4f&,
                          SkBlendMode) override {}
    void onDrawEdgeAAImageSet2(const ImageSetEntry[], int, const SkPoint[], const SkMatrix[],
                               const SkSamplingOptions&, const SkPaint*,
                               SrcRectConstraint) override {}

private:
    using INHERITED = SkCanvasVirtualEnforcer<SkCanvas>;
};

SkNoDrawCanvas继承了SkCanvasVirtualEnforcer<SkCanvas>,重写了所有onDraw方法为空方法,也就是不执行任何具体的绘制代码。

RecordingCanvas就是继承了SkCanvasVirtualEnforcer<SkNoDrawCanvas>。

Android 中 Canvas的功能 Android 中 Canvas的功能 一. 如何在一屏幕上绘图 三个概念: 需要有画布Canvas 需要有笔Paint 需要有坐标系,画笔默认是在左上角(0,0)位置绘制的。我们可以通过移动画布坐标原点的形式,实现在不同位置绘制。 1. 移动坐标原点 简单绘制一个图 我们将画笔Canvas.translate(100,400)移动之后再进行绘制,这时画布的原点已经变化了。 2. 视图坐标系 理论上 Canvas 这张纸是没有边界的,但是我们的手机屏幕是有界的。我们可以理解为我们透 阅读详情

相关推荐

微信小程序Canvas层级问题解决方案与实战

在微信小程序开发中,Canvas作为原生组件存在层级问题,会遮挡其他前端组件。这是由于微信小程序的渲染架构决定的:逻辑层运行JavaScript代码,视图层渲染页面结构,而原生组件由客户端原生渲染,导致常规CSS层级控制失效。理解这一原理后,开发者可以采用cover-view、动态隐藏Canvas、同层渲染或WebGL等解决方案。这些技术方案在电商小程序、签名板等场景中尤为重要,能有效提升用户体验。通过合理选择方案并注意iOS/Android平台差异,可以解决Canvas遮挡问题,实现复杂的交互界面。

weixin_29015127的博客 379

Android canvas

1.Canvas Canvas是一个画布,可以在这个画布上进行各种绘图操作。 在Android中,想要获得一个Canvas对象主要有三种方法: ①继承一个View,并重写onDraw()方法。View的Canvas对象会被当做参数传递过来,在这个Canvas上进行的操作会直接反映在View中。 ②调用SurfaceHolder.lockCanvas()返回一个Canvas对象。 ③通过构造方法自己创建一个Canvas对象。 Bitmap bitmap = Bitmap.createBitmap(1

zenmela2011的专栏 1万+

Android图片解码普通/HARDWARE Bitmap到GPU绘制最终上屏显示链路

本文分析了Android中普通Bitmap和HARDWARE Bitmap从解码到屏幕显示的完整链路及核心区别。普通Bitmap解码后在CPU内存存储像素数据,绘制时需上传GPU纹理,适合需要像素处理的场景;HARDWARE Bitmap则将像素直接存入GPU/硬件缓冲,减少纹理上传开销,但不可修改像素且不兼容软件渲染。文章详细比较了两者在解码、绘制阶段的技术实现差异,列举了HARDWARE Bitmap的使用限制和适用场景,最后总结了两者的核心区别:普通Bitmap是"CPU内存像素图&quot

Zhang Phil 362

HenCoder Android 开发进阶: 自定义 View 1-1 绘制基础

从今天开始,HenCoder 就正式开讲知识技能了。按照我的计划,第一季是 UI,UI 一共分为三部分:绘制、布局和触摸反馈。本期是绘制部分的第一期。绘制大概会用 5~6 期的时间讲完,整个 UI 的绘制、布局和触摸反馈三部分大概会用十来期。更新频率大约为每周一更(不承诺哟)。 如果你不知道 HenCoder 是什么,可以先看这里: HenCoder:给高级 Android 工程师的进阶手册 自定...

weixin_33911824的博客 546

深入解析Android软件渲染中的VSync信号与绘制流程

本文深入解析Android软件渲染中的VSync信号与绘制流程,详细介绍了VSync信号如何作为渲染节拍器确保画面流畅,以及从UI线程到SurfaceFlinger的完整绘制生命周期。通过实际案例和代码示例,帮助开发者理解软件渲染的关键路径和优化技巧,提升应用性能。

weixin_30823001的博客 345

(论文速读)低光照图像增强综述(一)

深度学习在弱光图像增强(LLIE)领域取得显著进展,但仍面临泛化性和应用评估不足的挑战。本研究通过统计分析30多种深度学习方法,揭示了监督学习在性能指标上的优势(如DLL的PSNR达25.67dB)与无监督方法(如Zero-DCE++)在实际任务中的潜力。实验发现,增强后的人脸检测mAP最高可提升14.54%,但视觉质量与机器性能存在差异。研究指出当前主要瓶颈包括极端光照处理能力不足、4K+实时处理困难及评估标准不统一,并提出了自适应增强、扩散模型应用等未来方向。

LJ1147517021的博客 2243

Android中Canvas绘图基础详解(附源码下载)

Android中,如果我们想绘制复杂的自定义View或游戏,我们就需要熟悉绘图API。Android通过Canvas类暴露了很多drawXXX方法,我们可以通过这些方法绘制各种各样的图形。Canvas绘图有三个基本要素:Canvas、绘图坐标系以及Paint。Canvas是画布,我们通过Canvas的各种drawXXX方法将图形绘制到Canvas上面,在drawXXX方法中我们需要传入要绘制的图形的

孙群 4万+

从零搭建企业网络:使用Packet Tracer模拟多区域OSPF组网全过程

本文通过Cisco Packet Tracer模拟,详细演示了从零搭建一个典型企业多区域OSPF网络的完整流程。内容涵盖网络拓扑规划、IP地址设计、多区域划分原理,并提供了从设备初始化、接口配置到OSPF多区域核心配置与验证的详细步骤。文章旨在帮助网络学习者与IT支持人员通过动手实验,将理论知识转化为实际的配置与排障能力。

day7的博客 1031

Android 如何在不显示文字时获取文字宽度

String text = “测试:XYZ xyz”;//1. 粗略计算文字宽度//2. 计算文字所在矩形,可以得到宽高//3. 精确计算文字宽度if (str!j < len;j++) {String text = “测试:XYZ xyz”;Log.e(TAG, “测试字符串:” + text);

2301_79098336的博客 720

项目计划书、规划文档模板

项目计划书、规划文档模板 最近通过项目实践经历,整理出的一套项目计划书(仅供自己参考梳理思路使用) 有喜欢的文末加VX 辛苦码字如有转载请标明出处谢谢!——木研 更新的简版项目规划文档! 原版文件请+vx :baierfa ...

weixin_43194923的博客 990

Laser-Marking 激光振镜打标机的开源代码 - 下载 - 搜珍网

为了方便开发者的使用和定制化需求,我们在这里提供激光振镜打标机的开源代码,并附带详细的描述。首先,让我们来了解一下激光振镜打标机的基本原理。振镜系统由两个可移动的反射镜组成,可以控制激光束在水平和垂直方向上的偏转角度,从而实现对工件的精确控制。通过这段开源代码,开发者可以根据自己的需求对激光振镜打标机进行二次开发和定制化。你可以修改振镜角度、打标速度等参数,也可以添加其他功能,例如自动识别和追踪等。首先,我们创建了一个串口对象,用于与激光振镜打标机进行通信。函数实现了对用户输入文本的打标操作。

TechProX的博客 1279

Android中new Canvas(Bitmap)引发的对Canvas和Bitmap关系的疑问

说可以改变的位图。

2401_86963909的博客 1066

看GRE协议的数据封装

GRE网络数据封装流程

FanFF的博客 80

【Android】浅谈View绘制流程

measureSpec是一个int类型,在java占32字节,其1~30位表示实际测量长度,31~32位即测量模式通过MODE_MASK获得mode,MODE_MASK 即。

qq_40900926的博客 910

【附代码】SSVEP解码算法 - 扩展典型相关分析(eCCA)

本文主要对SSVEP解码算法-eCCA进行复现和测试。本文提供程序的所有代码,只要在Matlab上即可运行。读者可以根据程序自主调整,方便学习以及初学者更好地了解SSVEP的经典算法。

计算机小白 1337

uniapp(全端兼容)-最新详细实现接入腾讯云IM即时通讯教程,uniapp在安卓苹果app端/小程序/h5移动端等集成使用腾讯云im插件,提供消息列表与聊天对话示例,支持发图片/私聊/实时监听消息

uni-app,腾讯云IM,安卓app,苹果app,微信小程序,移动端,H5网页,手机网站,微信小程序、支付宝小程序,手机app端,nvue,im即时通讯,腾讯云IM详细教程,如何接入腾讯IM即时通讯,使用腾讯im有没有插件,怎么使用腾讯云IM插件,tim-wx-sdk,聊天sdk,即时通讯im uniapp私聊/群聊/聊天室,实现IM即时通讯仿微信聊天功能,对接腾讯云IM+音视频,调用腾讯即时通信IM的示例,卡顿很卡不流畅,音视频,全局监听,仿微信IM,uni-huanxin-im,sdk,实现多端实时通

✅ 精选推荐文章 1万+

Android Drawable常见子类及其用法整理

概述 Drawable是一个抽象类,是对可绘制物件的抽象。与View不同的是Drawable没有事件和交互方法。Drawable有很多子类操作具体资源类型,比如BitmapDrawable操作位图,ColorDrawable操作颜色,ClipDrawable操作剪切板.。 参考资料:《Android开发艺术探索》 原理 draw(Canvas canvas)    //在draw中对画

liuconen的博客 1001

OP-TEE虚拟化支持详解

OP-TEE虚拟化支持解析 OP-TEE提供实验性虚拟化方案,支持多虚拟机共享安全执行环境。核心特性包括: 配置选项:CFG_VIRTUALIZATION启用虚拟化,CFG_VIRT_GUEST_COUNT限制虚拟机数量并分配均等内存资源; Hypervisor协作:需实现虚拟机生命周期通知、SMC参数转换、内存固定等关键协议; 当前限制:仅支持Armv8架构,硬件资源共享有限,采用静态内存配额机制; 架构实现:通过MMU分区切换和扩展内存分配器实现多租户隔离,Nexus核心层管理虚拟机上下文。该方案适用于

Linux内核架构师 109

Android Bitmap 知识点梳理

View绘制基本流程Android系统View的绘制流程:依次执行View类里面的如下三个方法: measure(int ,int) :测量View的大小layout(int ,int ,int ,int) :设置子View的位置draw(Canvas) :绘制View内容到Canvas画布上整个View树的绘图流程是在ViewRoot.Java类的performTraversals()函数展开的

学渣罗小贱9*w4YBsXS@3W 373

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

android 最全 图片相关知识整理

android 最全 图片相关知识整理 图片基础知识梳理 图片基础知识梳理(1) - ImageView 的 ScaleType 属性解析 图片基础知识梳理(2) - Bitmap 占用内存分析 图片基础知识梳理(3) - Bitmap&BitmapFactory 解析 图片压缩知识梳理 图片压缩知识梳理(0) - 图片压缩学习计划 图片压缩知识梳理(1) - PNG 原理 图片压缩知识

philos3的博客 661

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

hwui介绍与分析

hwui全称**HardwareAcceleratedRenderingEngineforUI,**hwui是一个基于GPU加速的2D图形引擎。HWUI的目标是提供高效、稳定、高质量的2D图形渲染能力,为Android系统的UI体验提供技术支持。相关源码位于目录android/platform/framework/base/libs/hwui文末有福利~hwui的大部分代码以C++实现,Andro...

eclipse_xu 2907

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

Pico VR开发避坑指南:串流调试、XRIT配置与性能优化实战

虚拟现实(VR)开发通过构建沉浸式三维环境,为用户提供全新的交互体验。其核心原理在于利用头戴显示设备与空间定位技术,实时渲染双眼视差图像并追踪用户动作。这项技术的价值在于能够突破二维屏幕限制,在游戏、教育、培训等领域创造高临场感的应用。在具体工程实践中,开发者常需使用Unity引擎配合特定XR工具链进行开发。针对Pico VR一体机开发时,**串流调试**是实现高效实时预览的关键环节,它直接影响开发迭代速度。而**UI交互**在三维空间中的实现方式与传统2D界面截然不同,需采用World Space渲染模式

weixin_33720956的博客 362

android 中各种阴影 汇总

通过layer-list或自定义Drawable手动绘制阴影,适合需要非对称阴影(如仅底部阴影)或复杂形状阴影(如圆形、多边形)的场景。在styles.xml中定义主题:xml-- 1. 弹窗背景(带圆角和阴影) -->-- 2. 弹窗阴影大小(API 21+) -->-- 3. 弹窗边框(可选,避免边缘生硬) -->-- 4. 取消默认标题栏 --></style>定义弹窗背景xml</shape>在代码中使用主题:java运行.setTitle("自定义阴影 Dialog")

hcgeng的专栏 1644

Android直通路线(工作)

前言:公司最近来了很多应届实习生,看着他们充满信心但略带稚气的脸庞上,想到了去年的自己,那是的我是不是也和    现在的他们一样呢?现在的我又改变了什么?公司的给这些实习生指定的相关培训制度还是比较完善的(我可是个菜鸟)。但我反    过来想,假如是我,我会制定一个怎样的短期或者长远的计划?总结了自己学到的知识,形成了一个学习线路,希望能给予同    道中人一点儿小小帮助。

xiaocheng2290的专栏 391

hongyangWeixinArticles自定义View合集:20个炫酷UI效果实现原理

hongyangWeixinArticles是一个记录微信公众号"hongyangAndroid"推送文章的项目,其中包含了大量关于Android自定义View的精彩内容。本文将为你精选20个炫酷UI效果的实现原理,帮助你快速掌握自定义View的核心技巧。 ## 一、基础自定义View实现 ### 1. 自定义密码输入框 自定义密码输入框是很多App的常见需求,通过自定义View可以实现各种炫

gitblog_00654的博客 1098

Android硬件加速深度解析:原理、优化与实战避坑指南

图形渲染是移动应用性能优化的核心环节,其本质是将视觉元素高效转换为屏幕像素的过程。在Android系统中,硬件加速机制通过将图形计算任务从CPU卸载到GPU,利用GPU的并行处理能力大幅提升渲染效率。这项技术对于实现流畅动画、复杂界面绘制至关重要,直接影响用户体验和应用性能。在Android开发实践中,硬件加速广泛应用于自定义View绘制、属性动画优化等场景。然而,开发者常会遇到**Canvas操作兼容性**和**图形内存管理**等挑战,例如`clipPath`等API在硬件加速模式下可能失效,不当的图层缓

weixin_29191761的博客 325
上一篇: 安卓RenderEffect原理
下一篇: 如何使用Rust写一个游戏引擎
SSSxCCC
博客等级 码龄10年 238粉丝 74原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SSSxCCC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值