1
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
||||
package="io.dcloud.uni_modules.xMlkitScannigU">
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-feature android:name="android.hardware.camera.any" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<application>
|
||||
<meta-data android:name="UTS_MLKIT_SCANNIG" android:value="xMlkitScannigU" />
|
||||
<activity android:name="uts.sdk.modules.xMlkitScannigU.DemoActivity"></activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,821 @@
|
||||
import ListView from 'android.widget.ListView';
|
||||
import FrameLayout from 'android.widget.FrameLayout';
|
||||
import CameraManager from 'android.hardware.camera2.CameraManager'
|
||||
import CameraDevice from 'android.hardware.camera2.CameraDevice'
|
||||
import StateCallback from 'android.hardware.camera2.CameraDevice.StateCallback'
|
||||
import CameraCaptureSession from 'android.hardware.camera2.CameraCaptureSession'
|
||||
|
||||
import CaptureRequest from 'android.hardware.camera2.CaptureRequest'
|
||||
import TotalCaptureResult from 'android.hardware.camera2.TotalCaptureResult'
|
||||
import StreamConfigurationMap from 'android.hardware.camera2.params.StreamConfigurationMap'
|
||||
import MeteringRectangle from 'android.hardware.camera2.params.MeteringRectangle'
|
||||
import OutputConfiguration from 'android.hardware.camera2.params.OutputConfiguration'
|
||||
import SessionConfiguration from 'android.hardware.camera2.params.SessionConfiguration'
|
||||
import CameraCharacteristics from 'android.hardware.camera2.CameraCharacteristics'
|
||||
import CameraMetadata from 'android.hardware.camera2.CameraMetadata'
|
||||
import Range from 'android.util.Range';
|
||||
|
||||
import Executor from 'java.util.concurrent.Executor'
|
||||
import Looper from 'android.os.Looper'
|
||||
import ImageFormat from 'android.graphics.ImageFormat'
|
||||
import Size from 'android.util.Size'
|
||||
import SurfaceTexture from 'android.graphics.SurfaceTexture'
|
||||
import Handler from 'android.os.Handler'
|
||||
import Surface from 'android.view.Surface'
|
||||
import Image from 'android.media.Image'
|
||||
import LinearLayout from 'android.widget.LinearLayout';
|
||||
import Intent from 'android.content.Intent';
|
||||
import Integer from 'java.lang.Integer';
|
||||
import ByteOrder from 'java.nio.ByteOrder';
|
||||
|
||||
import SurfaceHolder from 'android.view.SurfaceHolder'
|
||||
import SurfaceView from 'android.view.SurfaceView'
|
||||
import TextureView from 'android.view.TextureView'
|
||||
import View from 'android.view.View'
|
||||
import Context from 'android.content.Context'
|
||||
import Manifest from "android.Manifest";
|
||||
import Uri from 'android.net.Uri';
|
||||
import File from 'java.io.File'
|
||||
import System from 'java.lang.System'
|
||||
import Activity from 'android.app.Activity';
|
||||
import Bundle from 'android.os.Bundle';
|
||||
import Intent from 'android.content.Intent';
|
||||
import IntentFilter from 'android.content.IntentFilter';
|
||||
|
||||
import Rect from 'android.graphics.Rect';
|
||||
import Matrix from 'android.graphics.Matrix'
|
||||
import ImageReader from 'android.media.ImageReader'
|
||||
import OnImageAvailableListener from 'android.media.ImageReader.OnImageAvailableListener'
|
||||
|
||||
|
||||
import ByteBuffer from 'java.nio.ByteBuffer'
|
||||
import MediaStore from 'android.provider.MediaStore'
|
||||
import Plane from 'android.media.Image.Plane'
|
||||
import Bitmap from "android.graphics.Bitmap"
|
||||
import BitmapFactory from "android.graphics.BitmapFactory"
|
||||
import Vibrator from "android.os.Vibrator"
|
||||
import Animation from "android.view.animation.Animation"
|
||||
import ScaleAnimation from "android.view.animation.ScaleAnimation"
|
||||
import AnimationUtils from "android.view.animation.AnimationUtils"
|
||||
|
||||
|
||||
import Color from "android.graphics.Color"
|
||||
import Window from "android.view.Window"
|
||||
import WindowManager from "android.view.WindowManager"
|
||||
import TextView from "android.widget.TextView"
|
||||
import RelativeLayout from "android.widget.RelativeLayout"
|
||||
import ImageView from "android.widget.ImageView"
|
||||
import HandlerThread from 'android.os.HandlerThread'
|
||||
import YuvImage from 'android.graphics.YuvImage'
|
||||
import ByteArrayOutputStream from 'java.io.ByteArrayOutputStream'
|
||||
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
|
||||
import MotionEvent from 'android.view.MotionEvent';
|
||||
import ViewGroup from 'android.view.ViewGroup';
|
||||
|
||||
|
||||
import IOException from 'java.io.IOException'
|
||||
import InputStream from 'java.io.InputStream'
|
||||
import BarcodeScanner from "com.google.mlkit.vision.barcode.BarcodeScanner"
|
||||
import BarcodeScannerOptions from "com.google.mlkit.vision.barcode.BarcodeScannerOptions"
|
||||
import BarcodeScanning from "com.google.mlkit.vision.barcode.BarcodeScanning"
|
||||
import ZoomSuggestionOptions from "com.google.mlkit.vision.barcode.ZoomSuggestionOptions"
|
||||
import ZoomCallback from "com.google.mlkit.vision.barcode.ZoomSuggestionOptions.ZoomCallback"
|
||||
import Barcode from "com.google.mlkit.vision.barcode.common.Barcode"
|
||||
import InputImage from "com.google.mlkit.vision.common.InputImage"
|
||||
import xView from "./libview/view.uts"
|
||||
import Task from "com.google.android.gms.tasks.Task"
|
||||
import List from "java.util.List"
|
||||
type buffoptsPlanesType = {
|
||||
data : ByteBuffer,
|
||||
getRowStride : number,
|
||||
getPixelStride : number,
|
||||
}
|
||||
type buffoptsType = {
|
||||
width : number,
|
||||
height : number,
|
||||
format : number,
|
||||
rect : Rect,
|
||||
planes : buffoptsPlanesType[],
|
||||
}
|
||||
|
||||
import { SCANNING_OPTIONS, CAMERA_PHOTO_SIZE, CallFunType, SCANNING_PHOTO_RESULT, SCANNING_PHOTO_RESULT_BOUND } from "../interface.uts"
|
||||
import { decoderUriPathToQr, decoderBuffToQr } from "./index.uts"
|
||||
|
||||
type cameraListNameType = { id : string, device : CameraCharacteristics }
|
||||
|
||||
function px2dp(n : number) : number {
|
||||
const mets = UTSAndroid.getAppContext()!.resources!.getDisplayMetrics()
|
||||
return mets.density * n
|
||||
}
|
||||
|
||||
export class xCamera {
|
||||
parentView : FrameLayout;
|
||||
// 相机区域视图
|
||||
cameraView : SurfaceView | null = null;
|
||||
// 识别点的区域
|
||||
resultView : RelativeLayout | null = null;
|
||||
|
||||
|
||||
viewBox_width = 0
|
||||
viewBox_height = 0
|
||||
|
||||
imgPointsBycanmarea = [] as RelativeLayout[]
|
||||
|
||||
options = {
|
||||
width: 800,
|
||||
height: 800,
|
||||
cameraDevice: 'BACK',
|
||||
scanSpeedduration: 450,
|
||||
scanningQrCode: true,
|
||||
isSavePhoto: false,
|
||||
qrCodeType: null,
|
||||
vibatar: true,
|
||||
zoom: 3,
|
||||
continuousScan: false
|
||||
} as SCANNING_OPTIONS
|
||||
|
||||
/** 手动震动服务 */
|
||||
vibrator = null as null | Vibrator
|
||||
context : Context;
|
||||
// 相机旋转的角度。
|
||||
cameraRate = new Map<Number, Number>([
|
||||
[Surface.ROTATION_0, 0],
|
||||
[Surface.ROTATION_90, 0],
|
||||
[Surface.ROTATION_180, 0],
|
||||
[Surface.ROTATION_270, 0]
|
||||
])
|
||||
// 相机服务
|
||||
cameraService : CameraManager
|
||||
// 相机列表,name,id
|
||||
cameraListName = new Map<string, cameraListNameType>();
|
||||
//
|
||||
cameraPreviewSize = { width: 800, height: 800 } as CAMERA_PHOTO_SIZE
|
||||
// 当前预览的什么是设备
|
||||
cameraDevice : CameraDevice | null = null
|
||||
cameraDeviceCaptureSession : CameraCaptureSession | null = null
|
||||
sufaceHolderView : Surface | null = null
|
||||
// 当前是否在解析码制中。
|
||||
isDecoderQring = false
|
||||
// 当前相机是否正在预览中。
|
||||
isOpeningCameraing = false;
|
||||
|
||||
callFunEvent : CallFunType = (str : string[], type : 'auto' | 'click') => { }
|
||||
|
||||
// 临时存放的识别成功的结果,防止重复扫码。
|
||||
tempresultList = [] as string[];
|
||||
|
||||
isAppPause = false
|
||||
|
||||
constructor(GroupView : FrameLayout, options : SCANNING_OPTIONS | null = null) {
|
||||
this.context = UTSAndroid.getAppContext()! as Context;
|
||||
this.parentView = GroupView;
|
||||
if (options != null) {
|
||||
this.options = {
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
cameraDevice: options.cameraDevice,
|
||||
scanSpeedduration: options.scanSpeedduration == null ? this.options.scanSpeedduration : options.scanSpeedduration,
|
||||
scanningQrCode: options.scanningQrCode == null ? this.options.scanningQrCode : options.scanningQrCode,
|
||||
isSavePhoto: options.isSavePhoto == null ? this.options.isSavePhoto : options.isSavePhoto,
|
||||
qrCodeType: options.qrCodeType == null ? this.options.qrCodeType : options.qrCodeType,
|
||||
vibatar: options.vibatar == null ? this.options.vibatar : options.vibatar,
|
||||
zoom: options.zoom == null ? this.options.zoom : options.zoom,
|
||||
continuousScan: options.continuousScan == null ? this.options.continuousScan : options.continuousScan
|
||||
} as SCANNING_OPTIONS;
|
||||
}
|
||||
this.cameraService = this.context!.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
this.vibrator = this.context!.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator;
|
||||
this._createCameraView()
|
||||
this._creatCameraResultView()
|
||||
}
|
||||
//震动手机
|
||||
setVibatar() {
|
||||
if (this.vibrator == null) return
|
||||
// 检查设备是否支持震动
|
||||
if (this.vibrator!.hasVibrator()) {
|
||||
// 触发短暂的震动,持续时间为200毫秒
|
||||
this.vibrator!.vibrate(80);
|
||||
}
|
||||
}
|
||||
/** 创建相机预览区域 */
|
||||
private _createCameraView() {
|
||||
let sv = new SurfaceView(this.context)
|
||||
// 让相机的高和宽与组件对齐。
|
||||
let layaout = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
sv.setLayoutParams(layaout)
|
||||
this.cameraView = sv;
|
||||
|
||||
this.parentView.addView(this.cameraView!)
|
||||
}
|
||||
// 创建一个动画层容器
|
||||
private _creatCameraResultView() {
|
||||
let t = this;
|
||||
let resultView = new RelativeLayout(this.context)
|
||||
let layaout = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
resultView.setLayoutParams(layaout)
|
||||
// resultView.setBackgroundColor(Color.argb(30,255,0,0))
|
||||
this.resultView = resultView;
|
||||
this.parentView.addView(this.resultView!)
|
||||
|
||||
//监听尺寸变化。
|
||||
class previewChangeRectadd extends View.OnLayoutChangeListener {
|
||||
override onLayoutChange(v : View, left : Int, top : Int, right : Int, bottom : Int,
|
||||
oldLeft : Int, oldTop : Int, oldRight : Int, oldBottom : Int
|
||||
) {
|
||||
t.viewBox_width = right - left;
|
||||
t.viewBox_height = bottom - top;
|
||||
|
||||
|
||||
// t.parentView!.removeOnLayoutChangeListener(this);
|
||||
}
|
||||
}
|
||||
this.parentView!.addOnLayoutChangeListener(new previewChangeRectadd())
|
||||
|
||||
|
||||
}
|
||||
openCamera() : Promise<boolean> {
|
||||
let _this = this;
|
||||
if (this.isOpeningCameraing) {
|
||||
this.closeCamera()
|
||||
}
|
||||
return new Promise((res, rej) => {
|
||||
_this.checkPermissions()
|
||||
.then((res : boolean) => {
|
||||
if (!res) {
|
||||
uni.showToast({ title: "未授权相机使用权", icon: "none" })
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_this.getCamreaListName()
|
||||
_this.getCamreaDeviceSizeList()
|
||||
_this.setCamreaSize()
|
||||
_this._openCamera();
|
||||
_this.setCamreaFocalLength(1)
|
||||
_this.addListenApp()
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
res(true)
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: "未授权相机使用权", icon: "none" })
|
||||
rej(false)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
addListenApp() {
|
||||
let t = this;
|
||||
let listener : (() => void) | null = null;
|
||||
listener = () => {
|
||||
t.isAppPause = true;
|
||||
// console.log(UTSAndroid.getDispatcher())
|
||||
if (t.isOpeningCameraing) {
|
||||
t.pauseCamera()
|
||||
}
|
||||
if (listener != null) {
|
||||
UTSAndroid.offAppActivityPause(listener!);
|
||||
}
|
||||
}
|
||||
UTSAndroid.onAppActivityPause(listener);
|
||||
|
||||
let listener2 : (() => void) | null = null;
|
||||
listener2 = () => {
|
||||
t.isAppPause = false
|
||||
if (t.isOpeningCameraing) {
|
||||
t.openCamera()
|
||||
}
|
||||
if (listener2 != null) {
|
||||
UTSAndroid.offAppActivityResume(listener2!);
|
||||
}
|
||||
|
||||
}
|
||||
UTSAndroid.onAppActivityResume(listener2);
|
||||
|
||||
}
|
||||
pauseCamera() {
|
||||
this.sufaceHolderView?.release()
|
||||
this.cameraDeviceCaptureSession?.close()
|
||||
this.cameraDevice?.close()
|
||||
this.cameraDevice = null;
|
||||
this.cameraDeviceCaptureSession = null;
|
||||
this.sufaceHolderView = null;
|
||||
}
|
||||
closeCamera() {
|
||||
this.sufaceHolderView?.release()
|
||||
this.cameraDeviceCaptureSession?.close()
|
||||
this.cameraDevice?.close()
|
||||
this.cameraDevice = null;
|
||||
this.cameraDeviceCaptureSession = null;
|
||||
this.isOpeningCameraing = false;
|
||||
this.sufaceHolderView = null;
|
||||
for (let i = 0; i < this.imgPointsBycanmarea.length; i++) {
|
||||
this.resultView?.removeView(this.imgPointsBycanmarea[i])
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
private _openCamera() {
|
||||
let _this = this;
|
||||
if (this.cameraListName.size == 0) return;
|
||||
let camreaNow = this.cameraListName.get(this.options.cameraDevice);
|
||||
if (camreaNow == null) return;
|
||||
class CamerCallTstasBack implements StateCallback {
|
||||
constructor() {
|
||||
|
||||
}
|
||||
override onOpened(camera : CameraDevice) : void {
|
||||
console.log('打开成功')
|
||||
_this.cameraDevice = camera
|
||||
_this.createCameraPreviewSession()
|
||||
|
||||
}
|
||||
override onDisconnected(camera : CameraDevice) : void {
|
||||
console.log('连接失败')
|
||||
_this.cameraDevice?.close()
|
||||
_this.cameraDevice = null
|
||||
}
|
||||
override onError(camera : CameraDevice, error : Int) : void {
|
||||
console.log('出错')
|
||||
_this.cameraDevice?.close()
|
||||
_this.cameraDevice = null
|
||||
}
|
||||
override onClosed(camera : CameraDevice) : void {
|
||||
console.log('关闭')
|
||||
_this.cameraDevice?.close()
|
||||
_this.cameraDevice = null
|
||||
}
|
||||
|
||||
}
|
||||
this.cameraService.openCamera(camreaNow.id! as string, new CamerCallTstasBack(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查相机权限
|
||||
*/
|
||||
checkPermissions() : Promise<boolean> {
|
||||
let permissionCheck = ["android.permission.CAMERA"]
|
||||
let _this = this;
|
||||
// 请求拍照权限
|
||||
return new Promise(res => {
|
||||
if (UTSAndroid.checkSystemPermissionGranted(UTSAndroid.getUniActivity()!, permissionCheck)) {
|
||||
// _this.cameraPermissioPass = true
|
||||
res(true)
|
||||
} else {
|
||||
console.log("当前不具备指定权限")
|
||||
// 请求拍照权限
|
||||
UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, permissionCheck, function (allRight : boolean, _ : string[]) {
|
||||
// _this.cameraPermissioPass = allRight
|
||||
res(true)
|
||||
}, function (_ : boolean, _ : string[]) {
|
||||
//用户拒绝了部分权限
|
||||
// _this.cameraPermissioPass = false
|
||||
res(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相机设备列表
|
||||
*/
|
||||
getCamreaListName(deviceName : 'FRONT' | 'BACK' | null = null) {
|
||||
// 前置还是后置。
|
||||
let deviceLens = CameraCharacteristics.LENS_FACING_BACK;
|
||||
let dname = this.options.cameraDevice
|
||||
if (deviceName != null) {
|
||||
dname = deviceName!
|
||||
}
|
||||
if (dname == 'FRONT') {
|
||||
deviceLens = CameraCharacteristics.LENS_FACING_FRONT;
|
||||
}
|
||||
if (dname == 'BACK') {
|
||||
deviceLens = CameraCharacteristics.LENS_FACING_BACK;
|
||||
}
|
||||
this.cameraListName.clear()
|
||||
|
||||
|
||||
for (caIds in this.cameraService.getCameraIdList()) {
|
||||
let ps = this.cameraService.getCameraCharacteristics(caIds as string);
|
||||
let facing = ps.get(CameraCharacteristics.LENS_FACING);
|
||||
if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
|
||||
this.cameraListName.set('FRONT', { id: caIds as string, device: ps } as cameraListNameType)
|
||||
}
|
||||
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
|
||||
this.cameraListName.set('BACK', { id: caIds as string, device: ps } as cameraListNameType)
|
||||
}
|
||||
}
|
||||
|
||||
this.setCamreaSize();
|
||||
}
|
||||
/**
|
||||
* 获取相机设备尺寸列表
|
||||
* 调用此函数的前提是要先获取设备列表,要先调用getCamreaListName函数
|
||||
*/
|
||||
getCamreaDeviceSizeList() : CAMERA_PHOTO_SIZE[] {
|
||||
let sizelist = [] as CAMERA_PHOTO_SIZE[]
|
||||
if (this.cameraListName.size == 0) {
|
||||
console.error("请先调用getCamreaListName获取设备列表")
|
||||
return sizelist;
|
||||
}
|
||||
let camrea = this.cameraListName.get(this.options.cameraDevice) as cameraListNameType;
|
||||
if (camrea != null) {
|
||||
let maps = camrea!.device!.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) as StreamConfigurationMap
|
||||
if (maps != null) {
|
||||
|
||||
let testSize = maps!.getOutputSizes(ImageFormat.JPEG)
|
||||
for (sz in testSize) {
|
||||
let w = sz.getWidth()
|
||||
let h = sz.getHeight()
|
||||
sizelist.push({ width: w, height: h } as CAMERA_PHOTO_SIZE)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return sizelist;
|
||||
}
|
||||
/**
|
||||
* 设置相机焦距
|
||||
* 0-1之间。
|
||||
* 暂未实现。
|
||||
*/
|
||||
setCamreaFocalLength(focal : number) {
|
||||
|
||||
if (this.cameraListName.size == 0) {
|
||||
console.error("请先调用getCamreaListName获取设备列表")
|
||||
return
|
||||
}
|
||||
let camrea = this.cameraListName.get(this.options.cameraDevice) as cameraListNameType;
|
||||
|
||||
if (camrea != null) {
|
||||
let zoomMax = camrea!.device!.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)
|
||||
let nowZoom = camrea!.device!.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)
|
||||
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置相机前置还是后置,设置后需要重新打开相机。
|
||||
* BACK 后置
|
||||
* FRONT 前置
|
||||
*/
|
||||
setCamreaDevice(name : 'FRONT' | 'BACK') {
|
||||
this.options.cameraDevice = name;
|
||||
}
|
||||
/**
|
||||
* 设置相机摄像尺寸,通过options中的width,height来匹配设置
|
||||
* 返回的不一定是真实的options中的宽高,只是与其相近的值。
|
||||
* 以设置后要重新打开。相机生效。
|
||||
* 返回相机生效的尺寸
|
||||
*/
|
||||
setCamreaSize(width : number | null = null, height : number | null = null) : CAMERA_PHOTO_SIZE {
|
||||
// 宽高列表
|
||||
const dimensions = this.getCamreaDeviceSizeList()
|
||||
// 目标宽高
|
||||
const targetWidth = width != null ? width! : this.options.width;
|
||||
const targetHeight = height != null ? height! : this.options.height;
|
||||
const targetRatio = targetWidth / targetHeight;
|
||||
// 找到最接近目标比例的宽高值
|
||||
const closestDimension = dimensions.reduce((prev : CAMERA_PHOTO_SIZE, curr : CAMERA_PHOTO_SIZE) : CAMERA_PHOTO_SIZE => {
|
||||
const currRatio = curr.width / curr.height;
|
||||
const prevRatio = prev.width / prev.height;
|
||||
const currDiffRatio = Math.abs(currRatio - targetRatio);
|
||||
const prevDiffRatio = Math.abs(prevRatio - targetRatio);
|
||||
|
||||
// 计算宽高值与目标值的差的平方和
|
||||
const currDiffValue = Math.pow(curr.width - targetWidth, 2) + Math.pow(curr.height - targetHeight, 2);
|
||||
const prevDiffValue = Math.pow(prev.width - targetWidth, 2) + Math.pow(prev.height - targetHeight, 2);
|
||||
|
||||
// 给比例差和值差赋予不同的权重,这里假设两者权重相同
|
||||
const currTotalDiff = currDiffRatio + currDiffValue;
|
||||
const prevTotalDiff = prevDiffRatio + prevDiffValue;
|
||||
|
||||
return currTotalDiff < prevTotalDiff ? curr : prev;
|
||||
}, dimensions[0]);
|
||||
this.cameraPreviewSize = closestDimension
|
||||
|
||||
return closestDimension;
|
||||
}
|
||||
|
||||
// 获取当前相机的旋转角度。
|
||||
getCamraRotation() : Number {
|
||||
let sensorOrientation = 0
|
||||
let tempra = 0
|
||||
if (this.cameraListName.size == 0) return sensorOrientation;
|
||||
let camreaNow = this.cameraListName.get(this.options.cameraDevice);
|
||||
if (camreaNow == null) return sensorOrientation;
|
||||
|
||||
let windowManager = UTSAndroid.getUniActivity()!.getWindowManager() as WindowManager
|
||||
|
||||
let deviceRotation = windowManager!.getDefaultDisplay().getRotation()
|
||||
let rotationCompensation = this.cameraRate.get(deviceRotation) as number
|
||||
|
||||
tempra = camreaNow.device.get(CameraCharacteristics.SENSOR_ORIENTATION)!! as number
|
||||
|
||||
if (this.options.cameraDevice == 'FRONT') {
|
||||
tempra = ((tempra + rotationCompensation) % 360)
|
||||
} else {
|
||||
tempra = ((tempra - rotationCompensation + 360) % 360)
|
||||
}
|
||||
return tempra
|
||||
}
|
||||
|
||||
// 设置识别成功的回调函数
|
||||
setCallEvent(call : CallFunType) {
|
||||
this.callFunEvent = call;
|
||||
}
|
||||
|
||||
// 创建视频预览流。
|
||||
private createCameraPreviewSession() {
|
||||
if (this.cameraView == null) return;
|
||||
|
||||
// 设置预览区域
|
||||
let previewAspectRatio = this.cameraPreviewSize.width / this.cameraPreviewSize.height;
|
||||
let layoutWidth = this.viewBox_width
|
||||
let layoutHeight = this.viewBox_height
|
||||
let layoutAspectRatio = layoutWidth / layoutHeight;
|
||||
|
||||
|
||||
let holder = this.cameraView!.getHolder()
|
||||
let suface = holder!.getSurface();
|
||||
this.sufaceHolderView = suface
|
||||
|
||||
if (this.cameraDevice == null) return;
|
||||
let t = this;
|
||||
let previewRequestBuilder = this.cameraDevice!.createCaptureRequest(CameraDevice.PREVIEW)
|
||||
previewRequestBuilder?.addTarget(suface)
|
||||
|
||||
let totalWidth = this.cameraPreviewSize.width
|
||||
let totalHeight = this.cameraPreviewSize.height
|
||||
|
||||
|
||||
// 再添加一个image来预览
|
||||
let minh = Math.min(this.options.width, this.options.height)
|
||||
let reader = ImageReader.newInstance(minh.toInt(), minh.toInt(), ImageFormat.YUV_420_888, 1)
|
||||
previewRequestBuilder?.addTarget(reader.getSurface())
|
||||
let outsufaces = [suface, reader.getSurface()] as Surface[]
|
||||
|
||||
|
||||
class sessionCallback extends CameraCaptureSession.StateCallback {
|
||||
constructor() { }
|
||||
override onConfigured(session : CameraCaptureSession) {
|
||||
t.cameraDeviceCaptureSession = session
|
||||
t.isOpeningCameraing = true;
|
||||
try {
|
||||
t.cameraDeviceCaptureSession?.setRepeatingRequest(previewRequestBuilder.build(), null, null)
|
||||
} catch (e) {
|
||||
console.log("Failed to start camera preview session.", e)
|
||||
}
|
||||
}
|
||||
override onConfigureFailed(session : CameraCaptureSession) {
|
||||
|
||||
}
|
||||
override onClosed(session : CameraCaptureSession) {
|
||||
t.closeCamera()
|
||||
}
|
||||
override onSurfacePrepared(session : CameraCaptureSession, testSuface : Surface) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
reader.setOnImageAvailableListener(this.createCameraImageRenderLisen() as OnImageAvailableListener, null)
|
||||
this.cameraDevice!.createCaptureSession(outsufaces, new sessionCallback(), null)
|
||||
|
||||
|
||||
}
|
||||
|
||||
private createCameraImageRenderLisen() : any {
|
||||
let t = this;
|
||||
class imageReaderCallback extends ImageReader.OnImageAvailableListener {
|
||||
private currentImage = null as Image | null;
|
||||
override onImageAvailable(irader : ImageReader) {
|
||||
if (this.currentImage != null) {
|
||||
this.currentImage!.close(); // 先关闭上一帧的Image
|
||||
}
|
||||
let curimage = irader.acquireNextImage();
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class IntentRunable extends Runnable {
|
||||
override run() {
|
||||
if (t.isDecoderQring || !t.isOpeningCameraing || t.isAppPause) return;
|
||||
try {
|
||||
t.qrDecoder(curimage!)
|
||||
} catch (e) {
|
||||
t.closeCamera()
|
||||
console.error('应用异常:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.currentImage = curimage
|
||||
if (this.currentImage != null) {
|
||||
new Thread(new IntentRunable()).start()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return new imageReaderCallback()
|
||||
}
|
||||
|
||||
// 设置图片上的小圆的识别位置。
|
||||
setImageViewRusletBox(width : number, height : number, realWidth : number, realHeight : number, jieguo : SCANNING_PHOTO_RESULT) {
|
||||
let t = this;
|
||||
let wRatio = t.viewBox_width / realWidth;
|
||||
let hRatio = t.viewBox_height / realHeight;
|
||||
let btnwidth = 120;
|
||||
let zoom = 10 - this.options.zoom!
|
||||
|
||||
for (let i = 0; i < jieguo.bounds.length; i++) {
|
||||
let item = jieguo.bounds[i];
|
||||
let layoutParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
|
||||
|
||||
let x = wRatio * item.centerX
|
||||
let y = hRatio * item.centerY
|
||||
|
||||
layoutParams.topMargin = (y - 60).toInt() // 设置顶部边距
|
||||
layoutParams.leftMargin = (x - 60).toInt() // 设置左边距
|
||||
let relativeLayout = new RelativeLayout(t.context)
|
||||
relativeLayout.layoutParams = layoutParams
|
||||
|
||||
const rtxt = jieguo.text[i]
|
||||
let v = new xView(t.context)
|
||||
v.setSize(120, 120)
|
||||
.setRadius(100)
|
||||
.setBackgroundColor('#21d429')
|
||||
.setBorder(12, "#ffffff", false)
|
||||
.setClick((event : MotionEvent) => {
|
||||
try {
|
||||
t.callFunEvent([rtxt], 'click')
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
})
|
||||
let ov = v.getView();
|
||||
|
||||
|
||||
// 创建缩放动画
|
||||
let scaleAnimation = new ScaleAnimation(1.0.toFloat(), 0.7.toFloat(), 1.0.toFloat(), 0.7.toFloat(), Animation.RELATIVE_TO_SELF, 0.7.toFloat(), Animation.RELATIVE_TO_SELF, 0.7.toFloat());
|
||||
scaleAnimation.setDuration(600); // 设置动画持续时间
|
||||
scaleAnimation.setRepeatMode(Animation.REVERSE); // 设置重复模式为反向
|
||||
scaleAnimation.setRepeatCount(Animation.INFINITE); // 设置重复次数为无限
|
||||
// 启动动画
|
||||
ov.startAnimation(scaleAnimation);
|
||||
|
||||
relativeLayout.addView(ov)
|
||||
|
||||
this.imgPointsBycanmarea.push(relativeLayout)
|
||||
this.resultView!.addView(relativeLayout)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
private YUV_420_888toNV21(opts : buffoptsType) : ByteArray {
|
||||
|
||||
let crop = opts.rect;
|
||||
let format = opts.format;
|
||||
let width = crop.width();
|
||||
let height = crop.height();
|
||||
let planes = opts.planes!;
|
||||
let data = new ByteArray((width * height * ImageFormat.getBitsPerPixel(format.toInt()) / 8).toInt());
|
||||
let rowData = new ByteArray((planes[0].getRowStride).toInt());
|
||||
let channelOffset = 0;
|
||||
let outputStride = 1;
|
||||
for (let i = 0; i < planes.length; i++) {
|
||||
switch (i) {
|
||||
case 0:
|
||||
channelOffset = 0;
|
||||
outputStride = 1;
|
||||
break;
|
||||
case 1:
|
||||
channelOffset = width * height;
|
||||
outputStride = 1;
|
||||
break;
|
||||
case 2:
|
||||
channelOffset = width * height * 1.25;
|
||||
outputStride = 1;
|
||||
break;
|
||||
}
|
||||
let buffer = planes[i.toInt()].data!;
|
||||
let rowStride = planes[i.toInt()].getRowStride;
|
||||
let pixelStride = planes[i.toInt()].getPixelStride;
|
||||
|
||||
let shift = (i == 0) ? 0 : 1;
|
||||
let w = width >> shift;
|
||||
let h = height >> shift;
|
||||
buffer.position((rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift)).toInt());
|
||||
for (let row = 0; row < h; row++) {
|
||||
let length = 0;
|
||||
if (pixelStride == 1 && outputStride == 1) {
|
||||
length = w;
|
||||
buffer.get(data, channelOffset.toInt(), length.toInt());
|
||||
channelOffset += length;
|
||||
} else {
|
||||
length = (w - 1) * pixelStride + 1;
|
||||
buffer.get(rowData, 0, length.toInt());
|
||||
for (let col = 0; col < w; col++) {
|
||||
data[channelOffset.toInt()] = rowData[(col * pixelStride).toInt()];
|
||||
channelOffset += outputStride;
|
||||
}
|
||||
}
|
||||
if (row < h - 1) {
|
||||
buffer.position((buffer.position() + rowStride - length).toInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private qrDecoder(image : Image) {
|
||||
let t = this;
|
||||
if (!this.isDecoderQring && !this.isAppPause) {
|
||||
this.isDecoderQring = true;
|
||||
|
||||
let width = image.getWidth();
|
||||
let height = image.getHeight();
|
||||
let frameSize = width * height;
|
||||
let qFrameSize = frameSize / 4;
|
||||
// 获取各平面的数据与信息
|
||||
let planes = image.getPlanes();
|
||||
|
||||
let yBufferCopy = [] as ByteArray[]
|
||||
let yrurvrNumber = [] as number[]
|
||||
|
||||
let yr = planes[0]
|
||||
let ur = planes[1]
|
||||
let vr = planes[2]
|
||||
let oraingMax = yr.getBuffer().limit()
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
let buffer = planes[i.toInt()].getBuffer();
|
||||
yBufferCopy.push(new ByteArray(buffer.remaining().toInt()))
|
||||
buffer.get(yBufferCopy[i.toInt()]);
|
||||
}
|
||||
let y = ByteBuffer.wrap(yBufferCopy[0]!)
|
||||
let u = ByteBuffer.wrap(yBufferCopy[1]!)
|
||||
let v = ByteBuffer.wrap(yBufferCopy[2]!)
|
||||
|
||||
let opts = {
|
||||
width,
|
||||
height,
|
||||
format: image.getFormat(),
|
||||
rect: image.getCropRect(),
|
||||
planes: [
|
||||
{
|
||||
data: y,
|
||||
getRowStride: yr.getRowStride(),
|
||||
getPixelStride: yr.getPixelStride(),
|
||||
},
|
||||
{
|
||||
data: u,
|
||||
getRowStride: ur.getRowStride(),
|
||||
getPixelStride: ur.getPixelStride(),
|
||||
},
|
||||
{
|
||||
data: v,
|
||||
getRowStride: vr.getRowStride(),
|
||||
getPixelStride: vr.getPixelStride(),
|
||||
}
|
||||
] as buffoptsPlanesType[]
|
||||
} as buffoptsType
|
||||
|
||||
decoderBuffToQr(this.YUV_420_888toNV21(opts), width, height, this.getCamraRotation())
|
||||
.then((res) => {
|
||||
|
||||
if (res != null && t.isOpeningCameraing && !t.isAppPause) {
|
||||
let texts = res.text as string[]
|
||||
if (texts.length > 0) {
|
||||
// texts.some((el:string):boolean=>t.tempresultList.includes(el))
|
||||
|
||||
if (t.options.vibatar == true) {
|
||||
t.setVibatar()
|
||||
}
|
||||
if (t.options.continuousScan == false) {
|
||||
t.closeCamera()
|
||||
}
|
||||
t.callFunEvent(texts, 'auto')
|
||||
t.setImageViewRusletBox(t.options.width, t.options.height, width, height, res as SCANNING_PHOTO_RESULT)
|
||||
}
|
||||
t.isDecoderQring = false
|
||||
} else {
|
||||
t.isDecoderQring = false
|
||||
}
|
||||
// image.close();
|
||||
})
|
||||
.catch(() => {
|
||||
// this.imageUrl = null
|
||||
})
|
||||
// setTimeout(function () {
|
||||
// t.isDecoderQring = false
|
||||
// }, t.options!.scanSpeedduration!);
|
||||
// image.close();
|
||||
}
|
||||
image.close();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
import ListView from 'android.widget.ListView';
|
||||
import FrameLayout from 'android.widget.FrameLayout';
|
||||
import CameraManager from 'android.hardware.camera2.CameraManager'
|
||||
import CameraDevice from 'android.hardware.camera2.CameraDevice'
|
||||
import StateCallback from 'android.hardware.camera2.CameraDevice.StateCallback'
|
||||
import CameraCaptureSession from 'android.hardware.camera2.CameraCaptureSession'
|
||||
|
||||
import CaptureRequest from 'android.hardware.camera2.CaptureRequest'
|
||||
import TotalCaptureResult from 'android.hardware.camera2.TotalCaptureResult'
|
||||
import StreamConfigurationMap from 'android.hardware.camera2.params.StreamConfigurationMap'
|
||||
import MeteringRectangle from 'android.hardware.camera2.params.MeteringRectangle'
|
||||
import OutputConfiguration from 'android.hardware.camera2.params.OutputConfiguration'
|
||||
import SessionConfiguration from 'android.hardware.camera2.params.SessionConfiguration'
|
||||
import CameraCharacteristics from 'android.hardware.camera2.CameraCharacteristics'
|
||||
import CameraMetadata from 'android.hardware.camera2.CameraMetadata'
|
||||
import Range from 'android.util.Range';
|
||||
|
||||
import Executor from 'java.util.concurrent.Executor'
|
||||
import Looper from 'android.os.Looper'
|
||||
import ImageFormat from 'android.graphics.ImageFormat'
|
||||
import Size from 'android.util.Size'
|
||||
import SurfaceTexture from 'android.graphics.SurfaceTexture'
|
||||
import Handler from 'android.os.Handler'
|
||||
import Surface from 'android.view.Surface'
|
||||
import Image from 'android.media.Image'
|
||||
import LinearLayout from 'android.widget.LinearLayout';
|
||||
import Intent from 'android.content.Intent';
|
||||
import Integer from 'java.lang.Integer';
|
||||
import ByteOrder from 'java.nio.ByteOrder';
|
||||
|
||||
import SurfaceHolder from 'android.view.SurfaceHolder'
|
||||
import SurfaceView from 'android.view.SurfaceView'
|
||||
import TextureView from 'android.view.TextureView'
|
||||
import View from 'android.view.View'
|
||||
import Context from 'android.content.Context'
|
||||
import Manifest from "android.Manifest";
|
||||
import Uri from 'android.net.Uri';
|
||||
import File from 'java.io.File'
|
||||
import System from 'java.lang.System'
|
||||
import Activity from 'android.app.Activity';
|
||||
import Bundle from 'android.os.Bundle';
|
||||
import Intent from 'android.content.Intent';
|
||||
import IntentFilter from 'android.content.IntentFilter';
|
||||
|
||||
import Rect from 'android.graphics.Rect';
|
||||
import Matrix from 'android.graphics.Matrix'
|
||||
import ImageReader from 'android.media.ImageReader'
|
||||
import OnImageAvailableListener from 'android.media.ImageReader.OnImageAvailableListener'
|
||||
|
||||
|
||||
import ByteBuffer from 'java.nio.ByteBuffer'
|
||||
import MediaStore from 'android.provider.MediaStore'
|
||||
import Plane from 'android.media.Image.Plane'
|
||||
import Bitmap from "android.graphics.Bitmap"
|
||||
import BitmapFactory from "android.graphics.BitmapFactory"
|
||||
import Vibrator from "android.os.Vibrator"
|
||||
import Animation from "android.view.animation.Animation"
|
||||
import ScaleAnimation from "android.view.animation.ScaleAnimation"
|
||||
import AnimationUtils from "android.view.animation.AnimationUtils"
|
||||
|
||||
|
||||
import Color from "android.graphics.Color"
|
||||
import Window from "android.view.Window"
|
||||
import WindowManager from "android.view.WindowManager"
|
||||
import TextView from "android.widget.TextView"
|
||||
import RelativeLayout from "android.widget.RelativeLayout"
|
||||
import ImageView from "android.widget.ImageView"
|
||||
import HandlerThread from 'android.os.HandlerThread'
|
||||
import YuvImage from 'android.graphics.YuvImage'
|
||||
import ByteArrayOutputStream from 'java.io.ByteArrayOutputStream'
|
||||
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
|
||||
import MotionEvent from 'android.view.MotionEvent';
|
||||
import ViewGroup from 'android.view.ViewGroup';
|
||||
|
||||
|
||||
import IOException from 'java.io.IOException'
|
||||
import InputStream from 'java.io.InputStream'
|
||||
import BarcodeScanner from "com.google.mlkit.vision.barcode.BarcodeScanner"
|
||||
import BarcodeScannerOptions from "com.google.mlkit.vision.barcode.BarcodeScannerOptions"
|
||||
import BarcodeScanning from "com.google.mlkit.vision.barcode.BarcodeScanning"
|
||||
import ZoomSuggestionOptions from "com.google.mlkit.vision.barcode.ZoomSuggestionOptions"
|
||||
import ZoomCallback from "com.google.mlkit.vision.barcode.ZoomSuggestionOptions.ZoomCallback"
|
||||
import Barcode from "com.google.mlkit.vision.barcode.common.Barcode"
|
||||
import InputImage from "com.google.mlkit.vision.common.InputImage"
|
||||
import xView from "./libview/view.uts"
|
||||
import Task from "com.google.android.gms.tasks.Task"
|
||||
import List from "java.util.List"
|
||||
|
||||
// x
|
||||
import ProcessCameraProvider from "androidx.camera.lifecycle.ProcessCameraProvider"
|
||||
import PreviewView from "androidx.camera.view.PreviewView"
|
||||
import CameraController from "androidx.camera.view.CameraController"
|
||||
import CameraSelector from "androidx.camera.core.CameraSelector"
|
||||
import Preview from 'androidx.camera.core.Preview'
|
||||
import Camera from 'androidx.camera.core.Camera'
|
||||
import LifecycleObserver from "androidx.lifecycle.LifecycleObserver"
|
||||
import LifecycleOwner from "androidx.lifecycle.LifecycleOwner"
|
||||
import OnLifecycleEvent from "androidx.lifecycle.OnLifecycleEvent"
|
||||
import ImageAnalysis from 'androidx.camera.core.ImageAnalysis'
|
||||
import ImageProxy from 'androidx.camera.core.ImageProxy'
|
||||
import NonNull from 'androidx.annotation.NonNull'
|
||||
import ExecutorService from 'java.util.concurrent.ExecutorService'
|
||||
import Executors from 'java.util.concurrent.Executors'
|
||||
// import TargetResolution from 'androidx.camera.core.ImageAnalysis.TargetResolution'
|
||||
|
||||
type buffoptsPlanesType = {
|
||||
data : ByteBuffer,
|
||||
getRowStride : number,
|
||||
getPixelStride : number,
|
||||
}
|
||||
type buffoptsType = {
|
||||
width : number,
|
||||
height : number,
|
||||
format : number,
|
||||
rect : Rect,
|
||||
planes : buffoptsPlanesType[],
|
||||
}
|
||||
|
||||
import { SCANNING_OPTIONS, CAMERA_PHOTO_SIZE, CallFunType, SCANNING_PHOTO_RESULT, SCANNING_PHOTO_RESULT_BOUND } from "../interface.uts"
|
||||
import { decoderUriPathToQr, decoderBuffToQr } from "./index.uts"
|
||||
import ContextCompat from 'androidx.core.content.ContextCompat';
|
||||
|
||||
type cameraListNameType = { id : string, device : CameraCharacteristics }
|
||||
|
||||
function px2dp(n : number) : number {
|
||||
const mets = UTSAndroid.getAppContext()!.resources!.getDisplayMetrics()
|
||||
return mets.density * n
|
||||
}
|
||||
|
||||
export class xCamera {
|
||||
parentView : FrameLayout;
|
||||
|
||||
// 识别点的区域
|
||||
resultView : RelativeLayout | null = null;
|
||||
|
||||
|
||||
viewBox_width = 0
|
||||
viewBox_height = 0
|
||||
|
||||
imgPointsBycanmarea = [] as RelativeLayout[]
|
||||
|
||||
options = {
|
||||
width: 800,
|
||||
height: 800,
|
||||
cameraDevice: 'BACK',
|
||||
scanSpeedduration: 450,
|
||||
scanningQrCode: true,
|
||||
isSavePhoto: false,
|
||||
qrCodeType: null,
|
||||
vibatar: true,
|
||||
zoom: 3,
|
||||
continuousScan: false
|
||||
} as SCANNING_OPTIONS
|
||||
|
||||
/** 手动震动服务 */
|
||||
vibrator = null as null | Vibrator
|
||||
context : Context;
|
||||
// 相机旋转的角度。
|
||||
cameraRate = new Map<Number, Number>([
|
||||
[Surface.ROTATION_0, 0],
|
||||
[Surface.ROTATION_90, 0],
|
||||
[Surface.ROTATION_180, 0],
|
||||
[Surface.ROTATION_270, 0]
|
||||
])
|
||||
// 相机服务
|
||||
cameraService : CameraManager
|
||||
// 相机列表,name,id
|
||||
cameraListName = new Map<string, cameraListNameType>();
|
||||
//
|
||||
cameraPreviewSize = { width: 800, height: 800 } as CAMERA_PHOTO_SIZE
|
||||
// 当前预览的什么是设备
|
||||
cameraDevice : CameraDevice | null = null
|
||||
cameraDeviceCaptureSession : CameraCaptureSession | null = null
|
||||
sufaceHolderView : Surface | null = null
|
||||
|
||||
// 相机区域视图
|
||||
cameraView : PreviewView | null = null;
|
||||
cameraProvider : ProcessCameraProvider | null = null
|
||||
camera : Camera | null = null;
|
||||
cameraPreview : Preview | null = null;
|
||||
|
||||
|
||||
// 当前是否在解析码制中。
|
||||
isDecoderQring = false
|
||||
// 当前相机是否正在预览中。
|
||||
isOpeningCameraing = false;
|
||||
|
||||
callFunEvent : CallFunType = (str : string[], type : 'auto' | 'click') => { }
|
||||
|
||||
// 临时存放的识别成功的结果,防止重复扫码。
|
||||
tempresultList = [] as string[];
|
||||
|
||||
isAppPause = false
|
||||
|
||||
flashMode = false;
|
||||
|
||||
constructor(GroupView : FrameLayout, options : SCANNING_OPTIONS | null = null) {
|
||||
this.context = UTSAndroid.getAppContext()! as Context;
|
||||
this.parentView = GroupView;
|
||||
if (options != null) {
|
||||
this.options = {
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
cameraDevice: options.cameraDevice,
|
||||
scanSpeedduration: options.scanSpeedduration == null ? this.options.scanSpeedduration : options.scanSpeedduration,
|
||||
scanningQrCode: options.scanningQrCode == null ? this.options.scanningQrCode : options.scanningQrCode,
|
||||
isSavePhoto: options.isSavePhoto == null ? this.options.isSavePhoto : options.isSavePhoto,
|
||||
qrCodeType: options.qrCodeType == null ? this.options.qrCodeType : options.qrCodeType,
|
||||
vibatar: options.vibatar == null ? this.options.vibatar : options.vibatar,
|
||||
zoom: options.zoom == null ? this.options.zoom : options.zoom,
|
||||
continuousScan: options.continuousScan == null ? this.options.continuousScan : options.continuousScan
|
||||
} as SCANNING_OPTIONS;
|
||||
this.flashMode = options.flashMode
|
||||
}
|
||||
this.cameraService = this.context!.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
this.vibrator = this.context!.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator;
|
||||
this._createCameraView()
|
||||
this._creatCameraResultView()
|
||||
}
|
||||
//震动手机
|
||||
setVibatar() {
|
||||
if (this.vibrator == null) return
|
||||
// 检查设备是否支持震动
|
||||
if (this.vibrator!.hasVibrator()) {
|
||||
// 触发短暂的震动,持续时间为200毫秒
|
||||
this.vibrator!.vibrate(80);
|
||||
}
|
||||
}
|
||||
/** 创建相机预览区域 */
|
||||
private _createCameraView() {
|
||||
|
||||
let sv = new PreviewView(UTSAndroid.getAppContext()!)
|
||||
sv.scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
// 让相机的高和宽与组件对齐。
|
||||
let layaout = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
sv.setLayoutParams(layaout)
|
||||
this.cameraView = sv;
|
||||
|
||||
this.parentView.addView(this.cameraView!)
|
||||
}
|
||||
// 创建一个动画层容器
|
||||
private _creatCameraResultView() {
|
||||
let t = this;
|
||||
let resultView = new RelativeLayout(this.context)
|
||||
let layaout = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
resultView.setLayoutParams(layaout)
|
||||
// resultView.setBackgroundColor(Color.argb(30,255,0,0))
|
||||
this.resultView = resultView;
|
||||
this.parentView.addView(this.resultView!)
|
||||
|
||||
//监听尺寸变化。
|
||||
class previewChangeRectadd extends View.OnLayoutChangeListener {
|
||||
override onLayoutChange(v : View, left : Int, top : Int, right : Int, bottom : Int,
|
||||
oldLeft : Int, oldTop : Int, oldRight : Int, oldBottom : Int
|
||||
) {
|
||||
t.viewBox_width = right - left;
|
||||
t.viewBox_height = bottom - top;
|
||||
}
|
||||
}
|
||||
this.parentView!.addOnLayoutChangeListener(new previewChangeRectadd())
|
||||
|
||||
|
||||
}
|
||||
openCamera() : Promise<boolean> {
|
||||
let _this = this;
|
||||
if (this.isOpeningCameraing) {
|
||||
this.closeCamera()
|
||||
}
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
_this.checkPermissions()
|
||||
.then((res : boolean) => {
|
||||
if (!res) {
|
||||
uni.showToast({ title: "未授权相机使用权", icon: "none" })
|
||||
return;
|
||||
}
|
||||
try {
|
||||
_this._openCamera();
|
||||
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
//TODO handle the exception
|
||||
}
|
||||
res(true)
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: "未授权相机使用权", icon: "none" })
|
||||
rej(false)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
// 设置识别成功的回调函数
|
||||
setCallEvent(call : CallFunType) {
|
||||
this.callFunEvent = call;
|
||||
}
|
||||
getCamraRotation() : Number {
|
||||
return 0
|
||||
}
|
||||
setFlashModel(flash:boolean){
|
||||
|
||||
if(this.camera!=null&&this.isOpeningCameraing){
|
||||
let cameraControl = this.camera!.cameraControl!
|
||||
cameraControl.enableTorch(flash)
|
||||
}
|
||||
}
|
||||
|
||||
closeCamera() {
|
||||
this.setFlashModel(false);
|
||||
this.cameraProvider?.unbindAll()
|
||||
this.cameraPreview?.setSurfaceProvider(null)
|
||||
this.isOpeningCameraing = false;
|
||||
|
||||
for (let i = 0; i < this.imgPointsBycanmarea.length; i++) {
|
||||
this.resultView?.removeView(this.imgPointsBycanmarea[i])
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
private _openCamera() {
|
||||
let _this = this;
|
||||
let cameraProviderFuture = ProcessCameraProvider.getInstance(this.context)
|
||||
|
||||
class IntentRunable extends Runnable {
|
||||
override run() {
|
||||
let cameraProvider : ProcessCameraProvider = cameraProviderFuture.get()
|
||||
let previewView = _this.cameraView! as PreviewView
|
||||
let cameraSelector : CameraSelector = new CameraSelector.Builder()
|
||||
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||
.build();
|
||||
let preview : Preview = new Preview.Builder().build();
|
||||
|
||||
preview.setSurfaceProvider(previewView.getSurfaceProvider())
|
||||
|
||||
let imageAnalysis = new ImageAnalysis.Builder()
|
||||
// .setTargetResolution(new Size(_this.viewBox_width.toInt(), _this.viewBox_height.toInt()))
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build();
|
||||
class MyImageAnalyzer implements ImageAnalysis.Analyzer {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
override analyze(image : ImageProxy) {
|
||||
_this.qrDecoder(image)
|
||||
}
|
||||
override getDefaultTargetResolution() : Size {
|
||||
return new Size(_this.viewBox_width.toInt(), _this.viewBox_height.toInt());
|
||||
}
|
||||
|
||||
}
|
||||
let cameraExecutor = Executors.newSingleThreadExecutor();
|
||||
imageAnalysis.setAnalyzer(cameraExecutor, new MyImageAnalyzer());
|
||||
let camera : Camera = cameraProvider.bindToLifecycle(
|
||||
UTSAndroid.getUniActivity()! as LifecycleOwner, cameraSelector, preview, imageAnalysis);
|
||||
_this.camera = camera
|
||||
_this.cameraPreview = preview
|
||||
|
||||
_this.cameraProvider = cameraProvider
|
||||
_this.isDecoderQring = false
|
||||
_this.isOpeningCameraing = true
|
||||
_this.setFlashModel(_this.flashMode)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
cameraProviderFuture.addListener(new IntentRunable(), ContextCompat.getMainExecutor(this.context))
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查相机权限
|
||||
*/
|
||||
checkPermissions() : Promise<boolean> {
|
||||
let permissionCheck = ["android.permission.CAMERA"]
|
||||
let _this = this;
|
||||
// 请求拍照权限
|
||||
return new Promise(res => {
|
||||
if (UTSAndroid.checkSystemPermissionGranted(UTSAndroid.getUniActivity()!, permissionCheck)) {
|
||||
// _this.cameraPermissioPass = true
|
||||
res(true)
|
||||
} else {
|
||||
console.log("当前不具备指定权限")
|
||||
// 请求拍照权限
|
||||
UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, permissionCheck, function (allRight : boolean, _ : string[]) {
|
||||
// _this.cameraPermissioPass = allRight
|
||||
res(true)
|
||||
}, function (_ : boolean, _ : string[]) {
|
||||
//用户拒绝了部分权限
|
||||
// _this.cameraPermissioPass = false
|
||||
res(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 设置图片上的小圆的识别位置。
|
||||
setImageViewRusletBox(width : number, height : number, realWidth : number, realHeight : number, jieguo : SCANNING_PHOTO_RESULT) {
|
||||
let t = this;
|
||||
let btnwidth = 100;
|
||||
let btnwidth2 = 50;
|
||||
|
||||
for (let i = 0; i < jieguo.bounds.length; i++) {
|
||||
let item = jieguo.bounds[i];
|
||||
let layoutParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
|
||||
let x = item.centerX
|
||||
let y = item.centerY
|
||||
|
||||
layoutParams.topMargin = (y - btnwidth2).toInt() // 设置顶部边距
|
||||
layoutParams.leftMargin = (x - btnwidth2).toInt() // 设置左边距
|
||||
let relativeLayout = new RelativeLayout(t.context)
|
||||
relativeLayout.layoutParams = layoutParams
|
||||
|
||||
const rtxt = jieguo.text[i]
|
||||
let v = new xView(t.context)
|
||||
v.setSize(btnwidth, btnwidth)
|
||||
.setRadius(100)
|
||||
.setBackgroundColor('#21d429')
|
||||
.setBorder(12, "#ffffff", false)
|
||||
.setClick((event : MotionEvent) => {
|
||||
try {
|
||||
t.callFunEvent([rtxt], 'click')
|
||||
} catch (e) {
|
||||
//TODO handle the exception
|
||||
}
|
||||
})
|
||||
let ov = v.getView();
|
||||
|
||||
|
||||
// 创建缩放动画
|
||||
let scaleAnimation = new ScaleAnimation(1.0.toFloat(), 0.7.toFloat(), 1.0.toFloat(), 0.7.toFloat(), Animation.RELATIVE_TO_SELF, 0.7.toFloat(), Animation.RELATIVE_TO_SELF, 0.7.toFloat());
|
||||
scaleAnimation.setDuration(600); // 设置动画持续时间
|
||||
scaleAnimation.setRepeatMode(Animation.REVERSE); // 设置重复模式为反向
|
||||
scaleAnimation.setRepeatCount(Animation.INFINITE); // 设置重复次数为无限
|
||||
// 启动动画
|
||||
ov.startAnimation(scaleAnimation);
|
||||
relativeLayout.addView(ov)
|
||||
this.imgPointsBycanmarea.push(relativeLayout)
|
||||
this.resultView!.addView(relativeLayout)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
private YUV_420_888toNV21(opts : buffoptsType) : ByteArray {
|
||||
|
||||
let crop = opts.rect;
|
||||
let format = opts.format;
|
||||
let width = crop.width();
|
||||
let height = crop.height();
|
||||
let planes = opts.planes!;
|
||||
let data = new ByteArray((width * height * ImageFormat.getBitsPerPixel(format.toInt()) / 8).toInt());
|
||||
let rowData = new ByteArray((planes[0].getRowStride).toInt());
|
||||
let channelOffset = 0;
|
||||
let outputStride = 1;
|
||||
for (let i = 0; i < planes.length; i++) {
|
||||
switch (i) {
|
||||
case 0:
|
||||
channelOffset = 0;
|
||||
outputStride = 1;
|
||||
break;
|
||||
case 1:
|
||||
channelOffset = width * height;
|
||||
outputStride = 1;
|
||||
break;
|
||||
case 2:
|
||||
channelOffset = width * height * 1.25;
|
||||
outputStride = 1;
|
||||
break;
|
||||
}
|
||||
let buffer = planes[i.toInt()].data!;
|
||||
let rowStride = planes[i.toInt()].getRowStride;
|
||||
let pixelStride = planes[i.toInt()].getPixelStride;
|
||||
|
||||
let shift = (i == 0) ? 0 : 1;
|
||||
let w = width >> shift;
|
||||
let h = height >> shift;
|
||||
buffer.position((rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift)).toInt());
|
||||
for (let row = 0; row < h; row++) {
|
||||
let length = 0;
|
||||
if (pixelStride == 1 && outputStride == 1) {
|
||||
length = w;
|
||||
buffer.get(data, channelOffset.toInt(), length.toInt());
|
||||
channelOffset += length;
|
||||
} else {
|
||||
length = (w - 1) * pixelStride + 1;
|
||||
buffer.get(rowData, 0, length.toInt());
|
||||
for (let col = 0; col < w; col++) {
|
||||
data[channelOffset.toInt()] = rowData[(col * pixelStride).toInt()];
|
||||
channelOffset += outputStride;
|
||||
}
|
||||
}
|
||||
if (row < h - 1) {
|
||||
buffer.position((buffer.position() + rowStride - length).toInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
getCorrectionMatrix(imageProxy : ImageProxy, previewView : PreviewView) : Matrix {
|
||||
let cropRect = imageProxy.cropRect
|
||||
let rotationDegrees = imageProxy.imageInfo.rotationDegrees
|
||||
let matrix = new Matrix()
|
||||
|
||||
// A float array of the source vertices (crop rect) in clockwise order.
|
||||
|
||||
let source = floatArrayOf(cropRect.left.toFloat(),
|
||||
cropRect.top.toFloat(),
|
||||
cropRect.right.toFloat(),
|
||||
cropRect.top.toFloat(),
|
||||
cropRect.right.toFloat(),
|
||||
cropRect.bottom.toFloat(),
|
||||
cropRect.left.toFloat(),
|
||||
cropRect.bottom.toFloat())
|
||||
|
||||
let d0 = (0).toFloat()
|
||||
// A float array of the destination vertices in clockwise order.
|
||||
let destination = floatArrayOf(d0,
|
||||
d0,
|
||||
previewView.width.toFloat(),
|
||||
d0,
|
||||
previewView.width.toFloat(),
|
||||
previewView.height.toFloat(),
|
||||
d0,
|
||||
previewView.height.toFloat())
|
||||
|
||||
// The destination vertexes need to be shifted based on rotation degrees. The
|
||||
// rotation degree represents the clockwise rotation needed to correct the image.
|
||||
|
||||
// Each vertex is represented by 2 float numbers in the vertices array.
|
||||
let vertexSize = 2
|
||||
// The destination needs to be shifted 1 vertex for every 90° rotation.
|
||||
let shiftOffset = rotationDegrees / 90 * vertexSize;
|
||||
let tempArray = destination.clone()
|
||||
for (toIndex in source.indices) {
|
||||
let fromIndex = (toIndex + shiftOffset) % source.size
|
||||
destination[toIndex] = tempArray[fromIndex.toInt()]
|
||||
}
|
||||
let success = matrix.setPolyToPoly(source, 0, destination, 0, 4)
|
||||
|
||||
return matrix
|
||||
}
|
||||
private qrDecoder(img : ImageProxy) {
|
||||
let t = this;
|
||||
let image = img.getImage();
|
||||
|
||||
if (!this.isDecoderQring && image != null) {
|
||||
this.isDecoderQring = true;
|
||||
|
||||
let width = image.getWidth();
|
||||
let height = image.getHeight();
|
||||
let frameSize = width * height;
|
||||
let qFrameSize = frameSize / 4;
|
||||
// 获取各平面的数据与信息
|
||||
let planes = image.getPlanes();
|
||||
|
||||
let yBufferCopy = [] as ByteArray[]
|
||||
let yrurvrNumber = [] as number[]
|
||||
|
||||
let yr = planes[0]
|
||||
let ur = planes[1]
|
||||
let vr = planes[2]
|
||||
let oraingMax = yr.getBuffer().limit()
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
let buffer = planes[i.toInt()].getBuffer();
|
||||
yBufferCopy.push(new ByteArray(buffer.remaining().toInt()))
|
||||
buffer.get(yBufferCopy[i.toInt()]);
|
||||
}
|
||||
let y = ByteBuffer.wrap(yBufferCopy[0]!)
|
||||
let u = ByteBuffer.wrap(yBufferCopy[1]!)
|
||||
let v = ByteBuffer.wrap(yBufferCopy[2]!)
|
||||
|
||||
let opts = {
|
||||
width,
|
||||
height,
|
||||
format: image.getFormat(),
|
||||
rect: image.getCropRect(),
|
||||
planes: [
|
||||
{
|
||||
data: y,
|
||||
getRowStride: yr.getRowStride(),
|
||||
getPixelStride: yr.getPixelStride(),
|
||||
},
|
||||
{
|
||||
data: u,
|
||||
getRowStride: ur.getRowStride(),
|
||||
getPixelStride: ur.getPixelStride(),
|
||||
},
|
||||
{
|
||||
data: v,
|
||||
getRowStride: vr.getRowStride(),
|
||||
getPixelStride: vr.getPixelStride(),
|
||||
}
|
||||
] as buffoptsPlanesType[]
|
||||
} as buffoptsType
|
||||
|
||||
decoderBuffToQr(this.YUV_420_888toNV21(opts), width, height,0)
|
||||
.then((res) => {
|
||||
|
||||
if (res != null && t.isOpeningCameraing) {
|
||||
let matrix = t.getCorrectionMatrix(img, this.cameraView!)
|
||||
let texts = res.text as string[]
|
||||
if (texts.length > 0) {
|
||||
// texts.some((el:string):boolean=>t.tempresultList.includes(el))
|
||||
|
||||
if (t.options.vibatar == true) {
|
||||
t.setVibatar()
|
||||
}
|
||||
if (t.options.continuousScan == false) {
|
||||
t.closeCamera()
|
||||
}
|
||||
let ruslt = res as SCANNING_PHOTO_RESULT;
|
||||
for (let i = 0; i < ruslt.bounds.length; i++) {
|
||||
let item = ruslt.bounds[i]
|
||||
let ponit = floatArrayOf(item.centerX.toFloat(), item.centerY.toFloat())
|
||||
|
||||
matrix.mapPoints(ponit)
|
||||
|
||||
item.centerX = ponit[0]
|
||||
item.centerY = ponit[1]
|
||||
|
||||
}
|
||||
t.callFunEvent(texts, 'auto')
|
||||
t.setImageViewRusletBox(t.options.width, t.options.height, width, height, ruslt)
|
||||
}
|
||||
t.isDecoderQring = false
|
||||
} else {
|
||||
t.isDecoderQring = false
|
||||
}
|
||||
// image.close();
|
||||
})
|
||||
.catch(() => {
|
||||
// this.imageUrl = null
|
||||
})
|
||||
// setTimeout(function () {
|
||||
// t.isDecoderQring = false
|
||||
// }, t.options!.scanSpeedduration!);
|
||||
// image.close();
|
||||
}
|
||||
img.close();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"minSdkVersion": "21",
|
||||
"dependencies":[
|
||||
"com.google.mlkit:barcode-scanning:17.2.0",
|
||||
"androidx.camera:camera-core:1.3.4",
|
||||
"androidx.camera:camera-camera2:1.3.4",
|
||||
"androidx.camera:camera-lifecycle:1.3.4",
|
||||
"androidx.camera:camera-video:1.3.4",
|
||||
"androidx.camera:camera-view:1.3.4",
|
||||
"androidx.camera:camera-extensions:1.3.4"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 引用 Android 系统库,示例如下:
|
||||
* import { Context } from "android.content.Context";
|
||||
* [可选实现,按需引入]
|
||||
*/
|
||||
|
||||
/* 引入 interface.uts 文件中定义的变量 */
|
||||
import { SCANNING_PHOTO_RESULT, SCANNING_PHOTO_RESULT_BOUND, SCANNING_PHOTO_RESULT_POS } from '../interface.uts';
|
||||
|
||||
/* 引入 unierror.uts 文件中定义的变量 */
|
||||
|
||||
import Context from 'android.content.Context'
|
||||
import IOException from 'java.io.IOException'
|
||||
import InputStream from 'java.io.InputStream'
|
||||
import Uri from 'android.net.Uri';
|
||||
import File from 'java.io.File'
|
||||
import Bitmap from "android.graphics.Bitmap"
|
||||
import BarcodeScanner from "com.google.mlkit.vision.barcode.BarcodeScanner"
|
||||
import BarcodeScannerOptions from "com.google.mlkit.vision.barcode.BarcodeScannerOptions"
|
||||
import BarcodeScanning from "com.google.mlkit.vision.barcode.BarcodeScanning"
|
||||
import ZoomSuggestionOptions from "com.google.mlkit.vision.barcode.ZoomSuggestionOptions"
|
||||
import ZoomCallback from "com.google.mlkit.vision.barcode.ZoomSuggestionOptions.ZoomCallback"
|
||||
import Barcode from "com.google.mlkit.vision.barcode.common.Barcode"
|
||||
import InputImage from "com.google.mlkit.vision.common.InputImage"
|
||||
|
||||
import Task from "com.google.android.gms.tasks.Task"
|
||||
import List from "java.util.List"
|
||||
import ByteBuffer from 'java.nio.ByteBuffer'
|
||||
import ImageFormat from 'android.graphics.ImageFormat'
|
||||
|
||||
/**
|
||||
* 从相册中选取图片进行解码
|
||||
* @public chooseImageDecodeQr
|
||||
*/
|
||||
export function chooseImage() : Promise<SCANNING_PHOTO_RESULT | null> {
|
||||
return new Promise((res) => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success(evt : ChooseImageSuccess) {
|
||||
console.log('**')
|
||||
if (evt.tempFilePaths.length > 0) {
|
||||
console.log(evt, "----")
|
||||
const imgs = evt.tempFilePaths[0] as string;
|
||||
try {
|
||||
const decodedPath = Uri.decode(imgs).substring(7)
|
||||
const file = File(decodedPath)
|
||||
let image = InputImage.fromFilePath(UTSAndroid.getAppContext() as Context, Uri.fromFile(file));
|
||||
let scanner = BarcodeScanning.getClient();
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener((barcodes) => {
|
||||
let listpos = [] as number[][][]
|
||||
let bounedlist = [] as SCANNING_PHOTO_RESULT_BOUND[]
|
||||
let rulstText = [] as string[]
|
||||
for (barcode in barcodes) {
|
||||
// 位置信息
|
||||
let bounds = barcode.boundingBox
|
||||
// 坐标
|
||||
let corners = barcode.cornerPoints
|
||||
// 解析的内容
|
||||
let rawValue = barcode.rawValue
|
||||
// 码的格式
|
||||
let valueType = barcode.valueType
|
||||
|
||||
if (rawValue != null && bounds != null && corners != null) {
|
||||
let iminfo = { width: bounds!.width(), height: bounds!.height(), centerX: bounds!.centerX(), centerY: bounds!.centerY() } as SCANNING_PHOTO_RESULT_BOUND
|
||||
let posinfo = [] as number[][]
|
||||
for (pos in corners) {
|
||||
posinfo.push([pos!.x as number, pos!.y as number] as number[])
|
||||
}
|
||||
listpos.push(posinfo)
|
||||
bounedlist.push(iminfo)
|
||||
rulstText.push(rawValue)
|
||||
}
|
||||
}
|
||||
res({
|
||||
url: imgs,
|
||||
position: listpos,
|
||||
bounds: bounedlist,
|
||||
text: rulstText
|
||||
} as SCANNING_PHOTO_RESULT)
|
||||
})
|
||||
|
||||
} catch (e : IOException) {
|
||||
res(null)
|
||||
}
|
||||
}
|
||||
},
|
||||
fail() {
|
||||
res(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定图片路径Uri解析图片。
|
||||
* @public decoderUriPathToQr
|
||||
*/
|
||||
export function decoderUriPathToQr(filePath : Uri) : Promise<SCANNING_PHOTO_RESULT | null> {
|
||||
return new Promise((res) => {
|
||||
let image = InputImage.fromFilePath(UTSAndroid.getAppContext() as Context, filePath);
|
||||
let scanner = BarcodeScanning.getClient();
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener((barcodes) => {
|
||||
let listpos = [] as number[][][]
|
||||
let bounedlist = [] as SCANNING_PHOTO_RESULT_BOUND[]
|
||||
let rulstText = [] as string[]
|
||||
for (barcode in barcodes) {
|
||||
// 位置信息
|
||||
let bounds = barcode.boundingBox
|
||||
// 坐标
|
||||
let corners = barcode.cornerPoints
|
||||
// 解析的内容
|
||||
let rawValue = barcode.rawValue
|
||||
// 码的格式
|
||||
let valueType = barcode.valueType
|
||||
|
||||
if (rawValue != null && bounds != null && corners != null) {
|
||||
let iminfo = { width: bounds!.width(), height: bounds!.height(), centerX: bounds!.centerX(), centerY: bounds!.centerY() } as SCANNING_PHOTO_RESULT_BOUND
|
||||
let posinfo = [] as number[][]
|
||||
for (pos in corners) {
|
||||
posinfo.push([pos!.x as number, pos!.y as number] as number[])
|
||||
}
|
||||
listpos.push(posinfo)
|
||||
bounedlist.push(iminfo)
|
||||
rulstText.push(rawValue)
|
||||
}
|
||||
}
|
||||
res({
|
||||
url: "",
|
||||
position: listpos,
|
||||
bounds: bounedlist,
|
||||
text: rulstText
|
||||
} as SCANNING_PHOTO_RESULT)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定ImageBuffer
|
||||
* @public decoderBuffToQr
|
||||
*/
|
||||
export function decoderBuffToQr(bitmap : ByteArray, width : Number, height : Number, ration : Number) : Promise<SCANNING_PHOTO_RESULT | null> {
|
||||
return new Promise((res) => {
|
||||
let options = BarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(
|
||||
Barcode.FORMAT_QR_CODE,
|
||||
Barcode.FORMAT_EAN_13,
|
||||
Barcode.FORMAT_CODE_128,
|
||||
Barcode.FORMAT_CODABAR
|
||||
)
|
||||
.build()
|
||||
let image = InputImage.fromByteArray(bitmap, width.toInt(), height.toInt(), ration.toInt(), InputImage.IMAGE_FORMAT_NV21)
|
||||
// 如果只扫qr开启这行,速度快一点。
|
||||
let scanner = BarcodeScanning.getClient(options);
|
||||
// let scanner = BarcodeScanning.getClient();
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener((barcodes) => {
|
||||
let listpos = [] as number[][][]
|
||||
let bounedlist = [] as SCANNING_PHOTO_RESULT_BOUND[]
|
||||
let rulstText = [] as string[]
|
||||
for (barcode in barcodes) {
|
||||
// 位置信息
|
||||
let bounds = barcode.boundingBox
|
||||
// 坐标
|
||||
let corners = barcode.cornerPoints
|
||||
// 解析的内容
|
||||
let rawValue = barcode.rawValue
|
||||
// 码的格式
|
||||
let valueType = barcode.valueType
|
||||
|
||||
if (rawValue != null && bounds != null && corners != null) {
|
||||
let iminfo = { width: bounds!.width(), height: bounds!.height(), centerX: bounds!.centerX(), centerY: bounds!.centerY() } as SCANNING_PHOTO_RESULT_BOUND
|
||||
let posinfo = [] as number[][]
|
||||
for (pos in corners) {
|
||||
posinfo.push([pos!.x as number, pos!.y as number] as number[])
|
||||
}
|
||||
listpos.push(posinfo)
|
||||
bounedlist.push(iminfo)
|
||||
rulstText.push(rawValue)
|
||||
}
|
||||
}
|
||||
res({
|
||||
url: "",
|
||||
position: listpos,
|
||||
bounds: bounedlist,
|
||||
text: rulstText
|
||||
} as SCANNING_PHOTO_RESULT)
|
||||
})
|
||||
.addOnFailureListener((e) => {
|
||||
|
||||
res(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 提供一个网络图片地址进行解码
|
||||
* @public urlPathDecoderQr
|
||||
*/
|
||||
export function urlPathDecoderQr(url : String) : Promise<SCANNING_PHOTO_RESULT | null> {
|
||||
return new Promise((res) => {
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success(res) {
|
||||
|
||||
const contentResolver = UTSAndroid.getAppContext()!.contentResolver
|
||||
let decodedPath = res.tempFilePath
|
||||
if (res.tempFilePath.indexOf('file://') > -1) {
|
||||
decodedPath = Uri.decode(res.tempFilePath).substring(7)
|
||||
}
|
||||
|
||||
try {
|
||||
const file = File(decodedPath)
|
||||
let image = InputImage.fromFilePath(UTSAndroid.getAppContext() as Context, Uri.fromFile(file));
|
||||
let scanner = BarcodeScanning.getClient();
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener((barcodes) => {
|
||||
let listpos = [] as number[][][]
|
||||
let bounedlist = [] as SCANNING_PHOTO_RESULT_BOUND[]
|
||||
let rulstText = [] as string[]
|
||||
for (barcode in barcodes) {
|
||||
// 位置信息
|
||||
let bounds = barcode.boundingBox
|
||||
// 坐标
|
||||
let corners = barcode.cornerPoints
|
||||
// 解析的内容
|
||||
let rawValue = barcode.rawValue
|
||||
// 码的格式
|
||||
let valueType = barcode.valueType
|
||||
|
||||
if (rawValue != null && bounds != null && corners != null) {
|
||||
let iminfo = { width: bounds!.width(), height: bounds!.height(), centerX: bounds!.centerX(), centerY: bounds!.centerY() } as SCANNING_PHOTO_RESULT_BOUND
|
||||
let posinfo = [] as number[][]
|
||||
for (pos in corners) {
|
||||
posinfo.push([pos!.x as number, pos!.y as number] as number[])
|
||||
}
|
||||
listpos.push(posinfo)
|
||||
bounedlist.push(iminfo)
|
||||
rulstText.push(rawValue)
|
||||
}
|
||||
}
|
||||
res({
|
||||
url: url,
|
||||
position: listpos,
|
||||
bounds: bounedlist,
|
||||
text: rulstText
|
||||
} as SCANNING_PHOTO_RESULT)
|
||||
})
|
||||
|
||||
} catch (e : IOException) {
|
||||
res(null)
|
||||
}
|
||||
|
||||
},
|
||||
fail() {
|
||||
res(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<view class="boxView">
|
||||
|
||||
|
||||
</view>
|
||||
</template>
|
||||
<script lang="uts">
|
||||
import Context from 'android.content.Context'
|
||||
import LinearLayout from 'android.widget.LinearLayout';
|
||||
import FrameLayout from 'android.widget.FrameLayout';
|
||||
import ViewGroup from 'android.view.ViewGroup';
|
||||
import View from 'android.view.View';
|
||||
import ImageView from "android.widget.ImageView"
|
||||
import InputStream from 'java.io.InputStream'
|
||||
import Uri from 'android.net.Uri';
|
||||
import File from 'java.io.File'
|
||||
import BufferedImage from 'java.awt.image.BufferedImage'
|
||||
import IOException from 'java.io.IOException'
|
||||
import NotFoundException from 'com.google.zxing.NotFoundException'
|
||||
import FormatException from 'com.google.zxing.FormatException'
|
||||
import ChecksumException from 'com.google.zxing.ChecksumException'
|
||||
|
||||
import BarcodeScanner from "com.google.mlkit.vision.barcode.BarcodeScanner"
|
||||
import BarcodeScannerOptions from "com.google.mlkit.vision.barcode.BarcodeScannerOptions"
|
||||
import BarcodeScanning from "com.google.mlkit.vision.barcode.BarcodeScanning"
|
||||
import ZoomSuggestionOptions from "com.google.mlkit.vision.barcode.ZoomSuggestionOptions"
|
||||
import ZoomCallback from "com.google.mlkit.vision.barcode.ZoomSuggestionOptions.ZoomCallback"
|
||||
import Barcode from "com.google.mlkit.vision.barcode.common.Barcode"
|
||||
import InputImage from "com.google.mlkit.vision.common.InputImage"
|
||||
|
||||
import Task from "com.google.android.gms.tasks.Task"
|
||||
import List from "java.util.List"
|
||||
import ByteBuffer from 'java.nio.ByteBuffer'
|
||||
import ImageFormat from 'android.graphics.ImageFormat'
|
||||
|
||||
|
||||
import Bitmap from "android.graphics.Bitmap"
|
||||
import BitmapFactory from "android.graphics.BitmapFactory"
|
||||
|
||||
import Color from "android.graphics.Color"
|
||||
import BarcodeFormat from "com.google.zxing.BarcodeFormat"
|
||||
import EncodeHintType from "com.google.zxing.EncodeHintType"
|
||||
import MultiFormatWriter from "com.google.zxing.MultiFormatWriter"
|
||||
import MultiFormatReader from "com.google.zxing.MultiFormatReader"
|
||||
import RGBLuminanceSource from "com.google.zxing.RGBLuminanceSource"
|
||||
|
||||
|
||||
import BinaryBitmap from "com.google.zxing.BinaryBitmap"
|
||||
import HybridBinarizer from "com.google.zxing.common.HybridBinarizer"
|
||||
import WriterException from "com.google.zxing.WriterException"
|
||||
import BitMatrix from "com.google.zxing.common.BitMatrix"
|
||||
import BufferedImageLuminanceSource from "com.google.zxing.client.j2se.BufferedImageLuminanceSource"
|
||||
|
||||
import ByteArrayOutputStream from 'java.io.ByteArrayOutputStream'
|
||||
import Base64 from 'java.util.Base64'
|
||||
|
||||
import Context from 'android.content.Context'
|
||||
import SurfaceView from 'android.view.SurfaceView'
|
||||
import { chooseImage } from "./index.uts"
|
||||
import { xCamera } from "./camera.uts"
|
||||
import { SCANNING_OPTIONS,CAMERA_PHOTO_SIZE,BARCODEFORMAT,CallFunType,SCANNING_PHOTO_RESULT_BOUND ,SCANNING_PHOTO_RESULT} from "../interface.uts"
|
||||
|
||||
|
||||
export default {
|
||||
name: "x-mlkit-scannig-u",
|
||||
data() {
|
||||
return {
|
||||
CBOX: null as null | FrameLayout,
|
||||
xcamera:null as null|xCamera,
|
||||
defaultscaneventFun:null as CallFunType|null
|
||||
}
|
||||
},
|
||||
emits:[
|
||||
/**
|
||||
* 多个码的时候点击触发,返回码内容
|
||||
* @param {string} str - 点的码
|
||||
*/
|
||||
'clickQr',
|
||||
/**
|
||||
* 识别成功触发
|
||||
* @param {string[]} str[] - 识别的码结果集
|
||||
*/
|
||||
'scan'
|
||||
],
|
||||
props:{
|
||||
|
||||
/**
|
||||
* 相机预览像素宽,请最好为正方形
|
||||
*/
|
||||
cameraWidth:{
|
||||
type:Number,
|
||||
default:800
|
||||
},
|
||||
/**
|
||||
* 相机预览像高,请最好为正方形
|
||||
*/
|
||||
cameraeiHght:{
|
||||
type:Number,
|
||||
default:800
|
||||
},
|
||||
/**
|
||||
* 是否自动打开相机
|
||||
*/
|
||||
autoOpenCamera:{
|
||||
type:Boolean,
|
||||
default:true
|
||||
},
|
||||
/**
|
||||
* 是否开启闪光灯
|
||||
*/
|
||||
flash:{
|
||||
type:Boolean,
|
||||
default:false
|
||||
}
|
||||
|
||||
},
|
||||
NVLoad() : FrameLayout {
|
||||
let grop = new FrameLayout(UTSAndroid!.getAppContext()!)
|
||||
this.CBOX = grop;
|
||||
return grop
|
||||
},
|
||||
NVLayouted() {
|
||||
if(this.autoOpenCamera){
|
||||
this.createXcamrera();
|
||||
}
|
||||
},
|
||||
NVBeforeUnload(){
|
||||
this.xcamera?.closeCamera()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 检查相机权限
|
||||
*/
|
||||
checkPermissions() : Promise<boolean> {
|
||||
let permissionCheck = ["android.permission.CAMERA"]
|
||||
let _this = this;
|
||||
// 请求拍照权限
|
||||
return new Promise(res => {
|
||||
if (UTSAndroid.checkSystemPermissionGranted(UTSAndroid.getUniActivity()!, permissionCheck)) {
|
||||
// _this.cameraPermissioPass = true
|
||||
res(true)
|
||||
} else {
|
||||
console.log("当前不具备指定权限")
|
||||
// 请求拍照权限
|
||||
UTSAndroid.requestSystemPermission(UTSAndroid.getUniActivity()!, permissionCheck, function (allRight : boolean, _ : string[]) {
|
||||
// _this.cameraPermissioPass = allRight
|
||||
res(true)
|
||||
}, function (_ : boolean, _ : string[]) {
|
||||
//用户拒绝了部分权限
|
||||
// _this.cameraPermissioPass = false
|
||||
res(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 私有函数,外部不要调用。 */
|
||||
createXcamrera(){
|
||||
let _this = this;
|
||||
this.xcamera = new xCamera(this.CBOX!,{
|
||||
width:this.cameraWidth,height:this.cameraeiHght,cameraDevice:'BACK',
|
||||
zoom:5,
|
||||
flashMode:this.flash
|
||||
} as SCANNING_OPTIONS)
|
||||
|
||||
this.xcamera!.setCallEvent((str:string[],type:'auto'|'click')=>{
|
||||
if(type=='auto'){
|
||||
_this.$emit('scan',str)
|
||||
}else if(type=='click'){
|
||||
_this.$emit('clickQr',str[0]!)
|
||||
}
|
||||
})
|
||||
this.xcamera!.openCamera()
|
||||
|
||||
},
|
||||
/** 手动开启和关闭闪光灯 */
|
||||
setFlashModel(flash:boolean){
|
||||
if(this.xcamera != null){
|
||||
this.xcamera!.setFlashModel(flash)
|
||||
}
|
||||
},
|
||||
/** 手动打开相机预览 */
|
||||
openCamera(){
|
||||
let _this = this;
|
||||
let isPrivacyAgreeCamrea = uni.getStorageSync('isPrivacyAgreeCamreaByTmui')
|
||||
if(isPrivacyAgreeCamrea!='on'){
|
||||
uni.showModal({
|
||||
title:"提醒",
|
||||
content:"将授权你的设备相机使用权限进行拍摄识别功能,是否允许?",
|
||||
success(isPermisseions){
|
||||
if(isPermisseions.confirm){
|
||||
_this.checkPermissions()
|
||||
.then((res:boolean)=>{
|
||||
_this.createXcamrera()
|
||||
uni.setStorageSync('isPrivacyAgreeCamreaByTmui','on')
|
||||
})
|
||||
.catch(()=>{
|
||||
uni.setStorageSync('isPrivacyAgreeCamreaByTmui','off')
|
||||
})
|
||||
}
|
||||
},
|
||||
fail(){
|
||||
uni.showToast({title:"未授权相机使用权",icon:"none"})
|
||||
}
|
||||
})
|
||||
}else{
|
||||
_this.createXcamrera()
|
||||
}
|
||||
},
|
||||
/** 关闭相机 */
|
||||
closeCamera(){
|
||||
this.xcamera?.closeCamera()
|
||||
},
|
||||
chooseImage(){
|
||||
let _this = this;
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
albumMode:"system",
|
||||
sourceType:['album'],
|
||||
success(evt : ChooseImageSuccess) {
|
||||
|
||||
if (evt.tempFilePaths.length > 0) {
|
||||
|
||||
const imgs = evt.tempFilePaths[0] as string;
|
||||
try {
|
||||
const decodedPath = Uri.decode(imgs).substring(7)
|
||||
const file = File(decodedPath)
|
||||
let image = InputImage.fromFilePath(UTSAndroid.getAppContext() as Context, Uri.fromFile(file));
|
||||
let scanner = BarcodeScanning.getClient();
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener((barcodes) => {
|
||||
let listpos = [] as number[][][]
|
||||
let bounedlist = [] as SCANNING_PHOTO_RESULT_BOUND[]
|
||||
let rulstText = [] as string[]
|
||||
for (barcode in barcodes) {
|
||||
// 位置信息
|
||||
let bounds = barcode.boundingBox
|
||||
// 坐标
|
||||
let corners = barcode.cornerPoints
|
||||
// 解析的内容
|
||||
let rawValue = barcode.rawValue
|
||||
// 码的格式
|
||||
let valueType = barcode.valueType
|
||||
|
||||
if (rawValue != null && bounds != null && corners != null) {
|
||||
let iminfo = { width: bounds!.width(), height: bounds!.height(), centerX: bounds!.centerX(), centerY: bounds!.centerY() } as SCANNING_PHOTO_RESULT_BOUND
|
||||
let posinfo = [] as number[][]
|
||||
for (pos in corners) {
|
||||
posinfo.push([pos!.x as number, pos!.y as number] as number[])
|
||||
}
|
||||
listpos.push(posinfo)
|
||||
bounedlist.push(iminfo)
|
||||
rulstText.push(rawValue)
|
||||
}
|
||||
}
|
||||
// res({
|
||||
// url: imgs,
|
||||
// position: listpos,
|
||||
// bounds: bounedlist,
|
||||
// text: rulstText
|
||||
// } as SCANNING_PHOTO_RESULT)
|
||||
// let resultstr = uni.getStorageSync("xMlKITScaning")
|
||||
_this.$emit('scan',rulstText.length==0?[]:rulstText)
|
||||
})
|
||||
|
||||
} catch (e : IOException) {
|
||||
_this.$emit('scan',[] as string[])
|
||||
}
|
||||
}
|
||||
},
|
||||
fail() {
|
||||
_this.$emit('scan',[] as string[])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,468 @@
|
||||
import Context from 'android.content.Context'
|
||||
import LinearLayout from 'android.widget.LinearLayout';
|
||||
import ViewGroup from 'android.view.ViewGroup';
|
||||
import View from 'android.view.View';
|
||||
import TextView from 'android.widget.TextView';
|
||||
import ImageView from 'android.widget.ImageView';
|
||||
import Gravity from 'android.view.Gravity';
|
||||
import Color from 'android.graphics.Color';
|
||||
import Activity from 'android.app.Activity';
|
||||
// import CardView from 'androidx.cardview.widget.CardView';
|
||||
import { ref, } from "vue"
|
||||
import TextUtils from 'android.text.TextUtils';
|
||||
import GradientDrawable from 'android.graphics.drawable.GradientDrawable'
|
||||
import MotionEvent from 'android.view.MotionEvent';
|
||||
import ColorStateList from 'android.content.res.ColorStateList';
|
||||
import attr from 'android.R.attr'
|
||||
import {hexToRgb,getDefaultColor} from "./xCoreColorUtil.uts"
|
||||
|
||||
|
||||
/**
|
||||
* 随机一个uid
|
||||
* @param rdix 随机因子
|
||||
* @param length 取的长度
|
||||
* @param isAddStr 是否限制随机结果中的长度,不允许输出长度
|
||||
* @returns String
|
||||
*/
|
||||
function getUid(rdix = 1, length = 12) : string {
|
||||
let ix = "";
|
||||
// #ifndef APP
|
||||
ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length);
|
||||
// #endif
|
||||
|
||||
// #ifdef APP
|
||||
ix = Math.floor(Math.random() * rdix * Math.floor(Math.random() * Date.now())).toString().substring(0, length as Int);
|
||||
// #endif
|
||||
return ix;
|
||||
}
|
||||
|
||||
class MyClickListener implements View.OnClickListener {
|
||||
callbackFun:()=>void;
|
||||
constructor(cal?:()=>void){
|
||||
this.callbackFun = cal as ()=>void;
|
||||
}
|
||||
override onClick(v?:View):void {
|
||||
if(typeof this.callbackFun !='underfinde'){
|
||||
this.callbackFun();
|
||||
}
|
||||
}
|
||||
}
|
||||
class MyTouchListener implements View.OnTouchListener {
|
||||
//down,up,move,cancel,longClick
|
||||
callbackFun:(types:string,event : MotionEvent,detail:UTSJSONObject)=>void;
|
||||
dubleTime = 0;
|
||||
tid = 56;
|
||||
_x = 0;
|
||||
_y = 0;
|
||||
// 判断方向的差值
|
||||
swipe_mindiff = 40
|
||||
// 滑动时,一定时间内只能触发一次事件。不能连续触发。
|
||||
tid_siper = 100;
|
||||
swipeDirection = ""
|
||||
constructor(call:(types:string,event : MotionEvent,detail?:UTSJSONObject)=>void) {
|
||||
this.callbackFun = call;
|
||||
}
|
||||
override onTouch(view : View, event : MotionEvent) : Boolean {
|
||||
|
||||
if(event.action == MotionEvent.ACTION_DOWN ){
|
||||
this._x = event.getX();
|
||||
this._y = event.getY();
|
||||
this.swipeDirection = ""
|
||||
this.callbackFun('down',event,{});
|
||||
let difftime = new Date().getTime() - this.dubleTime;
|
||||
if(difftime >0 && difftime<=300){
|
||||
this.callbackFun('doubleClick',event ,{});
|
||||
}
|
||||
this.dubleTime = new Date().getTime();
|
||||
clearTimeout(this.tid)
|
||||
tid = setTimeout(function() {
|
||||
this.callbackFun('longPress',event,{});
|
||||
}, 800);
|
||||
}
|
||||
if(event.action == MotionEvent.ACTION_UP ){
|
||||
clearTimeout(this.tid)
|
||||
this.callbackFun('up',event,{});
|
||||
if(new Date().getTime() - this.dubleTime > 50){
|
||||
this.callbackFun('click',event,{});
|
||||
}
|
||||
}
|
||||
if(event.action == MotionEvent.ACTION_CANCEL || event.action== MotionEvent.ACTION_OUTSIDE ){
|
||||
this.callbackFun('cancel',event,{});
|
||||
clearTimeout(this.tid)
|
||||
|
||||
}
|
||||
|
||||
if(event.action == MotionEvent.ACTION_MOVE ){
|
||||
let x = event.getX();
|
||||
let y = event.getY();
|
||||
let deltaX = Math.abs(x - this._x);
|
||||
let deltaY = Math.abs(y - this._y);
|
||||
if(deltaX > deltaY && deltaX > this.swipe_mindiff){
|
||||
this.swipeDirection = (this._x > x) ? "left" : "right";
|
||||
}else if(deltaY > deltaX && deltaY > this.swipe_mindiff){
|
||||
this.swipeDirection = (this._y < y) ? "down" : "up";
|
||||
}
|
||||
if(this.swipeDirection !=""){
|
||||
this.callbackFun('swiper',event,{x,y,diffX:deltaX,diffY:deltaY,direction:this.swipeDirection});
|
||||
}
|
||||
// this._x = x;
|
||||
// this._y = y;
|
||||
this.callbackFun('move',event,{});
|
||||
clearTimeout(this.tid)
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class xView {
|
||||
public view : any;
|
||||
public tagId:string = 'x_view_id_'+getUid();
|
||||
public bgView:GradientDrawable
|
||||
private _onclickFun= (_event : MotionEvent):void=>{};
|
||||
private _onTouchStart= (_event : MotionEvent):void=>{};
|
||||
private _onTouchEnd= (_event : MotionEvent):void=>{};
|
||||
private _onTouchCancel= (_event : MotionEvent):void=>{};
|
||||
private _onTouchMove= (_event : MotionEvent):void=>{};
|
||||
private _onTouchLongPress= (_event : MotionEvent):void=>{};
|
||||
private _onTouchDubleClick= (_event : MotionEvent):void=>{};
|
||||
private _onTouchSwiper= (_event : MotionEvent,detail?:UTSJSONObject):void=>{};
|
||||
// 设置禁用,不会触发上面的事件。
|
||||
private _disabled = false;
|
||||
constructor(context : Context) {
|
||||
let view = new View(context);
|
||||
|
||||
view.setTag(this.tagId)
|
||||
let layoutParams_crd = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
view.setLayoutParams(layoutParams_crd);
|
||||
this.bgView = new GradientDrawable();
|
||||
|
||||
// let statuesInts= [[attr.state_enabled].toKotlinList().toIntArray(),[attr.state_pressed].toKotlinList().toIntArray(),[-attr.state_enabled].toKotlinList().toIntArray()]
|
||||
// let statuesColors= [Color.WHITE,Color.RED,Color.RED]
|
||||
// let states = new ColorStateList(statuesInts.toKotlinList().toTypedArray(),statuesColors.toKotlinList().toIntArray())
|
||||
|
||||
this.bgView.setShape(GradientDrawable.RECTANGLE)
|
||||
this.bgView.setColor(Color.WHITE)
|
||||
this.bgView.mutate()
|
||||
view.setBackground(this.bgView)
|
||||
|
||||
this.view = view;
|
||||
|
||||
// view.setOnClickListener(new MyClickListener(():void=>{
|
||||
// this._onclickFun()
|
||||
// }));
|
||||
view.setOnTouchListener(new MyTouchListener((types:string,event : MotionEvent,detail?:UTSJSONObject):void=>{
|
||||
|
||||
if(this._disabled == false){
|
||||
if(types=='down'){
|
||||
this._onTouchStart(event)
|
||||
}
|
||||
if(types=='up'){
|
||||
this._onTouchEnd(event);
|
||||
}
|
||||
if(types=='click'){
|
||||
this._onclickFun(event);
|
||||
}
|
||||
if(types=='move'){
|
||||
this._onTouchMove(event)
|
||||
}
|
||||
if(types=='cancel'){
|
||||
this._onTouchCancel(event)
|
||||
}
|
||||
if(types=='longPress'){
|
||||
this._onTouchLongPress(event)
|
||||
}
|
||||
if(types=='doubleClick'){
|
||||
this._onTouchDubleClick(event)
|
||||
}
|
||||
if(types=='swiper'){
|
||||
this._onTouchSwiper(event,detail)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
}
|
||||
getView():View{
|
||||
return this.view as View;
|
||||
}
|
||||
setDisabled(dis:boolean) : xView{
|
||||
this._disabled = dis;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发距离是大于50时触发并判断方向
|
||||
* x,y当前坐标,diffX滑动时距离开始时按下的横向x距离,diffY表示竖向。Direction为方向:left,right,up,dowon
|
||||
*detail:
|
||||
*{x,y,diffX,diffY,direction}
|
||||
*/
|
||||
setSwiper(fun?:(event: MotionEvent,detail?:UTSJSONObject)=>void) : xView{
|
||||
if(typeof fun !== 'undefined'){
|
||||
this._onTouchSwiper = fun;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
setDoubleClick(fun?:(event: MotionEvent)=>void) : xView{
|
||||
if(typeof fun !== 'undefined'){
|
||||
this._onTouchDubleClick = fun;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
setClick(fun?:(event: MotionEvent)=>void) : xView{
|
||||
if(typeof fun !== 'undefined'){
|
||||
this._onclickFun = fun;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
setTouchStart(fun?:(event: MotionEvent)=>void) : xView{
|
||||
if(typeof fun !== 'undefined'){
|
||||
this._onTouchStart = fun;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
setTouchMove(fun?:(event: MotionEvent)=>void) : xView{
|
||||
if(typeof fun !== 'undefined'){
|
||||
this._onTouchMove = fun;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
setTouchEnd(fun?:(event: MotionEvent)=>void) : xView{
|
||||
|
||||
if(typeof fun !== 'undefined'){
|
||||
this._onTouchEnd = fun;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
setTouchCancel(fun?:(event: MotionEvent)=>void) : xView{
|
||||
if(typeof fun !== 'undefined'){
|
||||
this._onTouchCancel = fun;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
setTouchLongPress(fun?:(event: MotionEvent)=>void) : xView{
|
||||
if(typeof fun !== 'undefined'){
|
||||
this._onTouchLongPress = fun;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
setBackgroundColor(colorStr : string) : xView {
|
||||
this.bgView.mutate()
|
||||
if(colorStr == 'transparent'){
|
||||
this.bgView.setColor(Color.TRANSPARENT)
|
||||
}else{
|
||||
this.bgView.setColor(Color.parseColor(getDefaultColor(colorStr)))
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setBorder(w?:number,colorStr?:string,dashed?:boolean) : xView {
|
||||
this.bgView.mutate()
|
||||
|
||||
let tw:number = 1;
|
||||
let tcolor = 'transparent';
|
||||
let tdashed = false;
|
||||
if(typeof tw !== 'undefined'){
|
||||
tw = w as number;
|
||||
}
|
||||
if(typeof tw !== 'undefined'){
|
||||
tcolor = colorStr as string;
|
||||
}
|
||||
|
||||
if(typeof dashed !== 'undefined'){
|
||||
tdashed = dashed as boolean;
|
||||
}
|
||||
|
||||
let colorNum = Color.TRANSPARENT;
|
||||
if(tcolor != 'transparent'){
|
||||
colorNum = Color.parseColor(getDefaultColor(tcolor));
|
||||
}
|
||||
if(dashed == true){
|
||||
let dashedWidth = 8
|
||||
let dashedGap = 5
|
||||
this.bgView.setStroke(tw.toInt(),colorNum,dashedWidth.toFloat(),dashedGap.toFloat())
|
||||
}else{
|
||||
this.bgView.setStroke(tw.toInt(),colorNum)
|
||||
}
|
||||
|
||||
|
||||
return this;
|
||||
}
|
||||
setMargin(n:any):xView{
|
||||
let view = this.view as View;
|
||||
let selfPrams = view.getLayoutParams()
|
||||
let params = new LinearLayout.LayoutParams(
|
||||
selfPrams.width,
|
||||
selfPrams.height
|
||||
)
|
||||
|
||||
if(Array.isArray(n)){
|
||||
let rd = n as number[];
|
||||
if(rd.length==0){
|
||||
let rds = 0;
|
||||
let rdf = (rds).toInt();
|
||||
params.setMargins(rdf,rdf,rdf,rdf)
|
||||
}else if(rd.length==1){
|
||||
let rdf = (rd[0]).toInt();
|
||||
params.setMargins(rdf,rdf,rdf,rdf)
|
||||
}else if(rd.length==2){
|
||||
let rds = 0;
|
||||
params.setMargins((rd[0]).toInt(),(rd[1]).toInt(),(rd[0]).toInt(),(rd[1]).toInt())
|
||||
}else if(rd.length==3){
|
||||
let rds = 0;
|
||||
let rdf = rds.toInt();
|
||||
params.setMargins((rd[0]).toInt(),(rd[1]).toInt(),(rd[2]).toInt(),rdf)
|
||||
}else if(rd.length==4){
|
||||
params.setMargins((rd[0]).toInt(),(rd[1]).toInt(),(rd[2]).toInt(),(rd[3]).toInt())
|
||||
}
|
||||
}else{
|
||||
|
||||
let rd = n as number;
|
||||
let rdf = (rd).toInt();
|
||||
params.setMargins(rdf,rdf,rdf,rdf)
|
||||
}
|
||||
|
||||
view.setLayoutParams(params);
|
||||
return this;
|
||||
}
|
||||
|
||||
setPadding(n:any):xView{
|
||||
let view = this.view as View;
|
||||
if(Array.isArray(n)){
|
||||
let rd = n as number[];
|
||||
if(rd.length==0){
|
||||
let rds = 0;
|
||||
let rdf = (rds).toInt();
|
||||
view.setPadding(rdf,rdf,rdf,rdf)
|
||||
}else if(rd.length==1){
|
||||
let rdf = (rd[0]).toInt();
|
||||
view.setPadding(rdf,rdf,rdf,rdf)
|
||||
}else if(rd.length==2){
|
||||
view.setPadding((rd[0]).toInt(),(rd[1]).toInt(),(rd[0]).toInt(),(rd[1]).toInt())
|
||||
}else if(rd.length==3){
|
||||
let rds = 0;
|
||||
let rdf = rds.toInt();
|
||||
view.setPadding((rd[0]).toInt(),(rd[1]).toInt(),(rd[2]).toInt(),rdf)
|
||||
}else if(rd.length==4){
|
||||
view.setPadding((rd[0]).toInt(),(rd[1]).toInt(),(rd[2]).toInt(),(rd[3]).toInt())
|
||||
}
|
||||
}else{
|
||||
|
||||
let rd = n as number;
|
||||
let rdf = (rd).toInt();
|
||||
view.setPadding(rdf,rdf,rdf,rdf)
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
setRadius(radius:any) : xView{
|
||||
let x =0
|
||||
let y =0
|
||||
this.bgView.mutate()
|
||||
if(Array.isArray(radius)){
|
||||
let rd = radius as number[];
|
||||
if(rd.length==0){
|
||||
let rdfs = 0;
|
||||
this.bgView.setCornerRadius((rdfs).toFloat());
|
||||
// view.setBackground(this.bgView)
|
||||
}else if(rd.length==1){
|
||||
let rdfs = rd[0];
|
||||
this.bgView.setCornerRadius((rdfs).toFloat());
|
||||
// view.setBackground(this.bgView)
|
||||
}else if(rd.length==2){
|
||||
let tl = [
|
||||
(rd[0]).toFloat(),(rd[0]).toFloat(),
|
||||
(rd[1]).toFloat(),(rd[1]).toFloat(),
|
||||
x.toFloat(),x.toFloat(),
|
||||
x.toFloat(),x.toFloat()
|
||||
]
|
||||
let tlint = tl.toKotlinList().toFloatArray()
|
||||
this.bgView.setCornerRadii(tlint);
|
||||
}else if(rd.length==3){
|
||||
let tl = [
|
||||
(rd[0]).toFloat(),(rd[0]).toFloat(),
|
||||
(rd[1]).toFloat(),(rd[1]).toFloat(),
|
||||
(rd[2]).toFloat(),(rd[2]).toFloat(),
|
||||
x.toFloat(),x.toFloat()
|
||||
]
|
||||
let tlint = tl.toKotlinList().toFloatArray()
|
||||
this.bgView.setCornerRadii(tlint);
|
||||
}else if(rd.length==4){
|
||||
let tl = [
|
||||
(rd[0]).toFloat(),(rd[0]).toFloat(),
|
||||
(rd[1]).toFloat(),(rd[1]).toFloat(),
|
||||
(rd[2]).toFloat(),(rd[2]).toFloat(),
|
||||
(rd[3]).toFloat(),(rd[3]).toFloat(),
|
||||
]
|
||||
let tlint = tl.toKotlinList().toFloatArray()
|
||||
this.bgView.setCornerRadii(tlint);
|
||||
}
|
||||
}else{
|
||||
|
||||
let rd = radius as number;
|
||||
|
||||
this.bgView.setCornerRadius((rd).toFloat());
|
||||
}
|
||||
|
||||
|
||||
return this;
|
||||
}
|
||||
/** 设置当前宽和高为父级的相对宽和高
|
||||
*w,h有效值为:auto|100%
|
||||
*/
|
||||
setSizeBy(w:string="100%",h:string = "auto"){
|
||||
let view = this.view as View;
|
||||
let ww = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
let hh = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
if(w=="auto"){
|
||||
ww = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
if(h=="auto"){
|
||||
hh = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
|
||||
let layoutParams_crd = new LinearLayout.LayoutParams(ww,hh)
|
||||
view.setLayoutParams(layoutParams_crd);
|
||||
}
|
||||
setSize(width : number, height : number) : xView {
|
||||
let view = this.view as View;
|
||||
let layoutParams_crd = new LinearLayout.LayoutParams(
|
||||
(width).toInt(),
|
||||
(height).toInt()
|
||||
)
|
||||
view.setLayoutParams(layoutParams_crd);
|
||||
return this;
|
||||
}
|
||||
setWidth(width : number) : xView {
|
||||
let view = this.view as View;
|
||||
let lpar = view.getLayoutParams();
|
||||
let layoutParams_crd = new LinearLayout.LayoutParams(
|
||||
(width).toInt(),
|
||||
(lpar.height).toInt(),
|
||||
)
|
||||
view.setLayoutParams(layoutParams_crd);
|
||||
return this;
|
||||
}
|
||||
setHeight(height : number) : xView {
|
||||
let view = this.view as View;
|
||||
let lpar = view.getLayoutParams();
|
||||
let layoutParams_crd = new LinearLayout.LayoutParams(
|
||||
(lpar.width).toInt(),
|
||||
(height).toInt(),
|
||||
)
|
||||
view.setLayoutParams(layoutParams_crd);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default xView;
|
||||
@@ -0,0 +1,669 @@
|
||||
|
||||
/**
|
||||
* 数字转16进制
|
||||
*/
|
||||
function toHex(numbers : number) : string {
|
||||
let i = 0 as number
|
||||
if (numbers === i) {
|
||||
return '00';
|
||||
}
|
||||
let hex = '';
|
||||
const hexChars = '0123456789abcdef';
|
||||
while (numbers > 0) {
|
||||
const remainder = numbers % 16;
|
||||
hex = hexChars[remainder] + hex;
|
||||
numbers = Math.floor(numbers / 16);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色值列表
|
||||
*/
|
||||
|
||||
export const colors = new Map<string, string>([
|
||||
['primary', '#0579FF'],
|
||||
['success', '#09CC54'],
|
||||
['danger', '#FE6D1B'],
|
||||
['warn', '#F0A22C'],
|
||||
['error', '#E2001B'],
|
||||
['info', '#F2F2F2'],
|
||||
|
||||
['kleinblue', '#002FA7'], // 克莱因蓝
|
||||
['chinesered', '#FF0000'], // 中国红
|
||||
['internationalorange', '#FF4F00'], // 国际橙
|
||||
['egyptianvlue', '#1034A6'], // 埃及艳蓝
|
||||
['parisviolet', '#6C3082'], // 巴黎紫
|
||||
['moroccanblue', '#1256A7'], // 摩洛哥蓝
|
||||
['brazilgreen', '#009B3A'], // 巴西绿
|
||||
['britishracinggreen', '#004225'], // 英国赛车绿
|
||||
['indianyellow', '#E3A857'], // 印度黄
|
||||
['australiangold', '#FFDF00'], // 澳大利亚金
|
||||
['venetianred', '#C80815'], // 威尼斯红
|
||||
['majorelleblue', '#6050DC'], // 马若雷蓝
|
||||
['tuscanred', '#7C3030'], // 托斯卡红
|
||||
['naplesyellow', '#FADA5E'], // 那不勒斯黄
|
||||
['capumortuum', '#592720'], // 死者之首
|
||||
['mayablue', '#73C2FB'], // 玛雅蓝
|
||||
['persianrose', '#FE28A2'], // 波斯玫瑰
|
||||
['tyrianpurple', '#66023C'], // 泰尔紫
|
||||
['saharasand', '#F1E788'], // 撒哈拉沙
|
||||
['burmeseruby', '#B00A0A'], // 缅甸红宝石
|
||||
|
||||
['transparent', 'rgba(0,0,0,0)'],
|
||||
['aliceblue', '#F0F8FF'],
|
||||
['antiquewhite', '#FAEBD7'],
|
||||
['aqua', '#00FFFF'],
|
||||
['aquamarine', '#7FFFD4'],
|
||||
['azure', '#F0FFFF'],
|
||||
['beige', '#F5F5DC'],
|
||||
['bisque', '#FFE4C4'],
|
||||
['black', '#000000'],
|
||||
['blanchedalmond', '#FFEBCD'],
|
||||
['blue', '#0000FF'],
|
||||
['blueviolet', '#8A2BE2'],
|
||||
['brown', '#A52A2A'],
|
||||
['burlywood', '#DEB887'],
|
||||
['cadetblue', '#5F9EA0'],
|
||||
['chartreuse', '#7FFF00'],
|
||||
['chocolate', '#D2691E'],
|
||||
['coral', '#FF7F50'],
|
||||
['cornflowerblue', '#6495ED'],
|
||||
['cornsilk', '#FFF8DC'],
|
||||
['crimson', '#DC143C'],
|
||||
['cyan', '#00FFFF'],
|
||||
['darkblue', '#00008B'],
|
||||
['darkcyan', '#008B8B'],
|
||||
['darkgoldenrod', '#B8860B'],
|
||||
['darkgray', '#A9A9A9'],
|
||||
['darkgreen', '#006400'],
|
||||
['darkkhaki', '#BDB76B'],
|
||||
['darkmagenta', '#8B008B'],
|
||||
['darkolivegreen', '#556B2F'],
|
||||
['darkorange', '#FF8C00'],
|
||||
['darkorchid', '#9932CC'],
|
||||
['darkred', '#8B0000'],
|
||||
['darksalmon', '#E9967A'],
|
||||
['darkseagreen', '#8FBC8F'],
|
||||
['darkslateblue', '#483D8B'],
|
||||
['darkslategray', '#2F4F4F'],
|
||||
['darkturquoise', '#00CED1'],
|
||||
['darkviolet', '#9400D3'],
|
||||
['deeppink', '#FF1493'],
|
||||
['deepskyblue', '#00BFFF'],
|
||||
['dimgray', '#696969'],
|
||||
['dodgerblue', '#1E90FF'],
|
||||
['firebrick', '#B22222'],
|
||||
['floralwhite', '#FFFAF0'],
|
||||
['forestgreen', '#228B22'],
|
||||
['fuchsia', '#FF00FF'],
|
||||
['gainsboro', '#DCDCDC'],
|
||||
['ghostwhite', '#F8F8FF'],
|
||||
['gold', '#FFD700'],
|
||||
['goldenrod', '#DAA520'],
|
||||
['gray', '#808080'],
|
||||
['green', '#008000'],
|
||||
['greenyellow', '#ADFF2F'],
|
||||
['honeydew', '#F0FFF0'],
|
||||
['hotpink', '#FF69B4'],
|
||||
['indianred', '#CD5C5C'],
|
||||
['indigo', '#4B0082'],
|
||||
['ivory', '#FFFFF0'],
|
||||
['khaki', '#F0E68C'],
|
||||
['lavender', '#E6E6FA'],
|
||||
['lavenderblush', '#FFF0F5'],
|
||||
['lawngreen', '#7CFC00'],
|
||||
['lemonchiffon', '#FFFACD'],
|
||||
['lightblue', '#ADD8E6'],
|
||||
['lightcoral', '#F08080'],
|
||||
['lightcyan', '#E0FFFF'],
|
||||
['lightgoldenrodyellow', '#FAFAD2'],
|
||||
['lightgray', '#D3D3D3'],
|
||||
['lightgreen', '#90EE90'],
|
||||
['lightpink', '#FFB6C1'],
|
||||
['lightsalmon', '#FFA07A'],
|
||||
['lightseagreen', '#20B2AA'],
|
||||
['lightskyblue', '#87CEFA'],
|
||||
['lightslategray', '#778899'],
|
||||
['lightsteelblue', '#B0C4DE'],
|
||||
['lightyellow', '#FFFFE0'],
|
||||
['lime', '#00FF00'],
|
||||
['limegreen', '#32CD32'],
|
||||
['linen', '#FAF0E6'],
|
||||
['magenta', '#FF00FF'],
|
||||
['maroon', '#800000'],
|
||||
['mediumaquamarine', '#66CDAA'],
|
||||
['mediumblue', '#0000CD'],
|
||||
['mediumorchid', '#BA55D3'],
|
||||
['mediumpurple', '#9370DB'],
|
||||
['mediumseagreen', '#3CB371'],
|
||||
['mediumslateblue', '#7B68EE'],
|
||||
['mediumspringgreen', '#00FA9A'],
|
||||
['mediumturquoise', '#48D1CC'],
|
||||
['mediumvioletred', '#C71585'],
|
||||
['midnightblue', '#191970'],
|
||||
['mintcream', '#F5FFFA'],
|
||||
['mistyrose', '#FFE4E1'],
|
||||
['moccasin', '#FFE4B5'],
|
||||
['navajowhite', '#FFDEAD'],
|
||||
['navy', '#000080'],
|
||||
['oldlace', '#FDF5E6'],
|
||||
['olive', '#808000'],
|
||||
['olivedrab', '#6B8E23'],
|
||||
['orange', '#FFA500'],
|
||||
['orangered', '#FF4500'],
|
||||
['orchid', '#DA70D6'],
|
||||
['palegoldenrod', '#EEE8AA'],
|
||||
['palegreen', '#98FB98'],
|
||||
['paleturquoise', '#AFEEEE'],
|
||||
['palevioletred', '#DB7093'],
|
||||
['papayawhip', '#FFEFD5'],
|
||||
['peachpuff', '#FFDAB9'],
|
||||
['peru', '#CD853F'],
|
||||
['pink', '#FFC0CB'],
|
||||
['plum', '#DDA0DD'],
|
||||
['powderblue', '#B0E0E6'],
|
||||
['purple', '#800080'],
|
||||
['rebeccapurple', '#663399'],
|
||||
['red', '#FF0000'],
|
||||
['rosybrown', '#BC8F8F'],
|
||||
['royalblue', '#4169E1'],
|
||||
['saddlebrown', '#8B4513'],
|
||||
['salmon', '#FA8072'],
|
||||
['sandybrown', '#F4A460'],
|
||||
['seagreen', '#2E8B57'],
|
||||
['seashell', '#FFF5EE'],
|
||||
['sienna', '#A0522D'],
|
||||
['silver', '#C0C0C0'],
|
||||
['skyblue', '#87CEEB'],
|
||||
['slateblue', '#6A5ACD'],
|
||||
['slategray', '#708090'],
|
||||
['snow', '#FFFAFA'],
|
||||
['springgreen', '#00FF7F'],
|
||||
['steelblue', '#4682B4'],
|
||||
['tan', '#D2B48C'],
|
||||
['teal', '#008080'],
|
||||
['thistle', '#D8BFD8'],
|
||||
['tomato', '#FF6347'],
|
||||
['turquoise', '#40E0D0'],
|
||||
['violet', '#EE82EE'],
|
||||
['wheat', '#F5DEB3'],
|
||||
['white', '#FFFFFF'],
|
||||
['whitesmoke', '#F5F5F5'],
|
||||
['yellow', '#FFFF00'],
|
||||
['yellowgreen', '#9ACD32']
|
||||
|
||||
])
|
||||
|
||||
export function isValidColor(color : string) : boolean {
|
||||
// return chroma.valid(color);
|
||||
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8})$/;
|
||||
const rgbRegex = /^rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)$/;
|
||||
const rgbaRegex = /^rgba\((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s*,\s*((1(\.0{1,2})?)|(0(\.\d{1,2})?))\)$/;
|
||||
const hslRegex = /^hsl\((\d{1,3}), (\d{1,3})%, (\d{1,3})%\)$/;
|
||||
const hslaRegex = /^hsla\(\s*((\d{1,2}|[1-2]\d{2}|3[0-5]\d)(\.\d+)?|\d+(\.\d+)?)\s*,\s*((\d{1,2}|\d{0,1}\d{1}\d{1}|[1-2]\d{2}|3[0-5]\d)(\.\d+)?|\d+(\.\d+)?)%\s*,\s*((\d{1,2}|\d{0,1}\d{1}\d{1}|[1-2]\d{2}|3[0-5]\d)(\.\d+)?|\d+(\.\d+)?)%\s*,\s*((1|0(\.\d{1,2})?|(\.\d{1,2})))\)$/i;
|
||||
|
||||
|
||||
if (color == '') {
|
||||
return false; // 颜色值为空
|
||||
} else if (color === 'inherit' || color === 'transparent') {
|
||||
return false; // 特殊颜色值
|
||||
} else if (color === 'currentColor') {
|
||||
return false; // currentColor 不是有效的颜色值
|
||||
} else if (hexRegex.test(color) || rgbRegex.test(color) || rgbaRegex.test(color) || hslRegex.test(color) || hslaRegex.test(color)) {
|
||||
return true; // 符合颜色值的格式
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 获取默认的颜色值
|
||||
* @param [string] 颜色名称或者16进制颜色值
|
||||
*/
|
||||
export function getDefaultColor(sColor : string) : string {
|
||||
if(sColor=="") return ""
|
||||
let sc = sColor.toLocaleLowerCase().trim().replace(" ","");
|
||||
|
||||
if (isValidColor(sc)) {
|
||||
// 如果符合所有颜色值,进行转换为16进制。css方便统一解析。
|
||||
return sc;
|
||||
}
|
||||
// 检测是否是颜色名称。
|
||||
let sco = colors.get(sc);
|
||||
if (typeof sco == 'string') return sco as string;
|
||||
return colors.get("primary")!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 16进制转rgb
|
||||
*/
|
||||
export function hexToRgb(sColors : string) : UTSJSONObject {
|
||||
if (sColors == "") {
|
||||
return { r: 0, g: 0, b: 0, a: 0 }
|
||||
}
|
||||
let reg = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{8})$/;
|
||||
let sColor : string = sColors.toLowerCase();
|
||||
|
||||
if (sColor != '' && reg.test(sColor)) {
|
||||
|
||||
if (sColor.length == 4) {
|
||||
let sColorNew = "#";
|
||||
for (let i = 1; i < 4; i += 1) {
|
||||
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
|
||||
}
|
||||
sColor = sColorNew;
|
||||
}
|
||||
|
||||
//处理六位的颜色值
|
||||
let sColorChange : number[] = [];
|
||||
sColorChange.push(parseInt(sColor.substring(1, 3), 16));
|
||||
sColorChange.push(parseInt(sColor.substring(3, 5), 16));
|
||||
sColorChange.push(parseInt(sColor.substring(5, 7), 16));
|
||||
if(sColor.length==9){
|
||||
sColorChange.push(parseInt(sColor.substring(7, 9), 16)/255);
|
||||
}
|
||||
return {
|
||||
r: sColorChange[0],
|
||||
g: sColorChange[1],
|
||||
b: sColorChange[2],
|
||||
a: sColorChange.length==4?sColorChange[3]:1
|
||||
}
|
||||
} else if (/^(rgb|RGB|rgba|RGBA)/.test(sColor)) {
|
||||
let arr : string[] = sColor.replace(/(?:\(|\)|rgba|rgb|RGB|RGBA)*/g, "").split(",")
|
||||
let p : number[] = arr.map((val : string) : number => parseInt(val));
|
||||
if (p.length < 3) {
|
||||
return {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 1
|
||||
}
|
||||
}
|
||||
if (p.length == 3) {
|
||||
p.push(1)
|
||||
}
|
||||
return {
|
||||
r: p[0],
|
||||
g: p[1],
|
||||
b: p[2],
|
||||
a: p[3]
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* rgb转hsl
|
||||
*/
|
||||
export function rgbToHsl(rgb : UTSJSONObject) : UTSJSONObject {
|
||||
let r = rgb.getNumber("r")
|
||||
r = r as number / 255
|
||||
let g = rgb.getNumber("g")
|
||||
g = g as number / 255
|
||||
let b = rgb.getNumber("b")
|
||||
b = b as number / 255
|
||||
let a = rgb.getNumber("a")
|
||||
|
||||
var max = Math.max(r, g, b);
|
||||
var min = Math.min(r, g, b);
|
||||
let h=60 * (4 + (r - g) / (max - min));
|
||||
let s=(max - min) / (2 - max - min);
|
||||
let l= (max + min) / 2;
|
||||
|
||||
if (max === r) {
|
||||
h = (60 * (g - b)) / (max - min);
|
||||
} else if (max === g) {
|
||||
h = 60 * (2 + (b - r) / (max - min));
|
||||
}
|
||||
|
||||
if (h < 0) {
|
||||
h += 360;
|
||||
}
|
||||
|
||||
|
||||
if (max === min) {
|
||||
s = 0;
|
||||
} else if (l < 0.5) {
|
||||
s = (max - min) / (max + min);
|
||||
}
|
||||
|
||||
return { h: h, s: s * 100, l: l * 100, a: a };
|
||||
}
|
||||
|
||||
/**
|
||||
* hsl转rgb
|
||||
*/
|
||||
export function hslToRgb(hsl : UTSJSONObject) : UTSJSONObject {
|
||||
let h = hsl.getNumber("h")!
|
||||
let s = hsl.getNumber("s")!
|
||||
let l = hsl.getNumber("l")!
|
||||
let a = hsl.getNumber("a")!
|
||||
|
||||
h = h / 360;
|
||||
s = s / 100;
|
||||
l = l / 100;
|
||||
let r = l;
|
||||
let g = l;
|
||||
let b = l;
|
||||
|
||||
function hue2rgb(pxx : number, q : number, txs : number) : number {
|
||||
let t = txs;
|
||||
let p = pxx;
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (s > 0) {
|
||||
let q = l * (1 + s);
|
||||
if (l >= 0.5) {
|
||||
q = l + s - l * s;
|
||||
}
|
||||
var p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
|
||||
}
|
||||
|
||||
r = Math.round(r * 255);
|
||||
g = Math.round(g * 255);
|
||||
b = Math.round(b * 255);
|
||||
return { r, g, b, a };
|
||||
}
|
||||
|
||||
/**
|
||||
* rgb转16进制
|
||||
*/
|
||||
export function rgbToHex(rgb : UTSJSONObject) : string {
|
||||
let r = rgb.getNumber("r")!
|
||||
let g = rgb.getNumber("g")!
|
||||
let b = rgb.getNumber("b")!
|
||||
let a = rgb.getNumber("a")!
|
||||
return "#" + toHex(r) + toHex(g) + toHex(b) + toHex(a*255)
|
||||
}
|
||||
export function hslaToCss(hsl : UTSJSONObject) : string {
|
||||
|
||||
let rgb = hslToRgb(hsl)
|
||||
|
||||
return rgbToHex(rgb)
|
||||
}
|
||||
|
||||
export function hslaToRgbCss(hsl : UTSJSONObject) : string {
|
||||
|
||||
let rgb = hslToRgb(hsl)
|
||||
let r = rgb.getNumber("r")!
|
||||
let g = rgb.getNumber("g")!
|
||||
let b = rgb.getNumber("b")!
|
||||
let a = rgb.getNumber("a")!
|
||||
return `rgba(${r},${g},${b},${a})`
|
||||
}
|
||||
|
||||
// 根据 HSLA 颜色计算亮度
|
||||
export function getLuminance(color : UTSJSONObject, type = "rgba") : number {
|
||||
|
||||
|
||||
let colordefault : UTSJSONObject = type == 'rgba' ? color : hslToRgb(color);
|
||||
const r= colordefault.getNumber("r")!
|
||||
const g= colordefault.getNumber("g")!
|
||||
const b= colordefault.getNumber("b")!
|
||||
const rs = r / 255
|
||||
const gs = g / 255
|
||||
const bs = b / 255
|
||||
const rl = Math.pow(rs <= 0.03928 ? rs / 12.92 : ((rs + 0.055) / 1.055), 2.4);
|
||||
const gl = Math.pow(gs <= 0.03928 ? gs / 12.92 : ((gs + 0.055) / 1.055), 2.4);
|
||||
const bl = Math.pow(bs <= 0.03928 ? bs / 12.92 : ((bs + 0.055) / 1.055), 2.4);
|
||||
|
||||
return 0.2126 * rl + 0.7152 * gl + 0.0722 * bl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对颜色加深
|
||||
*/
|
||||
export function colorAddDeepen(sColor : string) : string {
|
||||
|
||||
let rgb = hexToRgb(getDefaultColor(sColor));
|
||||
|
||||
let hsl = rgbToHsl(rgb);
|
||||
|
||||
let l = hsl.getNumber("l")! - 5;
|
||||
l = Math.max(0, Math.min(l, 100))
|
||||
|
||||
return hslaToRgbCss({ h: hsl.getNumber("h"), s: hsl.getNumber("s"), l, a: hsl.getNumber("a")! })
|
||||
}
|
||||
/**
|
||||
* 对颜色提亮
|
||||
*/
|
||||
export function colorAddBrighten(sColor : string) : string {
|
||||
let rgb = hexToRgb(getDefaultColor(sColor));
|
||||
let hsl = rgbToHsl(rgb);
|
||||
let l = hsl.getNumber("l")! + 10;
|
||||
l = Math.max(0, Math.min(l, 100))
|
||||
return rgbToHex(hslToRgb({ h: hsl.getNumber("h"), s:hsl.getNumber("s"), l, a: hsl.getNumber("a")! }))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取浅色系主题,根据颜色值计算。返回正常值和hover值。
|
||||
*/
|
||||
export function colorGetThint(sColor : string) {
|
||||
let rgb = hexToRgb(getDefaultColor(sColor));
|
||||
let hsl = rgbToHsl(rgb);
|
||||
let s = hsl.getNumber("s")!;
|
||||
// let l = hsl.getNumber("l");
|
||||
if (Math.ceil(s) > 0) {
|
||||
s = 45
|
||||
}
|
||||
return {
|
||||
default: rgbToHex(hslToRgb({ h: hsl.getNumber("h"), s, l: 93, a: hsl.getNumber("a")! })),
|
||||
hover: rgbToHex(hslToRgb({ h: hsl.getNumber("h"), s, l: 85, a: hsl.getNumber("a")! }))
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取正常主题,根据颜色值计算。返回正常值和hover值。
|
||||
*/
|
||||
export function colorGetHover(sColor : string) {
|
||||
|
||||
return {
|
||||
default: sColor,
|
||||
hover: colorAddDeepen(sColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理相主题色。
|
||||
* @param color 需要处理的颜色值
|
||||
* @param hoverColor 对应按下去的颜色默认与color相等。
|
||||
* @returns UTSJSONObject
|
||||
*/
|
||||
export function getDefaultColorObj(color : string, hoverColor : string) : UTSJSONObject {
|
||||
let hsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(color)));
|
||||
let hoverHsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
|
||||
let shadow = hslaToRgbCss({ h: hsla.getNumber("h")!, s: (hsla.getNumber("s")!)== 0 ? 0 : 60, l: 40, a: 0.4 });
|
||||
let shadowHOver = hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: (hoverHsla.getNumber("s")!)== 0 ? 0 : 60, l: 40, a: 0.4 });
|
||||
if((hsla.getNumber("h")!)==0&&(hsla.getNumber("s")!)==0){
|
||||
shadow = "transparent"
|
||||
}
|
||||
if((hoverHsla.getNumber("h")!)==0&&(hoverHsla.getNumber("s")!)==0){
|
||||
shadowHOver = "transparent"
|
||||
}
|
||||
let o = {
|
||||
default: {
|
||||
background: hslaToRgbCss(hsla),
|
||||
borderColor: hslaToRgbCss({ h: hsla.getNumber("h")!, s: hsla.getNumber("s")!, a: hsla.getNumber("a")!, l: Math.min(Math.max(0, hsla.getNumber("l")! - 5), hsla.getNumber("l")!) }),
|
||||
fontColor: getLuminance(hsla, 'hsla') < 0.46 && hsla.getNumber("a")! > 0.1 ? '#ffffff' : '#000000',
|
||||
shadow: shadow
|
||||
},
|
||||
active: {
|
||||
background: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: Math.min(Math.max(0, hoverHsla.getNumber("l")! - 10), hoverHsla.getNumber("l")!) }),
|
||||
borderColor: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: Math.max(Math.max(0, hoverHsla.getNumber("l")! - 10), hoverHsla.getNumber("l")!) }),
|
||||
fontColor: getLuminance(hoverHsla, 'hsla') < 0.46 && hoverHsla.getNumber("a")! > 0.1 ? '#ffffff' : '#000000',
|
||||
shadow: shadowHOver
|
||||
}
|
||||
} as UTSJSONObject
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理相主题色。
|
||||
* @param color 需要处理的颜色值
|
||||
* @param hoverColor 对应按下去的颜色默认与color相等。
|
||||
* @returns UTSJSONObject
|
||||
*/
|
||||
export function getTextColorObj(color : string, hoverColor : string) : UTSJSONObject {
|
||||
let hsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(color)));
|
||||
let hoverHsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
|
||||
// tmxColor.hslaToCss({ ...hsla}) : tmxColor.hslaToCss({ ...hsla,l:30})
|
||||
let o = {
|
||||
default: {
|
||||
background: "transparent",
|
||||
borderColor: "transparent",
|
||||
fontColor: getLuminance(hsla, 'hsla') < 0.3 && hsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hsla) : hslaToRgbCss({ h: hsla.getNumber("h")!, s: hsla.getNumber("s")!, a: hsla.getNumber("a")!, l: 30 }),
|
||||
shadow: "transparent",
|
||||
},
|
||||
active: {
|
||||
background: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: 95 }),
|
||||
borderColor:"transparent",
|
||||
fontColor: getLuminance(hoverHsla, 'hsla') < 0.3 && hoverHsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hoverHsla) : hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: 30 }),
|
||||
shadow: "transparent",
|
||||
}
|
||||
} as UTSJSONObject
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理相主题色。
|
||||
* @param color 需要处理的颜色值
|
||||
* @param hoverColor 对应按下去的颜色默认与color相等。
|
||||
* @returns UTSJSONObject
|
||||
*/
|
||||
export function getThinColorObj(color : string, hoverColor : string,isCoverDark?:boolean) : UTSJSONObject {
|
||||
let dark = isCoverDark==null?false:isCoverDark
|
||||
// #ifdef WEB
|
||||
dark = isCoverDark==undefined?false:dark
|
||||
// #endif
|
||||
|
||||
let hsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(color)));
|
||||
let hoverHsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
|
||||
// tmxColor.hslaToCss({ ...hsla}) : tmxColor.hslaToCss({ ...hsla,l:30})
|
||||
let fontcolor = getLuminance(hsla, 'hsla') < 0.3 && hsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hsla) : hslaToRgbCss({ h: hsla.getNumber("h")!, s: hsla.getNumber("s")!, a: hsla.getNumber("a")!, l:30 })
|
||||
let bordercolor = hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: dark==true?21:92 })
|
||||
let bgcolor = hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: dark==true?20:95 })
|
||||
if(dark){
|
||||
let p = hsla;
|
||||
p.set('l',90)
|
||||
fontcolor = hslaToRgbCss(p)
|
||||
p.set('l',20)
|
||||
p.set('s',p.getNumber('s')!==0?0:20)
|
||||
bgcolor = hslaToRgbCss(p)
|
||||
p.set('l',22)
|
||||
bordercolor = hslaToRgbCss(p)
|
||||
}
|
||||
let o = {
|
||||
default: {
|
||||
background: bgcolor,
|
||||
borderColor:bordercolor,
|
||||
fontColor: fontcolor,
|
||||
shadow: "transparent",
|
||||
},
|
||||
active: {
|
||||
background: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: dark==true?18:88 }),
|
||||
borderColor: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a:0, l: dark==true?18:80 }),
|
||||
fontColor: getLuminance(hoverHsla, 'hsla') < 0.3 && hoverHsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hoverHsla) : hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: 30 }),
|
||||
shadow: "transparent",
|
||||
}
|
||||
} as UTSJSONObject
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理相主题色。
|
||||
* @param color 需要处理的颜色值
|
||||
* @param hoverColor 对应按下去的颜色默认与color相等。
|
||||
* @returns UTSJSONObject
|
||||
*/
|
||||
export function getOutlineColorObj(color : string, hoverColor : string) : UTSJSONObject {
|
||||
let hsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(color)));
|
||||
let hoverHsla : UTSJSONObject = rgbToHsl(hexToRgb(getDefaultColor(hoverColor)));
|
||||
// tmxColor.hslaToCss({ ...hsla}) : tmxColor.hslaToCss({ ...hsla,l:30})
|
||||
let o = {
|
||||
default: {
|
||||
background: 'transparent',
|
||||
borderColor:hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: 1, l: 75 }),
|
||||
fontColor: getLuminance(hsla, 'hsla') < 0.3 && hsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hsla) : hslaToRgbCss({ h: hsla.getNumber("h")!, s: hsla.getNumber("s")!, a: hsla.getNumber("a")!, l: 30 }),
|
||||
shadow: "transparent",
|
||||
},
|
||||
|
||||
active: {
|
||||
background: 'transparent',
|
||||
borderColor: hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a:1, l: 70 }),
|
||||
fontColor: getLuminance(hoverHsla, 'hsla') < 0.3 && hoverHsla.getNumber("a")! > 0.1 ? hslaToRgbCss(hoverHsla) : hslaToRgbCss({ h: hoverHsla.getNumber("h")!, s: hoverHsla.getNumber("s")!, a: hoverHsla.getNumber("a")!, l: 30 }),
|
||||
shadow: "transparent",
|
||||
}
|
||||
} as UTSJSONObject
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置颜色的亮度值。29
|
||||
* isCoverDark:是根据亮度值,是否要设置n的值,如果n超过了
|
||||
* 指定的30就不操作。
|
||||
*/
|
||||
export function setTextColorLightByDark(color:string):string{
|
||||
let realColor = getDefaultColor(color);
|
||||
let hsla = rgbToHsl(hexToRgb(realColor))
|
||||
let l = hsla.getNumber('l')!
|
||||
let s = hsla.getNumber('s')!
|
||||
if(l<30){
|
||||
if(s>0){
|
||||
hsla.set('l',50)
|
||||
}else{
|
||||
hsla.set('l',100)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return hslaToRgbCss(hsla)
|
||||
}
|
||||
/**
|
||||
* 变动的颜色是否需要变深。如果不需要返回原始颜色
|
||||
* 如果果需要自动加深。
|
||||
*/
|
||||
export function setBgColorLightByDark(color:string):string{
|
||||
let realColor = getDefaultColor(color);
|
||||
let hsla = rgbToHsl(hexToRgb(realColor))
|
||||
let l = hsla.getNumber('l')!
|
||||
let s = hsla.getNumber('s')!
|
||||
if(s==0&&l>50){
|
||||
hsla.set('l',100-l)
|
||||
}
|
||||
if(s>0){
|
||||
if(l>50){
|
||||
hsla.set('l',100-l)
|
||||
}
|
||||
}
|
||||
return hslaToRgbCss(hsla)
|
||||
}
|
||||
|
||||
export function isBlackAndWhite(color:string):boolean{
|
||||
let realColor = getDefaultColor(color);
|
||||
let hsla = rgbToHsl(hexToRgb(realColor))
|
||||
let s = hsla.getNumber('s')!
|
||||
return s==0
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user