大幅変更&環境バージョンアップ
This commit is contained in:
@ -0,0 +1,4 @@
|
||||
<manifest package="net.touchcapture.qr.flutterqr"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
||||
</manifest>
|
||||
@ -0,0 +1,45 @@
|
||||
package net.touchcapture.qr.flutterqr
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import com.journeyapps.barcodescanner.BarcodeView
|
||||
import com.journeyapps.barcodescanner.Size
|
||||
|
||||
class CustomFramingRectBarcodeView : BarcodeView {
|
||||
private var bottomOffset = BOTTOM_OFFSET_NOT_SET_VALUE
|
||||
|
||||
constructor(context: Context?) : super(context)
|
||||
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
override fun calculateFramingRect(container: Rect, surface: Rect): Rect {
|
||||
val containerArea = Rect(container)
|
||||
val intersects =
|
||||
containerArea.intersect(surface) //adjusts the containerArea (code from super.calculateFramingRect)
|
||||
val scanAreaRect = super.calculateFramingRect(container, surface)
|
||||
if (bottomOffset != BOTTOM_OFFSET_NOT_SET_VALUE) { //if the setFramingRect function was called, then we shift the scan area by Y
|
||||
val scanAreaRectWithOffset = Rect(scanAreaRect)
|
||||
scanAreaRectWithOffset.bottom -= bottomOffset
|
||||
scanAreaRectWithOffset.top -= bottomOffset
|
||||
val belongsToContainer = scanAreaRectWithOffset.intersect(containerArea)
|
||||
if (belongsToContainer) {
|
||||
return scanAreaRectWithOffset
|
||||
}
|
||||
}
|
||||
return scanAreaRect
|
||||
}
|
||||
|
||||
fun setFramingRect(rectWidth: Int, rectHeight: Int, bottomOffset: Int) {
|
||||
this.bottomOffset = bottomOffset
|
||||
framingRectSize = Size(rectWidth, rectHeight)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val BOTTOM_OFFSET_NOT_SET_VALUE = -1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package net.touchcapture.qr.flutterqr
|
||||
|
||||
import androidx.annotation.NonNull
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
|
||||
class FlutterQrPlugin : FlutterPlugin, ActivityAware {
|
||||
|
||||
/** Plugin registration embedding v2 */
|
||||
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterPluginBinding.platformViewRegistry
|
||||
.registerViewFactory(
|
||||
VIEW_TYPE_ID,
|
||||
QRViewFactory(flutterPluginBinding.binaryMessenger)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
// Leave empty
|
||||
// Nullifying QrShared.activity and QrShared.binding here will cause errors if plugin is detached by another plugin
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
|
||||
QrShared.activity = activityPluginBinding.activity
|
||||
QrShared.binding = activityPluginBinding
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
QrShared.activity = null
|
||||
QrShared.binding = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(activityPluginBinding: ActivityPluginBinding) {
|
||||
QrShared.activity = activityPluginBinding.activity
|
||||
QrShared.binding = activityPluginBinding
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
QrShared.activity = null
|
||||
QrShared.binding = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VIEW_TYPE_ID = "net.touchcapture.qr.flutterqr/qrview"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,386 @@
|
||||
package net.touchcapture.qr.flutterqr
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.ResultPoint
|
||||
import com.journeyapps.barcodescanner.BarcodeCallback
|
||||
import com.journeyapps.barcodescanner.BarcodeResult
|
||||
import com.journeyapps.barcodescanner.DefaultDecoderFactory
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
|
||||
class QRView(
|
||||
private val context: Context,
|
||||
messenger: BinaryMessenger,
|
||||
private val id: Int,
|
||||
private val params: HashMap<String, Any>
|
||||
) : PlatformView, MethodChannel.MethodCallHandler, PluginRegistry.RequestPermissionsResultListener {
|
||||
|
||||
private val cameraRequestCode = QrShared.CAMERA_REQUEST_ID + this.id
|
||||
|
||||
private val channel: MethodChannel = MethodChannel(
|
||||
messenger, "net.touchcapture.qr.flutterqr/qrview_$id"
|
||||
)
|
||||
private val cameraFacingBack = 0
|
||||
private val cameraFacingFront = 1
|
||||
|
||||
private var isRequestingPermission = false
|
||||
private var isTorchOn = false
|
||||
private var isPaused = false
|
||||
private var barcodeView: CustomFramingRectBarcodeView? = null
|
||||
private var unRegisterLifecycleCallback: UnRegisterLifecycleCallback? = null
|
||||
|
||||
init {
|
||||
QrShared.binding?.addRequestPermissionsResultListener(this)
|
||||
|
||||
channel.setMethodCallHandler(this)
|
||||
|
||||
unRegisterLifecycleCallback = QrShared.activity?.registerLifecycleCallbacks(
|
||||
onPause = {
|
||||
if (!isPaused && hasCameraPermission) barcodeView?.pause()
|
||||
|
||||
},
|
||||
onResume = {
|
||||
if (!hasCameraPermission && !isRequestingPermission) checkAndRequestPermission()
|
||||
else if (!isPaused && hasCameraPermission) barcodeView?.resume()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
unRegisterLifecycleCallback?.invoke()
|
||||
|
||||
QrShared.binding?.removeRequestPermissionsResultListener(this)
|
||||
|
||||
barcodeView?.pause()
|
||||
barcodeView = null
|
||||
}
|
||||
|
||||
override fun getView(): View = initBarCodeView()
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when (call.method) {
|
||||
"startScan" -> startScan(call.arguments as? List<Int>, result)
|
||||
|
||||
"stopScan" -> stopScan()
|
||||
|
||||
"flipCamera" -> flipCamera(result)
|
||||
|
||||
"toggleFlash" -> toggleFlash(result)
|
||||
|
||||
"pauseCamera" -> pauseCamera(result)
|
||||
|
||||
// Stopping camera is the same as pausing camera
|
||||
"stopCamera" -> pauseCamera(result)
|
||||
|
||||
"resumeCamera" -> resumeCamera(result)
|
||||
|
||||
"requestPermissions" -> checkAndRequestPermission()
|
||||
|
||||
"getCameraInfo" -> getCameraInfo(result)
|
||||
|
||||
"getFlashInfo" -> getFlashInfo(result)
|
||||
|
||||
"getSystemFeatures" -> getSystemFeatures(result)
|
||||
|
||||
"changeScanArea" -> changeScanArea(
|
||||
dpScanAreaWidth = requireNotNull(call.argument<Double>("scanAreaWidth")),
|
||||
dpScanAreaHeight = requireNotNull(call.argument<Double>("scanAreaHeight")),
|
||||
cutOutBottomOffset = requireNotNull(call.argument<Double>("cutOutBottomOffset")),
|
||||
result = result,
|
||||
)
|
||||
|
||||
"invertScan" -> setInvertScan(
|
||||
isInvert = call.argument<Boolean>("isInvertScan") ?: false,
|
||||
)
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initBarCodeView(): CustomFramingRectBarcodeView {
|
||||
var barcodeView = barcodeView
|
||||
|
||||
if (barcodeView == null) {
|
||||
barcodeView = CustomFramingRectBarcodeView(QrShared.activity).also {
|
||||
this.barcodeView = it
|
||||
}
|
||||
|
||||
barcodeView.decoderFactory = DefaultDecoderFactory(null, null, null, 2)
|
||||
|
||||
if (params[PARAMS_CAMERA_FACING] as Int == 1) {
|
||||
barcodeView.cameraSettings?.requestedCameraId = cameraFacingFront
|
||||
}
|
||||
} else if (!isPaused) {
|
||||
barcodeView.resume()
|
||||
}
|
||||
|
||||
return barcodeView
|
||||
}
|
||||
|
||||
// region Camera Info
|
||||
|
||||
private fun getCameraInfo(result: MethodChannel.Result) {
|
||||
val barcodeView = barcodeView ?: return barCodeViewNotSet(result)
|
||||
|
||||
result.success(barcodeView.cameraSettings.requestedCameraId)
|
||||
}
|
||||
|
||||
private fun getFlashInfo(result: MethodChannel.Result) {
|
||||
if (barcodeView == null) return barCodeViewNotSet(result)
|
||||
|
||||
result.success(isTorchOn)
|
||||
}
|
||||
|
||||
private fun hasFlash(): Boolean {
|
||||
return hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)
|
||||
}
|
||||
|
||||
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
||||
private fun hasBackCamera(): Boolean {
|
||||
return hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
||||
}
|
||||
|
||||
private fun hasFrontCamera(): Boolean {
|
||||
return hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)
|
||||
}
|
||||
|
||||
private fun hasSystemFeature(feature: String): Boolean =
|
||||
context.packageManager.hasSystemFeature(feature)
|
||||
|
||||
private fun getSystemFeatures(result: MethodChannel.Result) {
|
||||
try {
|
||||
result.success(
|
||||
mapOf(
|
||||
"hasFrontCamera" to hasFrontCamera(),
|
||||
"hasBackCamera" to hasBackCamera(),
|
||||
"hasFlash" to hasFlash(),
|
||||
"activeCamera" to barcodeView?.cameraSettings?.requestedCameraId
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
result.error("", e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Camera Controls
|
||||
|
||||
private fun flipCamera(result: MethodChannel.Result) {
|
||||
val barcodeView = barcodeView ?: return barCodeViewNotSet(result)
|
||||
|
||||
barcodeView.pause()
|
||||
|
||||
val settings = barcodeView.cameraSettings
|
||||
if (settings.requestedCameraId == cameraFacingFront) {
|
||||
settings.requestedCameraId = cameraFacingBack
|
||||
} else settings.requestedCameraId = cameraFacingFront
|
||||
|
||||
barcodeView.resume()
|
||||
|
||||
result.success(settings.requestedCameraId)
|
||||
}
|
||||
|
||||
private fun toggleFlash(result: MethodChannel.Result) {
|
||||
val barcodeView = barcodeView ?: return barCodeViewNotSet(result)
|
||||
|
||||
if (hasFlash()) {
|
||||
barcodeView.setTorch(!isTorchOn)
|
||||
isTorchOn = !isTorchOn
|
||||
result.success(isTorchOn)
|
||||
} else {
|
||||
result.error(ERROR_CODE_NOT_SET, ERROR_MESSAGE_FLASH_NOT_FOUND, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pauseCamera(result: MethodChannel.Result) {
|
||||
val barcodeView = barcodeView ?: return barCodeViewNotSet(result)
|
||||
|
||||
if (barcodeView.isPreviewActive) {
|
||||
isPaused = true
|
||||
barcodeView.pause()
|
||||
}
|
||||
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
private fun resumeCamera(result: MethodChannel.Result) {
|
||||
val barcodeView = barcodeView ?: return barCodeViewNotSet(result)
|
||||
|
||||
if (!barcodeView.isPreviewActive) {
|
||||
isPaused = false
|
||||
barcodeView.resume()
|
||||
}
|
||||
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
private fun startScan(arguments: List<Int>?, result: MethodChannel.Result) {
|
||||
checkAndRequestPermission()
|
||||
|
||||
val allowedBarcodeTypes = getAllowedBarcodeTypes(arguments, result)
|
||||
|
||||
if (arguments == null) {
|
||||
barcodeView?.decoderFactory = DefaultDecoderFactory(null, null, null, 2)
|
||||
} else {
|
||||
barcodeView?.decoderFactory = DefaultDecoderFactory(allowedBarcodeTypes, null, null, 2)
|
||||
}
|
||||
|
||||
barcodeView?.decodeContinuous(
|
||||
object : BarcodeCallback {
|
||||
override fun barcodeResult(result: BarcodeResult) {
|
||||
if (allowedBarcodeTypes.isEmpty() || allowedBarcodeTypes.contains(result.barcodeFormat)) {
|
||||
val code = mapOf(
|
||||
"code" to result.text,
|
||||
"type" to result.barcodeFormat.name,
|
||||
"rawBytes" to result.rawBytes
|
||||
)
|
||||
|
||||
channel.invokeMethod(CHANNEL_METHOD_ON_RECOGNIZE_QR, code)
|
||||
}
|
||||
}
|
||||
|
||||
override fun possibleResultPoints(resultPoints: List<ResultPoint>) = Unit
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun stopScan() {
|
||||
barcodeView?.stopDecoding()
|
||||
}
|
||||
|
||||
private fun setInvertScan(isInvert: Boolean) {
|
||||
val barcodeView = barcodeView ?: return
|
||||
with(barcodeView) {
|
||||
pause()
|
||||
cameraSettings.isScanInverted = isInvert
|
||||
resume()
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeScanArea(
|
||||
dpScanAreaWidth: Double,
|
||||
dpScanAreaHeight: Double,
|
||||
cutOutBottomOffset: Double,
|
||||
result: MethodChannel.Result
|
||||
) {
|
||||
setScanAreaSize(dpScanAreaWidth, dpScanAreaHeight, cutOutBottomOffset)
|
||||
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
private fun setScanAreaSize(
|
||||
dpScanAreaWidth: Double,
|
||||
dpScanAreaHeight: Double,
|
||||
dpCutOutBottomOffset: Double
|
||||
) {
|
||||
barcodeView?.setFramingRect(
|
||||
dpScanAreaWidth.convertDpToPixels(),
|
||||
dpScanAreaHeight.convertDpToPixels(),
|
||||
dpCutOutBottomOffset.convertDpToPixels(),
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region permissions
|
||||
|
||||
private val hasCameraPermission: Boolean
|
||||
get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.M ||
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.CAMERA
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
): Boolean {
|
||||
if (requestCode != cameraRequestCode) return false
|
||||
isRequestingPermission = false
|
||||
|
||||
val permissionGranted =
|
||||
grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
channel.invokeMethod(CHANNEL_METHOD_ON_PERMISSION_SET, permissionGranted)
|
||||
|
||||
return permissionGranted
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun checkAndRequestPermission() {
|
||||
if (hasCameraPermission) {
|
||||
channel.invokeMethod(CHANNEL_METHOD_ON_PERMISSION_SET, true)
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isRequestingPermission) {
|
||||
QrShared.activity?.requestPermissions(
|
||||
arrayOf(Manifest.permission.CAMERA),
|
||||
cameraRequestCode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region barcode common
|
||||
|
||||
private fun getAllowedBarcodeTypes(
|
||||
arguments: List<Int>?,
|
||||
result: MethodChannel.Result
|
||||
): List<BarcodeFormat> {
|
||||
return try {
|
||||
arguments?.map {
|
||||
BarcodeFormat.values()[it]
|
||||
}.orEmpty()
|
||||
} catch (e: Exception) {
|
||||
result.error("", e.message, null)
|
||||
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun barCodeViewNotSet(result: MethodChannel.Result) {
|
||||
result.error(
|
||||
ERROR_CODE_NOT_SET,
|
||||
ERROR_MESSAGE_NOT_SET,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region helpers
|
||||
|
||||
private fun Double.convertDpToPixels() =
|
||||
(this * context.resources.displayMetrics.density).toInt()
|
||||
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_METHOD_ON_PERMISSION_SET = "onPermissionSet"
|
||||
private const val CHANNEL_METHOD_ON_RECOGNIZE_QR = "onRecognizeQR"
|
||||
|
||||
private const val PARAMS_CAMERA_FACING = "cameraFacing"
|
||||
|
||||
private const val ERROR_CODE_NOT_SET = "404"
|
||||
|
||||
private const val ERROR_MESSAGE_NOT_SET = "No barcode view found"
|
||||
private const val ERROR_MESSAGE_FLASH_NOT_FOUND = "This device doesn't support flash"
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
package net.touchcapture.qr.flutterqr
|
||||
|
||||
import android.content.Context
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import io.flutter.plugin.platform.PlatformViewFactory
|
||||
|
||||
|
||||
class QRViewFactory(
|
||||
private val messenger: BinaryMessenger
|
||||
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
|
||||
|
||||
override fun create(
|
||||
context: Context?,
|
||||
viewId: Int,
|
||||
args: Any?
|
||||
): PlatformView {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val params = args as HashMap<String, Any>
|
||||
|
||||
return QRView(
|
||||
context = requireNotNull(context),
|
||||
id = viewId,
|
||||
messenger = messenger,
|
||||
params = params
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package net.touchcapture.qr.flutterqr
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
|
||||
class UnRegisterLifecycleCallback(
|
||||
private val application: Application,
|
||||
private val callback: Application.ActivityLifecycleCallbacks,
|
||||
) {
|
||||
operator fun invoke() = application.unregisterActivityLifecycleCallbacks(callback)
|
||||
}
|
||||
|
||||
fun Activity.registerLifecycleCallbacks(
|
||||
onPause: (() -> Unit)? = null,
|
||||
onResume: (() -> Unit)? = null,
|
||||
): UnRegisterLifecycleCallback {
|
||||
val callback = object : Application.ActivityLifecycleCallbacks {
|
||||
override fun onActivityPaused(p0: Activity) {
|
||||
if (p0 == this@registerLifecycleCallbacks) onPause?.invoke()
|
||||
}
|
||||
|
||||
override fun onActivityResumed(p0: Activity) {
|
||||
if (p0 == this@registerLifecycleCallbacks) onResume?.invoke()
|
||||
}
|
||||
|
||||
override fun onActivityStarted(p0: Activity) = Unit
|
||||
|
||||
override fun onActivityDestroyed(p0: Activity) = Unit
|
||||
|
||||
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) = Unit
|
||||
|
||||
override fun onActivityStopped(p0: Activity) = Unit
|
||||
|
||||
override fun onActivityCreated(p0: Activity, p1: Bundle?) = Unit
|
||||
}
|
||||
|
||||
application.registerActivityLifecycleCallbacks(callback)
|
||||
|
||||
return UnRegisterLifecycleCallback(application, callback)
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package net.touchcapture.qr.flutterqr
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object QrShared {
|
||||
const val CAMERA_REQUEST_ID = 513469796
|
||||
|
||||
var activity: Activity? = null
|
||||
|
||||
var binding: ActivityPluginBinding? = null
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user