+ * <com.mosect.lib.scanpanel.ScanPanel + * android:id="@+id/sp_main" + * android:layout_width="match_parent" + * android:layout_height="match_parent" + * app:scanAutostart="true" + * app:scanDecoder="你的解码器类路径" + * app:scanUseTextureView="false" /&rt; + *
+ * scanAutostart:是否自动开始,默认false;true,试图依附到父视图中就开始处理扫码;false,需要手动调用{@link #start()}和{@link #destroy()} + *
+ * scanDecoder:帧解码类路径,支持无参构造方法和带有一个{@link Context}参数的构造方法 + *
+ * scanUseTextureView:是否使用TextureView显示,默认false,使用SurfaceView;
+ */
+public class ScanPanel extends ViewGroup {
+
+ private Callback callback;
+ private boolean autostart = false; // 自动开始,即依附到父视图中就开始处理扫码
+ private boolean useTextureView = false; // 是否使用TextureView
+ private int displayRotation = 0; // 显示方向
+ private FrameDecoder frameDecoder = null; // 帧解码器
+
+ private ScanHandler scanHandler; // 扫码处理对象
+ private final Rect clipRect = new Rect(); // 裁剪区域
+
+ private final Rect clipRect2 = new Rect();
+
+ private View renderView;
+ private Surface surface;
+ private int surfaceWidth;
+ private int surfaceHeight;
+
+ public ScanPanel(Context context) {
+ super(context);
+ init(context, null);
+ }
+
+ public ScanPanel(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public ScanPanel(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ScanPanel);
+ autostart = ta.getBoolean(R.styleable.ScanPanel_scanAutostart, false);
+ useTextureView = ta.getBoolean(R.styleable.ScanPanel_scanUseTextureView, false);
+ String decoderText = ta.getString(R.styleable.ScanPanel_scanDecoder);
+ // 加载设定的帧解码器
+ if (!TextUtils.isEmpty(decoderText)) {
+ try {
+ Class> cls = Class.forName(decoderText);
+ try {
+ Constructor> def = cls.getConstructor();
+ frameDecoder = (FrameDecoder) def.newInstance();
+ } catch (NoSuchMethodException e) {
+ Constructor> c = cls.getConstructor(Context.class);
+ frameDecoder = (FrameDecoder) c.newInstance(context);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ ta.recycle();
+ if (useTextureView) {
+ initTextureView();
+ } else {
+ initSurfaceView();
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (autostart) {
+ start();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ if (autostart) {
+ destroy();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+ int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == GONE) continue;
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ setMeasuredDimension(width, height);
+
+ boolean callbackClip = false;
+ clipRect2.setEmpty();
+ if (null != callback) {
+ callbackClip = callback.onComputeClip(this, width, height, clipRect2);
+ }
+ if (!callbackClip) {
+ onComputeClip(width, height, clipRect2);
+ }
+ clipRect.set(clipRect2);
+ if (null != scanHandler) {
+ scanHandler.setClip(clipRect);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == GONE) continue;
+ child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
+ }
+ }
+
+ /**
+ * 开始处理扫码
+ */
+ public void start() {
+ if (null == scanHandler) {
+ scanHandler = new ScanHandler(getContext());
+ scanHandler.setSurface(surface, surfaceWidth, surfaceHeight);
+ scanHandler.setClip(clipRect);
+ scanHandler.setDisplayRotation(displayRotation);
+ scanHandler.setFrameDecoder(frameDecoder);
+ scanHandler.setCallback(new ScanHandler.Callback() {
+ @Override
+ public void onScanStart(ScanHandler handler) {
+ post(() -> {
+ if (null != callback) {
+ callback.onScanStart(ScanPanel.this);
+ }
+ });
+ }
+
+ @Override
+ public int onSwitchCamera(ScanHandler handler) {
+ Callback callback = ScanPanel.this.callback;
+ if (null != callback) {
+ int id = callback.onSwitchCamera(ScanPanel.this);
+ if (id >= 0) return id;
+ }
+ return ScanPanel.this.onSwitchCamera();
+ }
+
+ @Override
+ public void onDrawMask(ScanHandler handler, Canvas canvas, int width, int height, Rect clip) {
+ Callback callback = ScanPanel.this.callback;
+ if (null != callback) {
+ callback.onDrawMask(ScanPanel.this, canvas, width, height, clip);
+ }
+ }
+
+ @Override
+ public void onScanError(ScanHandler handler, Exception exp) {
+ post(() -> {
+ if (null != callback) {
+ callback.onScanError(ScanPanel.this, exp);
+ }
+ });
+ }
+
+ @Override
+ public void onScanResult(ScanHandler handler, String result) {
+ post(() -> {
+ if (null != callback) {
+ callback.onScanResult(ScanPanel.this, result);
+ }
+ });
+ }
+
+ @Override
+ public void onScanEnd(ScanHandler handler) {
+ post(() -> {
+ if (null != callback) {
+ callback.onScanEnd(ScanPanel.this);
+ }
+ });
+ }
+ });
+ scanHandler.start();
+ }
+ }
+
+ /**
+ * 继续解码下一帧
+ */
+ public void next() {
+ if (null != scanHandler) {
+ scanHandler.next();
+ }
+ }
+
+ /**
+ * 刷新遮罩层
+ */
+ public void invalidateMask() {
+ if (null != scanHandler) {
+ scanHandler.invalidateMask();
+ }
+ }
+
+ /**
+ * 销毁扫码处理对象
+ */
+ public void destroy() {
+ if (null != scanHandler) {
+ scanHandler.destroy();
+ scanHandler = null;
+ }
+ }
+
+ /**
+ * 设置显示方法,{@link ScanHandler#setDisplayRotation(int)}
+ *
+ * @param rotation 方向
+ */
+ public void setDisplayRotation(int rotation) {
+ if (this.displayRotation != rotation) {
+ this.displayRotation = rotation;
+ if (null != scanHandler) {
+ scanHandler.setDisplayRotation(rotation);
+ }
+ }
+ }
+
+ /**
+ * 设置帧解码器,{@link ScanHandler#setFrameDecoder(FrameDecoder)}
+ *
+ * @param decoder 解码器
+ */
+ public void setFrameDecoder(FrameDecoder decoder) {
+ frameDecoder = decoder;
+ if (null != scanHandler) {
+ scanHandler.setFrameDecoder(frameDecoder);
+ }
+ }
+
+ /**
+ * 计算裁剪区域,如果{@link Callback#onComputeClip(ScanPanel, int, int, Rect)}返回false,则触发此方法
+ *
+ * @param width 宽
+ * @param height 高
+ * @param out 输出的裁剪区域
+ */
+ protected void onComputeClip(int width, int height, Rect out) {
+ out.set(0, 0, width, height);
+ }
+
+ private void initSurfaceView() {
+ SurfaceView surfaceView = new SurfaceView(getContext());
+ renderView = surfaceView;
+ surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ if (renderView == surfaceView) {
+ setSurface(holder.getSurface(), width, height);
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ if (renderView == surfaceView) {
+ setSurface(null, 0, 0);
+ }
+ }
+ });
+ addView(surfaceView);
+ }
+
+ private void initTextureView() {
+ TextureView textureView = new TextureView(getContext());
+ renderView = textureView;
+ textureView.setLayerType(LAYER_TYPE_HARDWARE, null);
+ textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
+
+ private Surface surface;
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ if (renderView == textureView) {
+ this.surface = new Surface(surface);
+ setSurface(this.surface, width, height);
+ }
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ if (null != this.surface) {
+ this.surface.release();
+ this.surface = null;
+ }
+ if (renderView == textureView) {
+ this.surface = new Surface(surface);
+ setSurface(this.surface, width, height);
+ }
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ if (null != this.surface) {
+ this.surface.release();
+ this.surface = null;
+ }
+ if (renderView == textureView) {
+ setSurface(null, 0, 0);
+ }
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ }
+ });
+ addView(textureView);
+ }
+
+ /**
+ * 选择摄像头,{@link Callback#onSwitchCamera(ScanPanel)}
+ *
+ * @return 摄像头id
+ */
+ protected int onSwitchCamera() {
+ int count = Camera.getNumberOfCameras();
+ Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
+ for (int i = 0; i < count; i++) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * 设置是否自动开始,非自动模式大多用于处理摄像头权限;自动模式适用于已经确保获取摄像头权限的场景
+ *
+ * @param autostart true,自动开始;false,不自动开始,需要手动处理{@link #start()}和{@link #destroy()}
+ */
+ public void setAutostart(boolean autostart) {
+ if (isAttachedToWindow()) {
+ throw new IllegalStateException("Unsupported changed autostart on attached state");
+ }
+ this.autostart = autostart;
+ }
+
+ /**
+ * 判断是否自动开始
+ *
+ * @return true,自动开始;false,不自动开始
+ */
+ public boolean isAutostart() {
+ return autostart;
+ }
+
+ /**
+ * 设置回调
+ *
+ * @param callback 回调
+ */
+ public void setCallback(Callback callback) {
+ if (this.callback != callback) {
+ this.callback = callback;
+ requestLayout();
+ }
+ }
+
+ /**
+ * 判断是否使用TextureView
+ *
+ * @return true,使用TextureView;false,不使用TextureView,使用SurfaceView
+ */
+ public boolean isUseTextureView() {
+ return useTextureView;
+ }
+
+ /**
+ * 设置是否使用TextureView
+ *
+ * @param useTextureView true,使用TextureView;false,不使用TextureView,使用SurfaceView
+ */
+ public void setUseTextureView(boolean useTextureView) {
+ if (this.useTextureView != useTextureView) {
+ this.useTextureView = useTextureView;
+ if (null != renderView) {
+ removeView(renderView);
+ renderView = null;
+ }
+ setSurface(null, 0, 0);
+ if (useTextureView) {
+ initTextureView();
+ } else {
+ initSurfaceView();
+ }
+ }
+ }
+
+ private void setSurface(Surface surface, int surfaceWidth, int surfaceHeight) {
+ this.surface = surface;
+ this.surfaceWidth = surfaceWidth;
+ this.surfaceHeight = surfaceHeight;
+ if (null != scanHandler) {
+ scanHandler.setSurface(surface, surfaceWidth, surfaceHeight);
+ }
+ }
+
+ /**
+ * 回调
+ */
+ public interface Callback {
+
+ /**
+ * 扫码开始
+ *
+ * @param panel 扫码面板
+ */
+ void onScanStart(ScanPanel panel);
+
+ /**
+ * 绘制遮罩层
+ *
+ * @param panel 扫码面板
+ * @param canvas 画布
+ * @param width 宽
+ * @param height 高
+ * @param clip 裁剪区域,即扫码区域
+ */
+ void onDrawMask(ScanPanel panel, Canvas canvas, int width, int height, Rect clip);
+
+ /**
+ * 选择摄像头,返回负数表示使用内部{@link #onSwitchCamera()}方法获取
+ *
+ * @param panel 扫码面板
+ * @return 摄像头id
+ */
+ int onSwitchCamera(ScanPanel panel);
+
+ /**
+ * 扫码错误
+ *
+ * @param panel 扫码面板
+ * @param exp 异常
+ */
+ void onScanError(ScanPanel panel, Exception exp);
+
+ /**
+ * 扫码成功
+ *
+ * @param panel 扫码面板
+ * @param result 扫码后的文本
+ */
+ void onScanResult(ScanPanel panel, String result);
+
+ /**
+ * 计算裁剪区域,即扫码区域
+ *
+ * @param panel 扫码面板
+ * @param width 宽
+ * @param height 高
+ * @param out 输出的裁剪区域
+ * @return true,计算成功;false,使用内部{@link #onComputeClip(int, int, Rect)}方法提供的区域
+ */
+ boolean onComputeClip(ScanPanel panel, int width, int height, Rect out);
+
+ /**
+ * 扫码结束
+ *
+ * @param panel 扫码面板
+ */
+ void onScanEnd(ScanPanel panel);
+ }
+}
diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/coder/FrameDecoder.java b/lib/src/main/java/com/mosect/lib/scanpanel/coder/FrameDecoder.java
new file mode 100644
index 0000000..a79a72d
--- /dev/null
+++ b/lib/src/main/java/com/mosect/lib/scanpanel/coder/FrameDecoder.java
@@ -0,0 +1,22 @@
+package com.mosect.lib.scanpanel.coder;
+
+import android.graphics.Rect;
+
+/**
+ * 帧解码器
+ */
+public interface FrameDecoder {
+
+ /**
+ * 解码帧
+ *
+ * @param format 格式,目前只会是{@link android.graphics.ImageFormat#NV21}
+ * @param data 数据
+ * @param width 宽
+ * @param height 高
+ * @param clip 裁剪区域
+ * @return 解码后的字符串,返回null,表示无内容
+ * @throws Exception 解码异常
+ */
+ String decodeFrame(int format, byte[] data, int width, int height, Rect clip) throws Exception;
+}
diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/coder/FrameHandler.java b/lib/src/main/java/com/mosect/lib/scanpanel/coder/FrameHandler.java
new file mode 100644
index 0000000..de00f61
--- /dev/null
+++ b/lib/src/main/java/com/mosect/lib/scanpanel/coder/FrameHandler.java
@@ -0,0 +1,110 @@
+package com.mosect.lib.scanpanel.coder;
+
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.util.Log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class FrameHandler {
+
+ private static final String TAG = "FrameHandler";
+
+ private int state = 0;
+ private final byte[] lock = new byte[0];
+ private FrameDecoder decoder;
+ private Camera camera;
+ private int width;
+ private int height;
+ private int format;
+ private Rect clip;
+ private ExecutorService decodeExecutor;
+ private Callback callback;
+
+ public void start(Camera camera) {
+ synchronized (lock) {
+ if (state == 0) {
+ state = 1;
+ this.camera = camera;
+ Camera.Parameters parameters = camera.getParameters();
+ Camera.Size size = parameters.getPreviewSize();
+ width = size.width;
+ height = size.height;
+ format = parameters.getPreviewFormat();
+ decodeExecutor = Executors.newFixedThreadPool(1);
+ }
+ }
+ }
+
+ public void destroy() {
+ synchronized (lock) {
+ if (state != 2) {
+ state = 2;
+ decodeExecutor.shutdown();
+ }
+ }
+ }
+
+ public void requestNextFrame() {
+ synchronized (lock) {
+ if (state == 1) {
+ camera.setOneShotPreviewCallback((data, camera1) -> {
+ synchronized (lock) {
+ if (state == 1) {
+ handleFrameData(data);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ private void handleFrameData(byte[] data) {
+ decodeExecutor.execute(() -> {
+ FrameDecoder decoder = this.decoder;
+ String text = null;
+ if (null != decoder) {
+ try {
+ text = decoder.decodeFrame(format, data, width, height, clip);
+ } catch (Exception e) {
+ Log.e(TAG, "handleFrameData: ", e);
+ }
+ }
+ handleDecodeResult(text);
+ });
+ }
+
+ private void handleDecodeResult(String text) {
+ Callback callback = null;
+ synchronized (lock) {
+ if (state == 1) {
+ callback = this.callback;
+ }
+ }
+ if (null != callback) {
+ callback.onFrameDecodeResult(text);
+ }
+ }
+
+ public void setDecoder(FrameDecoder decoder) {
+ this.decoder = decoder;
+ }
+
+ public FrameDecoder getDecoder() {
+ return decoder;
+ }
+
+ public void setClip(Rect clip) {
+ this.clip = clip;
+ }
+
+ public void setCallback(Callback callback) {
+ this.callback = callback;
+ }
+
+ public interface Callback {
+
+ void onFrameDecodeResult(String text);
+ }
+}
diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/graphics/BitmapTexture.java b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/BitmapTexture.java
new file mode 100644
index 0000000..0e930a4
--- /dev/null
+++ b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/BitmapTexture.java
@@ -0,0 +1,66 @@
+package com.mosect.lib.scanpanel.graphics;
+
+import android.graphics.Bitmap;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+import android.opengl.Matrix;
+
+import com.mosect.lib.easygl2.GLBitmapProvider;
+import com.mosect.lib.easygl2.GLException;
+import com.mosect.lib.easygl2.GLTexture;
+import com.mosect.lib.easygl2.g2d.GLTexture2D;
+
+public class BitmapTexture extends GLTexture implements GLTexture2D {
+
+ private final GLBitmapProvider bitmapProvider;
+ private Bitmap bitmap;
+
+ public BitmapTexture(GLBitmapProvider bitmapProvider) {
+ super(GLES20.GL_TEXTURE_2D);
+ this.bitmapProvider = bitmapProvider;
+ }
+
+ @Override
+ protected void onInit() throws GLException {
+ super.onInit();
+ bitmap = bitmapProvider.getBitmap(this);
+ if (null != bitmap) {
+ GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
+ }
+ }
+
+ public void updateTextureImage() {
+ if (null != bitmap) {
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureId());
+ GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, bitmap);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+ }
+ }
+
+ @Override
+ protected void onClear() throws GLException {
+ super.onClear();
+ if (null != bitmap) {
+ bitmapProvider.destroyBitmap(this, bitmap);
+ bitmap = null;
+ }
+ }
+
+ @Override
+ public int getWidth() {
+ if (null != bitmap) return bitmap.getWidth();
+ return 0;
+ }
+
+ @Override
+ public int getHeight() {
+ if (null != bitmap) return bitmap.getHeight();
+ return 0;
+ }
+
+ @Override
+ public void getMatrix(float[] matrix, int offset) {
+ Matrix.setIdentityM(matrix, offset);
+ }
+}
diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/graphics/ContentMatrix.java b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/ContentMatrix.java
new file mode 100644
index 0000000..c3292af
--- /dev/null
+++ b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/ContentMatrix.java
@@ -0,0 +1,193 @@
+package com.mosect.lib.scanpanel.graphics;
+
+import android.graphics.RectF;
+
+/**
+ * 内容矩阵,无特别说明,使用的是左下角为原点的坐标系
+ */
+public class ContentMatrix {
+
+ private final RectF contentRect;
+ private final RectF viewportRect;
+ private final Matrix3D matrix = new Matrix3D();
+ private final Matrix3D matrix2 = new Matrix3D();
+ private final float[] contentPoints;
+ private final float[] viewportPoints;
+ private ScaleType scaleType;
+ private int degrees;
+ private boolean flipX;
+
+ public ContentMatrix(RectF contentRect, RectF viewportRect) {
+ this.contentRect = contentRect;
+ this.viewportRect = viewportRect;
+ contentPoints = getVec4Points(contentRect);
+ viewportPoints = getVec4Points(viewportRect);
+
+ update(ScaleType.CENTER_CROP, 0, false);
+ }
+
+ public void update(ScaleType scaleType, int degrees, boolean flipX) {
+ // 初始化矩阵
+ matrix.reset();
+ // 旋转
+ matrix.postRotate(degrees, 0, 0, -1f);
+
+ float[] temp = new float[contentPoints.length];
+
+ // 计算缩放
+ matrix.mapVec4Points(contentPoints, temp);
+ float contentWidth = getRectPointsWidth(temp);
+ float contentHeight = getRectPointsHeight(temp);
+ float viewportWidth = getRectPointsWidth(viewportPoints);
+ float viewportHeight = getRectPointsHeight(viewportPoints);
+ float scaleX = viewportWidth / contentWidth;
+ float scaleY = viewportHeight / contentHeight;
+ float contentScale = contentWidth / contentHeight;
+ float viewportScale = viewportWidth / viewportHeight;
+ float finalScaleX, finalScaleY;
+ switch (scaleType) {
+ case CENTER_INSIDE:
+ if (contentScale > viewportScale) {
+ finalScaleX = scaleX;
+ finalScaleY = scaleX;
+ } else {
+ finalScaleX = scaleY;
+ finalScaleY = scaleY;
+ }
+ break;
+ case CENTER_CROP:
+ if (contentScale > viewportScale) {
+ finalScaleX = scaleY;
+ finalScaleY = scaleY;
+ } else {
+ finalScaleX = scaleX;
+ finalScaleY = scaleX;
+ }
+ break;
+ case FIT_XY:
+ finalScaleX = scaleX;
+ finalScaleY = scaleY;
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported scaleType: " + scaleType);
+ }
+ if (flipX) finalScaleX = -finalScaleX;
+ // 缩放
+ matrix.postScale(finalScaleX, finalScaleY, 1f);
+
+ // 居中处理
+ matrix.mapVec4Points(contentPoints, temp);
+ float ccx = getRectPointsCenterX(temp);
+ float ccy = getRectPointsCenterY(temp);
+ float vcx = getRectPointsCenterX(viewportPoints);
+ float vcy = getRectPointsCenterY(viewportPoints);
+ float ox = vcx - ccx;
+ float oy = vcy - ccy;
+ // 偏移
+ matrix.postTranslate(ox, oy, 0);
+
+ // 计算反转后的矩阵
+ matrix.invert(matrix2);
+
+ this.scaleType = scaleType;
+ this.degrees = degrees;
+ this.flipX = flipX;
+ }
+
+ /**
+ * 转换点
+ *
+ * @param points 点列表:x1,y1,z1,x2,y2,z2 ... xN,yN,zN
+ * @param out 输出
+ */
+ public void mapVec3Points(float[] points, float[] out) {
+ matrix.mapPoints(points, out);
+ }
+
+ /**
+ * viewport上的矩阵转换成content上的矩形,注意:此方法使用左上角为原点的坐标系
+ *
+ * @param src 矩形
+ */
+ public void viewportToContent2D(RectF src, RectF dst) {
+ float[] points = getVec4Points(src);
+ float vh = getRectPointsHeight(viewportPoints);
+ // 坐标系转换
+ points[1] = vh - points[1];
+ points[5] = vh - points[5];
+ float[] dstPoints = new float[points.length];
+ matrix2.mapVec4Points(points, dstPoints);
+ // 转换会原本坐标系
+ float ch = getRectPointsHeight(contentPoints);
+ dstPoints[1] = ch - dstPoints[1];
+ dstPoints[5] = ch - dstPoints[5];
+ // 转换成RectF对象
+ pointsToRect(dstPoints, dst);
+ }
+
+ public ScaleType getScaleType() {
+ return scaleType;
+ }
+
+ public int getDegrees() {
+ return degrees;
+ }
+
+ public boolean isFlipX() {
+ return flipX;
+ }
+
+ public RectF getContentRect() {
+ return contentRect;
+ }
+
+ public RectF getViewportRect() {
+ return viewportRect;
+ }
+
+ private static float[] getVec4Points(RectF rect) {
+ return new float[]{
+ rect.left, rect.bottom, 0, 1,
+ rect.right, rect.top, 0, 1,
+ };
+ }
+
+ private static float getRectPointsWidth(float[] points) {
+ return Math.abs(points[0] - points[4]);
+ }
+
+ private static float getRectPointsHeight(float[] points) {
+ return Math.abs(points[1] - points[5]);
+ }
+
+ private static float getRectPointsCenterX(float[] points) {
+ return (points[0] + points[4]) / 2f;
+ }
+
+ private static float getRectPointsCenterY(float[] points) {
+ return (points[1] + points[5]) / 2f;
+ }
+
+ private static void pointsToRect(float[] points, RectF out) {
+ if (points[0] > points[4]) {
+ out.left = points[4];
+ out.right = points[0];
+ } else {
+ out.left = points[0];
+ out.right = points[4];
+ }
+ if (points[1] > points[5]) {
+ out.top = points[5];
+ out.bottom = points[1];
+ } else {
+ out.top = points[1];
+ out.bottom = points[5];
+ }
+ }
+
+ public enum ScaleType {
+ CENTER_INSIDE,
+ CENTER_CROP,
+ FIT_XY,
+ }
+}
diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/graphics/Drawer2D.java b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/Drawer2D.java
new file mode 100644
index 0000000..e065d81
--- /dev/null
+++ b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/Drawer2D.java
@@ -0,0 +1,59 @@
+package com.mosect.lib.scanpanel.graphics;
+
+import android.graphics.RectF;
+import android.opengl.Matrix;
+
+import com.mosect.lib.easygl2.GLTexture;
+import com.mosect.lib.scanpanel.shader.Shader2D;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+public abstract class Drawer2D