diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..e2cc87f --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Scan panel \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..5db59e9 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5bd3a1c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..6da343e --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdk 32 + + defaultConfig { + applicationId "com.mosect.app.scanpanel" + minSdk 19 + targetSdk 32 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.3.0' + implementation 'com.google.android.material:material:1.4.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + implementation project(':lib') + implementation 'com.google.zxing:core:3.5.0' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/mosect/app/scanpanel/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/mosect/app/scanpanel/ExampleInstrumentedTest.java new file mode 100644 index 0000000..4a43faa --- /dev/null +++ b/app/src/androidTest/java/com/mosect/app/scanpanel/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.mosect.app.scanpanel; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.mosect.app.scanpanel", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..62cdfa2 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/shader_simple.frag b/app/src/main/assets/shader_simple.frag new file mode 100644 index 0000000..65ef5f9 --- /dev/null +++ b/app/src/main/assets/shader_simple.frag @@ -0,0 +1,5 @@ +uniform vec4 color; + +void main() { + gl_FragColor = color; +} \ No newline at end of file diff --git a/app/src/main/assets/shader_simple.vert b/app/src/main/assets/shader_simple.vert new file mode 100644 index 0000000..a14e29c --- /dev/null +++ b/app/src/main/assets/shader_simple.vert @@ -0,0 +1,5 @@ +attribute vec3 position; + +void main() { + gl_Position = vec4(position, 1.0f); +} \ No newline at end of file diff --git a/app/src/main/java/com/mosect/app/scanpanel/AutostartActivity.java b/app/src/main/java/com/mosect/app/scanpanel/AutostartActivity.java new file mode 100644 index 0000000..9226d48 --- /dev/null +++ b/app/src/main/java/com/mosect/app/scanpanel/AutostartActivity.java @@ -0,0 +1,8 @@ +package com.mosect.app.scanpanel; + +public class AutostartActivity extends ScanActivity { + @Override + protected void onInitContentView() { + setContentView(R.layout.activity_autostart); + } +} diff --git a/app/src/main/java/com/mosect/app/scanpanel/MainActivity.java b/app/src/main/java/com/mosect/app/scanpanel/MainActivity.java new file mode 100644 index 0000000..20edada --- /dev/null +++ b/app/src/main/java/com/mosect/app/scanpanel/MainActivity.java @@ -0,0 +1,57 @@ +package com.mosect.app.scanpanel; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.widget.CheckBox; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +public class MainActivity extends AppCompatActivity { + + private CheckBox cbFront; + private CheckBox cbUseTextureView; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ActivityResultLauncher cameraLauncher = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + result -> { + if (result) { + jumpAutostartActivity(); + } + }); + + setContentView(R.layout.activity_main); + + findViewById(R.id.btn_autostart).setOnClickListener(v -> { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + jumpAutostartActivity(); + } else { + cameraLauncher.launch(Manifest.permission.CAMERA); + } + }); + findViewById(R.id.btn_manual).setOnClickListener(v -> { + Intent intent = new Intent(this, ManualActivity.class); + intent.putExtra("facing", cbFront.isChecked() ? "front" : "back"); + intent.putExtra("useTextureView", cbUseTextureView.isChecked()); + startActivity(intent); + }); + cbFront = findViewById(R.id.cb_front); + cbUseTextureView = findViewById(R.id.cb_useTextureView); + } + + private void jumpAutostartActivity() { + Intent intent = new Intent(this, AutostartActivity.class); + intent.putExtra("facing", cbFront.isChecked() ? "front" : "back"); + intent.putExtra("useTextureView", cbUseTextureView.isChecked()); + startActivity(intent); + } +} diff --git a/app/src/main/java/com/mosect/app/scanpanel/MainApplication.java b/app/src/main/java/com/mosect/app/scanpanel/MainApplication.java new file mode 100644 index 0000000..3f15599 --- /dev/null +++ b/app/src/main/java/com/mosect/app/scanpanel/MainApplication.java @@ -0,0 +1,91 @@ +package com.mosect.app.scanpanel; + +import android.app.Application; +import android.graphics.RectF; +import android.opengl.Matrix; + +import com.mosect.lib.scanpanel.graphics.ContentMatrix; +import com.mosect.lib.scanpanel.graphics.Matrix3D; + +import java.util.Arrays; + +public class MainApplication extends Application { + + private void testMatrix() { + float[] point1 = {1, 1, 0f, 1f}; + float[] point2 = new float[point1.length]; + float[] matrix = new float[16 * 3]; + Matrix.setIdentityM(matrix, 0); + + Matrix.setIdentityM(matrix, 32); + Matrix.rotateM(matrix, 32, 90, 0, 0, -1f); + System.arraycopy(matrix, 0, matrix, 16, 16); + Matrix.multiplyMM(matrix, 0, matrix, 16, matrix, 32); + Matrix.multiplyMV(point2, 0, matrix, 0, point1, 0); + System.out.printf("%s >>> %s%n", Arrays.toString(point1), Arrays.toString(point2)); + + Matrix.setIdentityM(matrix, 32); + Matrix.translateM(matrix, 32, 5, 5, 0f); + System.arraycopy(matrix, 0, matrix, 16, 16); + Matrix.multiplyMM(matrix, 0, matrix, 16, matrix, 32); + Matrix.multiplyMV(point2, 0, matrix, 0, point1, 0); + System.out.printf("%s >>> %s%n", Arrays.toString(point1), Arrays.toString(point2)); + + Matrix.setIdentityM(matrix, 32); + Matrix.scaleM(matrix, 32, 2, 2, 1f); + System.arraycopy(matrix, 0, matrix, 16, 16); + Matrix.multiplyMM(matrix, 0, matrix, 16, matrix, 32); + Matrix.multiplyMV(point2, 0, matrix, 0, point1, 0); + System.out.printf("%s >>> %s%n", Arrays.toString(point1), Arrays.toString(point2)); + } + + private void testMatrix3D() { + Matrix3D matrix3D = new Matrix3D(); + float[] point1 = {1, 1, 0f}; + float[] point2 = new float[3]; + + matrix3D.postRotate(90, 0, 0, -1f); + matrix3D.mapPoints(point1, point2); + System.out.printf("%s >>> %s%n", Arrays.toString(point1), Arrays.toString(point2)); + + matrix3D.postScale(2f, 2f, 1f); + matrix3D.mapPoints(point1, point2); + System.out.printf("%s >>> %s%n", Arrays.toString(point1), Arrays.toString(point2)); + + matrix3D.postTranslate(5f, 5f, 0f); + matrix3D.mapPoints(point1, point2); + System.out.printf("%s >>> %s%n", Arrays.toString(point1), Arrays.toString(point2)); + } + + private void testContentMatrix() { + RectF viewportRect = new RectF(0, 1920, 1080, 0); + RectF contentRect = new RectF(0, 300, 400, 0); + ContentMatrix contentMatrix = new ContentMatrix(contentRect, viewportRect); + contentMatrix.update(ContentMatrix.ScaleType.CENTER_CROP, 90, false); + float[] points = { + 0, 0, 1, + 400, 0, 1, + 0, 300, 1, + 400, 300, 1, + }; + float[] points2 = new float[points.length]; + contentMatrix.mapVec3Points(points, points2); + for (int i = 0; i < points.length; i += 3) { + System.out.printf("(%s, %s, %s) >>> (%s, %s, %s) %n", + points[i], points[i + 1], points[i + 2], + points2[i], points2[i + 1], points2[i + 2]); + } + RectF rect1 = new RectF(1000, 0, 1080, 300); + RectF rect2 = new RectF(); + contentMatrix.viewportToContent2D(rect1, rect2); + System.out.printf("%s >>> %s%n", rect1, rect2); + } + + @Override + public void onCreate() { + super.onCreate(); + testContentMatrix(); +// testMatrix(); +// testMatrix3D(); + } +} diff --git a/app/src/main/java/com/mosect/app/scanpanel/ManualActivity.java b/app/src/main/java/com/mosect/app/scanpanel/ManualActivity.java new file mode 100644 index 0000000..e1c2579 --- /dev/null +++ b/app/src/main/java/com/mosect/app/scanpanel/ManualActivity.java @@ -0,0 +1,43 @@ +package com.mosect.app.scanpanel; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.os.Bundle; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +public class ManualActivity extends ScanActivity { + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ActivityResultLauncher perLauncher = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), result -> { + if (result) { + // 获得权限 + spMain.start(); + } + }); + int status = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA); + if (status == PackageManager.PERMISSION_GRANTED) { + // 已获得权限 + spMain.start(); + } else { + perLauncher.launch(Manifest.permission.CAMERA); + } + } + + @Override + protected void onInitContentView() { + setContentView(R.layout.activity_manual); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + spMain.destroy(); + } +} diff --git a/app/src/main/java/com/mosect/app/scanpanel/ScanActivity.java b/app/src/main/java/com/mosect/app/scanpanel/ScanActivity.java new file mode 100644 index 0000000..299f9e3 --- /dev/null +++ b/app/src/main/java/com/mosect/app/scanpanel/ScanActivity.java @@ -0,0 +1,109 @@ +package com.mosect.app.scanpanel; + +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.hardware.Camera; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.mosect.lib.scanpanel.MaskBackground; +import com.mosect.lib.scanpanel.ScanPanel; + +public abstract class ScanActivity extends AppCompatActivity { + + private static final String TAG = "Act/Autostart"; + + protected ScanPanel spMain; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + onInitContentView(); + + spMain = findViewById(R.id.sp_main); + + int facing = "front".equals(getIntent().getStringExtra("facing")) ? + Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK; + boolean useTextureView = getIntent().getBooleanExtra("useTextureView", false); + spMain.setCallback(new ScanPanel.Callback() { + + private final MaskBackground maskBackground = new MaskBackground(); + private final TextMask textMask = new TextMask(ScanActivity.this); + + @Override + public void onScanStart(ScanPanel panel) { + Log.d(TAG, "onScanStart: "); + } + + @Override + public void onDrawMask(ScanPanel panel, Canvas canvas, int width, int height, Rect clip) { + maskBackground.draw(canvas, clip); + textMask.draw(canvas, width, height); + } + + @Override + public int onSwitchCamera(ScanPanel panel) { + int count = Camera.getNumberOfCameras(); + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + for (int i = 0; i < count; i++) { + Camera.getCameraInfo(i, cameraInfo); + if (facing == cameraInfo.facing) { + return i; + } + } + return -1; + } + + @Override + public void onScanError(ScanPanel panel, Exception exp) { + Log.e(TAG, "onScanError: ", exp); + } + + @Override + public void onScanResult(ScanPanel panel, String result) { + Log.d(TAG, "onScanResult: " + result); + panel.next(); + } + + @Override + public boolean onComputeClip(ScanPanel panel, int width, int height, Rect out) { + int cw = width / 2; + int ch; + if (width > height) { + ch = height / 2; + } else { + ch = Math.min(cw, height / 4); + } + out.left = (width - cw) / 2; + out.right = out.left + cw; + out.top = (height - ch) / 2; + out.bottom = out.top + ch; + return true; + } + + @Override + public void onScanEnd(ScanPanel panel) { + Log.d(TAG, "onScanEnd: "); + } + }); + + int rotation = getWindowManager().getDefaultDisplay().getRotation(); + spMain.setDisplayRotation(rotation); + spMain.setUseTextureView(useTextureView); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + int rotation = getWindowManager().getDefaultDisplay().getRotation(); + spMain.setDisplayRotation(rotation); + } + + protected abstract void onInitContentView(); +} diff --git a/app/src/main/java/com/mosect/app/scanpanel/TextMask.java b/app/src/main/java/com/mosect/app/scanpanel/TextMask.java new file mode 100644 index 0000000..ed6eb4b --- /dev/null +++ b/app/src/main/java/com/mosect/app/scanpanel/TextMask.java @@ -0,0 +1,35 @@ +package com.mosect.app.scanpanel; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.TypedValue; + +public class TextMask { + + private final Paint paint; + private final String text = "Hello world!"; + private final float textWidth; + private final float textHeight; + private final float yOffset; + + public TextMask(Context context) { + paint = new Paint(); + paint.setAntiAlias(true); + paint.setColor(Color.WHITE); + float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 18, + context.getResources().getDisplayMetrics()); + paint.setTextSize(textSize); + textWidth = paint.measureText(text); + Paint.FontMetrics fm = paint.getFontMetrics(); + textHeight = fm.bottom - fm.top; + yOffset = -fm.top; + } + + public void draw(Canvas canvas, int width, int height) { + float y = (height - textHeight) / 2 + yOffset; + float x = (width - textWidth) / 2; + canvas.drawText(text, x, y, paint); + } +} diff --git a/app/src/main/java/com/mosect/app/scanpanel/ZxingDecoder.java b/app/src/main/java/com/mosect/app/scanpanel/ZxingDecoder.java new file mode 100644 index 0000000..4685abf --- /dev/null +++ b/app/src/main/java/com/mosect/app/scanpanel/ZxingDecoder.java @@ -0,0 +1,49 @@ +package com.mosect.app.scanpanel; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; +import android.graphics.YuvImage; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; +import com.mosect.lib.scanpanel.coder.FrameDecoder; + +import java.io.ByteArrayOutputStream; + +public class ZxingDecoder implements FrameDecoder { + + private final MultiFormatReader multiFormatReader; + + public ZxingDecoder() { + multiFormatReader = new MultiFormatReader(); + } + + @Override + public String decodeFrame(int format, byte[] data, int width, int height, Rect clip) throws Exception { +// YuvImage image = new YuvImage(data, format, width, height, new int[]{width, width}); +// ByteArrayOutputStream temp = new ByteArrayOutputStream(512); +// image.compressToJpeg(clip, 100, temp); +// byte[] jpeg = temp.toByteArray(); +// Bitmap bm = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length); +// bm.recycle(); + int cl = 0, ct = 0, cw = width, ch = height; + if (null != clip) { + cl = clip.left; + ct = clip.top; + cw = clip.width(); + ch = clip.height(); + } + PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(data, width, height, cl, ct, cw, ch, false); + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + try { + Result rawResult = multiFormatReader.decode(bitmap); + return rawResult.getText(); + } finally { + multiFormatReader.reset(); + } + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_autostart.xml b/app/src/main/res/layout/activity_autostart.xml new file mode 100644 index 0000000..37f316c --- /dev/null +++ b/app/src/main/res/layout/activity_autostart.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..35ab2ac --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_manual.xml b/app/src/main/res/layout/activity_manual.xml new file mode 100644 index 0000000..ecd3af3 --- /dev/null +++ b/app/src/main/res/layout/activity_manual.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..e0c4756 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Scan panel + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..ee91800 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/mosect/app/scanpanel/ExampleUnitTest.java b/app/src/test/java/com/mosect/app/scanpanel/ExampleUnitTest.java new file mode 100644 index 0000000..f13e197 --- /dev/null +++ b/app/src/test/java/com/mosect/app/scanpanel/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.mosect.app.scanpanel; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5ae9a7b --- /dev/null +++ b/build.gradle @@ -0,0 +1,9 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.2.2' apply false + id 'com.android.library' version '7.2.2' apply false +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..dab7c28 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c3681c9 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jan 23 19:00:25 CST 2023 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle new file mode 100644 index 0000000..45586b7 --- /dev/null +++ b/lib/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'com.android.library' +} + +android { + compileSdk 32 + + defaultConfig { + minSdk 19 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + implementation 'com.github.Mosect:EasyGL2:1.0.3' + +} \ No newline at end of file diff --git a/lib/consumer-rules.pro b/lib/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/lib/proguard-rules.pro b/lib/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/lib/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/lib/src/androidTest/java/com/mosect/lib/scanpanel/ExampleInstrumentedTest.java b/lib/src/androidTest/java/com/mosect/lib/scanpanel/ExampleInstrumentedTest.java new file mode 100644 index 0000000..39653a5 --- /dev/null +++ b/lib/src/androidTest/java/com/mosect/lib/scanpanel/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.mosect.lib.scanpanel; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.mosect.lib.scanpanel.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d6ce8c2 --- /dev/null +++ b/lib/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/lib/src/main/assets/ScanPanel/shader_2d.vert b/lib/src/main/assets/ScanPanel/shader_2d.vert new file mode 100644 index 0000000..7752814 --- /dev/null +++ b/lib/src/main/assets/ScanPanel/shader_2d.vert @@ -0,0 +1,11 @@ +uniform mat4 cameraMatrix; +uniform mat4 textureMatrix; +attribute vec3 position; +attribute vec2 textureCoord; +varying vec2 colorCoord; + +void main() { + gl_Position = cameraMatrix * vec4(position, 1.0f); + vec4 cp = textureMatrix * vec4(textureCoord.x, textureCoord.y, 0.0f, 1.0f); + colorCoord = cp.xy; +} diff --git a/lib/src/main/assets/ScanPanel/shader_oes.frag b/lib/src/main/assets/ScanPanel/shader_oes.frag new file mode 100644 index 0000000..c6a1541 --- /dev/null +++ b/lib/src/main/assets/ScanPanel/shader_oes.frag @@ -0,0 +1,8 @@ +#extension GL_OES_EGL_image_external: require +precision mediump float; +varying vec2 colorCoord; +uniform samplerExternalOES sTexture; + +void main() { + gl_FragColor = texture2D(sTexture, colorCoord); +} \ No newline at end of file diff --git a/lib/src/main/assets/ScanPanel/shader_tex.frag b/lib/src/main/assets/ScanPanel/shader_tex.frag new file mode 100644 index 0000000..e189be1 --- /dev/null +++ b/lib/src/main/assets/ScanPanel/shader_tex.frag @@ -0,0 +1,7 @@ +precision mediump float; +varying vec2 colorCoord; +uniform sampler2D textureNum; + +void main() { + gl_FragColor = texture2D(textureNum, colorCoord); +} \ No newline at end of file diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/MaskBackground.java b/lib/src/main/java/com/mosect/lib/scanpanel/MaskBackground.java new file mode 100644 index 0000000..72c1d7b --- /dev/null +++ b/lib/src/main/java/com/mosect/lib/scanpanel/MaskBackground.java @@ -0,0 +1,39 @@ +package com.mosect.lib.scanpanel; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; + +/** + * 遮罩层背景,可以在{@link ScanHandler.Callback#onDrawMask(Canvas, int, int, Rect)}中执行{@link #draw(Canvas, Rect)},实现遮罩效果 + */ +public class MaskBackground { + + private final Paint paint; + private int color = Color.parseColor("#a0000000"); + + public MaskBackground() { + paint = new Paint(); + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.TRANSPARENT); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + } + + public void setColor(int color) { + this.color = color; + } + + public int getColor() { + return color; + } + + public void draw(Canvas canvas, Rect clip) { + if (null != clip) { + canvas.drawColor(color, PorterDuff.Mode.DST_OVER); + canvas.drawRect(clip, paint); + } + } +} diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/ScanHandler.java b/lib/src/main/java/com/mosect/lib/scanpanel/ScanHandler.java new file mode 100644 index 0000000..9e250df --- /dev/null +++ b/lib/src/main/java/com/mosect/lib/scanpanel/ScanHandler.java @@ -0,0 +1,833 @@ +package com.mosect.lib.scanpanel; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ImageFormat; +import android.graphics.Matrix; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.RectF; +import android.hardware.Camera; +import android.opengl.GLES20; +import android.text.TextUtils; +import android.util.Log; +import android.view.Display; +import android.view.Surface; + +import com.mosect.lib.easygl2.GLBitmapProvider; +import com.mosect.lib.easygl2.GLContext; +import com.mosect.lib.easygl2.GLException; +import com.mosect.lib.easygl2.GLSurfaceWindow; +import com.mosect.lib.easygl2.GLTextureWindow; +import com.mosect.lib.scanpanel.coder.FrameDecoder; +import com.mosect.lib.scanpanel.coder.FrameHandler; +import com.mosect.lib.scanpanel.graphics.BitmapTexture; +import com.mosect.lib.scanpanel.graphics.ContentMatrix; +import com.mosect.lib.scanpanel.graphics.DrawerOES; +import com.mosect.lib.scanpanel.graphics.DrawerTEX; +import com.mosect.lib.scanpanel.shader.ShaderOES; +import com.mosect.lib.scanpanel.shader.ShaderTEX; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * 扫码处理,不推荐使用此类进行扫码处理,推荐使用{@link ScanPanel} + */ +public class ScanHandler { + + private static final String TAG = "ScanHandler"; + + private final Context context; + private Callback callback; + private int state = 0; // 状态:0,未开始;1,运行中;2,已销毁 + private int displayRotation = 0; // 屏幕显示方向 + + private final byte[] lock = new byte[0]; // 锁 + private final LinkedList actions = new LinkedList<>(); // 在loop中执行的action + + private Camera camera = null; // 摄像头 + private final Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); // 摄像头信息 + + private GLContext glContext; // GL上下文 + private GLTextureWindow glTextureWindow; // 纹理窗口,用来接收摄像头预览图像 + private GLSurfaceWindow glSurfaceWindow; // Surface窗口,用来显示摄像头预览图像以及遮罩层 + private ShaderOES shaderOES; // OES shader + private ShaderTEX shaderTEX; // 纹理shader + private DrawerOES cameraDrawer; // 摄像头预览图像绘制器 + private ContentMatrix cameraMatrix; // 摄像头预览图像变换矩阵 + + private Rect clipRect; // 裁剪部分,即扫码部分 + private FrameDecoder frameDecoder; // 帧解码器 + private FrameHandler frameHandler; // 帧解码处理 + + private DrawerTEX maskDrawer; // 遮罩层绘制器 + private Bitmap maskBitmap; // 遮罩层位图 + private Canvas maskCanvas; // 遮罩层画布 + private BitmapTexture maskTexture; // 遮罩层纹理 + private boolean maskChanged; // 遮罩层是否发生更改,如果发生更改,则会更新maskTexture + private long nextFocusTime = -1; // 下次对焦的时间,负数,表示对焦中 + + public ScanHandler(Context context) { + this.context = context; + } + + /** + * 开始处理扫码 + */ + public void start() { + synchronized (lock) { + if (state == 0) { + state = 1; + new Thread(this::loop).start(); + } + } + } + + /** + * 设置surface,显示摄像头预览图像和遮罩层 + * + * @param surface surface + * @param width 宽 + * @param height 高 + */ + public void setSurface(Surface surface, int width, int height) { + runAction(() -> { + clearSurface(); + if (null != surface && width > 0 && height > 0) { + glSurfaceWindow = new GLSurfaceWindow(surface, width, height); + glSurfaceWindow.init(glContext); + // 创建遮罩层 + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + maskBitmap = bitmap; + maskCanvas = new Canvas(maskBitmap); + maskTexture = new BitmapTexture(new GLBitmapProvider() { + @Override + public Bitmap getBitmap(Object host) { + return bitmap; + } + + @Override + public void destroyBitmap(Object host, Bitmap bitmap) { + bitmap.recycle(); + } + }); + maskTexture.init(glContext); + maskChanged = true; + RectF contentRect = new RectF(0, 0, width, height); + RectF viewportRect = new RectF(0, 0, width, height); + ContentMatrix maskMatrix = new ContentMatrix(contentRect, viewportRect); + maskMatrix.update(ContentMatrix.ScaleType.CENTER_CROP, 0, false); + maskDrawer = new DrawerTEX(maskMatrix, 0.9f); + if (null == camera) { + // 打开相机 + camera = openCamera(cameraInfo); + if (null != camera) { + // 初始化相机 + initCamera(); + } + } + } else { + // 关闭相机 + closeCamera(); + } + }); + } + + /** + * 设置显示方向,可以从{@link Display#getRotation()}获取
+ * activity.getWindowManager().getDefaultDisplay().getRotation() + * + * @param displayRotation 显示方向 + */ + public void setDisplayRotation(int displayRotation) { + runAction(() -> { + this.displayRotation = displayRotation; + if (null != camera && null != glSurfaceWindow) { + initCamera(); + } + }); + } + + /** + * 设置裁剪区域,即扫码部分 + * + * @param rect 区域 + */ + public void setClip(Rect rect) { + runAction(() -> { + if (null == rect) { + clipRect = null; + } else { + clipRect = new Rect(rect); + } + // 同步到FrameHandler + if (null != frameHandler) { + frameHandler.setClip(convertClipRect()); + } + maskChanged = true; + }); + } + + /** + * 设置帧解码器 + * + * @param frameDecoder 帧解码器 + */ + public void setFrameDecoder(FrameDecoder frameDecoder) { + runAction(() -> { + this.frameDecoder = frameDecoder; + // 同步到FrameHandler + if (null != frameHandler) { + frameHandler.setDecoder(frameDecoder); + } + }); + } + + /** + * 继续解码下一帧;注意:在{@link Callback#onScanResult(ScanHandler, String)}触发后,将停止解码,需要再次调用此方法才能继续解码 + */ + public void next() { + runAction(() -> { + if (null != frameHandler) { + frameHandler.requestNextFrame(); + } + }); + } + + /** + * 销毁并释放此对象,此对象将不可用 + */ + public void destroy() { + synchronized (lock) { + if (state != 2) { + state = 2; + } + } + } + + /** + * 如果需要刷新遮罩层图像,需要调用此方法 + */ + public void invalidateMask() { + runAction(() -> { + if (!maskChanged) { + maskChanged = true; + } + }); + } + + /** + * 扫码处理核心循环 + */ + private void loop() { + Log.d(TAG, "loop: start"); + onStart(); + try { + // 创建opengl环境 + glContext = new GLContext(64, 64); + glContext.init(); + glContext.makeCurrentWithException(); + + // 创建shader + shaderOES = new ShaderOES(context); + shaderOES.init(glContext); + shaderTEX = new ShaderTEX(context); + shaderTEX.init(glContext); + + // 进入循环 + while (true) { + // 切换成默认输出surface,必要,否则GLContext不可用 + glContext.makeCurrentWithException(); + // 执行action + synchronized (lock) { + if (state != 1) break; + while (actions.size() > 0) { + actions.removeFirst().run(); + } + } + + if (null != glSurfaceWindow) { + // 存在surface,则需要进行绘制 + if (glSurfaceWindow.makeCurrent()) { + // 图像输出成功切换成surface + + // 渲染背景 + GLES20.glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLException.checkGLError("glClear"); + + // 绘制摄像头图像 + if (null != cameraDrawer) { + cameraDrawer.draw(glTextureWindow, shaderOES); + } + + // 绘制遮罩层 + if (maskChanged) { + maskCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.DST_OVER); + onDrawMask(maskCanvas, glSurfaceWindow.getWidth(), glSurfaceWindow.getHeight(), clipRect); + // 将位图内容同步到纹理中 + maskTexture.updateTextureImage(); + maskChanged = false; + } + maskDrawer.draw(maskTexture, shaderTEX); + + // 提交最终图像 + boolean ok = glSurfaceWindow.commit(); + if (!ok) { + Log.w(TAG, "loop: commitFailed"); + } + } else { + Log.w(TAG, "loop: makeCurrentFailed"); + } + } + + // 处理对焦 + long now = System.currentTimeMillis(); + if (null != camera) { + if (nextFocusTime >= 0 && now >= nextFocusTime) { + // 符合对焦时间 + nextFocusTime = -1; // 需要设置成对焦中 + camera.autoFocus((success, camera1) -> { + // 对焦完成 + runAction(() -> { + if (nextFocusTime < 0) { + // 还在对焦中,处理此处对焦结果 + if (success) { + // 自动对焦成功,4秒后重新对焦 + nextFocusTime = System.currentTimeMillis() + 4000; + } else { + // 自动对焦失败,下一次马上对焦 + nextFocusTime = System.currentTimeMillis(); + } + } + }); + }); + } + } + } + } catch (Exception e) { + onError(e); + } finally { + // 释放必要的资源 + closeCamera(); + clearSurface(); + if (null != shaderOES) { + shaderOES.close(); + shaderOES = null; + } + if (null != shaderTEX) { + shaderTEX.close(); + shaderTEX = null; + } + if (null != glContext) { + glContext.close(); + glContext = null; + } + } + Log.d(TAG, "loop: end"); + onEnd(); + } + + /** + * 打开摄像头 + * + * @param cameraInfo 输出摄像头信息 + * @return 摄像头对象,返回null,表示打开摄像头失败 + */ + protected Camera openCamera(Camera.CameraInfo cameraInfo) { + Callback callback = this.callback; + int id = -1; + if (null != callback) { + // 从callback中获取需要打开的摄像头 + id = callback.onSwitchCamera(this); + } + if (id >= 0) { + Camera camera = Camera.open(id); + if (null != camera) { + // 需要获取对应摄像头信息 + Camera.getCameraInfo(id, cameraInfo); + Log.d(TAG, String.format("openCamera: id=%s, facing=%s, orientation=%s", + id, cameraInfo.facing, cameraInfo.orientation)); + return camera; + } + } + return null; + } + + /** + * 初始化摄像头 + */ + protected void initCamera() { + camera.stopPreview(); + if (null != glTextureWindow) { + glTextureWindow.close(); + } + int degrees = getCameraDisplayOrientation(); + Camera.Parameters parameters = camera.getParameters(); + + // 设置帧率 + int[] fpsRange = switchFpsRange(parameters); + parameters.setPreviewFpsRange(fpsRange[0], fpsRange[1]); + + // 设置格式 + int previewFormat = switchPreviewFormat(parameters); + parameters.setPreviewFormat(previewFormat); + + // 设置大小 + PreviewSize previewSize = switchPreviewSize(parameters, degrees); + parameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight()); + camera.setParameters(parameters); + + // 创建纹理窗口,用于接收摄像头预览图像 + glTextureWindow = new GLTextureWindow(previewSize.getWidth(), previewSize.getHeight(), false); + glTextureWindow.init(glContext); + // 创建摄像头绘制器 + RectF contentRect = new RectF(0, 0, glTextureWindow.getWidth(), glTextureWindow.getHeight()); + RectF viewportRect = new RectF(0, 0, glSurfaceWindow.getWidth(), glSurfaceWindow.getHeight()); + cameraMatrix = new ContentMatrix(contentRect, viewportRect); + cameraMatrix.update(ContentMatrix.ScaleType.CENTER_CROP, degrees, false); + cameraDrawer = new DrawerOES(cameraMatrix, 0.5f); + + // 创建帧处理 + FrameHandler frameHandler = new FrameHandler(); + this.frameHandler = frameHandler; + // 同步裁剪和解码器 + frameHandler.setClip(convertClipRect()); + frameHandler.setDecoder(frameDecoder); + // 监听扫码回调 + frameHandler.setCallback(text -> { + // 扫码结果 + runAction(() -> { + if (!TextUtils.isEmpty(text)) { + Callback callback = this.callback; + if (null != callback) { + callback.onScanResult(this, text); + } + } else { + // 没有解析到文本,继续解析下一帧 + frameHandler.requestNextFrame(); + } + }); + }); + + try { + // 设置预览纹理 + camera.setPreviewTexture(glTextureWindow.getSurfaceTexture()); + // 开始预览 + camera.startPreview(); + // 开始解码 + frameHandler.start(camera); + frameHandler.requestNextFrame(); + // 需要马上对焦 + nextFocusTime = System.currentTimeMillis(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * 选择摄像头帧率范围 + * + * @param parameters 摄像头参数 + * @return 摄像头帧率范围 + */ + protected int[] switchFpsRange(Camera.Parameters parameters) { + List fpsRangeList = parameters.getSupportedPreviewFpsRange(); + Collections.sort(fpsRangeList, (o1, o2) -> { + if (o1[1] > o2[1]) return -1; + else if (o1[1] < o2[1]) return 1; + else if (o1[0] > o2[0]) return -1; + else if (o1[0] < o2[0]) return 1; + return 0; + }); + return fpsRangeList.get(0); + } + + /** + * 选择预览格式,默认使用{@link ImageFormat#NV21} + * + * @param parameters 摄像头参数 + * @return 预览格式 + */ + protected int switchPreviewFormat(Camera.Parameters parameters) { + // 默认使用NV21格式 + return ImageFormat.NV21; + } + + /** + * 选择预览大小,默认返回最接近surface大小的预览大小 + * + * @param parameters 摄像头参数 + * @param degrees 角度 + * @return 预览大小 + */ + protected PreviewSize switchPreviewSize(Camera.Parameters parameters, int degrees) { + List previewSizes = new ArrayList<>(); + for (Camera.Size size : parameters.getSupportedPreviewSizes()) { + previewSizes.add(new PreviewSize(size.width, size.height, degrees)); + } + Collections.sort(previewSizes, (o1, o2) -> { + int h1 = Math.abs(o1.getRotateHeight() - glSurfaceWindow.getHeight()); + int h2 = Math.abs(o2.getRotateHeight() - glSurfaceWindow.getHeight()); + if (h1 < h2) return -1; + else if (h1 > h2) return 1; + int w1 = Math.abs(o1.getRotateWidth() - glSurfaceWindow.getWidth()); + int w2 = Math.abs(o2.getRotateHeight() - glSurfaceWindow.getHeight()); + if (w1 < w2) return -1; + else if (w1 > w2) return 1; + return 0; + }); + return previewSizes.get(0); + } + + /** + * 转换裁剪区域,因为可能会发生旋转,所以不能直接使用设定的裁剪,无特殊需求,不要复写此方法 + * + * @return 符合预览图像的裁剪 + */ + protected Rect convertClipRect() { + if (null != clipRect) { + RectF rect = new RectF(clipRect); + RectF temp = new RectF(); + // 转换裁剪区域 + cameraMatrix.viewportToContent2D(rect, temp); + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + // 注意:前置摄像头需要翻转水平方向 + float width = cameraMatrix.getContentRect().width(); + float right = width - temp.left; + float left = width - temp.right; + temp.left = left; + temp.right = right; + } + Rect result = new Rect(); + result.left = (int) temp.left; + result.top = (int) temp.top; + result.right = (int) temp.right; + result.bottom = (int) temp.bottom; + return result; + } + return null; + } + + /** + * 关闭摄像头,只要是释放摄像头相关资源 + */ + protected void closeCamera() { + if (null != glTextureWindow) { + glTextureWindow.close(); + glTextureWindow = null; + } + if (null != camera) { + camera.release(); + camera = null; + } + if (null != frameHandler) { + frameHandler.destroy(); + frameHandler = null; + } + cameraDrawer = null; + cameraMatrix = null; + } + + /** + * 清空surface,只要是释放surface相关资源 + */ + protected void clearSurface() { + if (null != glSurfaceWindow) { + glSurfaceWindow.close(); + glSurfaceWindow = null; + } + if (null != maskTexture) { + maskTexture.close(); + maskTexture = null; + } + if (null != maskBitmap) { + maskBitmap.recycle(); + maskBitmap = null; + } + maskCanvas = null; + maskDrawer = null; + } + + /** + * 获取画面旋转的角度,使用了{@link Camera#setDisplayOrientation(int)}推荐算法,无特殊需求,不要复写此方法 + * + * @return 画面旋转角度 + */ + protected int getCameraDisplayOrientation() { + int degrees = 0; + switch (displayRotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + } + + int result; + if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (cameraInfo.orientation + degrees) % 360; + result = (360 - result) % 360; // compensate the mirror + } else { // back-facing + result = (cameraInfo.orientation - degrees + 360) % 360; + } + return result; + } + + /** + * 执行一个action,此action不会马上执行,会方法队列最后 + * + * @param action action + */ + protected void runAction(Runnable action) { + synchronized (lock) { + if (state != 2) { + actions.addLast(action); + } + } + } + + /** + * 绘制遮罩层 + * + * @param canvas 画布 + * @param width 宽度 + * @param height 高度 + * @param clip 裁剪区域 + */ + protected void onDrawMask(Canvas canvas, int width, int height, Rect clip) { + Callback callback = this.callback; + if (null != callback) { + callback.onDrawMask(this, canvas, width, height, clip); + } + } + + protected void onStart() { + Callback callback = this.callback; + if (null != callback) { + callback.onScanStart(this); + } + } + + /** + * 发生错误 + * + * @param exp 异常 + */ + protected void onError(Exception exp) { + Callback callback = this.callback; + if (null != callback) { + callback.onScanError(this, exp); + } + } + + protected void onEnd() { + Callback callback = this.callback; + if (null != callback) { + callback.onScanEnd(this); + } + } + + /** + * 设置回调 + * + * @param callback 回调对象 + */ + public void setCallback(Callback callback) { + this.callback = callback; + } + + /** + * 获取摄像头 + * + * @return 摄像头 + */ + protected Camera getCamera() { + return camera; + } + + /** + * 获取摄像头信息 + * + * @return 摄像头信息 + */ + protected Camera.CameraInfo getCameraInfo() { + return cameraInfo; + } + + protected Context getContext() { + return context; + } + + /** + * 获取显示方向 + * + * @return 显示方向 + */ + protected int getDisplayRotation() { + return displayRotation; + } + + /** + * 获取裁剪区域 + * + * @return 裁剪区域 + */ + protected Rect getClipRect() { + return clipRect; + } + + /** + * 获取裁剪区域 + * + * @param out 输出的裁剪区域 + */ + public void getClipRect(Rect out) { + synchronized (lock) { + if (null != clipRect) { + out.set(clipRect); + } else { + out.setEmpty(); + } + } + } + + /** + * opengl上下文 + * + * @return opengl上下文 + */ + protected GLContext getGLContext() { + return glContext; + } + + /** + * Surface窗口对象 + * + * @return surface窗口对象 + */ + protected GLSurfaceWindow getGLSurfaceWindow() { + return glSurfaceWindow; + } + + /** + * 纹理窗口对象,用于接收摄像头图像 + * + * @return 纹理窗口对象 + */ + protected GLTextureWindow getGLTextureWindow() { + return glTextureWindow; + } + + /** + * 获取回调 + * + * @return 回调 + */ + public Callback getCallback() { + return callback; + } + + /** + * 回调 + */ + public interface Callback { + + /** + * 扫码开始 + * + * @param handler 扫码处理对象 + */ + void onScanStart(ScanHandler handler); + + /** + * 选择摄像头 + * + * @param handler 扫码处理对象 + * @return 摄像头id + */ + int onSwitchCamera(ScanHandler handler); + + /** + * 绘制遮罩层 + * + * @param handler 扫码处理对象 + * @param canvas 画布 + * @param width 宽 + * @param height 高 + * @param clip 裁剪区域 + */ + void onDrawMask(ScanHandler handler, Canvas canvas, int width, int height, Rect clip); + + /** + * 扫码错误 + * + * @param handler 扫码处理对象 + * @param exp 异常 + */ + void onScanError(ScanHandler handler, Exception exp); + + /** + * 扫码结果,触发此方法,表示已经扫码成功 + * + * @param handler 扫码处理对象 + * @param result 结果 + */ + void onScanResult(ScanHandler handler, String result); + + /** + * 扫码结束 + * + * @param handler 扫码处理对象 + */ + void onScanEnd(ScanHandler handler); + } + + public static class PreviewSize { + + private final int width; + private final int height; + private final int rotateWidth; + private final int rotateHeight; + + public PreviewSize(int width, int height, int degrees) { + this.width = width; + this.height = height; + RectF rect = new RectF(0, 0, width, height); + Matrix matrix = new Matrix(); + matrix.postRotate(degrees); + matrix.mapRect(rect); + rotateWidth = (int) rect.width(); + rotateHeight = (int) rect.height(); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getRotateWidth() { + return rotateWidth; + } + + public int getRotateHeight() { + return rotateHeight; + } + } +} diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/ScanPanel.java b/lib/src/main/java/com/mosect/lib/scanpanel/ScanPanel.java new file mode 100644 index 0000000..c24d796 --- /dev/null +++ b/lib/src/main/java/com/mosect/lib/scanpanel/ScanPanel.java @@ -0,0 +1,514 @@ +package com.mosect.lib.scanpanel; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; + +import com.mosect.lib.scanpanel.coder.FrameDecoder; + +import java.lang.reflect.Constructor; + +/** + * 扫码面板 + *

+ * <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 { + + private final static float NERA = 0.5f; + private final static float FAR = 2.5f; + private final float[] cameraMatrix = new float[16]; + private final FloatBuffer position; + private final float[] textureMatrix = new float[16]; + private final FloatBuffer textureCoord; + + public Drawer2D(ContentMatrix contentMatrix, float z) { + RectF contentRect = contentMatrix.getContentRect(); + RectF viewportRect = contentMatrix.getViewportRect(); + float safeZ = -NERA - (FAR - NERA) * z; + float[] points = new float[]{ + contentRect.left, contentRect.bottom, safeZ, + contentRect.right, contentRect.bottom, safeZ, + contentRect.left, contentRect.top, safeZ, + contentRect.right, contentRect.top, safeZ, + }; + float[] points2 = new float[points.length]; + contentMatrix.mapVec3Points(points, points2); + + position = ByteBuffer.allocateDirect(points.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + position.put(points2); + textureCoord = ByteBuffer.allocateDirect(4 * 2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + textureCoord.put(new float[]{ + 0f, 0f, + 1f, 0f, + 0f, 1f, + 1f, 1f, + }); + + Matrix.orthoM(cameraMatrix, 0, viewportRect.left, viewportRect.right, + viewportRect.top, viewportRect.bottom, NERA, FAR); + } + + public void draw(T texture, S shader) { + position.position(0); + textureCoord.position(0); + getTextureMatrix(texture, textureMatrix); + shader.draw(texture.getTextureId(), getTextureType(), cameraMatrix, position, textureMatrix, textureCoord); + } + + protected abstract void getTextureMatrix(T texture, float[] out); + + protected abstract int getTextureType(); +} diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/graphics/DrawerOES.java b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/DrawerOES.java new file mode 100644 index 0000000..281387f --- /dev/null +++ b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/DrawerOES.java @@ -0,0 +1,27 @@ +package com.mosect.lib.scanpanel.graphics; + +import android.opengl.GLES11Ext; +import android.opengl.Matrix; + +import com.mosect.lib.easygl2.GLTextureWindow; +import com.mosect.lib.scanpanel.shader.ShaderOES; + +public class DrawerOES extends Drawer2D { + + public DrawerOES(ContentMatrix contentMatrix, float z) { + super(contentMatrix, z); + } + + @Override + protected void getTextureMatrix(GLTextureWindow texture, float[] out) { + texture.updateTexImage(); + texture.getSurfaceTexture().getTransformMatrix(out); + Matrix.scaleM(out, 0, 1f, -1f, 1f); + Matrix.translateM(out, 0, 0, -1f, 0f); + } + + @Override + protected int getTextureType() { + return GLES11Ext.GL_TEXTURE_EXTERNAL_OES; + } +} diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/graphics/DrawerTEX.java b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/DrawerTEX.java new file mode 100644 index 0000000..73839e9 --- /dev/null +++ b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/DrawerTEX.java @@ -0,0 +1,23 @@ +package com.mosect.lib.scanpanel.graphics; + +import android.opengl.GLES20; +import android.opengl.Matrix; + +import com.mosect.lib.scanpanel.shader.ShaderTEX; + +public class DrawerTEX extends Drawer2D { + + public DrawerTEX(ContentMatrix contentMatrix, float z) { + super(contentMatrix, z); + } + + @Override + protected void getTextureMatrix(BitmapTexture texture, float[] out) { + Matrix.setIdentityM(out, 0); + } + + @Override + protected int getTextureType() { + return GLES20.GL_TEXTURE_2D; + } +} diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/graphics/Matrix3D.java b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/Matrix3D.java new file mode 100644 index 0000000..615dbfc --- /dev/null +++ b/lib/src/main/java/com/mosect/lib/scanpanel/graphics/Matrix3D.java @@ -0,0 +1,84 @@ +package com.mosect.lib.scanpanel.graphics; + +import android.opengl.Matrix; + +import java.util.ArrayList; +import java.util.List; + +/** + * 3D矩阵计算 + */ +public class Matrix3D { + + private final List matrixList = new ArrayList<>(); + + public void reset() { + matrixList.clear(); + } + + public void mapPoints(float[] points, float[] out) { + if (matrixList.isEmpty()) { + System.arraycopy(points, 0, out, 0, points.length); + return; + } + + float[] temp = new float[8]; + for (int i = 0; i < points.length; i += 3) { + System.arraycopy(points, i, temp, 0, 3); + temp[3] = 1f; + for (float[] matrix : matrixList) { + Matrix.multiplyMV(temp, 4, matrix, 0, temp, 0); + System.arraycopy(temp, 4, temp, 0, 4); + } + System.arraycopy(temp, 4, out, i, 3); + } + } + + public void mapVec4Points(float[] points, float[] out) { + if (matrixList.isEmpty()) { + System.arraycopy(points, 0, out, 0, points.length); + return; + } + + float[] temp = new float[8]; + for (int i = 0; i < points.length; i += 4) { + System.arraycopy(points, i, temp, 0, 4); + for (float[] matrix : matrixList) { + Matrix.multiplyMV(temp, 4, matrix, 0, temp, 0); + System.arraycopy(temp, 4, temp, 0, 4); + } + System.arraycopy(temp, 4, out, i, 4); + } + } + + public void postScale(float sx, float sy, float sz) { + float[] matrix = new float[16]; + Matrix.setIdentityM(matrix, 0); + Matrix.scaleM(matrix, 0, sx, sy, sz); + matrixList.add(matrix); + } + + public void postRotate(float degrees, float x, float y, float z) { + float[] matrix = new float[16]; + Matrix.setIdentityM(matrix, 0); + Matrix.rotateM(matrix, 0, degrees, x, y, z); + matrixList.add(matrix); + } + + public void postTranslate(float ox, float oy, float oz) { + float[] matrix = new float[16]; + Matrix.setIdentityM(matrix, 0); + Matrix.translateM(matrix, 0, ox, oy, oz); + matrixList.add(matrix); + } + + public void invert(Matrix3D out) { + out.reset(); + for (int i = matrixList.size() - 1; i >= 0; i--) { + float[] matrix = matrixList.get(i); + float[] im = new float[16]; + Matrix.invertM(im, 0, matrix, 0); + out.matrixList.add(im); + } + } +} diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/shader/Shader2D.java b/lib/src/main/java/com/mosect/lib/scanpanel/shader/Shader2D.java new file mode 100644 index 0000000..5ba86e5 --- /dev/null +++ b/lib/src/main/java/com/mosect/lib/scanpanel/shader/Shader2D.java @@ -0,0 +1,100 @@ +package com.mosect.lib.scanpanel.shader; + +import android.content.Context; +import android.opengl.GLES20; + +import com.mosect.lib.easygl2.GLException; +import com.mosect.lib.easygl2.GLShader; +import com.mosect.lib.easygl2.util.GLUtils; + +import java.nio.Buffer; + +public abstract class Shader2D extends GLShader { + + protected final Context context; + + private int cameraMatrixHandle; + private int textureMatrixHandle; + private int positionHandle; + private int textureCoordHandle; + + public Shader2D(Context context) { + this.context = context; + } + + @Override + protected void onInitProgram() { + super.onInitProgram(); + cameraMatrixHandle = getUniformLocation("cameraMatrix"); + textureMatrixHandle = getUniformLocation("textureMatrix"); + positionHandle = getAttribLocation("position"); + textureCoordHandle = getAttribLocation("textureCoord"); + } + + @Override + protected String onLoadVertSource() { + return GLUtils.loadAssetsText(context, "ScanPanel/shader_2d.vert"); + } + + protected void putCameraMatrix(float[] matrix) { + putMatrix(cameraMatrixHandle, matrix); + } + + protected void putTextureMatrix(float[] matrix) { + putMatrix(textureMatrixHandle, matrix); + } + + protected void putPosition(Buffer data) { + putAttributeValue(positionHandle, 3, data); + } + + protected void putTextureCoord(Buffer data) { + putAttributeValue(textureCoordHandle, 2, data); + } + + protected void putAttributeValue(int handle, int size, Buffer data) { + GLES20.glVertexAttribPointer(handle, size, GLES20.GL_FLOAT, false, 4 * size, data); + GLException.checkGLError("glVertexAttribPointer"); + GLES20.glEnableVertexAttribArray(handle); + GLException.checkGLError("glEnableVertexAttribArray"); + } + + protected void putMatrix(int handle, float[] matrix) { + GLES20.glUniformMatrix4fv(handle, 1, false, matrix, 0); + GLException.checkGLError("glUniformMatrix4fv"); + } + + public void draw(int textureID, int textureType, float[] cameraMatrix, Buffer position, + float[] textureMatrix, Buffer textureCoord) { + // 禁止深度写入 + GLES20.glDepthMask(false); + // 开启混合模式 + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(textureType, textureID); + + GLES20.glUseProgram(getProgramId()); + GLException.checkGLError("glUseProgram"); + + putCameraMatrix(cameraMatrix); + putPosition(position); + putTextureCoord(textureCoord); + putTextureMatrix(textureMatrix); + putExt(); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + GLException.checkGLError("glDrawArrays"); + + GLES20.glBindTexture(textureType, 0); + + // 开启深度写入 + GLES20.glDepthMask(true); + // 关闭混合模式 + GLES20.glDisable(GLES20.GL_BLEND); + } + + protected void putExt() { + } +} diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/shader/ShaderOES.java b/lib/src/main/java/com/mosect/lib/scanpanel/shader/ShaderOES.java new file mode 100644 index 0000000..30a744e --- /dev/null +++ b/lib/src/main/java/com/mosect/lib/scanpanel/shader/ShaderOES.java @@ -0,0 +1,25 @@ +package com.mosect.lib.scanpanel.shader; + +import android.content.Context; +import android.opengl.GLES11Ext; + +import com.mosect.lib.easygl2.util.GLUtils; + +import java.nio.Buffer; + +public class ShaderOES extends Shader2D { + + public ShaderOES(Context context) { + super(context); + } + + @Override + protected String onLoadFragSource() { + return GLUtils.loadAssetsText(context, "ScanPanel/shader_oes.frag"); + } + + public void draw(int textureID, float[] cameraMatrix, Buffer position, + float[] textureMatrix, Buffer textureCoord) { + draw(textureID, GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraMatrix, position, textureMatrix, textureCoord); + } +} diff --git a/lib/src/main/java/com/mosect/lib/scanpanel/shader/ShaderTEX.java b/lib/src/main/java/com/mosect/lib/scanpanel/shader/ShaderTEX.java new file mode 100644 index 0000000..3d9ea8f --- /dev/null +++ b/lib/src/main/java/com/mosect/lib/scanpanel/shader/ShaderTEX.java @@ -0,0 +1,44 @@ +package com.mosect.lib.scanpanel.shader; + +import android.content.Context; +import android.opengl.GLES20; + +import com.mosect.lib.easygl2.GLException; +import com.mosect.lib.easygl2.util.GLUtils; + +import java.nio.Buffer; + +public class ShaderTEX extends Shader2D { + + private int textureNumHandle; + + public ShaderTEX(Context context) { + super(context); + } + + @Override + protected String onLoadFragSource() { + return GLUtils.loadAssetsText(context, "ScanPanel/shader_tex.frag"); + } + + @Override + protected void onInitProgram() { + super.onInitProgram(); + textureNumHandle = getUniformLocation("textureNum"); + } + + protected void putTextureNum(int num) { + GLES20.glUniform1i(textureNumHandle, num); + GLException.checkGLError("glUniform1i"); + } + + @Override + protected void putExt() { + super.putExt(); + putTextureNum(0); + } + + public void draw(int textureID, float[] cameraMatrix, Buffer position, float[] textureMatrix, Buffer textureCoord) { + draw(textureID, GLES20.GL_TEXTURE_2D, cameraMatrix, position, textureMatrix, textureCoord); + } +} diff --git a/lib/src/main/res/values/attrs.xml b/lib/src/main/res/values/attrs.xml new file mode 100644 index 0000000..153427e --- /dev/null +++ b/lib/src/main/res/values/attrs.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/lib/src/test/java/com/mosect/lib/scanpanel/ExampleUnitTest.java b/lib/src/test/java/com/mosect/lib/scanpanel/ExampleUnitTest.java new file mode 100644 index 0000000..960f6a4 --- /dev/null +++ b/lib/src/test/java/com/mosect/lib/scanpanel/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.mosect.lib.scanpanel; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..7d36a72 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url 'https://jitpack.io' } + } +} +rootProject.name = "Scan panel" +include ':app' +include ':lib'