Camera2 android Preview problem with orientation - android
I have a Camera2 activity with which i want to capture an Image and a Video and of course i want the Preview of the Camera. However i want those capabilities to be able to work fine in both orientations. I will post the whole activity and then the 2 different screenshots
class CameraActivity : BaseActivity() {
private val mSurfaceTextureListener = object: TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
setUpCamera(width, height)
connectCamera()
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
configureTransform(width, height)
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { return false }
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
}
private val mImageAvailableListener = ImageReader.OnImageAvailableListener {
it?.let {
image = it.acquireLatestImage()
if (image != null) mBackgroundHandler?.post(imageSaver)
}
}
private val imageSaver = Runnable {
var fileOutputStream: FileOutputStream? = null
try {
val byteBuffer = image!!.planes[0].buffer
val bytes = ByteArray(byteBuffer.remaining())
byteBuffer.get(bytes)
println("$$ imageFileName $imageFilename")
fileOutputStream = FileOutputStream(imageFilename)
fileOutputStream.write(bytes)
} catch (e: Exception) {
e.printStackTrace()
} finally {
image?.close()
tryOrNull { fileOutputStream?.close() }
runOnUiThread { navigateToImagePreview() }
}
}
private var mCameraId: String = ""
private var mCameraDevice: CameraDevice? = null
private var mCaptureState = STATE_PREVIEW
private lateinit var mPreviewCaptureSession: CameraCaptureSession
private val mPreviewCaptureCallback = object: CameraCaptureSession.CaptureCallback() {
private fun process(captureResult: CaptureResult) {
when (mCaptureState) {
STATE_PREVIEW -> {}
STATE_WAIT_LOCK -> {
mCaptureState = STATE_PREVIEW
val afState = captureResult.get(CaptureResult.CONTROL_AF_STATE)
if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
startStillCaptureRequest()
}
}
}
}
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
process(result)
}
}
private lateinit var mCaptureRequestBuilder: CaptureRequest.Builder
private val mCameraDeviceStateCallback = object: CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
mCameraDevice = camera
startPreview()
}
override fun onDisconnected(camera: CameraDevice) {
closeCamera()
toast("mCameraDevice disconnected")
}
override fun onError(camera: CameraDevice, error: Int) {
closeCamera()
toast("mCameraDevice onError $error")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
if (!allPermissionsGranted())
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
)
createImageFolder()
createVideoFolder()
isAbove(
Build.VERSION_CODES.S,
code = { mediaRecorder = MediaRecorder(this) },
other = { mediaRecorder = MediaRecorder() }
)
binding.captureBtn.setOnClickListener {
if (isRecording) {
stopRecording()
stopVideoRecordUi()
navigateToVideoPreview()
} else {
takePhotoAnimation(it)
lockFocus()
}
}
binding.captureBtn.setOnLongClickListener {
if (!isRecording) {
startRecording()
startVideoRecordUi()
}
true
}
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) SystemUtils.hideSystemBars(window, window.decorView)
}
override fun onResume() {
super.onResume()
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
startBackgroundThread()
if (binding.textureView.isAvailable) {
setUpCamera(binding.textureView.width, binding.textureView.height)
connectCamera()
} else
binding.textureView.surfaceTextureListener = mSurfaceTextureListener
}
private fun setUpCamera(width: Int, height: Int) {
val cameraManager = SystemServiceUtils.getCameraManager(this)
for (cameraId in cameraManager.cameraIdList) {
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId)
if (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT)
continue
val map = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: continue
var deviceOrientation = 0
isAbove(Build.VERSION_CODES.R,
code = {
deviceOrientation = display?.rotation ?: 0
},
other = {
deviceOrientation = windowManager.defaultDisplay.rotation
}
)
//noinspection ConstantConditions
val mSensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)
var swappedDimensions = false
when (deviceOrientation) {
Surface.ROTATION_0, Surface.ROTATION_180 -> if (mSensorOrientation == 90 || mSensorOrientation == 270) {
swappedDimensions = true
}
Surface.ROTATION_90, Surface.ROTATION_270 -> if (mSensorOrientation == 0 || mSensorOrientation == 180) {
swappedDimensions = true
}
else -> {
toast("Invalid device orientation")
}
}
val displaySize = Point()
windowManager.defaultDisplay.getSize(displaySize)
var rotatedPreviewWidth = width
var rotatedPreviewHeight = height
var maxPreviewWidth: Int = displaySize.x
var maxPreviewHeight: Int = displaySize.y
if (swappedDimensions) {
rotatedPreviewWidth = height
rotatedPreviewHeight = width
maxPreviewWidth = displaySize.y
maxPreviewHeight = displaySize.x
}
// if (maxPreviewWidth > MAX_WIDTH) {
// maxPreviewWidth = MAX_WIDTH
// }
//
// if (maxPreviewHeight > MAX_HEIGHT) {
// maxPreviewHeight = MAX_HEIGHT
// }
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture::class.java).toList(), rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth, maxPreviewHeight) ?: continue
mVideoSize = chooseOptimalSize(map.getOutputSizes(MediaRecorder::class.java).toList(), rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth, maxPreviewHeight) ?: continue
mImageSize = chooseOptimalSize(map.getOutputSizes(ImageFormat.JPEG).toList(), rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth, maxPreviewHeight) ?: continue
mImageReader = ImageReader.newInstance(mImageSize.width, mImageSize.height, ImageFormat.JPEG, 1)
mImageReader.setOnImageAvailableListener(mImageAvailableListener, mBackgroundHandler)
mCameraId = cameraId
// We fit the aspect ratio of TextureView to the size of preview we picked.
val orientation = resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.textureView.setAspectRatio(
mPreviewSize.width, mPreviewSize.height, maxPreviewWidth, maxPreviewHeight, mPreviewSize
)
} else {
binding.textureView.setAspectRatio(
mPreviewSize.height, mPreviewSize.width, maxPreviewWidth, maxPreviewHeight, mPreviewSize
)
}
configureTransform(width, height)
return
}
}
#SuppressLint("MissingPermission")
private fun connectCamera() {
val cameraManager = SystemServiceUtils.getCameraManager(this)
cameraManager.openCamera(mCameraId, mCameraDeviceStateCallback, mBackgroundHandler)
}
private fun startPreview() {
val surfaceTexture = binding.textureView.surfaceTexture
surfaceTexture?.setDefaultBufferSize(mPreviewSize.width, mPreviewSize.height)
val previewSurface = Surface(surfaceTexture)
tryOrNull {
mCaptureRequestBuilder = mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
mCaptureRequestBuilder.addTarget(previewSurface)
// ToDo https://stackoverflow.com/questions/67077568/how-to-correctly-use-the-new-createcapturesession-in-camera2-in-android
mCameraDevice!!.createCaptureSession(listOf(previewSurface, mImageReader.surface), object: CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
mPreviewCaptureSession = session
mPreviewCaptureSession.setRepeatingRequest(mCaptureRequestBuilder.build(), null, mBackgroundHandler)
}
override fun onConfigureFailed(session: CameraCaptureSession) {
}
}, null)
}
}
private fun startStillCaptureRequest() {
createImageFileName()
mCaptureRequestBuilder = mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
mCaptureRequestBuilder.addTarget(mImageReader.surface)
mCaptureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, mTotalRotation)
val stillCaptureCallback = object: CameraCaptureSession.CaptureCallback() {
override fun onCaptureStarted(
session: CameraCaptureSession,
request: CaptureRequest,
timestamp: Long,
frameNumber: Long
) {
super.onCaptureStarted(session, request, timestamp, frameNumber)
}
}
mPreviewCaptureSession.capture(mCaptureRequestBuilder.build(), stillCaptureCallback, null)
}
private fun startRecording() {
createVideoFile()
setUpMediaRecorder()
lockOrientation()
val surfaceTexture = binding.textureView.surfaceTexture
surfaceTexture?.setDefaultBufferSize(mPreviewSize.width, mPreviewSize.height)
val previewSurface = Surface(surfaceTexture)
val recordSurface = mediaRecorder.surface
tryOrNull {
mCaptureRequestBuilder = mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_RECORD)
mCaptureRequestBuilder.addTarget(previewSurface)
mCaptureRequestBuilder.addTarget(recordSurface)
// ToDo https://stackoverflow.com/questions/67077568/how-to-correctly-use-the-new-createcapturesession-in-camera2-in-android
mCameraDevice!!.createCaptureSession(listOf(previewSurface, recordSurface), object: CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
session.setRepeatingRequest(mCaptureRequestBuilder.build(), null, null)
}
override fun onConfigureFailed(session: CameraCaptureSession) {
}
}, null)
mediaRecorder.start()
isRecording = true
}
}
private fun stopRecording() {
mediaRecorder.stop()
mediaRecorder.reset()
isRecording = false
unlockOrientation()
}
private fun closeCamera() {
mCameraDevice?.close()
mCameraDevice = null
}
private fun startBackgroundThread() {
mBackgroundHandlerThread = HandlerThread(BuildConfig.APPLICATION_ID + ".CameraActivity.cameraThread")
mBackgroundHandlerThread?.start()
mBackgroundHandler = Handler(mBackgroundHandlerThread!!.looper)
}
private fun stopBackgroundThread() {
tryOrNull {
mBackgroundHandlerThread?.quitSafely()
mBackgroundHandlerThread?.join()
mBackgroundHandlerThread = null
mBackgroundHandler = null
}
}
private fun setUpMediaRecorder() {
mediaRecorder.apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setVideoSource(MediaRecorder.VideoSource.SURFACE)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setOutputFile(videoFilename)
setVideoEncodingBitRate(400000)
setVideoFrameRate(30)
setVideoSize(mVideoSize.width, mVideoSize.height)
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
setOrientationHint(mTotalRotation)
val bitDepth = 8
val sampleRate = 44100
val bitRate = sampleRate * bitDepth
setAudioEncodingBitRate(bitRate)
setAudioSamplingRate(sampleRate)
prepare()
}
}
override fun onPause() {
if (isRecording) cameraViewModel.setIsRecording(false)
closeCamera()
stopBackgroundThread()
super.onPause()
}
override fun onBackPressed() {
if (isRecording) cameraViewModel.setIsRecording(false)
super.onBackPressed()
}
private fun chooseOptimalSize(
choices: List<Size>, textureViewWidth: Int,
textureViewHeight: Int, maxWidth: Int, maxHeight: Int
): Size? {
// Collect the supported resolutions that are at least as big as the preview Surface
val bigEnough: MutableList<Size> = ArrayList()
// Collect the supported resolutions that are smaller than the preview Surface
val notBigEnough: MutableList<Size> = ArrayList()
for (option in choices) {
if (option.width <= maxWidth && option.height <= maxHeight && option.height == option.width * textureViewHeight / textureViewWidth) {
if (option.width >= textureViewWidth &&
option.height >= textureViewHeight
) {
bigEnough.add(option)
} else {
notBigEnough.add(option)
}
}
}
// Pick the smallest of those big enough. If there is no one big enough, pick the
// largest of those not big enough.
return if (bigEnough.size > 0) {
Collections.min(bigEnough, CompareSizesByArea())
} else if (notBigEnough.size > 0) {
Collections.max(notBigEnough, CompareSizesByArea())
} else {
choices[0]
}
}
private class CompareSizesByArea : Comparator<Size?> {
override fun compare(lhs: Size?, rhs: Size?): Int {
if (lhs == null || rhs == null) return 0
return java.lang.Long.signum(
lhs.width.toLong() * lhs.height -
rhs.width.toLong() * rhs.height
)
}
}
private fun lockFocus() {
mCaptureState = STATE_WAIT_LOCK
mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START)
mPreviewCaptureSession.capture(mCaptureRequestBuilder.build(), mPreviewCaptureCallback, mBackgroundHandler)
}
private var orientations : SparseIntArray = SparseIntArray(4).apply {
append(Surface.ROTATION_0, 0)
append(Surface.ROTATION_90, 90)
append(Surface.ROTATION_180, 180)
append(Surface.ROTATION_270, 270)
}
private fun configureTransform(viewWidth: Int, viewHeight: Int) {
val rotation = windowManager.defaultDisplay.rotation
val matrix = Matrix()
val viewRect = RectF(0.toFloat(), 0.toFloat(), viewWidth.toFloat(), viewHeight.toFloat())
val bufferRect = RectF(
0.toFloat(), 0.toFloat(), mPreviewSize.height.toFloat(),
mPreviewSize.width.toFloat()
)
val centerX = viewRect.centerX()
val centerY = viewRect.centerY()
if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY())
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL)
val scale = Math.max(
viewHeight.toFloat() / mPreviewSize.height,
viewWidth.toFloat() / mPreviewSize.width
)
matrix.postScale(scale, scale, centerX, centerY)
matrix.postRotate(90 * (rotation - 2).toFloat(), centerX, centerY)
} else if (Surface.ROTATION_180 == rotation) {
matrix.postRotate(180.toFloat(), centerX, centerY)
}
binding.textureView.setTransform(matrix)
}
private fun lockOrientation() {
val currentOrientation = resources.configuration.orientation
requestedOrientation = if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} else {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
}
fun unlockOrientation() {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
companion object {
const val CAPTURE_TYPE = "capture_type"
const val IMAGE_CAPTURE_TYPE = 0
const val VIDEO_CAPTURE_TYPE = 1
const val IMAGE_STRING_URI = "imageStringUri"
const val VIDEO_STRING_URI = "videoStringUri"
const val STATE_PREVIEW = 0
const val STATE_WAIT_LOCK = 1
const val MIN_HEIGHT = 640
const val MIN_WIDTH = 360
const val MAX_HEIGHT = 1920
const val MAX_WIDTH = 1080
private const val FILENAME_FORMAT = "yyyyMMdd_HHmmss"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS =
mutableListOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P)
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}.toTypedArray()
}
}
I also have taken the AutoFitTextureView from the Google sample for the Camera2 API which has the custom view code and i transformed it a bit because it didn't work either. I changed the OnMeasure method in order to be able to set the AspectRation of the preview
class AutoFitTextureView(context: Context, attrs: AttributeSet?, defStyle: Int) :
TextureView(context, attrs, defStyle) {
var maxWidth = 0
var maxHeight = 0
private var mRatioWidth = 0
private var mRatioHeight = 0
private var previewSize: Size? = null
constructor(context: Context) : this(context, null) {}
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) {}
fun setAspectRatio(width: Int, height: Int, maxwidth: Int, maxheight: Int, preview: Size) {
require(!(width < 0 || height < 0)) { "Size cannot be negative." }
mRatioWidth = width
mRatioHeight = height
maxWidth = maxwidth
maxHeight = maxheight
this.previewSize = preview
enterTheMatrix()
requestLayout()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
val isFullBleed = true
if (0 == mRatioWidth || 0 == mRatioHeight) {
setMeasuredDimension(width, height)
} else {
setMeasuredDimension(height * mRatioWidth / mRatioHeight, height)
}
}
private fun adjustAspectRatio(
previewWidth: Int,
previewHeight: Int,
rotation: Int
) {
val txform = Matrix()
val viewWidth = width
val viewHeight = height
val rectView = RectF(0.toFloat(), 0.toFloat(), viewWidth.toFloat(), viewHeight.toFloat())
val viewCenterX = rectView.centerX()
val viewCenterY = rectView.centerY()
val rectPreview = RectF(0.toFloat(), 0.toFloat(), previewHeight.toFloat(), previewWidth.toFloat())
val previewCenterX = rectPreview.centerX()
val previewCenterY = rectPreview.centerY()
if (Surface.ROTATION_90 == rotation ||
Surface.ROTATION_270 == rotation
) {
rectPreview.offset(
viewCenterX - previewCenterX,
viewCenterY - previewCenterY
)
txform.setRectToRect(
rectView, rectPreview,
Matrix.ScaleToFit.FILL
)
val scale = Math.max(
viewHeight.toFloat() / previewHeight,
viewWidth.toFloat() / previewWidth
)
txform.postScale(scale, scale, viewCenterX, viewCenterY)
txform.postRotate(
90 * (rotation - 2).toFloat(), viewCenterX,
viewCenterY
)
} else {
if (Surface.ROTATION_180 == rotation) {
txform.postRotate(180.toFloat(), viewCenterX, viewCenterY)
}
}
setTransform(txform)
}
private fun enterTheMatrix() {
if (previewSize != null) {
adjustAspectRatio(
mRatioWidth,
mRatioHeight,
(context as Activity).windowManager.defaultDisplay.rotation
)
}
}
}
The result in both modes is the following
Here is the Portrait mode
And here is the Landscape mode
As you can see, in Landscape mode the textureView does not apply to the whole screen. What is the problem and what should i do?
Related
Bitmap image appears dark
I'm using the CameraX Image Analyzer to scan the first image below. The camera manages to analyse it and I'm able to convert it into a bitmap, but when it converts, it appears dark (the second image). How do I get the bitmap to not appear dark? The code below handles the image analysis and the conversion to a bitmap. private class ImageAnalyzer(mContext: Context, mOnImageRequest: (File) -> Unit) : ImageAnalysis.Analyzer { val context = mContext val onImageRequest = mOnImageRequest #SuppressLint("UnsafeExperimentalUsageError", "UnsafeOptInUsageError") override fun analyze(imageProxy: ImageProxy) { val mediaImage = imageProxy.image if (mediaImage != null) { var bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888) bitmap.setHasAlpha(true) val baos = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.PNG, 80, baos) val converter = YuvToRgbConverter(context) converter.yuvToRgb(mediaImage, bitmap) } }} class YuvToRgbConverter(context: Context) { private val rs = RenderScript.create(context) private val scriptYuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) private var pixelCount: Int = -1 private lateinit var yuvBuffer: ByteArray private lateinit var inputAllocation: Allocation private lateinit var outputAllocation: Allocation #Synchronized fun yuvToRgb(image: Image, output: Bitmap) { if (!::yuvBuffer.isInitialized) { pixelCount = image.cropRect.width() * image.cropRect.height() val pixelSizeBits = ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) yuvBuffer = ByteArray(pixelCount * pixelSizeBits / 8) } imageToByteArray(image, yuvBuffer) if (!::inputAllocation.isInitialized) { val elemType = Type.Builder(rs, Element.YUV(rs)).setYuvFormat(ImageFormat.NV21).create() inputAllocation = Allocation.createSized(rs, elemType.element, yuvBuffer.size) } if (!::outputAllocation.isInitialized) { outputAllocation = Allocation.createFromBitmap(rs, output) } inputAllocation.copyFrom(yuvBuffer) scriptYuvToRgb.setInput(inputAllocation) scriptYuvToRgb.forEach(outputAllocation) outputAllocation.copyTo(output) } private fun imageToByteArray(image: Image, outputBuffer: ByteArray) { assert(image.format == ImageFormat.YUV_420_888) val imageCrop = image.cropRect val imagePlanes = image.planes imagePlanes.forEachIndexed { planeIndex, plane -> val outputStride: Int var outputOffset: Int when (planeIndex) { 0 -> { outputStride = 1 outputOffset = 0 } 1 -> { outputStride = 2 outputOffset = pixelCount + 1 } 2 -> { outputStride = 2 outputOffset = pixelCount } else -> { return#forEachIndexed } } val planeBuffer = plane.buffer val rowStride = plane.rowStride val pixelStride = plane.pixelStride val planeCrop = if (planeIndex == 0) { imageCrop } else { Rect( imageCrop.left / 2, imageCrop.top / 2, imageCrop.right / 2, imageCrop.bottom / 2 ) } val planeWidth = planeCrop.width() val planeHeight = planeCrop.height() val rowBuffer = ByteArray(plane.rowStride) val rowLength = if (pixelStride == 1 && outputStride == 1) { planeWidth } else { (planeWidth - 1) * pixelStride + 1 } for (row in 0 until planeHeight) { planeBuffer.position( (row + planeCrop.top) * rowStride + planeCrop.left * pixelStride) if (pixelStride == 1 && outputStride == 1) { planeBuffer.get(outputBuffer, outputOffset, rowLength) outputOffset += rowLength } else { planeBuffer.get(rowBuffer, 0, rowLength) for (col in 0 until planeWidth) { outputBuffer[outputOffset] = rowBuffer[col * pixelStride] outputOffset += outputStride } } } } }}
How to click block in Google MLKit android vision
I am trying to build a realtime text recognization app using google MLKit vision, it's showing text properly but when I am trying to click on a particular line it is only showing the last line text. Here is Overlay Code : TextGraphic.kt class TextGraphic(overlay: GraphicOverlay?, private val element: Text.Line, font: Typeface, fontSize: Float, color: Int) : Graphic(overlay!!) { private val rectPaint: Paint = Paint() private val textPaint: Paint override fun draw(canvas: Canvas?) { val rect = RectF(element.boundingBox) canvas!!.drawRect(rect, rectPaint) canvas.drawText(element.text, rect.left, rect.bottom, textPaint) } companion object { private const val TAG = "TextGraphic" private const val TEXT_COLOR = Color.BLACK private const val STROKE_WIDTH = 2.0f } init { rectPaint.color = color rectPaint.style = Paint.Style.FILL_AND_STROKE rectPaint.strokeWidth = STROKE_WIDTH textPaint = Paint() textPaint.color = TEXT_COLOR textPaint.textSize = fontSize textPaint.typeface = font postInvalidate() }} GraphicOverlay.kt class GraphicOverlay(context: Context?, attrs: AttributeSet?) : View(context, attrs) { private val lock = Any() private var previewWidth = 0 private var widthScaleFactor = 1.0f private var previewHeight = 0 private var heightScaleFactor = 1.0f private var facing = CameraCharacteristics.LENS_FACING_BACK private val graphics: MutableSet<Graphic> = HashSet() abstract class Graphic(private val overlay: GraphicOverlay) { abstract fun draw(canvas: Canvas?) fun scaleX(horizontal: Float): Float { return horizontal * overlay.widthScaleFactor } fun scaleY(vertical: Float): Float { return vertical * overlay.heightScaleFactor } val applicationContext: Context get() = overlay.context.applicationContext fun translateX(x: Float): Float { return if (overlay.facing == CameraCharacteristics.LENS_FACING_FRONT) { overlay.width - scaleX(x) } else { scaleX(x) } } fun translateY(y: Float): Float { return scaleY(y) } fun postInvalidate() { overlay.postInvalidate() } } fun clear() { synchronized(lock) { graphics.clear() } postInvalidate() } fun add(graphic: Graphic) { synchronized(lock) { graphics.add(graphic) } postInvalidate() } fun remove(graphic: Graphic) { synchronized(lock) { graphics.remove(graphic) } postInvalidate() } fun setCameraInfo(previewWidth: Int, previewHeight: Int, facing: Int) { synchronized(lock) { this.previewWidth = previewWidth this.previewHeight = previewHeight this.facing = facing } postInvalidate() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) synchronized(lock) { if (previewWidth != 0 && previewHeight != 0) { widthScaleFactor = width.toFloat() / previewWidth.toFloat() heightScaleFactor = height.toFloat() / previewHeight.toFloat() } for (graphic in graphics) { graphic.draw(canvas) } } }} Inside my Fragment where I am clicking : private fun processTextFromImage(visionText: Text, imageProxy: ImageProxy) { binding.graphicOverlay.clear() for (block in visionText.textBlocks) { for (line in block.lines) { val textGraphic = TextGraphic(binding.graphicOverlay, line, font, fontSize, color = fontColor) binding.graphicOverlay.apply { add(textGraphic) setOnClickListener { Toast.makeText(it.context, line.text, Toast.LENGTH_SHORT).show() } } for (element in line.elements) { textFoundListener(element.text) } } } } Is there any better way to to display overlay, this overlay is too fast and my click is only displays the last line text. If anyone can help me in this, thanks a lot.
Android Canvas.drawText() double decimal number not rendering correctly
I get strange behaviour when trying to render some text along with a number. It is likely that the problem is related to threading. My problem is that when I draw with Canvas.drawText() the number is always displayed as 0.0 but if I Log.d() the number is not 0.0 but rather around ~60. Here is my code: GameView: class GameView( context: Context?, attrs: AttributeSet ) : SurfaceView(context, attrs), SurfaceHolder.Callback { private var gameLoop: GameLoop = GameLoop(this, holder) private val paint: Paint = Paint() init { holder.addCallback(this) isFocusable = true } fun update() { // TODO: update game state } override fun surfaceCreated(surfaceHolder: SurfaceHolder) { gameLoop.startLoop() } override fun surfaceChanged(surfaceHolder: SurfaceHolder, p1: Int, p2: Int, p3: Int) { // do nothing } override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) { gameLoop.stopLoop() } override fun draw(drawCanvas: Canvas) { super.draw(drawCanvas) drawInfo(drawCanvas) } private fun drawInfo(canvas: Canvas) { val averageUps = gameLoop.getAverageUps() val averageFps = gameLoop.getAverageFps() val color = ContextCompat.getColor(context, DEBUG_TEXT_COLOR) paint.color = color paint.textSize = DEBUG_TEXT_SIZE canvas.drawText("UPS: $averageUps", 40f, DEBUG_TEXT_SIZE, paint) canvas.drawText("FPS: $averageFps", 40f, DEBUG_TEXT_SIZE*2 + 10f, paint) } companion object { const val DEBUG_TEXT_SIZE = 40f const val DEBUG_TEXT_COLOR = R.color.light_blue_A200 } } GameLoop: class GameLoop( private val gameView: GameView, private val surfaceHolder: SurfaceHolder ) : Thread() { private var isRunning: Boolean = false private var averageUps: Double = 0.0 private var averageFps: Double = 0.0 fun getAverageUps(): Double { return averageUps } fun getAverageFps(): Double { return averageFps } fun startLoop() { isRunning = true start() } override fun run() { super.run() var updateCount = 0 var frameCount = 0 var startTime = 0L var elapsedTime = 0L var sleepTime = 0L var canvas: Canvas? = null startTime = System.currentTimeMillis() while (isRunning) { try { canvas = surfaceHolder.lockCanvas() synchronized(canvas) { gameView.update() updateCount++ gameView.draw(canvas) } } catch (iae: IllegalArgumentException) { iae.printStackTrace() } finally { if (canvas != null) { surfaceHolder.unlockCanvasAndPost(canvas) frameCount++ } } elapsedTime = System.currentTimeMillis() - startTime sleepTime = (updateCount*UPS_PERIOD - elapsedTime) if (sleepTime > 0) { try { sleep(sleepTime) } catch (ie: InterruptedException) { ie.printStackTrace() } } while (sleepTime < 0 && updateCount < MAX_UPS-1) { gameView.update() updateCount++ elapsedTime = System.currentTimeMillis() - startTime sleepTime = (updateCount*UPS_PERIOD - elapsedTime) } elapsedTime = System.currentTimeMillis() - startTime if (elapsedTime >= 1000.0) { averageUps = updateCount.toDouble() / (elapsedTime/1000.0) averageFps = frameCount.toDouble() / (elapsedTime/1000.0) updateCount = 0 frameCount = 0 startTime = System.currentTimeMillis() } } } fun stopLoop() { // TODO: finish me... } companion object { private const val MAX_UPS: Long = 60 const val UPS_PERIOD: Long = 1000/MAX_UPS } } The problem is in GameView.drawInfo(canvas: Canvas). Output is always "UPS: 0.0 FPS: 0.0" but when I log out averageUps and averageFps they are both around ~60. I hope that you can help me resolve this issue :-)
To answer my own question: The problem was in the activity that contains GameView. I had GameView in the xml. When i instantiated the GameView manually and sat it with setContentView(gameView) all rendering started to work.
Move a dot over circular arc which can move over the arc of the circle for a certain time ,can be stoped
I have to make following Ui, In which when I play this small circle will move in an arc, when I stop it will stop. I can also set the timing of rotation of the small blue circle. Till now I have implemented the following code: this gives me an arc of the circle, but I am not able to rotate the smaller circler over the bigger one. public class CustoCustomProgressBar : View{ private var path : Path ? =null private var mPrimaryPaint: Paint? = null private var mSecondaryPaint: Paint? = null private var mBackgroundPaint: Paint? = null private var mTextPaint: TextPaint? = null private var mProgressDrawable : Drawable ? = null private var mRectF: RectF? = null private var mDrawText = false private var mTextColor = 0 private var mSecondaryProgressColor = 0 private var mPrimaryProgressColor = 0 private var mBackgroundColor = 0 private var mStrokeWidth = 0 private var mProgress = 0 var secodaryProgress = 0 private set private var firstArcprogress = 0 private var secondArcProgress = 0 private var thirdArcProgress = 0 private var mFristArcCapSize = 0 private var mSecondArcCapSize = 0 private var mThirdArcCapSize = 0 private var isFristCapVisible = false private var isSecondCapVisible = false private var isThirdCapVisible = false private var capColor = 0 private var mPrimaryCapSize = 0 private var mSecondaryCapSize = 0 var isPrimaryCapVisible = false var isSecondaryCapVisible = false private var x = 0 private var y = 0 private var mWidth = 0 private var mHeight = 0 constructor(context: Context) : super(context) { init(context, null) } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { init(context, attrs) } constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) { init(context, attrs) } fun init(context: Context, attrs: AttributeSet?) { val a: TypedArray a = if (attrs != null) { context.getTheme().obtainStyledAttributes( attrs, R.styleable.CustomProgressBar, 0, 0 ) } else { throw IllegalArgumentException("Must have to pass the attributes") } try { mProgressDrawable = resources.getDrawable(R.drawable.test) mDrawText = a.getBoolean(R.styleable.CustomProgressBar_showProgressText, false) mBackgroundColor = a.getColor( R.styleable.CustomProgressBar_backgroundColor, resources.getColor(R.color.white) ) mPrimaryProgressColor = a.getColor( R.styleable.CustomProgressBar_progressColor, resources.getColor(R.color.white) ) mSecondaryProgressColor = a.getColor( R.styleable.CustomProgressBar_secondaryProgressColor, resources.getColor(R.color.black) ) capColor = a.getColor( R.styleable.CustomProgressBar_capColor, resources.getColor(R.color.color_9bc6e6_mind) ) firstArcprogress =a.getInt(R.styleable.CustomProgressBar_firstArcProgress, 0) mProgress = a.getInt(R.styleable.CustomProgressBar_progress, 0) secodaryProgress = a.getInt(R.styleable.CustomProgressBar_secondaryProgress, 0) mStrokeWidth = a.getDimensionPixelSize(R.styleable.CustomProgressBar_strokeWidth, 10) mTextColor = a.getColor( R.styleable.CustomProgressBar_textColor, resources.getColor(R.color.black) ) mPrimaryCapSize = a.getInt(R.styleable.CustomProgressBar_primaryCapSize, 20) mSecondaryCapSize = a.getInt(R.styleable.CustomProgressBar_secodaryCapSize, 20) isPrimaryCapVisible = a.getBoolean(R.styleable.CustomProgressBar_primaryCapVisibility, true) isSecondaryCapVisible = a.getBoolean(R.styleable.CustomProgressBar_secodaryCapVisibility, true) isFristCapVisible = a.getBoolean(R.styleable.CustomProgressBar_firstCapVisibility, true) isSecondCapVisible = a.getBoolean(R.styleable.CustomProgressBar_secodaryCapVisibility, false) isThirdCapVisible = a.getBoolean(R.styleable.CustomProgressBar_thirdCapVisibility, false) } finally { a.recycle() } mBackgroundPaint = Paint() mBackgroundPaint!!.setAntiAlias(true) mBackgroundPaint!!.setStyle(Paint.Style.STROKE) mBackgroundPaint!!.setStrokeWidth(mStrokeWidth.toFloat()) mBackgroundPaint!!.setColor(mBackgroundColor) mPrimaryPaint = Paint() mPrimaryPaint!!.setAntiAlias(true) mPrimaryPaint!!.setStyle(Paint.Style.STROKE) mPrimaryPaint!!.setStrokeWidth(mStrokeWidth.toFloat()) mPrimaryPaint!!.setColor(capColor) mSecondaryPaint = Paint() mSecondaryPaint!!.setAntiAlias(true) mSecondaryPaint!!.setStyle(Paint.Style.STROKE) mSecondaryPaint!!.setStrokeWidth((mStrokeWidth - 2).toFloat()) mSecondaryPaint!!.setColor(mSecondaryProgressColor) mTextPaint = TextPaint() mTextPaint!!.color = mTextColor mRectF = RectF() } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) mRectF!![paddingLeft.toFloat(), paddingTop.toFloat(), (w - paddingRight).toFloat()] = (h - paddingBottom).toFloat() mTextPaint!!.textSize = (w / 5).toFloat() x = w / 2 - (mTextPaint!!.measureText("$mProgress%") / 2).toInt() y = (h / 2 - (mTextPaint!!.descent() + mTextPaint!!.ascent()) / 2).toInt() mWidth = w mHeight = h invalidate() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) mPrimaryPaint?.setStyle(Paint.Style.STROKE) mSecondaryPaint?.setStyle(Paint.Style.STROKE) // for drawing a full progress .. The background circle mRectF?.let { mBackgroundPaint?.let { it1 -> canvas.drawArc(it, 270F, 180F, false, it1) } } path = Path() path?.arcTo(mRectF,270F,180F,true) mRectF?.let { mBackgroundPaint?.let { it1 -> canvas.drawArc(it, 95F, 80F, false, it1) } } mRectF?.let { mBackgroundPaint?.let { it1 -> canvas.drawArc(it, 180F, 80F, false, it1) } } // // for drawing a secondary progress circle // val secondarySwipeangle = secodaryProgress * 360 / 100 // mRectF?.let { mSecondaryPaint?.let { it1 -> // canvas.drawArc(it, 270F, secondarySwipeangle.toFloat(), false, // it1 // ) // } } // val firstArcProgresSwipeAngle = firstArcprogress * 180 / 100 // mRectF?.let { // mPrimaryPaint?.let { it1 -> // canvas.drawArc( // it, 270F, firstArcProgresSwipeAngle.toFloat(), false, // it1 // ) // } // } // // for drawing a main progress circle // val primarySwipeangle = mProgress * 360 / 100 // mRectF?.let { mPrimaryPaint?.let { it1 -> // canvas.drawArc(it, 270F, primarySwipeangle.toFloat(), false, // it1 // ) // } } // // for cap of secondary progress // val r = (height - paddingLeft * 2) / 2 // Calculated from canvas width // var trad = (secondarySwipeangle - 90) * (Math.PI / 180.0) // = 5.1051 // var x = (r * Math.cos(trad)).toInt() // var y = (r * Math.sin(trad)).toInt() // mSecondaryPaint?.setStyle(Paint.Style.FILL) // if (isSecondaryCapVisible) mSecondaryPaint?.let { // canvas.drawCircle( // (x + mWidth / 2).toFloat(), // (y + mHeight / 2).toFloat(), // mSecondaryCapSize.toFloat(), // it // ) // } val r = (height - paddingLeft * 2) / 2 // for cap of primary progress var trad = (firstArcProgresSwipeAngle - 90) * (Math.PI / 180.0) // = 5.1051 x = (r * Math.cos(trad)).toInt() y = (r * Math.sin(trad)).toInt() mPrimaryPaint?.setStyle(Paint.Style.FILL) if (isPrimaryCapVisible) mPrimaryPaint?.let { canvas.drawCircle( (x + mWidth / 2).toFloat(), (y + mHeight / 2).toFloat(), mPrimaryCapSize.toFloat(), it ) } if (mDrawText) mTextPaint?.let { canvas.drawText( "$mProgress%", x.toFloat(), y.toFloat(), it ) } } fun setDrawText(mDrawText: Boolean) { this.mDrawText = mDrawText invalidate() } override fun setBackgroundColor(mBackgroundColor: Int) { this.mBackgroundColor = mBackgroundColor mBackgroundPaint?.setColor(mBackgroundColor) invalidate() } fun setStrokeWidth(mStrokeWidth: Int) { this.mStrokeWidth = mStrokeWidth invalidate() } fun setSecondaryProgress(mSecondaryProgress: Int) { secodaryProgress = mSecondaryProgress invalidate() } fun setTextColor(mTextColor: Int) { this.mTextColor = mTextColor mTextPaint!!.color = mTextColor invalidate() } var secondaryProgressColor: Int get() = mSecondaryProgressColor set(mSecondaryProgressColor) { this.mSecondaryProgressColor = mSecondaryProgressColor mSecondaryPaint?.setColor(mSecondaryProgressColor) invalidate() } var primaryProgressColor: Int get() = mPrimaryProgressColor set(mPrimaryProgressColor) { this.mPrimaryProgressColor = mPrimaryProgressColor mPrimaryPaint?.setColor(mPrimaryProgressColor) invalidate() } var progress: Int get() = mProgress set(mProgress) { while (this.mProgress <= mProgress) { postInvalidateDelayed(150) this.mProgress++ } } fun getBackgroundColor(): Int { return mBackgroundColor } var primaryCapSize: Int get() = mPrimaryCapSize set(mPrimaryCapSize) { this.mPrimaryCapSize = mPrimaryCapSize invalidate() } var secondaryCapSize: Int get() = mSecondaryCapSize set(mSecondaryCapSize) { this.mSecondaryCapSize = mSecondaryCapSize invalidate() } var arcprogress: Int get() = firstArcprogress set(firstArcprogress) { while (this.firstArcprogress <= firstArcprogress) { postInvalidateDelayed(150) this.firstArcprogress = firstArcprogress } } fun getPath(): Path? { return path } fun getXCOORDINTE(): Float{ return x.toFloat() } fun getYCoordinate(): Float{ return y.toFloat() } }
Cannot not get the right value of custom attribute in custom transition
I'm trying to make custom image view that have rounded corners and a custom transition to change a border radius smoothly. In CircleTransition, I try to get imageCornerRadius but it's always return 0 which ruined the transaction. But in activity, when I get imageCornerRadius, it returns the value in xml file. So how i can get the imageCornerRadius to perform the transition. This is declare of my custom view RoundedImageView custom attribute <declare-styleable name="RoundedImageView"> <attr name="imageCornerRadius" format="dimension" /> </declare-styleable> class RoundedImageView : AppCompatImageView { constructor(context: Context) : super(context) { Log.d("debug", "first constructor") } constructor(context: Context, attrSet: AttributeSet) : super(context, attrSet) { Log.d("debug", "second constructor") init(attrSet) } constructor(context: Context, attrSet: AttributeSet, defStyleAttr: Int) : super( context, attrSet, defStyleAttr ) { Log.d("debug", "third constructor") init(attrSet) } private fun init(attrSet: AttributeSet){ context.theme.obtainStyledAttributes( attrSet, R.styleable.RoundedImageView, 0, 0 ).apply { try { imageCornerRadius = getDimensionPixelSize( R.styleable.RoundedImageView_imageCornerRadius, 0 ).toFloat() } finally { recycle() } } } // Custom attr var imageCornerRadius: Float = 0F //Attr for drawing private lateinit var bitmapRect: RectF val rect = RectF(drawable.bounds) val holePath = Path() override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) Log.d("size changed", "w = $w h = $h") bitmapRect = RectF(0f, 0f, w.toFloat(), h.toFloat()) } override fun onDraw(canvas: Canvas?) { val drawableWidth = this.width val drawableHeight = this.height /* Clip */ holePath.apply { reset() addRoundRect( 0F, 0F, rect.right + drawableWidth, rect.bottom + drawableHeight, imageCornerRadius, imageCornerRadius, Path.Direction.CW ) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { canvas?.clipPath(holePath) } else { #Suppress("DEPRECATION") canvas?.clipPath(holePath, Region.Op.REPLACE) } // Draw image super.onDraw(canvas) } } My custom transition change Size, Coordinate, imageCornerRadius CircleTransition.kt class CircleTransition() : Transition() { private val TAG = CircleTransition::class.java.simpleName private val BOUNDS = TAG + ":viewBounds" private val CORNER_RADIUS = TAG + ":imageCornerRadius" private val PROPS = arrayOf(BOUNDS, CORNER_RADIUS) init { Log.d("debug", "Circle Transition called") } override fun captureStartValues(transitionValues: TransitionValues?) { captureValues(transitionValues) } override fun captureEndValues(transitionValues: TransitionValues?) { captureValues(transitionValues) } fun captureValues(transitionValues: TransitionValues?) { val view = transitionValues?.view //get View Bound val bound = RectF() bound.left = view?.left?.toFloat() ?: return bound.top = view.top.toFloat() bound.right = view.right.toFloat() bound.bottom = view.bottom.toFloat() transitionValues.values.put(BOUNDS, bound) //get view Corner radius if(view is RoundedImageView){ val cornerRadius = view.imageCornerRadius transitionValues.values.put(CORNER_RADIUS, cornerRadius) } } override fun getTransitionProperties(): Array<String> { return PROPS } override fun createAnimator( sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues? ): Animator? { if (startValues == null || endValues == null) { return null } val view = endValues.view as RoundedImageView //startScene val sBound = startValues.values[BOUNDS] as RectF? ?: return null //How I get imageCornerRadius val sCornerRadius = startValues.values[CORNER_RADIUS] as Float? ?: return null val sWidth = sBound.right - sBound.left val sHeight = sBound.top - sBound.bottom //endScene val eBound = endValues.values[BOUNDS] as RectF? ?: return null //How I get imageCornerRadius val eCornerRadius = endValues.values[CORNER_RADIUS] as Float? ?: return null val eWidth = eBound.right - eBound.left val eHeight = eBound.top - eBound.bottom if (sBound == eBound && sCornerRadius == eCornerRadius) { return null } val widthAnimator: ValueAnimator = ValueAnimator.ofInt(sWidth.toInt(), eWidth.toInt()).apply { addUpdateListener { val layoutParams = view.layoutParams layoutParams.width = it.animatedValue as Int view.layoutParams = layoutParams } } val heightAnimator: ValueAnimator = ValueAnimator.ofInt(sHeight.toInt() * -1, eHeight.toInt() * -1).apply { interpolator = AccelerateInterpolator() addUpdateListener { val layoutParams = view.layoutParams layoutParams.height = it.animatedValue as Int view.layoutParams = layoutParams } } val cornerRadiusAnimator = ValueAnimator.ofFloat(96F, 0F).apply { addUpdateListener { view.imageCornerRadius = it.animatedValue as Float } } // set endView have the same size, coorinate like startScene view.x = sBound.left view.y = sBound.top // view.layoutParams = ViewGroup.LayoutParams(sBound.width().toInt(), sBound.height().toInt()) // move view val startX = sBound.left val startY = sBound.top val moveXTo = eBound.left val moveYTo = eBound.top val moveXAnimator: Animator = ObjectAnimator.ofFloat(view, "x", startX, moveXTo.toFloat()) val moveYAnimator: Animator = ObjectAnimator.ofFloat(view, "y", startY, moveYTo.toFloat()).apply { addUpdateListener { view.invalidate() } } val animatorSet = AnimatorSet() animatorSet.playTogether( widthAnimator, heightAnimator, cornerRadiusAnimator, moveXAnimator, moveYAnimator ) return animatorSet } }