diff --git a/build.gradle b/build.gradle index 333e9907..1ac0e753 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,16 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext { + kotlin_version = '2.0.20' + } repositories { mavenCentral() google() } dependencies { classpath 'com.android.tools.build:gradle:7.3.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/cgeDemo/build.gradle b/cgeDemo/build.gradle index f2d860ef..9472762c 100644 --- a/cgeDemo/build.gradle +++ b/cgeDemo/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'org.jetbrains.kotlin.android' def usingCMakeCompile() { /// define gradle.ext.usingCMakeCompile = false to disable CMakeCompile Mode. @@ -33,6 +34,9 @@ android { } } namespace 'org.wysaid.cgeDemo' + kotlinOptions { + jvmTarget = '1.8' + } } @@ -40,4 +44,5 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':library') implementation 'androidx.appcompat:appcompat:' + rootProject.ext.android.appcompatX + implementation 'androidx.core:core-ktx:1.1.0' } diff --git a/cgeDemo/src/main/AndroidManifest.xml b/cgeDemo/src/main/AndroidManifest.xml index 35b08002..ec3256f7 100644 --- a/cgeDemo/src/main/AndroidManifest.xml +++ b/cgeDemo/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ - + + @@ -50,6 +51,7 @@ android:label="@string/title_activity_video_player" /> + diff --git a/cgeDemo/src/main/java/org/wysaid/cgeDemo/GLTextureView.java b/cgeDemo/src/main/java/org/wysaid/cgeDemo/GLTextureView.java new file mode 100644 index 00000000..a710705b --- /dev/null +++ b/cgeDemo/src/main/java/org/wysaid/cgeDemo/GLTextureView.java @@ -0,0 +1,1749 @@ +package org.wysaid.cgeDemo; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLExt; +import android.opengl.GLDebugHelper; +import android.os.Build; +import android.os.Trace; +import android.util.AttributeSet; +import android.util.Log; +import android.view.TextureView; + +import java.io.Writer; +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; + +/** + * create by xieyangxuejun on 2018/3/16 + */ +public abstract class GLTextureView extends TextureView implements TextureView.SurfaceTextureListener { + private final static String TAG = "GLSurfaceView"; + private final static boolean LOG_ATTACH_DETACH = false; + private final static boolean LOG_THREADS = false; + private final static boolean LOG_PAUSE_RESUME = false; + private final static boolean LOG_SURFACE = false; + private final static boolean LOG_RENDERER = false; + private final static boolean LOG_RENDERER_DRAW_FRAME = false; + private final static boolean LOG_EGL = false; + + + /** + * The renderer only renders + * when the surface is created, or when {@link #requestRender} is called. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + * @see #requestRender() + */ + public final static int RENDERMODE_WHEN_DIRTY = 0; + /** + * The renderer is called + * continuously to re-render the scene. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + */ + public final static int RENDERMODE_CONTINUOUSLY = 1; + + /** + * Check glError() after every GL call and throw an exception if glError indicates + * that an error has occurred. This can be used to help track down which OpenGL ES call + * is causing an error. + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_CHECK_GL_ERROR = 1; + + /** + * Log GL calls to the system log at "verbose" level with tag "GLTextureView". + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_LOG_GL_CALLS = 2; + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLTextureView(Context context) { + super(context); + init(); + } + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLTextureView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + + private void init() { + // Install a SurfaceTexture.Callback so we get notified when the + // underlying surface is created and destroyed + SurfaceTexture holder = getSurfaceTexture(); + setSurfaceTextureListener(this); + // setFormat is done by SurfaceView in SDK 2.3 and newer. Uncomment + // this statement if back-porting to 2.2 or older: + // holder.setFormat(PixelFormat.RGB_565); + // + // setType is not needed for SDK 2.0 or newer. Uncomment this + // statement if back-porting this code to older SDKs. + // holder.setType(SurfaceTexture.SURFACE_TYPE_GPU); + } + + /** + * Set the glWrapper. If the glWrapper is not null, its + * {@link GLWrapper#wrap(GL)} method is called + * whenever a surface is created. A GLWrapper can be used to wrap + * the GL object that's passed to the renderer. Wrapping a GL + * object enables examining and modifying the behavior of the + * GL calls made by the renderer. + *

+ * Wrapping is typically used for debugging purposes. + *

+ * The default value is null. + * + * @param glWrapper the new GLWrapper + */ + public void setGLWrapper(GLWrapper glWrapper) { + mGLWrapper = glWrapper; + } + + /** + * Set the debug flags to a new value. The value is + * constructed by OR-together zero or more + * of the DEBUG_CHECK_* constants. The debug flags take effect + * whenever a surface is created. The default value is zero. + * + * @param debugFlags the new debug flags + * @see #DEBUG_CHECK_GL_ERROR + * @see #DEBUG_LOG_GL_CALLS + */ + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + + /** + * Get the current value of the debug flags. + * + * @return the current value of the debug flags. + */ + public int getDebugFlags() { + return mDebugFlags; + } + + /** + * Control whether the EGL context is preserved when the GLTextureView is paused and + * resumed. + *

+ * If set to true, then the EGL context may be preserved when the GLTextureView is paused. + *

+ * Prior to API level 11, whether the EGL context is actually preserved or not + * depends upon whether the Android device can support an arbitrary number of + * EGL contexts or not. Devices that can only support a limited number of EGL + * contexts must release the EGL context in order to allow multiple applications + * to share the GPU. + *

+ * If set to false, the EGL context will be released when the GLTextureView is paused, + * and recreated when the GLTextureView is resumed. + *

+ *

+ * The default is false. + * + * @param preserveOnPause preserve the EGL context when paused + */ + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + mPreserveEGLContextOnPause = preserveOnPause; + } + + /** + * @return true if the EGL context will be preserved when paused + */ + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + + /** + * Set the renderer associated with this view. Also starts the thread that + * will call the renderer, which in turn causes the rendering to start. + *

This method should be called once and only once in the life-cycle of + * a GLTextureView. + *

The following GLTextureView methods can only be called before + * setRenderer is called: + *

    + *
  • {@link #setEGLConfigChooser(boolean)} + *
  • {@link #setEGLConfigChooser(EGLConfigChooser)} + *
  • {@link #setEGLConfigChooser(int, int, int, int, int, int)} + *
+ *

+ * The following GLTextureView methods can only be called after + * setRenderer is called: + *

    + *
  • {@link #getRenderMode()} + *
  • {@link #onPause()} + *
  • {@link #onResume()} + *
  • {@link #queueEvent(Runnable)} + *
  • {@link #requestRender()} + *
  • {@link #setRenderMode(int)} + *
+ * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + public void setRenderer(Renderer renderer) { + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + + /** + * Install a custom EGLContextFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a context will be created with no shared context and + * with a null attribute list. + */ + public void setEGLContextFactory(EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + + /** + * Install a custom EGLWindowSurfaceFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a window surface will be created with a null attribute list. + */ + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + + /** + * Install a custom EGLConfigChooser. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an EGLConfig that is compatible with the current + * android.view.Surface, with a depth buffer depth of + * at least 16 bits. + * + * @param configChooser + */ + public void setEGLConfigChooser(EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + + /** + * Install a config chooser which will choose a config + * as close to 16-bit RGB as possible, with or without an optional depth + * buffer as close to 16-bits as possible. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + * @param needDepth + */ + public void setEGLConfigChooser(boolean needDepth) { + setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); + } + + /** + * Install a config chooser which will choose a config + * with at least the specified depthSize and stencilSize, + * and exactly the specified redSize, greenSize, blueSize and alphaSize. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + */ + public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { + setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, blueSize, alphaSize, depthSize, stencilSize)); + } + + /** + * Inform the default EGLContextFactory and default EGLConfigChooser + * which EGLContext client version to pick. + *

Use this method to create an OpenGL ES 2.0-compatible context. + * Example: + *

+     *     public MyView(Context context) {
+     *         super(context);
+     *         setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
+     *         setRenderer(new MyRenderer());
+     *     }
+     * 
+ *

Note: Activities which require OpenGL ES 2.0 should indicate this by + * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's + * AndroidManifest.xml file. + *

If this method is called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

This method only affects the behavior of the default EGLContexFactory and the + * default EGLConfigChooser. If + * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied + * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. + * If + * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied + * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. + * + * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 + */ + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + mGLThread.surfaceCreated(); + mGLThread.onWindowResize(width, height); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + mGLThread.onWindowResize(width, height); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + mGLThread.surfaceDestroyed(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + + } + + /** + * Set the rendering mode. When renderMode is + * RENDERMODE_CONTINUOUSLY, the renderer is called + * repeatedly to re-render the scene. When renderMode + * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface + * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. + *

+ * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance + * by allowing the GPU and CPU to idle when the view does not need to be updated. + *

+ * This method can only be called after {@link #setRenderer(Renderer)} + * + * @param renderMode one of the RENDERMODE_X constants + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public void setRenderMode(int renderMode) { + mGLThread.setRenderMode(renderMode); + } + + /** + * Get the current rendering mode. May be called + * from any thread. Must not be called before a renderer has been set. + * + * @return the current rendering mode. + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. + * May be called + * from any thread. Must not be called before a renderer has been set. + */ + public void requestRender() { + mGLThread.requestRender(); + } + + + /** + * Pause the rendering thread, optionally tearing down the EGL context + * depending upon the value of {@link #setPreserveEGLContextOnPause(boolean)}. + *

+ * This method should be called when it is no longer desirable for the + * GLTextureView to continue rendering, such as in response to + * {@link android.app.Activity Activity.onStop}. + *

+ * Must not be called before a renderer has been set. + */ + public void onPause() { + mGLThread.onPause(); + } + + /** + * Resumes the rendering thread, re-creating the OpenGL context if necessary. It + * is the counterpart to {@link #onPause()}. + *

+ * This method should typically be called in + * {@link android.app.Activity Activity.onStart}. + *

+ * Must not be called before a renderer has been set. + */ + public void onResume() { + mGLThread.onResume(); + } + + /** + * Queue a runnable to be run on the GL rendering thread. This can be used + * to communicate with the Renderer on the rendering thread. + * Must not be called before a renderer has been set. + * + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + mGLThread.queueEvent(r); + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLTextureView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + mGLThread.start(); + } + mDetached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + + + // ---------------------------------------------------------------------- + + /** + * An interface used to wrap a GL interface. + *

Typically + * used for implementing debugging and tracing on top of the default + * GL interface. You would typically use this by creating your own class + * that implemented all the GL methods by delegating to another GL instance. + * Then you could add your own behavior before or after calling the + * delegate. All the GLWrapper would do was instantiate and return the + * wrapper GL instance: + *

+     * class MyGLWrapper implements GLWrapper {
+     *     GL wrap(GL gl) {
+     *         return new MyGLImplementation(gl);
+     *     }
+     *     static class MyGLImplementation implements GL,GL10,GL11,... {
+     *         ...
+     *     }
+     * }
+     * 
+ * + * @see #setGLWrapper(GLWrapper) + */ + public interface GLWrapper { + /** + * Wraps a gl interface in another gl interface. + * + * @param gl a GL interface that is to be wrapped. + * @return either the input argument or another GL object that wraps the input argument. + */ + GL wrap(GL gl); + } + + /** + * A generic renderer interface. + *

+ * The renderer is responsible for making OpenGL calls to render a frame. + *

+ * GLTextureView clients typically create their own classes that implement + * this interface, and then call {@link GLTextureView#setRenderer} to + * register the renderer with the GLTextureView. + *

+ * + *

+ *

Developer Guides

+ *

For more information about how to use OpenGL, read the + * OpenGL developer guide.

+ *
+ * + *

Threading

+ * The renderer will be called on a separate thread, so that rendering + * performance is decoupled from the UI thread. Clients typically need to + * communicate with the renderer from the UI thread, because that's where + * input events are received. Clients can communicate using any of the + * standard Java techniques for cross-thread communication, or they can + * use the {@link GLTextureView#queueEvent(Runnable)} convenience method. + *

+ *

EGL Context Lost

+ * There are situations where the EGL rendering context will be lost. This + * typically happens when device wakes up after going to sleep. When + * the EGL context is lost, all OpenGL resources (such as textures) that are + * associated with that context will be automatically deleted. In order to + * keep rendering correctly, a renderer must recreate any lost resources + * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method + * is a convenient place to do this. + * + * @see #setRenderer(Renderer) + */ + public interface Renderer { + /** + * Called when the surface is created or recreated. + *

+ * Called when the rendering thread + * starts and whenever the EGL context is lost. The EGL context will typically + * be lost when the Android device awakes after going to sleep. + *

+ * Since this method is called at the beginning of rendering, as well as + * every time the EGL context is lost, this method is a convenient place to put + * code to create resources that need to be created when the rendering + * starts, and that need to be recreated when the EGL context is lost. + * Textures are an example of a resource that you might want to create + * here. + *

+ * Note that when the EGL context is lost, all OpenGL resources associated + * with that context will be automatically deleted. You do not need to call + * the corresponding "glDelete" methods such as glDeleteTextures to + * manually delete these lost resources. + *

+ * + * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + * @param config the EGLConfig of the created surface. Can be used + * to create matching pbuffers. + */ + void onSurfaceCreated(GL10 gl, EGLConfig config); + + /** + * Called when the surface changed size. + *

+ * Called after the surface is created and whenever + * the OpenGL ES surface size changes. + *

+ * Typically you will set your viewport here. If your camera + * is fixed then you could also set your projection matrix here: + *

+         * void onSurfaceChanged(GL10 gl, int width, int height) {
+         *     gl.glViewport(0, 0, width, height);
+         *     // for a fixed camera, set the projection too
+         *     float ratio = (float) width / height;
+         *     gl.glMatrixMode(GL10.GL_PROJECTION);
+         *     gl.glLoadIdentity();
+         *     gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
+         * }
+         * 
+ * + * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + * @param width + * @param height + */ + void onSurfaceChanged(GL10 gl, int width, int height); + + /** + * Called to draw the current frame. + *

+ * This method is responsible for drawing the current frame. + *

+ * The implementation of this method typically looks like this: + *

+         * void onDrawFrame(GL10 gl) {
+         *     gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
+         *     //... other gl calls to render the scene ...
+         * }
+         * 
+ * + * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + */ + void onDrawFrame(GL10 gl); + } + + /** + * An interface for customizing the eglCreateContext and eglDestroyContext calls. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLTextureView#setEGLContextFactory(EGLContextFactory)} + */ + public interface EGLContextFactory { + EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig); + + void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context); + } + + private class DefaultContextFactory implements EGLContextFactory { + private int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, EGL10.EGL_NONE}; + + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, mEGLContextClientVersion != 0 ? attrib_list : null); + } + + public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + if (LOG_THREADS) { + Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); + } + EglHelper.throwEglException("eglDestroyContex", egl.eglGetError()); + } + } + } + + /** + * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLTextureView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} + */ + public interface EGLWindowSurfaceFactory { + /** + * @return null if the surface cannot be constructed. + */ + EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow); + + void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); + } + + private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { + + public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceTexture.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e(TAG, "eglCreateWindowSurface", e); + } + return result; + } + + public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + } + + /** + * An interface for choosing an EGLConfig configuration from a list of + * potential configurations. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLTextureView#setEGLConfigChooser(EGLConfigChooser)} + */ + public interface EGLConfigChooser { + /** + * Choose a configuration from the list. Implementors typically + * implement this method by calling + * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the + * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. + * + * @param egl the EGL10 for the current display. + * @param display the current display. + * @return the chosen configuration. + */ + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); + } + + private abstract class BaseConfigChooser implements EGLConfigChooser { + public BaseConfigChooser(int[] configSpec) { + mConfigSpec = filterConfigSpec(configSpec); + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] num_config = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs); + + protected int[] mConfigSpec; + + private int[] filterConfigSpec(int[] configSpec) { + if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len - 1); + newConfigSpec[len - 1] = EGL10.EGL_RENDERABLE_TYPE; + if (mEGLContextClientVersion == 2) { + newConfigSpec[len] = EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */ + } else { + newConfigSpec[len] = EGLExt.EGL_OPENGL_ES3_BIT_KHR; /* EGL_OPENGL_ES3_BIT_KHR */ + } + newConfigSpec[len + 1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + private class ComponentSizeChooser extends BaseConfigChooser { + public ComponentSizeChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { + super(new int[]{EGL10.EGL_RED_SIZE, redSize, EGL10.EGL_GREEN_SIZE, greenSize, EGL10.EGL_BLUE_SIZE, blueSize, EGL10.EGL_ALPHA_SIZE, alphaSize, EGL10.EGL_DEPTH_SIZE, depthSize, EGL10.EGL_STENCIL_SIZE, stencilSize, EGL10.EGL_NONE}); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); + if ((d >= mDepthSize) && (s >= mStencilSize)) { + int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); + if ((r == mRedSize) && (g == mGreenSize) && (b == mBlueSize) && (a == mAlphaSize)) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + } + + /** + * This class will choose a RGB_888 surface with + * or without a depth buffer. + */ + private class SimpleEGLConfigChooser extends ComponentSizeChooser { + public SimpleEGLConfigChooser(boolean withDepthBuffer) { + super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); + } + } + + /** + * An EGL helper class. + */ + + private static class EglHelper { + public EglHelper(WeakReference glSurfaceViewWeakRef) { + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + /** + * Initialize EGL for a given configuration spec. + * // * @param configSpec + */ + public void start() { + if (LOG_EGL) { + Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); + } + /* + * Get an EGL instance + */ + mEgl = (EGL10) EGLContext.getEGL(); + + /* + * Get to the default display. + */ + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + /* + * We can now initialize EGL for that display + */ + int[] version = new int[2]; + if (!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view == null) { + mEglConfig = null; + mEglContext = null; + } else { + mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); + } + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + mEglContext = null; + throwEglException("createContext"); + } + if (LOG_EGL) { + Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); + } + + mEglSurface = null; + } + + /** + * Create an egl surface for the current SurfaceTexture surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + public boolean createSurface() { + if (LOG_EGL) { + Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); + } + /* + * Check preconditions. + */ + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl, mEglDisplay, mEglConfig, view.getSurfaceTexture()); + } else { + mEglSurface = null; + } + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * SurfaceView surface has been destroyed. + */ + logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + * + * @return + */ + GL createGL() { + + GL gl = mEglContext.getGL(); + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + if (view.mGLWrapper != null) { + gl = view.mGLWrapper.wrap(gl); + } + + if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) { + int configFlags = 0; + Writer log = null; + if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { + configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; + } + if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { + log = new LogWriter(); + } + gl = GLDebugHelper.wrap(gl, configFlags, log); + } + } + return gl; + } + + /** + * Display the current render surface. + * + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + return mEgl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + public void destroySurface() { + if (LOG_EGL) { + Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); + } + destroySurfaceImp(); + } + + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + + public void finish() { + if (LOG_EGL) { + Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); + } + if (mEglContext != null) { + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + private void throwEglException(String function) { + throwEglException(function, mEgl.eglGetError()); + } + + public static void throwEglException(String function, int error) { + String message = formatEglError(function, error); + if (LOG_THREADS) { + Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + message); + } + throw new RuntimeException(message); + } + + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + public static String formatEglError(String function, int error) { + return function + " failed: " + error; + } + + private WeakReference mGLSurfaceViewWeakRef; + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + + } + + /** + * A generic GL Thread. Takes care of initializing EGL and GL. Delegates + * to a Renderer instance to do the actual drawing. Can be configured to + * render continuously or on request. + *

+ * All potentially blocking synchronization is done through the + * sGLThreadManager object. This avoids multiple-lock ordering issues. + */ + static class GLThread extends Thread { + GLThread(WeakReference glSurfaceViewWeakRef) { + super(); + mWidth = 0; + mHeight = 0; + mRequestRender = true; + mRenderMode = RENDERMODE_CONTINUOUSLY; + mWantRenderNotification = false; + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + @Override + public void run() { + setName("GLThread " + getId()); + if (LOG_THREADS) { + Log.i("GLThread", "starting tid=" + getId()); + } + + try { + guardedRun(); + } catch (InterruptedException e) { + // fall thru and exit normally + } finally { + sGLThreadManager.threadExiting(this); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (mHaveEglSurface) { + mHaveEglSurface = false; + mEglHelper.destroySurface(); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (mHaveEglContext) { + mEglHelper.finish(); + mHaveEglContext = false; + sGLThreadManager.releaseEglContextLocked(this); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private void guardedRun() throws InterruptedException { + mEglHelper = new EglHelper(mGLSurfaceViewWeakRef); + mHaveEglContext = false; + mHaveEglSurface = false; + mWantRenderNotification = 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; + Runnable finishDrawingRunnable = null; + + while (true) { + synchronized (sGLThreadManager) { + while (true) { + if (mShouldExit) { + return; + } + + if (!mEventQueue.isEmpty()) { + event = mEventQueue.remove(0); + break; + } + + // Update the pause state. + boolean pausing = false; + if (mPaused != mRequestPaused) { + pausing = mRequestPaused; + mPaused = mRequestPaused; + sGLThreadManager.notifyAll(); + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); + } + } + + // Do we need to give up the EGL context? + if (mShouldReleaseEglContext) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); + } + stopEglSurfaceLocked(); + stopEglContextLocked(); + mShouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && mHaveEglSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); + } + stopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && mHaveEglContext) { + GLTextureView view = mGLSurfaceViewWeakRef.get(); + boolean preserveEglContextOnPause = view == null ? false : view.mPreserveEGLContextOnPause; + if (!preserveEglContextOnPause) { + stopEglContextLocked(); + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); + } + } + } + + // Have we lost the SurfaceView surface? + if ((!mHasSurface) && (!mWaitingForSurface)) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId()); + } + if (mHaveEglSurface) { + stopEglSurfaceLocked(); + } + mWaitingForSurface = true; + mSurfaceIsBad = false; + sGLThreadManager.notifyAll(); + } + + // Have we acquired the surface view surface? + if (mHasSurface && mWaitingForSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId()); + } + mWaitingForSurface = false; + sGLThreadManager.notifyAll(); + } + + if (doRenderNotification) { + if (LOG_SURFACE) { + Log.i("GLThread", "sending render notification tid=" + getId()); + } + mWantRenderNotification = false; + doRenderNotification = false; + mRenderComplete = true; + sGLThreadManager.notifyAll(); + } + + if (mFinishDrawingRunnable != null) { + finishDrawingRunnable = mFinishDrawingRunnable; + mFinishDrawingRunnable = null; + } + + // Ready to draw? + if (readyToDraw()) { + + // If we don't have an EGL context, try to acquire one. + if (!mHaveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else { + try { + mEglHelper.start(); + } catch (RuntimeException t) { + sGLThreadManager.releaseEglContextLocked(this); + throw t; + } + mHaveEglContext = true; + createEglContext = true; + + sGLThreadManager.notifyAll(); + } + } + + if (mHaveEglContext && !mHaveEglSurface) { + mHaveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (mHaveEglSurface) { + if (mSizeChanged) { + sizeChanged = true; + w = mWidth; + h = mHeight; + mWantRenderNotification = true; + if (LOG_SURFACE) { + Log.i("GLThread", "noticing that we want render notification tid=" + getId()); + } + + // Destroy and recreate the EGL surface. + createEglSurface = true; + + mSizeChanged = false; + } + mRequestRender = false; + sGLThreadManager.notifyAll(); + if (mWantRenderNotification) { + wantRenderNotification = true; + } + break; + } + } else { + if (finishDrawingRunnable != null) { + Log.w(TAG, "Warning, !readyToDraw() but waiting for " + "draw finished! Early reporting draw finished."); + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } + // By design, this is the only place in a GLThread thread where we wait(). + if (LOG_THREADS) { + Log.i("GLThread", "waiting tid=" + getId() + " mHaveEglContext: " + mHaveEglContext + " mHaveEglSurface: " + mHaveEglSurface + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface + " mPaused: " + mPaused + " mHasSurface: " + mHasSurface + " mSurfaceIsBad: " + mSurfaceIsBad + " mWaitingForSurface: " + mWaitingForSurface + " mWidth: " + mWidth + " mHeight: " + mHeight + " mRequestRender: " + mRequestRender + " mRenderMode: " + mRenderMode); + } + sGLThreadManager.wait(); + } + } // end of synchronized(sGLThreadManager) + + if (event != null) { + event.run(); + event = null; + continue; + } + + if (createEglSurface) { + if (LOG_SURFACE) { + Log.w("GLThread", "egl createSurface"); + } + if (mEglHelper.createSurface()) { + synchronized (sGLThreadManager) { + mFinishedCreatingEglSurface = true; + sGLThreadManager.notifyAll(); + } + } else { + synchronized (sGLThreadManager) { + mFinishedCreatingEglSurface = true; + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + } + + if (createGlInterface) { + gl = (GL10) mEglHelper.createGL(); + + createGlInterface = false; + } + + if (createEglContext) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceCreated"); + } + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + Trace.beginSection("onSurfaceChanged"); + view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); + } finally { + Trace.endSection(); + } + } + createEglContext = false; + } + + if (sizeChanged) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); + } + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + Trace.beginSection("onSurfaceChanged"); + view.mRenderer.onSurfaceChanged(gl, w, h); + } finally { + Trace.endSection(); + } + } + sizeChanged = false; + } + + if (LOG_RENDERER_DRAW_FRAME) { + Log.w("GLThread", "onDrawFrame tid=" + getId()); + } + { + GLTextureView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + Trace.beginSection("onDrawFrame"); + view.mRenderer.onDrawFrame(gl); + if (finishDrawingRunnable != null) { + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } finally { + Trace.endSection(); + } + } + } + int swapError = mEglHelper.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 SurfaceView 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 (sGLThreadManager) { + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + break; + } + + if (wantRenderNotification) { + doRenderNotification = true; + wantRenderNotification = false; + } + } + + } finally { + /* + * clean-up everything... + */ + synchronized (sGLThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + + public boolean ableToDraw() { + return mHaveEglContext && mHaveEglSurface && readyToDraw(); + } + + private boolean readyToDraw() { + return (!mPaused) && mHasSurface && (!mSurfaceIsBad) && (mWidth > 0) && (mHeight > 0) && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); + } + + public void setRenderMode(int renderMode) { + if (!((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY))) { + throw new IllegalArgumentException("renderMode"); + } + synchronized (sGLThreadManager) { + mRenderMode = renderMode; + sGLThreadManager.notifyAll(); + } + } + + public int getRenderMode() { + synchronized (sGLThreadManager) { + return mRenderMode; + } + } + + public void requestRender() { + synchronized (sGLThreadManager) { + mRequestRender = true; + sGLThreadManager.notifyAll(); + } + } + + public void requestRenderAndNotify(Runnable finishDrawing) { + synchronized (sGLThreadManager) { + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We will return to the client rendering code, so here we don't need to + // do anything. + if (Thread.currentThread() == this) { + return; + } + + mWantRenderNotification = true; + mRequestRender = true; + mRenderComplete = false; + mFinishDrawingRunnable = finishDrawing; + + sGLThreadManager.notifyAll(); + } + } + + public void surfaceCreated() { + synchronized (sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceCreated tid=" + getId()); + } + mHasSurface = true; + mFinishedCreatingEglSurface = false; + sGLThreadManager.notifyAll(); + while (mWaitingForSurface && !mFinishedCreatingEglSurface && !mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceDestroyed() { + synchronized (sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceDestroyed tid=" + getId()); + } + mHasSurface = false; + sGLThreadManager.notifyAll(); + while ((!mWaitingForSurface) && (!mExited)) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onPause() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onPause tid=" + getId()); + } + mRequestPaused = true; + sGLThreadManager.notifyAll(); + while ((!mExited) && (!mPaused)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onPause waiting for mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onResume() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onResume tid=" + getId()); + } + mRequestPaused = false; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + while ((!mExited) && mPaused && (!mRenderComplete)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onResume waiting for !mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onWindowResize(int w, int h) { + synchronized (sGLThreadManager) { + mWidth = w; + mHeight = h; + mSizeChanged = true; + mRequestRender = true; + mRenderComplete = false; + + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We need to process the size change eventually though and update our EGLSurface. + // So we set the parameters and return so they can be processed on our + // next iteration. + if (Thread.currentThread() == this) { + return; + } + + sGLThreadManager.notifyAll(); + + // Wait for thread to react to resize and render a frame + while (!mExited && !mPaused && !mRenderComplete && ableToDraw()) { + if (LOG_SURFACE) { + Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized (sGLThreadManager) { + mShouldExit = true; + sGLThreadManager.notifyAll(); + while (!mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestReleaseEglContextLocked() { + mShouldReleaseEglContext = true; + sGLThreadManager.notifyAll(); + } + + /** + * Queue an "event" to be run on the GL rendering thread. + * + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("r must not be null"); + } + synchronized (sGLThreadManager) { + mEventQueue.add(r); + sGLThreadManager.notifyAll(); + } + } + + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private boolean mShouldExit; + private boolean mExited; + private boolean mRequestPaused; + private boolean mPaused; + private boolean mHasSurface; + private boolean mSurfaceIsBad; + private boolean mWaitingForSurface; + private boolean mHaveEglContext; + private boolean mHaveEglSurface; + private boolean mFinishedCreatingEglSurface; + private boolean mShouldReleaseEglContext; + private int mWidth; + private int mHeight; + private int mRenderMode; + private boolean mRequestRender; + private boolean mWantRenderNotification; + private boolean mRenderComplete; + private ArrayList mEventQueue = new ArrayList<>(); + private boolean mSizeChanged = true; + private Runnable mFinishDrawingRunnable = null; + + // End of member variables protected by the sGLThreadManager monitor. + + private EglHelper mEglHelper; + + /** + * Set once at thread construction time, nulled out when the parent view is garbage + * called. This weak reference allows the GLTextureView to be garbage collected while + * the GLThread is still alive. + */ + private WeakReference mGLSurfaceViewWeakRef; + + } + + static class LogWriter extends Writer { + + @Override + public void close() { + flushBuilder(); + } + + @Override + public void flush() { + flushBuilder(); + } + + @Override + public void write(char[] buf, int offset, int count) { + for (int i = 0; i < count; i++) { + char c = buf[offset + i]; + if (c == '\n') { + flushBuilder(); + } else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.v("GLTextureView", mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } + + private StringBuilder mBuilder = new StringBuilder(); + } + + + private void checkRenderThreadState() { + if (mGLThread != null) { + throw new IllegalStateException("setRenderer has already been called for this instance."); + } + } + + private static class GLThreadManager { + private static String TAG = "GLThreadManager"; + + public synchronized void threadExiting(GLThread thread) { + if (LOG_THREADS) { + Log.i("GLThread", "exiting tid=" + thread.getId()); + } + thread.mExited = true; + notifyAll(); + } + + /* + * Releases the EGL context. Requires that we are already in the + * sGLThreadManager monitor when this is called. + */ + public void releaseEglContextLocked(GLThread thread) { + notifyAll(); + } + } + + private static final GLThreadManager sGLThreadManager = new GLThreadManager(); + + private final WeakReference mThisWeakRef = new WeakReference(this); + private GLThread mGLThread; + private Renderer mRenderer; + private boolean mDetached; + private EGLConfigChooser mEGLConfigChooser; + private EGLContextFactory mEGLContextFactory; + private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private GLWrapper mGLWrapper; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; +} \ No newline at end of file diff --git a/cgeDemo/src/main/java/org/wysaid/cgeDemo/ImageDemoWithMatrixActivity.kt b/cgeDemo/src/main/java/org/wysaid/cgeDemo/ImageDemoWithMatrixActivity.kt new file mode 100644 index 00000000..4ca3b2a5 --- /dev/null +++ b/cgeDemo/src/main/java/org/wysaid/cgeDemo/ImageDemoWithMatrixActivity.kt @@ -0,0 +1,75 @@ +package org.wysaid.cgeDemo + +import android.graphics.BitmapFactory +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.SeekBar +import android.widget.Spinner +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.doOnLayout +import org.wysaid.cgeDemo.view.MatrixMutualViewGroup + +private const val TAG = "ImageDemoWithMatrixActi" + +/** + * @author PengHaiChen + * @date 2024/9/30 21:32:29 + * @description + */ +class ImageDemoWithMatrixActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_image_demo_with_matrix) + + initView() + initEvent() + } + + private fun initEvent() { + val demoView: MatrixMutualViewGroup = findViewById(R.id.demo_view) + val intensity: SeekBar = findViewById(R.id.intensity) + val spinner: Spinner = findViewById(R.id.filter_item) + intensity.progress = intensity.max + intensity.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(p0: SeekBar, p1: Int, p2: Boolean) { + val progress = (p1.toFloat() / p0.max).coerceIn(0F, 1F) + Log.i(TAG, "onProgressChanged: $progress") + demoView.setIntensity(progress) + } + + override fun onStartTrackingTouch(p0: SeekBar?) { + } + + override fun onStopTrackingTouch(p0: SeekBar?) { + } + + }) + spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) { + demoView.setFilter(MainActivity.EFFECT_CONFIGS[p2]) + } + + override fun onNothingSelected(p0: AdapterView<*>?) { + } + } + } + + private fun initView() { + val spinner: Spinner = findViewById(R.id.filter_item) + val demoView: MatrixMutualViewGroup = findViewById(R.id.demo_view) + val mBitmap = BitmapFactory.decodeResource(resources, R.drawable.bgview) + demoView.doOnLayout { + demoView.initBitmap(mBitmap) + } + val effectConfigs: List = MainActivity.EFFECT_CONFIGS.toList().map { + it.ifBlank { "Select A Filter Apply To Image" } + } + val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, effectConfigs) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + spinner.adapter = adapter + + } +} \ No newline at end of file diff --git a/cgeDemo/src/main/java/org/wysaid/cgeDemo/MainActivity.java b/cgeDemo/src/main/java/org/wysaid/cgeDemo/MainActivity.java index 919dc171..299b2df6 100644 --- a/cgeDemo/src/main/java/org/wysaid/cgeDemo/MainActivity.java +++ b/cgeDemo/src/main/java/org/wysaid/cgeDemo/MainActivity.java @@ -192,6 +192,7 @@ public static class DemoClassDescription { new DemoClassDescription("SimplePlayerDemoActivity", "Simple Player Demo"), new DemoClassDescription("VideoPlayerDemoActivity", "Video Player Demo"), new DemoClassDescription("FaceTrackingDemoActivity", "Face Tracking Demo"), + new DemoClassDescription("ImageDemoWithMatrixActivity", "Image Demo WithMatrix"), new DemoClassDescription("TestCaseActivity", "Test Cases") }; diff --git a/cgeDemo/src/main/java/org/wysaid/cgeDemo/demoUtils/BaseGestureDetector.java b/cgeDemo/src/main/java/org/wysaid/cgeDemo/demoUtils/BaseGestureDetector.java new file mode 100644 index 00000000..becf2bab --- /dev/null +++ b/cgeDemo/src/main/java/org/wysaid/cgeDemo/demoUtils/BaseGestureDetector.java @@ -0,0 +1,150 @@ +package org.wysaid.cgeDemo.demoUtils; + +import android.content.Context; +import android.view.MotionEvent; + +/** + * @author Almer Thie (code.almeros.com) + * Copyright (c) 2013, Almer Thie (code.almeros.com) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public abstract class BaseGestureDetector { + protected final Context mContext; + protected boolean mGestureInProgress; + + protected MotionEvent mPrevEvent; + protected MotionEvent mCurrEvent; + + protected float mCurrPressure; + protected float mPrevPressure; + protected long mTimeDelta; + + + /** + * This value is the threshold ratio between the previous combined pressure + * and the current combined pressure. When pressure decreases rapidly + * between events the position values can often be imprecise, as it usually + * indicates that the user is in the process of lifting a pointer off of the + * device. This value was tuned experimentally. + */ + protected static final float PRESSURE_THRESHOLD = 0.67f; + + + public BaseGestureDetector(Context context) { + mContext = context; + } + + /** + * All gesture detectors need to be called through this method to be able to + * detect gestures. This method delegates work to handler methods + * (handleStartProgressEvent, handleInProgressEvent) implemented in + * extending classes. + * + * @param event + * @return + */ + public boolean onTouchEvent(MotionEvent event){ + final int actionCode = event.getAction() & MotionEvent.ACTION_MASK; + if (!mGestureInProgress) { + handleStartProgressEvent(actionCode, event); + } else { + handleInProgressEvent(actionCode, event); + } + return true; + } + + /** + * Called when the current event occurred when NO gesture is in progress + * yet. The handling in this implementation may set the gesture in progress + * (via mGestureInProgress) or out of progress + * @param actionCode + * @param event + */ + protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event); + + /** + * Called when the current event occurred when a gesture IS in progress. The + * handling in this implementation may set the gesture out of progress (via + * mGestureInProgress). + * @param actionCode + * @param event + */ + protected abstract void handleInProgressEvent(int actionCode, MotionEvent event); + + + protected void updateStateByEvent(MotionEvent curr){ + final MotionEvent prev = mPrevEvent; + + // Reset mCurrEvent + if (mCurrEvent != null) { + mCurrEvent.recycle(); + mCurrEvent = null; + } + mCurrEvent = MotionEvent.obtain(curr); + + + // Delta time + mTimeDelta = curr.getEventTime() - prev.getEventTime(); + + // Pressure + mCurrPressure = curr.getPressure(curr.getActionIndex()); + mPrevPressure = prev.getPressure(prev.getActionIndex()); + } + + protected void resetState() { + if (mPrevEvent != null) { + mPrevEvent.recycle(); + mPrevEvent = null; + } + if (mCurrEvent != null) { + mCurrEvent.recycle(); + mCurrEvent = null; + } + mGestureInProgress = false; + } + + + /** + * Returns {@code true} if a gesture is currently in progress. + * @return {@code true} if a gesture is currently in progress, {@code false} otherwise. + */ + public boolean isInProgress() { + return mGestureInProgress; + } + + /** + * Return the time difference in milliseconds between the previous accepted + * GestureDetector event and the current GestureDetector event. + * + * @return Time difference since the last move event in milliseconds. + */ + public long getTimeDelta() { + return mTimeDelta; + } + + /** + * Return the event time of the current GestureDetector event being + * processed. + * + * @return Current GestureDetector event time in milliseconds. + */ + public long getEventTime() { + return mCurrEvent.getEventTime(); + } + +} diff --git a/cgeDemo/src/main/java/org/wysaid/cgeDemo/demoUtils/MoveGestureDetector.java b/cgeDemo/src/main/java/org/wysaid/cgeDemo/demoUtils/MoveGestureDetector.java new file mode 100644 index 00000000..0ce02d10 --- /dev/null +++ b/cgeDemo/src/main/java/org/wysaid/cgeDemo/demoUtils/MoveGestureDetector.java @@ -0,0 +1,178 @@ +package org.wysaid.cgeDemo.demoUtils; + +import android.content.Context; +import android.graphics.PointF; +import android.view.MotionEvent; + +/** + * @author Almer Thie (code.almeros.com) + * Copyright (c) 2013, Almer Thie (code.almeros.com) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +public class MoveGestureDetector extends BaseGestureDetector { + + /** + * Listener which must be implemented which is used by MoveGestureDetector + * to perform callbacks to any implementing class which is registered to a + * MoveGestureDetector via the constructor. + * + * @see SimpleOnMoveGestureListener + */ + public interface OnMoveGestureListener { + public boolean onMove(MoveGestureDetector detector); + public boolean onMoveBegin(MoveGestureDetector detector); + public void onMoveEnd(MoveGestureDetector detector); + } + + /** + * Helper class which may be extended and where the methods may be + * implemented. This way it is not necessary to implement all methods + * of OnMoveGestureListener. + */ + public static class SimpleOnMoveGestureListener implements OnMoveGestureListener { + public boolean onMove(MoveGestureDetector detector) { + return false; + } + + public boolean onMoveBegin(MoveGestureDetector detector) { + return true; + } + + public void onMoveEnd(MoveGestureDetector detector) { + // Do nothing, overridden implementation may be used + } + } + + private static final PointF FOCUS_DELTA_ZERO = new PointF(); + + private final OnMoveGestureListener mListener; + + private PointF mCurrFocusInternal; + private PointF mPrevFocusInternal; + private PointF mFocusExternal = new PointF(); + private PointF mFocusDeltaExternal = new PointF(); + + + public MoveGestureDetector(Context context, OnMoveGestureListener listener) { + super(context); + mListener = listener; + } + + @Override + protected void handleStartProgressEvent(int actionCode, MotionEvent event){ + switch (actionCode) { + case MotionEvent.ACTION_DOWN: + resetState(); // In case we missed an UP/CANCEL event + + mPrevEvent = MotionEvent.obtain(event); + mTimeDelta = 0; + + updateStateByEvent(event); + break; + + case MotionEvent.ACTION_MOVE: + mGestureInProgress = mListener.onMoveBegin(this); + break; + } + } + + @Override + protected void handleInProgressEvent(int actionCode, MotionEvent event){ + switch (actionCode) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mListener.onMoveEnd(this); + resetState(); + break; + + case MotionEvent.ACTION_MOVE: + // If the gesture started before this detector was attached (somehow), + // mPrevEvent will be null at this point and BaseGestureDetector's + // updateStateByEvent() will crash. The following check will prevent this. + if (mPrevEvent == null) { + return; + } + updateStateByEvent(event); + + // Only accept the event if our relative pressure is within + // a certain limit. This can help filter shaky data as a + // finger is lifted. + if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { + final boolean updatePrevious = mListener.onMove(this); + if (updatePrevious) { + mPrevEvent.recycle(); + mPrevEvent = MotionEvent.obtain(event); + } + } + break; + } + } + + protected void updateStateByEvent(MotionEvent curr) { + super.updateStateByEvent(curr); + + final MotionEvent prev = mPrevEvent; + + // Focus intenal + mCurrFocusInternal = determineFocalPoint(curr); + mPrevFocusInternal = determineFocalPoint(prev); + + // Focus external + // - Prevent skipping of focus delta when a finger is added or removed + boolean mSkipNextMoveEvent = prev.getPointerCount() != curr.getPointerCount(); + mFocusDeltaExternal = mSkipNextMoveEvent ? FOCUS_DELTA_ZERO : new PointF(mCurrFocusInternal.x - mPrevFocusInternal.x, mCurrFocusInternal.y - mPrevFocusInternal.y); + + // - Don't directly use mFocusInternal (or skipping will occur). Add + // unskipped delta values to mFocusExternal instead. + mFocusExternal.x += mFocusDeltaExternal.x; + mFocusExternal.y += mFocusDeltaExternal.y; + } + + /** + * Determine (multi)finger focal point (a.k.a. center point between all + * fingers) + * + * @return PointF focal point + */ + private PointF determineFocalPoint(MotionEvent e){ + // Number of fingers on screen + final int pCount = e.getPointerCount(); + float x = 0f; + float y = 0f; + + for(int i = 0; i < pCount; i++){ + x += e.getX(i); + y += e.getY(i); + } + + return new PointF(x/pCount, y/pCount); + } + + public float getFocusX() { + return mFocusExternal.x; + } + + public float getFocusY() { + return mFocusExternal.y; + } + + public PointF getFocusDelta() { + return mFocusDeltaExternal; + } + +} diff --git a/cgeDemo/src/main/java/org/wysaid/cgeDemo/view/MatrixMutualImageView.kt b/cgeDemo/src/main/java/org/wysaid/cgeDemo/view/MatrixMutualImageView.kt new file mode 100644 index 00000000..f84a0b35 --- /dev/null +++ b/cgeDemo/src/main/java/org/wysaid/cgeDemo/view/MatrixMutualImageView.kt @@ -0,0 +1,202 @@ +package org.wysaid.cgeDemo.view + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.SurfaceTexture +import android.opengl.GLES20 +import android.util.Log +import org.wysaid.cgeDemo.GLTextureView +import org.wysaid.nativePort.CGEImageHandler +import org.wysaid.texUtils.TextureRenderer +import org.wysaid.view.ImageGLSurfaceView +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.opengles.GL10 + +private const val TAG = "MatrixMutualImageView" + +/** + * @author PengHaiChen + * @date 2024/5/31 21:53:55 + * @description 可以使用矩阵交互,控制缩放和位移的demo + */ +class MatrixMutualImageView(context: Context) : GLTextureView(context), GLTextureView.Renderer, SurfaceTexture.OnFrameAvailableListener { + + private var mImageHandler: CGEImageHandler? = null + private var mRenderViewport: TextureRenderer.Viewport = TextureRenderer.Viewport() + private var mDisplayMode: ImageGLSurfaceView.DisplayMode? = null + private var mSettingIntensityLock: Any? = null + private var mSettingIntensityCount: Int = 0 + private var mFilterIntensity: Float = 1.0f + + init { + this.mDisplayMode = ImageGLSurfaceView.DisplayMode.DISPLAY_SCALE_TO_FILL + this.mSettingIntensityLock = Any() + this.mSettingIntensityCount = 1 + this.setEGLContextClientVersion(2) + this.setEGLConfigChooser(8, 8, 8, 8, 8, 0) + this.setRenderer(this) + this.renderMode = 0 + Log.i("libCGE_java", "ImageGLSurfaceView Construct...") + } + + private var mImageWidth: Int = 0 + private var mImageHeight: Int = 0 + private var originImage: Bitmap? = null + private var scaleImageMatrix: Matrix = Matrix() + + + var image: Bitmap? = null + set(value) { + field = value + if (value == null) { + return + } + originImage = value + scaleImageMatrix.setScale(value.width / width.toFloat(), value.height / height.toFloat()) + setImageBitmap(value) + applyFilter(cacheFilterData) + } + var syncMatrix: Matrix = Matrix() + set(value) { + field = value + val finalMatrix = Matrix(value) + finalMatrix.preConcat(scaleImageMatrix) + setTransform(finalMatrix) + invalidate() + } + + var isParallel: Boolean = false + set(value) { + Log.i(TAG, "isParallel: 对比模式${value}") + field = value + if (value) { + applyFilter(null) + } else { + applyFilter(cacheFilterData) + } + } + + + private var cacheFilterData: String? = null + + var filterAdjust: Float = 0.6F + set(value) { + field = value + setFilterIntensity(value.coerceIn(0F, 1F)) + } + + + fun applyFilter(filter: String?) { + if (filter != null) { + cacheFilterData = filter + val glsl = "@adjust lut $filter" + Log.i(TAG, "applyFilter: 滤镜着色器为${glsl}") + setFilterWithConfig(glsl) + setFilterIntensity(filterAdjust.coerceIn(0F, 1F)) + } else { + Log.i(TAG, "applyFilter: 空滤镜着色器") + setFilterWithConfig("") + } + } + + @Synchronized + fun setFilterWithConfig(config: String?) { + queueEvent { + if (mImageHandler != null) { + mImageHandler?.setFilterWithConfig(config) + requestRender() + } else { + Log.e(TAG, "setFilterWithConfig after release!!") + } + } + } + + private fun setFilterIntensity(intensity: Float) { + if (this.mImageHandler != null) { + this.mFilterIntensity = intensity + synchronized(mSettingIntensityLock!!) { + if (this.mSettingIntensityCount <= 0) { + Log.i("libCGE_java", "Too fast, skipping...") + return + } + --this.mSettingIntensityCount + } + + this.queueEvent { + if (this.mImageHandler == null) { + Log.e("libCGE_java", "set intensity after release!!") + } else { + this.mImageHandler?.setFilterIntensity(this.mFilterIntensity, true) + this.requestRender() + } + synchronized(this.mSettingIntensityLock!!) { + ++this.mSettingIntensityCount + } + } + } + } + + private fun setImageBitmap(bmp: Bitmap?) { + if (bmp != null) { + if (this.mImageHandler == null) { + Log.e("libCGE_java", "Handler not initialized!") + } else { + this.mImageWidth = bmp.width + this.mImageHeight = bmp.height + queueEvent { + if (this.mImageHandler == null) { + Log.e("libCGE_java", "set image after release!!") + } else { + if (this.mImageHandler?.initWithBitmap(bmp) == true) { + this.calcViewport() + this.requestRender() + } else { + Log.e("libCGE_java", "setImageBitmap: init handler failed!") + } + } + } + } + } + } + + private fun calcViewport() { + this.mRenderViewport.x = 0 + this.mRenderViewport.y = 0 + this.mRenderViewport.width = width + this.mRenderViewport.height = height + } + + override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { + + Log.i("libCGE_java", "ImageGLSurfaceView onSurfaceCreated...") + GLES20.glDisable(2929) + GLES20.glDisable(2960) + this.mImageHandler = CGEImageHandler() + this.mImageHandler?.setDrawerFlipScale(1.0f, -1.0f) + image = image + } + + override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) { + Log.i(TAG, "onSurfaceChanged: $width $height") + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f) + image?.let { + scaleImageMatrix.setScale(it.width / width.toFloat(), it.height / height.toFloat()) + } + this.calcViewport() + } + + + override fun onDrawFrame(gl: GL10?) { + GLES20.glBindFramebuffer(36160, 0) + GLES20.glClear(16384) + if (this.mImageHandler != null) { + GLES20.glViewport(mRenderViewport.x, mRenderViewport.y, mRenderViewport.width, mRenderViewport.height) + mImageHandler!!.drawResult() + } + } + + override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) { + } + +} \ No newline at end of file diff --git a/cgeDemo/src/main/java/org/wysaid/cgeDemo/view/MatrixMutualViewGroup.kt b/cgeDemo/src/main/java/org/wysaid/cgeDemo/view/MatrixMutualViewGroup.kt new file mode 100644 index 00000000..fbfe05d8 --- /dev/null +++ b/cgeDemo/src/main/java/org/wysaid/cgeDemo/view/MatrixMutualViewGroup.kt @@ -0,0 +1,221 @@ +package org.wysaid.cgeDemo.view + +import android.animation.TypeEvaluator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.Rect +import android.graphics.RectF +import android.os.Build +import android.util.AttributeSet +import android.util.Log +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.widget.FrameLayout +import androidx.core.graphics.toRectF +import org.wysaid.cgeDemo.demoUtils.MoveGestureDetector + +private const val TAG = "MatrixMutualViewGroup" + +/** + * @author PengHaiChen + * @date 2024/9/30 21:10:58 + * @description + */ +class MatrixMutualViewGroup : FrameLayout { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) //判断图片当前是否合法 + val bitmap = if (currBitmap == null) return else currBitmap!! + val srcMatrix = Matrix(syncTotalMatrix) + val dstMatrix = Matrix(syncTotalMatrix) + getFixMatrix(bitmap, srcMatrix, dstMatrix) + syncTotalMatrix = dstMatrix + syncMatrix2Child() + } + + + private var matrixMutualImageView = MatrixMutualImageView(this.context) + + init { + addView(matrixMutualImageView) + } + + private var syncTotalMatrix = Matrix() + + + override fun onTouchEvent(event: MotionEvent?): Boolean { + if (event == null) return true + moveGestureDetector.onTouchEvent(event) + scaleGestureDetector.onTouchEvent(event) + when (event.action and MotionEvent.ACTION_MASK) { + + MotionEvent.ACTION_UP -> { + animFixSyncMatrix() + } + + MotionEvent.ACTION_CANCEL -> { + } + + else -> {} + } + return true + } + + private var moveGestureDetector = MoveGestureDetector(context, object : MoveGestureDetector.SimpleOnMoveGestureListener() { + override fun onMove(detector: MoveGestureDetector?): Boolean { + detector?.let { + val d = detector.focusDelta + syncTotalMatrix.postTranslate(d.x, d.y) + syncMatrix2Child() + } + return true + } + + }) + + private var scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScale(detector: ScaleGestureDetector): Boolean { + Log.i(TAG, "onScale: ${detector.scaleFactor}") + syncTotalMatrix.postScale(detector.scaleFactor, detector.scaleFactor, detector.focusX, detector.focusY) + syncMatrix2Child() + return true + } + + }).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + isQuickScaleEnabled = false + } + } + + + /** + * 动画修复矩阵位置 + */ + private fun animFixSyncMatrix() { + + val bitmap = if (currBitmap == null) return else currBitmap!! + val srcMatrix = Matrix(syncTotalMatrix) + val dstMatrix = Matrix(syncTotalMatrix) + + getFixMatrix(bitmap, srcMatrix, dstMatrix) + + val animator = ValueAnimator.ofObject(MatrixEvaluator(), srcMatrix, dstMatrix) + animator.duration = 300 + animator.addUpdateListener { + val mat = it.animatedValue as Matrix + syncTotalMatrix = mat + syncMatrix2Child() + } + animator.start() + } + + private fun getFixMatrix(bitmap: Bitmap, srcMatrix: Matrix, dstMatrix: Matrix) { + val maxScale = 8F + val minScale = 0.9F + + val imageOriginRect = Rect(0, 0, bitmap.width, bitmap.height).toRectF() + val imageFixDstRect = Rect(0, 0, bitmap.width, bitmap.height).toRectF() + val scale2target = Rect(0, 0, width, height).scale2dst(bitmap.width, bitmap.height) + val xOffset = (width - (bitmap.width * scale2target)) / 2F + val yOffset = (height - (bitmap.height * scale2target)) / 2F + imageFixDstRect.set(xOffset, yOffset, xOffset + (bitmap.width * scale2target), yOffset + (bitmap.height * scale2target)) + + val scaleFitImageRect = RectF() + srcMatrix.mapRect(scaleFitImageRect, imageOriginRect) + + //判断放大越界 + if (scaleFitImageRect.width() > imageFixDstRect.width() * maxScale) { + val scale = (imageFixDstRect.width() * maxScale) / scaleFitImageRect.width() + Log.i(TAG, "animFixSyncMatrix: 过大${scale}") + dstMatrix.postScale(scale, scale, width / 2F, height / 2F) + } //判断缩小越界 + if (scaleFitImageRect.width() < imageFixDstRect.width() * minScale) { + val scale = (imageFixDstRect.width() * minScale) / scaleFitImageRect.width() + Log.i(TAG, "animFixSyncMatrix: 过小${scale}") + dstMatrix.postScale(scale, scale, width / 2F, height / 2F) + } + + val transFitImageRect = RectF() + dstMatrix.mapRect(transFitImageRect, imageOriginRect) + val limitTransW = imageFixDstRect.width() * minScale + val limitTransH = imageFixDstRect.height() * minScale + val limitTransXOffset = (width - limitTransW) / 2F + val limitTransYOffset = (height - limitTransH) / 2F + val imageFixMinDstRect = RectF(limitTransXOffset, limitTransYOffset, limitTransXOffset + limitTransW, limitTransYOffset + limitTransH) + val dx = when { + transFitImageRect.left > imageFixMinDstRect.left -> imageFixMinDstRect.left - transFitImageRect.left + transFitImageRect.right < imageFixMinDstRect.right -> imageFixMinDstRect.right - transFitImageRect.right + else -> 0F + } + + val dy = when { + transFitImageRect.top > imageFixMinDstRect.top -> imageFixMinDstRect.top - transFitImageRect.top + transFitImageRect.bottom < imageFixMinDstRect.bottom -> imageFixMinDstRect.bottom - transFitImageRect.bottom + else -> 0F + } + + if (dx != 0F || dy != 0F) { + dstMatrix.postTranslate(dx, dy) + Log.i(TAG, "animFixSyncMatrix: dx = $dx, dy = $dy") + } + } + + private var currBitmap: Bitmap? = null + fun initBitmap(bitmap: Bitmap) { + val isEmptyMatrix = syncTotalMatrix.isIdentity + if (isEmptyMatrix) syncTotalMatrix.reset() + setBitmap(bitmap) + if (isEmptyMatrix) { + val scale2target = Rect(0, 0, width, height).scale2dst(bitmap.width, bitmap.height) + syncTotalMatrix.postScale(scale2target, scale2target) + val xOffset = (width - (bitmap.width * scale2target)) / 2F + val yOffset = (height - (bitmap.height * scale2target)) / 2F + syncTotalMatrix.postTranslate(xOffset, yOffset) + syncMatrix2Child() + } + } + + private fun setBitmap(bitmap: Bitmap) { + currBitmap = bitmap + matrixMutualImageView.image = bitmap + } + + + fun setFilter(filterData: String?) = matrixMutualImageView.applyFilter(filterData) + fun setIntensity(intensity: Float) { + matrixMutualImageView.filterAdjust = intensity + } + + private fun syncMatrix2Child() { + matrixMutualImageView.syncMatrix = syncTotalMatrix + } + + private fun Rect.scale2dst(width: Int, height: Int): Float { + val widthScale = this.width() / width.toFloat() + val heightScale = this.height() / height.toFloat() + return minOf(widthScale, heightScale) + } + + class MatrixEvaluator : TypeEvaluator { + private val tempStartValues = FloatArray(9) + private val tempEndValues = FloatArray(9) + private val tempMatrix: Matrix = Matrix() + override fun evaluate(fraction: Float, startValue: Matrix, endValue: Matrix): Matrix { + startValue.getValues(tempStartValues) + endValue.getValues(tempEndValues) + for (i in 0..8) { + val diff = tempEndValues[i] - tempStartValues[i] + tempEndValues[i] = tempStartValues[i] + fraction * diff + } + tempMatrix.setValues(tempEndValues) + return tempMatrix + } + } + +} \ No newline at end of file diff --git a/cgeDemo/src/main/res/layout/activity_image_demo_with_matrix.xml b/cgeDemo/src/main/res/layout/activity_image_demo_with_matrix.xml new file mode 100644 index 00000000..cc15be9f --- /dev/null +++ b/cgeDemo/src/main/res/layout/activity_image_demo_with_matrix.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + \ No newline at end of file