Developer技术博客 DevEngineer:林建有

Android-自定义水印+OpenGL画水印图片


自定义编辑水印篇

自定义编辑水印涉及到控件的自定义,控件位置的移动,图片大小和文字大小缩放,文字的颜色改变,还有水印的透明度。

水印元素包括了

  • 图片水印

  • 文字水印

涉及到的控件

- 模拟屏幕的一个容器控件,用于放置水印元素的
- 图片水印元素控件
- 文字水印元素控件
- 文字选择颜色值控件

水印需要保存的信息:数据库信息

  • 水印透明度alpha

  • 水印宽度widthRatio比例(涉及到横竖屏)

  • 水印高度heightRatio比例(涉及到横竖屏)

  • 水印位置centerX比例(涉及到横竖屏)

  • 水印位置centerY比例(涉及到横竖屏)

  • 文字水印颜色color

  • 文字水印文案信息

  • 文字缩放大小值textSize

  • 图片水印文件路径

  • 水印元素ID(数据库ID自动生成)

控件手势移动

/**
 * @author jaysen.lin@foxmail.com
 * @since 2019/8/8
 */
public class MoveGestureDetector {
    private float x = 0, y = 0;

    public MoveGestureDetector() {
    }

    public boolean detectMoveAction(View view, MotionEvent event) {
        switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                x = event.getRawX();
                y = event.getRawY();
                return true;
             case MotionEvent.ACTION_MOVE:
                float x1 = event.getRawX();
                float y1 = event.getRawY();

                int dx = (int) (x1 - x);
                int dy = (int) (y1 - y);

                int l = view.getLeft() + dx;
                int t = view.getTop() + dy;
                int r = view.getRight() + dx;
                int b = view.getBottom() + dy;
                if (l < 0) {//超出父类控件左边际,重置为0
                    l = 0;
                    r = l + view.getWidth();
                }
                if (t < 0) {//超出父类控件上边际,重置为0
                    t = 0;
                    b = t + view.getHeight();
                }
                ViewParent parent = view.getParent();
                ViewGroup p = (ViewGroup) parent;
                if (r > p.getWidth()) {//超出父类控件右边际,重置
                    r = p.getWidth();
                    l = r - view.getWidth();
                }
                if (b > p.getHeight()) {//超出父类控件下边际,重置
                    b = p.getHeight();
                    t = b - view.getHeight();
                }
                float halfW = view.getMeasuredWidth() / 2.0f;
                float halfH = view.getMeasuredHeight() / 2.0f;
                float centerXRatio = (l + halfW) / p.getMeasuredWidth();
                float centerYRatio = (t + halfH) / p.getMeasuredHeight();
                //todo 把centerXRatio,centerYRatio的横竖屏信息保存起来
                //还有就是刷新控件重新计算大小和布局
                view.requestLayout();
                
                x = x1;
                y = y1;
                return true;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                return true;
        }
        return false;
    }

控件位置计算

/**
 * @author jaysen.lin@foxmail.com
 * @since 2019/8/8
 */
public class PositionCalculator {
		//用于代理计算child比例大小
        public Size calculateChildSize(int pWidth, int pHeight) {
            
        }
		public void updateCenterXY() {
            //当缩放超出左边界或上边际时调用该方法更新centerX和centerY,需要考虑横竖屏
            //调用getLeftOrTop(...)获取原先的left或者top,来更新左边界上边界再重新计算centerX和
           //centerY占父容器的比例。
		}
        public int getLeft() {
            //在child layout()方法里调用该方法获取计算得到的left,需要考虑横竖屏
        }
        public int getTop() {
            //在child layout()方法里调用该方法获取计算得到的top,需要考虑横竖屏
        }
    
       public int getLeftOrTop(float centerRatio, int viewSize, int parentSize) {
        float half = viewSize / 2.0f;
        float pointSide = centerRatio * parentSize - half;
        if (pointSide < 0) {
            return 0;
        }
        return (int) pointSide;
    }
}

控件大小计算

View.onMeasure()调用周期

onMeasure调用

通过View.requestLayout()来刷新计算控件大小,控件重写了View.onMeasure()计算控件的大小,重写View.layout()来布局当前控件的位置和宽高。

TextView的水印自定义控件使用到了android.text.StaticLayout.java来自动计算字体的大小宽高

控件放大缩小算法和进度条计算算法

  • 图片水印控件放大缩小算法

     private void iniView(){
         //通过decodeImageBounds获取得到imageSize
          this.imageMaxSide = Math.max(imageSize.getWidth(), this.imageSize.getHeight());
            if (this.imageMaxSide == 0) {
                this.imageMaxSide = this.screenMinSide;
            }
          float scaledFactorInitProgress = 1.0f;
            if (imageItemInfo.widthRatio == 0.0f) {
                Size size = new Size(this.imageSize.getWidth(), this.imageSize.getHeight());
                if (this.imageMaxSide > this.screenMinSide / 3) {
                    scaledFactorInitProgress = 1.0f * this.screenMinSide / 3.0f / this.imageMaxSide;
                    size.setWidth((int) (size.getWidth() * scaledFactorInitProgress));
                    size.setHeight((int) (size.getHeight() * scaledFactorInitProgress));
                }
                imageItemInfo.widthRatio = getWidthRatio(size.getWidth());
                imageItemInfo.heightRatio = getHeightRatio(size.getHeight());
            } else {
                scaledFactorInitProgress = imageItemInfo.widthRatio * this.screenWidth / this.imageSize.getWidth();
            }
                 initProgressBar(scaledFactorInitProgress);
    }
      
    private float getWidthRatio(int scaledWidth) {
        return 1.0f * scaledWidth / this.screenWidth;
    }
      
    //
    private float getHeightRatio(int scaledHeight) {
        return 1.0f * scaledHeight / this.screenHeight;
    }
      
      
    private void initProgressBar(float initProgressRatio) {
            float progress = (int) (initProgressRatio * 10_0000.0f);
            float scaledFactor1_8 = 1.0f;//8分之1
            float scaledFactor1_1 = this.screenMinSide * 1.0f / this.imageMaxSide;
            if (this.imageMaxSide > this.screenMinSide / 8) {//8分之1
                scaledFactor1_8 = 1.0f * this.screenMinSide / 8.0f / this.imageMaxSide;
            } 
         	int progress1_8 = (int) (scaledFactor1_8 * 10_0000.0f);
            int progress1_1 = (int) (scaledFactor1_1 * 10_0000.0f);
            if (progress1_1 < progress1_8) {
                progress1_1 = progress1_8;
            }
            this.maxProgress = progress1_1 - progress1_8;
            this.startProgress = progress1_8;
            this.mSizeSeekBar.setMax(this.maxProgress);
         	this.mSizeSeekBar.setProgress((int) (progress - this.startProgress));
        }
    
  • 文字水印控件放大缩小算法

    // mSizeSeekBar.setMax(500);//最大值500
      
    private int getProgressFromTextSize(float textSize) {//textSize in sp
            return (int) ((textSize - 13.0f) * 1.0f / 67.0f * 500.0f);
    }
      
    private int getTextSizeFromProgress(int progress) {
            return (int) (13.0f + (1.0f * progress / 500.0f * 67.0f));
    }
    
  • 文字水印颜色的选择和进度条的转换算法

      public int getColorFromProgress(int progress) {
            int r = 0;
          int g = 0;
            int b = 0;
      
            if (progress < 256) {
                b = progress;
            } else if (progress < 256 * 2) {
                g = progress % 256;
                b = 256 - progress % 256;
            } else if (progress < 256 * 3) { g = 255;
                b = progress % 256;
            } else if (progress < 256 * 4) {
                r = progress % 256;
                g = 256 - progress % 256;
                b = 256 - progress % 256;
            } else if (progress < 256 * 5) {
                r = 255;
                g = 0;
                b = progress % 256;
            } else if (progress < 256 * 6) { r = 255;
                g = progress % 256;
                b = 256 - progress % 256;
            } else if (progress < 256 * 7) {
                r = 255;
                g = 255;
                b = progress % 256;
            }
            return Color.argb(255, r, g, b);
        }
      
      
     /**
         * @param color the color must be generated from  {@link ColorPickerSeekBar#getColorFromProgress(int)}
         */
        public void setProgressFromColor(int color) {
            int r = (color >> 16) & 0xff;
            int g = (color >> 8) & 0xff;
            int b = (color) & 0xff;
            int value = (255 << 24) | (r << 16) | (g << 8) | b;
            int progress[] = {
                    b
                    , 256 + g
                    , 256 * 2 + b
                    , 256 * 3 + r
                    , 256 * 4 + b , 256 * 5 + g
                    , 256 * 6 + b
            };
      
            for (int theProgress : progress) {
                int colorFromProgress = getColorFromProgress(theProgress);
                MyLog.e("colorFromProgress:" + colorFromProgress + " value:" + value+" theProgress:"+theProgress);
                if (colorFromProgress == value) {
                    setProgress(theProgress);
                    MyLog.e("hit theProgress:" +theProgress);
      
                    return;
                }
            }setProgress(MAX);//default white color
            MyLog.e("setProgress default:" +MAX);
      
        }
    

OpenGL 画图篇

OpenGL API V2

定义定点脚本和片段着色脚本

 protected     String   vertexShaderCode   =
            "uniform mat4 uMVPMatrix;" +
                    "attribute vec4 a_position; " +
                    "attribute vec2 a_texCoord; " +
                    "varying vec2 v_texCoord; " +
                    "void main() " +
                    "{ " +
                    "gl_Position = uMVPMatrix * a_position; " +
                    "v_texCoord = a_texCoord; " +
                    "} ";

protected     String   fragmentShaderCode =
            "precision lowp float;  " +
                    "varying vec2 v_texCoord; " +
                    "uniform sampler2D u_samplerTexture; " +
                    "uniform float opacity;" +
                    "void main() " +
                    "{ " +
                    "vec4 color = texture2D(u_samplerTexture, v_texCoord).rgba;" +
                    "gl_FragColor = vec4(color.xyz, color.a * opacity); " +
                    "} ";

创建OpenGL脚本画图程序编译,然后链接程序

 public static int loadShader(int shaderType, String source) {
        int shader = GLES20.glCreateShader(shaderType);
        GLES20.glShaderSource(shader, source);
        GLES20.glCompileShader(shader);
     	int[] compiled = new int[1];
        GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
        if (compiled[0] == 0) {
            MyLog.e("Could not compile shader(TYPE=" + shaderType + "):");
            MyLog.e(GLES20.glGetShaderInfoLog(shader));
            GLES20.glDeleteShader(shader);
            shader = 0;
        }  
     return shader;
 }

 public static int createProgram(String vertexShaderCode, String fragmentShaderCode) {
       int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
       int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

        // create empty OpenGL ES Program
        int program = GLES20.glCreateProgram();
        // add the vertex shader to program
        GLES20.glAttachShader(program, vertexShader);
     // add the fragment shader to program
        GLES20.glAttachShader(program, fragmentShader);
        // creates OpenGL ES program executables
        GLES20.glLinkProgram(program);
        return program;
    }

创建水印纹理

  public static int createTexture() {
        int[] textureIds = new int[1];
        GLES20.glGenTextures(1, textureIds, 0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[0]);

        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
        return textureIds[0];
    }

计算水印坐标转换成openGL的坐标矩阵

 protected     CoordinateUtils.CoordinatesF coordinatesF       = new CoordinateUtils.CoordinatesF(
            -1.0f, 1.0f,
            -1.0f, 1.0f);
/**
     * @param ratio
     * @return 自定义水印矩阵
     */
    private float[] calculateCustomWatermarkMatrix(float ratio) {
        this.coordinatesF.leftRatio = -ratio;
        this.coordinatesF.rightRatio = ratio;

        float[] scratch = new float[16];//最终的矩阵
        Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);//视锥体  视景体的6个裁剪平面(左、右、底、顶、近和远)
        Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 3, 0f, 0f, 0f, 0f, 1f, 0.0f);//eyeX eyeY eyeZ 0f, 0f, -3,    centerX centerY centerZ 0 0 0     upX upY upZ 0 1 0
        MyLog.e(Arrays.toString(mProjectionMatrix));//4 * 4 的矩阵
        MyLog.e(Arrays.toString(mViewMatrix));//4 * 4 的矩阵
        Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
        Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
        Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
        return scratch;
    }

引用创建的OpenGL水印渲染脚本程序开始画图

    // Use to access and set the view transformation 顶点着色器中的mMVPMatrix
    private       int                          mMVPMatrixHandle;
    private       int                          mPositionHandle;
    private       int                          mTexCoordHandle;
    private       int                          mTexSamplerHandle;
    private       int                          mOpacityHandle; 

    /**
     *
     * @param ratio         ratio = w * 1.0f / (h * 1.0f);
     * @param isPortrait
     */
public void draw(float ratio, boolean isPortrait) {
        this.isPortrait = isPortrait;
        GLES20.glUseProgram(mProgram);

        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "a_position");
        mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texCoord");
      	mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
        mTexSamplerHandle = GLES20.glGetUniformLocation(mProgram, "u_samplerTexture");
        mOpacityHandle = GLES20.glGetUniformLocation(mProgram, "opacity");

        float[] mvpMatrix = calculateCustomWatermarkMatrix(ratio);
        bindTexture2D(mvpMatrix, getBitmap(), getItemInfo().alpha);

    }
  private void bindTexture2D(float[] matrix, Bitmap bitmap, float mask_opacity) {
        FloatBuffer positionBuffer = getPositionBuffer(getItemInfo());//获取转换的坐标矩阵缓冲变量

        final int vertexCount = positionFloatArrCoordinate.length / COORDS_PER_VERTEX;
        final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
      GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
        GLES20.glUniform1f(mOpacityHandle, mask_opacity);


        GLES20.glEnableVertexAttribArray(mPositionHandle);
        GLES20.glEnableVertexAttribArray(mTexCoordHandle);

        GLES20.glUniform1i(mTexSamplerHandle, 0);
        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, matrix, 0);
       positionBuffer.position(0);
        GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, positionBuffer);
        positionBuffer.position(3);
        this.textureBuffer.position(0);
        GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, this.textureBuffer);
        this.drawOderBuffer.position(0);
      GLES20.glEnable(GLES20.GL_BLEND);
        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, this.drawOderBuffer);
        GLES20.glDisable(GLES20.GL_BLEND);

        GLES20.glDisableVertexAttribArray(mPositionHandle);
        GLES20.glDisableVertexAttribArray(mTexCoordHandle);
    }

/**
     * 
     * @param itemEntity
     * @return FloatBuffer
     */
protected FloatBuffer getPositionBuffer(BaseWatermarkItemEntity itemEntity) {
        //  2019/8/16  width and centerX
        float widthRatio, heightRatio;
        if (isPortrait) {
            widthRatio = itemEntity.widthRatio;
            heightRatio = itemEntity.heightRatio;
        } else {
            int widthPixels = mContext.getResources().getDisplayMetrics().widthPixels;
            int heightPixels = mContext.getResources().getDisplayMetrics().heightPixels;
            int min = Math.min(widthPixels, heightPixels);
            int max = Math.max(widthPixels, heightPixels);
            float originWidth = itemEntity.widthRatio * min;
            float originHeight = itemEntity.heightRatio * max;
            widthRatio = originWidth / max;
            heightRatio = originHeight / min;
        }
     return transformCoordinate(
                new PointF(widthRatio, heightRatio)
                , new PointF(isPortrait ? itemEntity.vCenterX : itemEntity.hCenterX
                        , isPortrait ? itemEntity.vCenterY : itemEntity.hCenterY)
                , coordinatesF);
    }
  /**
     * this.positionFloatArrCoordinate
     *
     * @param widthHeightRatio
     * @param centerXY
     * @param coordinatesF
     * @return
     */
    protected FloatBuffer transformCoordinate(PointF widthHeightRatio, PointF centerXY, CoordinateUtils.CoordinatesF coordinatesF) {
        RectF rectF = getRectF(widthHeightRatio, centerXY, coordinatesF);//获取宽高在OpenGL坐标系中的坐标占比
        float left = rectF.left;
        float top = rectF.top;
        float right = rectF.right;
        float bottom = rectF.bottom;
           this.positionFloatArrCoordinate = new float[]{
                left, top, 0.0f,
                left, bottom, 0.0f,
                right, bottom, 0.0f,
                right, top, 0.0f};
        return createFloatBuffer(this.positionFloatArrCoordinate);
    }

/**
**创建缓冲变量
*/
  public static FloatBuffer createFloatBuffer(float[] fArr) {
        ByteBuffer allocateDirect = ByteBuffer.allocateDirect(fArr.length * 4);
        allocateDirect.order(ByteOrder.nativeOrder());
        FloatBuffer asFloatBuffer = allocateDirect.asFloatBuffer();
        asFloatBuffer.put(fArr);
        asFloatBuffer.position(0);
        return asFloatBuffer;
    }

Open GL Android 知识

OpenGL ES

此链接的文档详细介绍了Android 如何使用OpenGL版本和如何转换坐标映射到屏幕,还有投影和相机视图矩阵变换来使得在手机屏幕上不会失真变形。

image

Figure 1. Default OpenGL coordinate system (left) mapped to a typical Android device screen (right).

Displaying graphics with OpenGL ES

  • Build an OpenGL ES environment

    Learn how to set up an Android application to be able to draw OpenGL graphics.

  • Define shapes

    Learn how to define shapes and why you need to know about faces and winding.

  • Draw shapes

    Learn how to draw OpenGL shapes in your application.

  • Apply projection and camera views

    Learn how to use projection and camera views to get a new perspective on your drawn objects.

  • Add motion

    Learn how to do basic movement and animation of drawn objects with OpenGL.

  • Respond to touch events

    Learn how to do basic interaction with OpenGL graphics.

Additional sample code

To download NDK samples, see NDK Samples.


Similar Posts

Comments