Recently a little time, look at GPUImageView source code, here mainly static for GPUImageView set a Bitmap and add filter process based on the analysis of the working principle.

Simple to use

GPUImageView is simple to use:



      
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <jp.co.cyberagent.android.gpuimage.GPUImageView
        android:id="@+id/gpuImageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        app:gpuimage_surface_type="texture_view" />

</RelativeLayout>

Copy the code

Simply add GPUImageView to the XML file with a property app:gpuimage_surface_type=”texture_view”, which means to draw the image using textureView as a proxy, and then in the code:


val bitmap = xxx// Get the bitmap object
val ratio = bitmap.width.toFloat() / bitmap.height.toFloat()// This step is to set the fixed aspect ratio
gpuImageView.setImage(bitmap)// Set the image resource
gpuImageView.filter = GPUImageFilter()// Set the filter
gpuImageView.requestRender()// this is called whenever you need to update the drawing

Copy the code

Ok, the process is simple, add a Bitmap image and add a default filter (the original image).

GPUImageView

Let’s look at the definition of GPUImageView:


public class GPUImageView extends FrameLayout {

    private int surfaceType = SURFACE_TYPE_SURFACE_VIEW;
    private View surfaceView;
    private View coverView;
    private GPUImage gpuImage;
    private boolean isShowLoading = true;
    private GPUImageFilter filter;
    public Size forceSize = null;
    private float ratio = 0.0 f;

    public final static int RENDERMODE_WHEN_DIRTY = 0;
    public final static int RENDERMODE_CONTINUOUSLY = 1;

    public GPUImageView(Context context) {
        super(context);
        init(context, null);
    }

    public GPUImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        GLUtil.init(context);// Save a reference to the current Context
        if(attrs ! =null) {// Parse the XML to write some attributes are not more than two
            TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.GPUImageView, 0.0);
            try {
                surfaceType = a.getInt(R.styleable.GPUImageView_gpuimage_surface_type, surfaceType);// Surface type SurfaceView or TextureView
                isShowLoading = a.getBoolean(R.styleable.GPUImageView_gpuimage_show_loading, isShowLoading);// Whether to display loading is useless
            } finally {
                a.recycle();
            }
        }
        gpuImage = new GPUImage(context);// Create a GPUImage object and hold a reference to it
        if (surfaceType == SURFACE_TYPE_TEXTURE_VIEW) {
            surfaceView = new GPUImageGLTextureView(context, attrs);//GPUImageGLTextureView and holds its reference
            gpuImage.setGLTextureView((GLTextureView) surfaceView);// Let GPUImage also hold a copy of its reference
        } else {
            surfaceView = new GPUImageGLSurfaceView(context, attrs);//GPUImageGLSurfaceView and holds its reference
            gpuImage.setGLSurfaceView((GLSurfaceView) surfaceView);// Let GPUImage also hold a copy of its reference
        }
        addView(surfaceView);// Add the created surfaceView to your FrameLayout
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if(ratio ! =0.0 f) {// Adjust width and height according to ratio
            int width = MeasureSpec.getSize(widthMeasureSpec);
            int height = MeasureSpec.getSize(heightMeasureSpec);

            int newHeight;
            int newWidth;
            if (width / ratio < height) {
                newWidth = width;
                newHeight = Math.round(width / ratio);
            } else {
                newHeight = height;
                newWidth = Math.round(height * ratio);
            }

            int newWidthSpec = MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY);
            int newHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY);
            super.onMeasure(newWidthSpec, newHeightSpec);
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec); }}}Copy the code

The GPUImageView itself is a FrameLayout that adds a SurfaceView inside and then holds a reference to the GPUImage object. If ratio is set, the width and height will change according to ratio. When you initialize it, you’re going to initialize the OpenGL utility class, which is essentially saving a reference to the context; In addition, the surfaceView implementation is selected based on the familiar configuration of the current XML.

Then look at GPUImage:


public class GPUImage {


    private final Context context;
    private final GPUImageRenderer renderer;
   
    /**
     * Instantiates a new GPUImage object.
     *
     * @param context the context
     */
    public GPUImage(final Context context) {
        if(! supportsOpenGLES2(context)) {throw new IllegalStateException("OpenGL ES 2.0 is not supported on this phone.");
        }

        this.context = context;
        filter = new GPUImageFilter();// The default filter is GPUImageFilter
        renderer = new GPUImageRenderer(filter);// Create a GPUImageRender object and hold its reference}}Copy the code

The GPUImage is initialized with the original image filter, which is the GPUImageFilter, and then used to create a GPUImageRenderer object. Let’s look at the filter implementation later. Here’s the Renderer:


public class GPUImageRenderer implements GLSurfaceView.Renderer.GLTextureView.Renderer.PreviewCallback {

    public static final float CUBE[] = {
            -1.0 f, -1.0 f.1.0 f, -1.0 f,
            -1.0 f.1.0 f.1.0 f.1.0 f};private GPUImageFilter filter;

    private final FloatBuffer glCubeBuffer;
    private final FloatBuffer glTextureBuffer;

    private final Queue<Runnable> runOnDraw;
    private final Queue<Runnable> runOnDrawEnd;


    public GPUImageRenderer(final GPUImageFilter filter) {
        this.filter = filter;
        // Two Runnable queues, namely task queues
        runOnDraw = new LinkedList<>();
        runOnDrawEnd = new LinkedList<>();

        // Vertex array and texture array
        glCubeBuffer = ByteBuffer.allocateDirect(CUBE.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        glCubeBuffer.put(CUBE).position(0);

        glTextureBuffer = ByteBuffer.allocateDirect(TEXTURE_NO_ROTATION.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        // Coordinate rotation
        setRotation(Rotation.NORMAL, false.false);
    }
    
    / /...
    
}

Copy the code

This Render will hold a reference to the filter with two task queues inside, as the name suggests. The other thing we’re going to do here is create two coordinate arrays using FloatBuffer. Buffer Here is the content of Java NIO, the coordinate array is a bit complex, involving a variety of coordinate transformations. First of all, we know that the screen coordinates in Android are the origin of the top left corner, the x axis to the right, and the Y axis to the bottom, as well as the coordinates of every View except the screen. In OpenGL, there are many, many coordinate concepts, and the main one is the world coordinate system, where the origin is right in the middle, the x is positive to the right, and the y is positive to the right, which is the same idea as the plane coordinate system that we use in mathematics. In addition, there is a texture coordinate system, the origin is the lower left corner of the texture, the right is the positive x direction, the upward is the positive Y direction, and the OpenGL coordinates are normalized values of -1 to 1 or 0-1. For example, if the screen width of the phone is 1080 and the abscess of a point on Android is 540, then in OpenGL it is 540/1080 = 0.5. If you want to display an image as an OpenGL texture, it involves a coordinate transformation or inversion, and I’m not sure what the logic is.

Back to GPUImageView, there are two implementations of the surfaceView, one is GPUImageGLTextureView, and the other is GPUImageGLSurfaceView. After the surfaceView object is created, GPUImage’s setGLTextureView or setGLSurfaceView is called to save a reference to the surfaceView:


 /**
     * Sets the GLSurfaceView which will display the preview.
     *
     * @param view the GLSurfaceView
     */
    public void setGLSurfaceView(final GLSurfaceView view) {
        surfaceType = SURFACE_TYPE_SURFACE_VIEW;
        glSurfaceView = view;
        // Do some initial configuration
        glSurfaceView.setEGLContextClientVersion(2);
        glSurfaceView.setEGLConfigChooser(8.8.8.8.16.0);
        glSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
        glSurfaceView.setRenderer(renderer);
        glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
        glSurfaceView.requestRender();
    }

    /**
     * Sets the GLTextureView which will display the preview.
     *
     * @param view the GLTextureView
     */
    public void setGLTextureView(final GLTextureView view) {
        surfaceType = SURFACE_TYPE_TEXTURE_VIEW;
        glTextureView = view;
        // Do some initial configuration
        glTextureView.setEGLContextClientVersion(2);
        glTextureView.setEGLConfigChooser(8.8.8.8.16.0);
        glTextureView.setOpaque(false);
        glTextureView.setRenderer(renderer);// Also similar to SurfaceView, holds a Render reference
        glTextureView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
        glTextureView.requestRender();
    }


Copy the code

It saves a reference to the View, and then sets up some OpenGL configuration, like version 2, color and depth, and then calls setRenderer() to make the view save a reference to the Renderer object in the current GPUImage object. Finally, set the render mode to active refresh (literally, refresh when the canvas is dirty, as opposed to passive refresh, which is automatically and repeatedly refreshed). Then actively call requestRender() to render once.

SurfaceView (surfaceView, surfaceView, surfaceView, surfaceView, surfaceView, surfaceView, surfaceView)


 private class GPUImageGLSurfaceView extends GLSurfaceView {
        public GPUImageGLSurfaceView(Context context) {
            super(context);
        }

        public GPUImageGLSurfaceView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            if(forceSize ! =null) {
                super.onMeasure(MeasureSpec.makeMeasureSpec(forceSize.width, MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(forceSize.height, MeasureSpec.EXACTLY));
            } else {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec); }}}private class GPUImageGLTextureView extends GLTextureView {
    public GPUImageGLTextureView(Context context) {
        super(context);
    }

    public GPUImageGLTextureView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if(forceSize ! =null) {
            super.onMeasure(MeasureSpec.makeMeasureSpec(forceSize.width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(forceSize.height, MeasureSpec.EXACTLY));
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec); }}}Copy the code

GPUImageGLSurfaceView is directly inherited from the Android native GLSurfaceView. GPUImageGLTextureView is inherited from the GLTextureView class of GPUImage. But this GLTextureView is inherited from Android native TextureView, and GLTextureView implementation is very interesting, in fact, is completely according to the GLSurfaceView design. Let’s go back to where we set render for GPUImage. Using setGLTextureView as an example, we can see the setRenderer() method that calls textureView:


public void setRenderer(Renderer renderer) {
        checkRenderThreadState();
        if (eglConfigChooser == null) {
            eglConfigChooser = new SimpleEGLConfigChooser(true);
        }
        if (eglContextFactory == null) {
            eglContextFactory = new DefaultContextFactory();
        }
        if (eglWindowSurfaceFactory == null) {
            eglWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
        }
        this.renderer = renderer;
        // Create a GLThread, which is a thread, and call the start() method to start running
        glThread = new GLThread(mThisWeakRef);
        glThread.start();
}

Copy the code

If the configuration is correct, a GLThread object is created and the start() method is called. This thread is called GL and handles OpenGL ES calls. Look at the implementation:


static class GLThread extends Thread {
    GLThread(WeakReference<GLTextureView> glTextureViewWeakRef) {
        super(a); width =0;
        height = 0;
        requestRender = true;
        renderMode = RENDERMODE_CONTINUOUSLY;
        this.glTextureViewWeakRef = glTextureViewWeakRef;
    }

    @Override
    public void run(a) {
        setName("GLThread " + getId());
        if (LOG_THREADS) {
            Log.i("GLThread"."starting tid=" + getId());
        }

        try {
            guardedRun();// What is guarded when run() checked?
        } catch (InterruptedException e) {
            // fall thru and exit normally
        } finally {
            glThreadManager.threadExiting(this); }}}Copy the code

MThisWeakRef is a weak reference to View itself, thread run() method will use try catch to safer call guardeRun() method:


        private void guardedRun(a) throws InterruptedException {
            eglHelper = new EglHelper(glTextureViewWeakRef);
            haveEglContext = false;
            haveEglSurface = false;
            try {
                GL10 gl = null;
                boolean createEglContext = false;
                boolean createEglSurface = false;
                boolean createGlInterface = false;
                boolean lostEglContext = false;
                boolean sizeChanged = false;
                boolean wantRenderNotification = false;
                boolean doRenderNotification = false;
                boolean askedToReleaseEglContext = false;
                int w = 0;
                int h = 0;
                Runnable event = null;

                while (true) {
                    synchronized (glThreadManager) {
                        while (true) {
                            if (shouldExit) {
                                return;
                            }

                            if(! eventQueue.isEmpty()) { event = eventQueue.remove(0);
                                break;
                            }

                            // Update the pause state.
                            boolean pausing = false;
                            if(paused ! = requestPaused) { pausing = requestPaused; paused = requestPaused; glThreadManager.notifyAll();if (LOG_PAUSE_RESUME) {
                                    Log.i("GLThread"."paused is now " + paused + " tid="+ getId()); }}// Do we need to give up the EGL context?
                            if (shouldReleaseEglContext) {
                                if (LOG_SURFACE) {
                                    Log.i("GLThread"."releasing EGL context because asked to tid=" + getId());
                                }
                                stopEglSurfaceLocked();
                                stopEglContextLocked();
                                shouldReleaseEglContext = false;
                                askedToReleaseEglContext = true;
                            }

                            // Have we lost the EGL context?
                            if (lostEglContext) {
                                stopEglSurfaceLocked();
                                stopEglContextLocked();
                                lostEglContext = false;
                            }

                            // When pausing, release the EGL surface:
                            if (pausing && haveEglSurface) {
                                if (LOG_SURFACE) {
                                    Log.i("GLThread"."releasing EGL surface because paused tid=" + getId());
                                }
                                stopEglSurfaceLocked();
                            }

                            // When pausing, optionally release the EGL Context:
                            if (pausing && haveEglContext) {
                                GLTextureView view = glTextureViewWeakRef.get();
                                boolean preserveEglContextOnPause =
                                        view == null ? false : view.preserveEGLContextOnPause;
                                if(! preserveEglContextOnPause || glThreadManager.shouldReleaseEGLContextWhenPausing()) { stopEglContextLocked();if (LOG_SURFACE) {
                                        Log.i("GLThread"."releasing EGL context because paused tid="+ getId()); }}}// When pausing, optionally terminate EGL:
                            if (pausing) {
                                if (glThreadManager.shouldTerminateEGLWhenPausing()) {
                                    eglHelper.finish();
                                    if (LOG_SURFACE) {
                                        Log.i("GLThread"."terminating EGL because paused tid="+ getId()); }}}// Have we lost the TextureView surface?
                            if((! hasSurface) && (! waitingForSurface)) {if (LOG_SURFACE) {
                                    Log.i("GLThread"."noticed textureView surface lost tid=" + getId());
                                }
                                if (haveEglSurface) {
                                    stopEglSurfaceLocked();
                                }
                                waitingForSurface = true;
                                surfaceIsBad = false;
                                glThreadManager.notifyAll();
                            }

                            // Have we acquired the surface view surface?
                            if (hasSurface && waitingForSurface) {
                                if (LOG_SURFACE) {
                                    Log.i("GLThread"."noticed textureView surface acquired tid=" + getId());
                                }
                                waitingForSurface = false;
                                glThreadManager.notifyAll();
                            }

                            if (doRenderNotification) {
                                if (LOG_SURFACE) {
                                    Log.i("GLThread"."sending render notification tid=" + getId());
                                }
                                wantRenderNotification = false;
                                doRenderNotification = false;
                                renderComplete = true;
                                glThreadManager.notifyAll();
                            }

                            // Ready to draw?
                            if (readyToDraw()) {

                                // If we don't have an EGL context, try to acquire one.
                                if(! haveEglContext) {if (askedToReleaseEglContext) {
                                        askedToReleaseEglContext = false;
                                    } else if (glThreadManager.tryAcquireEglContextLocked(this)) {
                                        try {
                                            eglHelper.start();
                                        } catch (RuntimeException t) {
                                            glThreadManager.releaseEglContextLocked(this);
                                            throw t;
                                        }
                                        haveEglContext = true;
                                        createEglContext = true; glThreadManager.notifyAll(); }}if(haveEglContext && ! haveEglSurface) { haveEglSurface =true;
                                    createEglSurface = true;
                                    createGlInterface = true;
                                    sizeChanged = true;
                                }

                                if (haveEglSurface) {
                                    if (this.sizeChanged) {
                                        sizeChanged = true;
                                        w = width;
                                        h = height;
                                        wantRenderNotification = true;
                                        if (LOG_SURFACE) {
                                            Log.i("GLThread"."noticing that we want render notification tid=" + getId());
                                        }

                                        // Destroy and recreate the EGL surface.
                                        createEglSurface = true;

                                        this.sizeChanged = false;
                                    }
                                    requestRender = false;
                                    glThreadManager.notifyAll();
                                    break; }}// By design, this is the only place in a GLThread thread where we wait().
                            if (LOG_THREADS) {
                                Log.i("GLThread"."waiting tid=" + getId() + " haveEglContext: " + haveEglContext
                                        + " haveEglSurface: " + haveEglSurface + " paused: " + paused + " hasSurface: "
                                        + hasSurface + " surfaceIsBad: " + surfaceIsBad + " waitingForSurface: "
                                        + waitingForSurface + " width: " + width + " height: " + height
                                        + " requestRender: " + requestRender + " renderMode: "+ renderMode); } glThreadManager.wait(); }}// end of synchronized(glThreadManager)

                    if(event ! =null) {
                        event.run();
                        event = null;
                        continue;
                    }

                    if (createEglSurface) {
                        if (LOG_SURFACE) {
                            Log.w("GLThread"."egl createSurface");
                        }
                        if(! eglHelper.createSurface()) {synchronized (glThreadManager) {
                                surfaceIsBad = true;
                                glThreadManager.notifyAll();
                            }
                            continue;
                        }
                        createEglSurface = false;
                    }

                    if (createGlInterface) {
                        gl = (GL10) eglHelper.createGL();

                        glThreadManager.checkGLDriver(gl);
                        createGlInterface = false;
                    }

                    if (createEglContext) {
                        if (LOG_RENDERER) {
                            Log.w("GLThread"."onSurfaceCreated");
                        }
                        GLTextureView view = glTextureViewWeakRef.get();
                        if(view ! =null) {
                            view.renderer.onSurfaceCreated(gl, eglHelper.eglConfig);
                        }
                        createEglContext = false;
                    }

                    if (sizeChanged) {
                        if (LOG_RENDERER) {
                            Log.w("GLThread"."onSurfaceChanged(" + w + "," + h + ")");
                        }
                        GLTextureView view = glTextureViewWeakRef.get();
                        if(view ! =null) {
                            view.renderer.onSurfaceChanged(gl, w, h);
                        }
                        sizeChanged = false;
                    }

                    if (LOG_RENDERER_DRAW_FRAME) {
                        Log.w("GLThread"."onDrawFrame tid=" + getId());
                    }
                    {
                        GLTextureView view = glTextureViewWeakRef.get();
                        if(view ! =null) { view.renderer.onDrawFrame(gl); }}int swapError = eglHelper.swap();
                    switch (swapError) {
                        case EGL10.EGL_SUCCESS:
                            break;
                        case EGL11.EGL_CONTEXT_LOST:
                            if (LOG_SURFACE) {
                                Log.i("GLThread"."egl context lost tid=" + getId());
                            }
                            lostEglContext = true;
                            break;
                        default:
                            // Other errors typically mean that the current surface is bad,
                            // probably because the TextureView surface has been destroyed,
                            // but we haven't been notified yet.
                            // Log the error to help developers understand why rendering stopped.
                            EglHelper.logEglErrorAsWarning("GLThread"."eglSwapBuffers", swapError);

                            synchronized (glThreadManager) {
                                surfaceIsBad = true;
                                glThreadManager.notifyAll();
                            }
                            break;
                    }

                    if (wantRenderNotification) {
                        doRenderNotification = true; }}}finally {
                /* * clean-up everything... * /
                synchronized(glThreadManager) { stopEglSurfaceLocked(); stopEglContextLocked(); }}}Copy the code

In the process of continuous loop execution, first set the environment variables, including the context, surface, etc., and then perform the drawing operation; Wait () will be blocked after glthreadManager.wait (), and the task will be executed when the refresh request is made. In passive refresh mode, tasks are executed in a continuous loop. GLTextureView view = glTextureViewWeakRef.get(); Get a reference to the current GLTextureView object and call its internal Render onSurfaceCreated onSurfaceChanged onDrawFrame method. The implementation of these three methods in Render looks like this:


@Override
public void onSurfaceCreated(final GL10 unused, final EGLConfig config) {
    GLES20.glClearColor(backgroundRed, backgroundGreen, backgroundBlue, 1);
    GLES20.glDisable(GLES20.GL_DEPTH_TEST);
    filter.ifNeedInit();// Call the filter's initialization method
}

@Override
public void onSurfaceChanged(final GL10 gl, final int width, final int height) {
    outputWidth = width;
    outputHeight = height;
    GLES20.glViewport(0.0, width, height);// Controls the drawing area of the current SurfaceView
    GLES20.glUseProgram(filter.getProgram());
    filter.onOutputSizeChanged(width, height);// Set the current width and height for the filter
    adjustImageScaling();// Adjust the width and height of the texture (image) by modifying the texture coordinates
    synchronized(surfaceChangedWaiter) { surfaceChangedWaiter.notifyAll(); }}@Override
public void onDrawFrame(final GL10 gl) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
    runAll(runOnDraw);// The tasks are taken out of the queue from the loop and executed in sequence
    generateOesTexture();// Get the current glTextureId, glCubeBuffer, glTextureBuffer
    filter.onDraw(glTextureId, glCubeBuffer, glTextureBuffer);// Apply the filter
    runAll(runOnDrawEnd);// Finally execute the second task queue
}

 private void runAll(Queue<Runnable> queue) {
    synchronized (queue) {
        while(! queue.isEmpty()) { queue.poll().run(); }}}Copy the code

OnDrawFrame () = onDrawFrame(); onDrawFrame() = onDrawFrame(); onDrawFrame() = onDrawFrame();

setImage()

Next, look at the process of setting up an image resource and rendering it.


/**
 * Sets the image on which the filter should be applied.
 *
 * @param bitmap the new image
 */
public void setImage(final Bitmap bitmap) {
    gpuImage.setImage(bitmap);
}

/**
 * Sets the image on which the filter should be applied from a Uri.
 *
 * @param uri the uri of the new image
 */
public void setImage(final Uri uri) {
    gpuImage.setImage(uri);
}

/**
 * Sets the image on which the filter should be applied from a File.
 *
 * @param file the file of the new image
 */
public void setImage(final File file) {
    gpuImage.setImage(file);
}

Copy the code

The GPUImageView setImage() calls the GPUImage setImage() method and has multiple overloaded methods that can receive different types of input, bitmap URIs and files, respectively. The last two inputs will either build an image loading process themselves, or eventually get a bitmap:


 /**
 * Sets the image on which the filter should be applied from a Uri.
 *
 * @param uri the uri of the new image
 */
public void setImage(final Uri uri) {
    new LoadImageUriTask(this, uri).execute();
}

/**
 * Sets the image on which the filter should be applied from a File.
 *
 * @param file the file of the new image
 */
public void setImage(final File file) {
    new LoadImageFileTask(this, file).execute();
}

Copy the code

Let’s take bitmap as an example:


/**
 * Sets the image on which the filter should be applied.
 *
 * @param bitmap the new image
 */
public void setImage(final Bitmap bitmap) {
    currentBitmap = bitmap;
    renderer.setImageBitmap(bitmap, false);
    requestRender();
}

Copy the code

Save a reference to a Bitmap, pass the bitmap to Render, and then actively request a render. Let’s see what render did:


public void setImageBitmap(final Bitmap bitmap, final boolean recycle) {
    if (bitmap == null) {
        return;
    }

    runOnDraw(new Runnable() {

        @Override
        public void run(a) {
            Bitmap resizedBitmap = null;
            if (bitmap.getWidth() % 2= =1) {
                resizedBitmap = Bitmap.createBitmap(bitmap.getWidth() + 1, bitmap.getHeight(),
                        Bitmap.Config.ARGB_8888);
                resizedBitmap.setDensity(bitmap.getDensity());
                Canvas can = new Canvas(resizedBitmap);
                can.drawARGB(0x00.0x00.0x00.0x00);
                can.drawBitmap(bitmap, 0.0.null);
                addedPadding = 1;
            } else {
                addedPadding = 0; } glTextureId = OpenGlUtils.loadTexture( resizedBitmap ! =null ? resizedBitmap : bitmap, glTextureId, recycle);
            if(resizedBitmap ! =null) { resizedBitmap.recycle(); } imageWidth = bitmap.getWidth(); imageHeight = bitmap.getHeight(); adjustImageScaling(); }}); }protected void runOnDraw(final Runnable runnable) {
    synchronized(runOnDraw) { runOnDraw.add(runnable); }}Copy the code

RunOnDraw () inserts the current operation as a Runnable task at the end of the draw task queue. As for the specific operation content, the first step is to detect the width of the picture. If it is odd, the first step is to create a picture with the width +1 of the original picture and the height equal to that of the original picture. A new picture is drawn in the way of Canvas. The adjustImageScaleing() method is then called. This method is used to make width/height adjustments and rotations by transforming texture coordinates:


private void adjustImageScaling(a) {
    float outputWidth = this.outputWidth;
    float outputHeight = this.outputHeight;
    if (rotation == Rotation.ROTATION_270 || rotation == Rotation.ROTATION_90) {
        outputWidth = this.outputHeight;
        outputHeight = this.outputWidth;
    }

    float ratio1 = outputWidth / imageWidth;
    float ratio2 = outputHeight / imageHeight;
    float ratioMax = Math.max(ratio1, ratio2);
    int imageWidthNew = Math.round(imageWidth * ratioMax);
    int imageHeightNew = Math.round(imageHeight * ratioMax);

    float ratioWidth = imageWidthNew / outputWidth;
    float ratioHeight = imageHeightNew / outputHeight;

    float[] cube = CUBE;
    float[] textureCords = TextureRotationUtil.getRotation(rotation, flipHorizontal, flipVertical);
    if (scaleType == GPUImage.ScaleType.CENTER_CROP) {
        float distHorizontal = (1 - 1 / ratioWidth) / 2;
        float distVertical = (1 - 1 / ratioHeight) / 2;
        textureCords = new float[]{
                addDistance(textureCords[0], distHorizontal), addDistance(textureCords[1], distVertical),
                addDistance(textureCords[2], distHorizontal), addDistance(textureCords[3], distVertical),
                addDistance(textureCords[4], distHorizontal), addDistance(textureCords[5], distVertical),
                addDistance(textureCords[6], distHorizontal), addDistance(textureCords[7], distVertical),
        };
    } else {
        cube = new float[]{
                CUBE[0] / ratioHeight, CUBE[1] / ratioWidth,
                CUBE[2] / ratioHeight, CUBE[3] / ratioWidth,
                CUBE[4] / ratioHeight, CUBE[5] / ratioWidth,
                CUBE[6] / ratioHeight, CUBE[7] / ratioWidth,
        };
    }

    glCubeBuffer.clear();
    glCubeBuffer.put(cube).position(0);
    glTextureBuffer.clear();
    glTextureBuffer.put(textureCords).position(0);
}

Copy the code

setFilter()

Next, add a filter:


 /**
 * Set the filter to be applied on the image.
 *
 * @param filter Filter that should be applied on the image.
 */
public void setFilter(GPUImageFilter filter) {
    this.filter = filter;
    gpuImage.setFilter(filter);
    requestRender();
}

Copy the code

First save a reference to filter, then call the GPUImage setFilter() method to pass the filter, and finally actively refresh the filter. Look at the setFilter() implementation of GPUImage:


/** * Sets the filter which should be applied to the image which was (or will * be) set by setImage(...) . * *@param filter the new filter
 */
public void setFilter(final GPUImageFilter filter) {
    this.filter = filter;
    renderer.setFilter(this.filter);
    requestRender();
}

Copy the code

Similarly, we end up in Render:


public void setFilter(final GPUImageFilter filter) {
    runOnDraw(new Runnable() {

        @Override
        public void run(a) {
            final GPUImageFilter oldFilter = GPUImageRenderer.this.filter;
            GPUImageRenderer.this.filter = filter;
            if(oldFilter ! =null) {
                oldFilter.destroy();
            }
            GPUImageRenderer.this.filter.ifNeedInit();
            GLES20.glUseProgram(GPUImageRenderer.this.filter.getProgram());
            GPUImageRenderer.this.filter.onOutputSizeChanged(outputWidth, outputHeight); }}); }Copy the code

Create a new drawing task that first gets the Filter saved in Render, which was drawn last time. After the Filter is updated, call the destroy() method of the old Filter to clean up some resources, and then call the initialization method of the new Filter to pass the current width and height data to the new Filter. To summarize, all subsequent Settings, including images and filters, are drawn in the GLTread onDrawFrame() method.