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
}
}
}
}
}}
Related
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?
was wondering if anyone knows how to produce elliptical/arched list in Compose?
Something along these lines:
Not sure If I'm overlooking an 'easy' way of doing it in Compose. Cheers!
I have an article here that shows how to do this. It is not a LazyList in that it computes all the items (but only renders the visible ones); you can use this as a starting point to build upon.
The full code is below as well:
data class CircularListConfig(
val contentHeight: Float = 0f,
val numItems: Int = 0,
val visibleItems: Int = 0,
val circularFraction: Float = 1f,
val overshootItems: Int = 0,
)
#Stable
interface CircularListState {
val verticalOffset: Float
val firstVisibleItem: Int
val lastVisibleItem: Int
suspend fun snapTo(value: Float)
suspend fun decayTo(velocity: Float, value: Float)
suspend fun stop()
fun offsetFor(index: Int): IntOffset
fun setup(config: CircularListConfig)
}
class CircularListStateImpl(
currentOffset: Float = 0f,
) : CircularListState {
private val animatable = Animatable(currentOffset)
private var itemHeight = 0f
private var config = CircularListConfig()
private var initialOffset = 0f
private val decayAnimationSpec = FloatSpringSpec(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow,
)
private val minOffset: Float
get() = -(config.numItems - 1) * itemHeight
override val verticalOffset: Float
get() = animatable.value
override val firstVisibleItem: Int
get() = ((-verticalOffset - initialOffset) / itemHeight).toInt().coerceAtLeast(0)
override val lastVisibleItem: Int
get() = (((-verticalOffset - initialOffset) / itemHeight).toInt() + config.visibleItems)
.coerceAtMost(config.numItems - 1)
override suspend fun snapTo(value: Float) {
val minOvershoot = -(config.numItems - 1 + config.overshootItems) * itemHeight
val maxOvershoot = config.overshootItems * itemHeight
animatable.snapTo(value.coerceIn(minOvershoot, maxOvershoot))
}
override suspend fun decayTo(velocity: Float, value: Float) {
val constrainedValue = value.coerceIn(minOffset, 0f).absoluteValue
val remainder = (constrainedValue / itemHeight) - (constrainedValue / itemHeight).toInt()
val extra = if (remainder <= 0.5f) 0 else 1
val target =((constrainedValue / itemHeight).toInt() + extra) * itemHeight
animatable.animateTo(
targetValue = -target,
initialVelocity = velocity,
animationSpec = decayAnimationSpec,
)
}
override suspend fun stop() {
animatable.stop()
}
override fun setup(config: CircularListConfig) {
this.config = config
itemHeight = config.contentHeight / config.visibleItems
initialOffset = (config.contentHeight - itemHeight) / 2f
}
override fun offsetFor(index: Int): IntOffset {
val maxOffset = config.contentHeight / 2f + itemHeight / 2f
val y = (verticalOffset + initialOffset + index * itemHeight)
val deltaFromCenter = (y - initialOffset)
val radius = config.contentHeight / 2f
val scaledY = deltaFromCenter.absoluteValue * (config.contentHeight / 2f / maxOffset)
val x = if (scaledY < radius) {
sqrt((radius * radius - scaledY * scaledY))
} else {
0f
}
return IntOffset(
x = (x * config.circularFraction).roundToInt(),
y = y.roundToInt()
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CircularListStateImpl
if (animatable.value != other.animatable.value) return false
if (itemHeight != other.itemHeight) return false
if (config != other.config) return false
if (initialOffset != other.initialOffset) return false
if (decayAnimationSpec != other.decayAnimationSpec) return false
return true
}
override fun hashCode(): Int {
var result = animatable.value.hashCode()
result = 31 * result + itemHeight.hashCode()
result = 31 * result + config.hashCode()
result = 31 * result + initialOffset.hashCode()
result = 31 * result + decayAnimationSpec.hashCode()
return result
}
companion object {
val Saver = Saver<CircularListStateImpl, List<Any>>(
save = { listOf(it.verticalOffset) },
restore = {
CircularListStateImpl(it[0] as Float)
}
)
}
}
#Composable
fun rememberCircularListState(): CircularListState {
val state = rememberSaveable(saver = CircularListStateImpl.Saver) {
CircularListStateImpl()
}
return state
}
#Composable
fun CircularList(
visibleItems: Int,
modifier: Modifier = Modifier,
state: CircularListState = rememberCircularListState(),
circularFraction: Float = 1f,
overshootItems: Int = 3,
content: #Composable () -> Unit,
) {
check(visibleItems > 0) { "Visible items must be positive" }
check(circularFraction > 0f) { "Circular fraction must be positive" }
Layout(
modifier = modifier.clipToBounds().drag(state),
content = content,
) { measurables, constraints ->
val itemHeight = constraints.maxHeight / visibleItems
val itemConstraints = Constraints.fixed(width = constraints.maxWidth, height = itemHeight)
val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
state.setup(
CircularListConfig(
contentHeight = constraints.maxHeight.toFloat(),
numItems = placeables.size,
visibleItems = visibleItems,
circularFraction = circularFraction,
overshootItems = overshootItems,
)
)
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
for (i in state.firstVisibleItem..state.lastVisibleItem) {
placeables[i].placeRelative(state.offsetFor(i))
}
}
}
}
private fun Modifier.drag(
state: CircularListState,
) = pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
coroutineScope {
while (true) {
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
state.stop()
val tracker = VelocityTracker()
awaitPointerEventScope {
verticalDrag(pointerId) { change ->
val verticalDragOffset = state.verticalOffset + change.positionChange().y
launch {
state.snapTo(verticalDragOffset)
}
tracker.addPosition(change.uptimeMillis, change.position)
change.consumePositionChange()
}
}
val velocity = tracker.calculateVelocity().y
val targetValue = decay.calculateTargetValue(state.verticalOffset, velocity)
launch {
state.decayTo(velocity, targetValue)
}
}
}
}
Android graphview load widget using androidplot library in build gradle
implementation "com.androidplot:androidplot-core:1.5.7"
Dynamicaally create chart using below code getresponse based from network call.
In class file my sample code...
val plot = XYPlot(context, "")
val h = context.resources.getDimension(R.dimen.sample_widget_height).toInt()
val w = context.resources.getDimension(R.dimen.sample_widget_width).toInt()
plot.graph.setMargins(100f, 0f, 0f, 16f)
plot.graph.setPadding(0f, 0f, 0f, 0f)
plot.graph.gridInsets.left = 100f
plot.graph.gridInsets.top = 0f
plot.graph.gridInsets.right = 0f
plot.graph.gridInsets.bottom = 40f
plot.legend.textPaint.textSize = 1f
plot.linesPerRangeLabel = 8
plot.linesPerDomainLabel = 8
plot.graph.setSize(Size.FILL)
plot.measure(w, h);
plot.layout(0, 0, w, h);
plot.graph.position(0f, HorizontalPositioning.ABSOLUTE_FROM_LEFT, 0f,
VerticalPositioning.ABSOLUTE_FROM_TOP, Anchor.LEFT_TOP)
val series1Numbers = mutableListOf<Int>()
val series2Numbers = mutableListOf<Int>()
val xLabels = mutableListOf<String>()
for (i in 0 until model.Details.Items.size) {
var item = model.Details.Items[i]
series1Numbers.add(item.TotalScore)
series2Numbers.add(0)
xLabels.add(item.Date)
}
val series1 = SimpleXYSeries(series1Numbers, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY,
"Series1")
val series2 = SimpleXYSeries(series2Numbers, SimpleXYSeries.ArrayFormat.Y_VALS_ONLY,
"Series1")
val series1Format = LineAndPointFormatter(context, R.xml.line_point_formatter_with_labels)
val series2Format = LineAndPointFormatter(context,
R.xml.line_point_formatter_with_labels_2)
plot.graph.setLineLabelEdges(XYGraphWidget.Edge.LEFT, XYGraphWidget.Edge.BOTTOM)
plot.setRangeBoundaries(-110, 110, BoundaryMode.FIXED)
plot.graph.getLineLabelStyle(XYGraphWidget.Edge.LEFT).format = object : Format() {
override fun format(obj: Any, toAppendTo:
StringBuffer, pos: FieldPosition): StringBuffer {
val i = Math.round((obj as Number).toFloat())
L.m("widget y axos label value ", i.toString())
plot.graph.setLineLabelRenderer(XYGraphWidget.Edge.LEFT, MyLineLabelRenderer())
return if (i > 50) {
return toAppendTo.append(context.getString(R.string.str_excellent))
} else if (i > 25 && i <= 50) {
return toAppendTo.append(context.getString(R.string.str_very_good))
} else if (i > 0 && i <= 25) {
return toAppendTo.append(context.getString(R.string.str_good))
} else if (i == 0) {
return toAppendTo.append(context.getString(R.string.str_neutral))
} else if (i < 0 && i >= -25) {
return toAppendTo.append(context.getString(R.string.str_not_good))
} else if (i < -25 && i >= -50) {
return toAppendTo.append(context.getString(R.string.str_be_aware))
} else if (i < -50) {
return toAppendTo.append(context.getString(R.string.str_time_out))
} else {
return toAppendTo.append(context.getString(R.string.str_neutral))
}
}
override fun parseObject(source: String, pos: ParsePosition): Any {
// unused
return ""
}
}
plot.legend.setVisible(false)
plot.getGraph().linesPerDomainLabel = 5
plot.getGraph().linesPerRangeLabel = 5
plot.graph.getLineLabelStyle(XYGraphWidget.Edge.BOTTOM).format = object : Format() {
override fun format(obj: Any, #NonNull toAppendTo:
StringBuffer, #NonNull pos: FieldPosition): StringBuffer {
val i = Math.round((obj as Number).toFloat())
val displayFormat = SimpleDateFormat(prefs.selectedTimeFormat, Locale.US)
val originalFormat = SimpleDateFormat(Constants.DATE_yyyyMMddHHmmss, Locale.US)
var displayTime = ""
if (prefs.selectedTimeFormat.equals(Constants.TWENTYFOUR_HOUR_FORMAT))
displayTime = displayFormat.format(originalFormat.parse(xLabels.get(i)))
else {
displayTime = displayFormat.format(originalFormat.parse(xLabels.get(i)))
displayTime.replace("AM", " am")
displayTime.replace("PM", " pm")
}
plot.graph.setLineLabelRenderer(XYGraphWidget.Edge.BOTTOM, MyLineLabelRenderer())
return toAppendTo.append(displayTime)
}
override fun parseObject(source: String, #NonNull pos: ParsePosition): Any {
return ""
}
}
plot.addSeries(series1, series1Format)
plot.addSeries(series2, series2Format)
val bitmap: Bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
plot.draw(Canvas(bitmap))
remoteViews!!.setImageViewBitmap(R.id.img_graph_view, bitmap)
appWidgetManager!!.updateAppWidget(appWidgetId, remoteViews)
I am tried to reduce bottom label shown text size is not working it's overlapping one another
How to reduce bottom label text size?
setTextsize used below code
plot.getGraph().getLineLabelStyle(XYGraphWidget.Edge.LEFT).getPaint().textSize = 16f
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()
}
}
I am using kotlin coroutinescope for loading thumnails from video uri and add imageview with that bitmaps in linear layout.
Currently after all thumnail loaded, I am adding into linearlayout. Can anyone suggest me for getting one by one bitmao and adding into linearlayout?
private fun loadThumbnails(uri: Uri) {
val metaDataSource = MediaMetadataRetriever()
metaDataSource.setDataSource(context, uri)
val videoLength = (metaDataSource.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION).toInt() * 1000).toLong()
val thumbnailCount = 8
val interval = videoLength / thumbnailCount
var listOfImage: ArrayList<Bitmap?> = ArrayList()
for (i in 0 until thumbnailCount - 1) {
try {
var bitmap: Bitmap? = null
val job = CoroutineScope(Dispatchers.IO).launch {
val frameTime = i * interval
bitmap = metaDataSource.getFrameAtTime(frameTime, MediaMetadataRetriever.OPTION_CLOSEST)
bitmap?.let {
val targetWidth: Int
val targetHeight: Int
if (it.height > it.width) {
targetHeight = frameDimension
val percentage = frameDimension.toFloat() / it.height
targetWidth = (it.width * percentage).toInt()
} else {
targetWidth = frameDimension
val percentage = frameDimension.toFloat() / it.width
targetHeight = (it.height * percentage).toInt()
}
bitmap = Bitmap.createScaledBitmap(it, targetWidth, targetHeight, false)
}
listOfImage.add(bitmap)
metaDataSource.release()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
listOfImage.forEach {
container_thumbnails.addView(ThumbnailView(context).apply { setImageBitmap(it) })
}
}
Please try next approach:
val job = CoroutineScope(Dispatchers.Main).launch {
val frameTime = i * interval
val bitmap = loadBitmap(frameTime) // loads bitmap asynchronously using withContext(Dispatchers.IO)
// ... use bitmap to set into a view
}
suspend fun loadBitmap(frameTime: Int): Bitmap? = withContext(Dispatchers.IO) {
bitmap = metaDataSource.getFrameAtTime(frameTime, MediaMetadataRetriever.OPTION_CLOSEST)
bitmap?.let {
val targetWidth: Int
val targetHeight: Int
if (it.height > it.width) {
targetHeight = frameDimension
val percentage = frameDimension.toFloat() / it.height
targetWidth = (it.width * percentage).toInt()
} else {
targetWidth = frameDimension
val percentage = frameDimension.toFloat() / it.width
targetHeight = (it.height * percentage).toInt()
}
Bitmap.createScaledBitmap(it, targetWidth, targetHeight, false)
}
}