I am showing a dots loader in my activity in Android using a Timer() object and passing an object of TimerTask() in the scheduleAtFixedRate() API of Timer.
This is my code snippet:-
private fun scheduleTimer() {
timer = Timer()
timer?.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
if (isSingleDir) {
selectedDotPos++
if (selectedDotPos > noOfDots) {
selectedDotPos = 1
}
} else {
if (isFwdDir) {
selectedDotPos++
if (selectedDotPos == noOfDots) {
isFwdDir = !isFwdDir
}
} else {
selectedDotPos--
if (selectedDotPos == 1) {
isFwdDir = !isFwdDir
}
}
}
(scanForActivity(context))?.runOnUiThread {
invalidate()
}
}
}, 0, animDur.toLong())
}
I am making sure that to cancel the timer by calling Timer::cancel(), when the visibility of the view changes. This is the code:-
override fun onVisibilityChanged(changedView: View, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
if (visibility != VISIBLE) {
timer?.cancel()
} else if (shouldAnimate) {
scheduleTimer()
}
}
But the TimerTask is leaking some memory. I am using Leak Canary to analyze the memory leak. This is the report:-
Looking for help to resolve this.
Complete code of my class:-
class LinearDotsLoader : DotsLoaderBaseView
{
private var timer: Timer? = null
var isSingleDir = true
private var diffRadius: Int = 0
private var isFwdDir = true
constructor(context: Context) : super(context) {
initCordinates()
initPaints()
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initAttributes(attrs)
initCordinates()
initPaints()
initShadowPaints()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initAttributes(attrs)
initCordinates()
initPaints()
initShadowPaints()
}
override fun initAttributes(attrs: AttributeSet) {
super.initAttributes(attrs)
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LinearDotsLoader, 0, 0)
this.noOfDots = typedArray.getInt(R.styleable.LinearDotsLoader_loader_noOfDots, 3)
this.selRadius = typedArray.getDimensionPixelSize(R.styleable.LinearDotsLoader_loader_selectedRadius, radius + 10)
this.dotsDistance = typedArray.getDimensionPixelSize(R.styleable.LinearDotsLoader_loader_dotsDist, 15)
this.isSingleDir = typedArray.getBoolean(R.styleable.LinearDotsLoader_loader_isSingleDir, false)
this.expandOnSelect = typedArray.getBoolean(R.styleable.LinearDotsLoader_loader_expandOnSelect, false)
typedArray.recycle()
}
override fun initCordinates() {
diffRadius = this.selRadius - radius
dotsXCorArr = FloatArray(this.noOfDots)
//init X cordinates for all dots
for (i in 0 until noOfDots) {
dotsXCorArr[i] = (i * dotsDistance + (i * 2 + 1) * radius).toFloat()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val calWidth: Int
val calHeight: Int
if (expandOnSelect) {
calWidth = (2 * this.noOfDots * radius + (this.noOfDots - 1) * dotsDistance + 2 * diffRadius)
calHeight = 2 * this.selRadius
} else {
calHeight = 2 * radius
calWidth = (2 * this.noOfDots * radius + (this.noOfDots - 1) * dotsDistance)
}
setMeasuredDimension(calWidth, calHeight)
}
override fun onVisibilityChanged(changedView: View, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
if (visibility != VISIBLE) {
timer?.cancel()
timer?.purge()
} else if (shouldAnimate) {
scheduleTimer()
}
}
private fun scheduleTimer() {
timer = Timer()
val dotsTimerTask = DotsTimerTask()
timer?.scheduleAtFixedRate(dotsTimerTask, 0, animDur.toLong())
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawCircle(canvas)
}
private fun drawCircle(canvas: Canvas) {
for (i in 0 until noOfDots) {
var xCor = dotsXCorArr[i]
if (expandOnSelect) {
if (i + 1 == selectedDotPos) {
xCor += diffRadius.toFloat()
} else if (i + 1 > selectedDotPos) {
xCor += (2 * diffRadius).toFloat()
}
}
var firstShadowPos: Int
var secondShadowPos: Int
if ((isFwdDir && selectedDotPos > 1) || selectedDotPos == noOfDots) {
firstShadowPos = selectedDotPos - 1
secondShadowPos = firstShadowPos - 1
} else {
firstShadowPos = selectedDotPos + 1
secondShadowPos = firstShadowPos + 1
}
if (i + 1 == selectedDotPos) {
selectedCirclePaint?.let {
canvas.drawCircle(
xCor,
(if (expandOnSelect) this.selRadius else radius).toFloat(),
(if (expandOnSelect) this.selRadius else radius).toFloat(),
it
)
}
} else if (showRunningShadow && i + 1 == firstShadowPos) {
canvas.drawCircle(
xCor,
(if (expandOnSelect) this.selRadius else radius).toFloat(),
radius.toFloat(),
firstShadowPaint)
} else if (showRunningShadow && i + 1 == secondShadowPos) {
canvas.drawCircle(
xCor,
(if (expandOnSelect) this.selRadius else radius).toFloat(),
radius.toFloat(),
secondShadowPaint)
} else {
defaultCirclePaint?.let {
canvas.drawCircle(
xCor,
(if (expandOnSelect) this.selRadius else radius).toFloat(),
radius.toFloat(),
it
)
}
}
}
}
var dotsDistance: Int = 15
set(value) {
field = value
initCordinates()
}
var noOfDots: Int = 3
set(noOfDots) {
field = noOfDots
initCordinates()
}
var selRadius: Int = 38
set(selRadius) {
field = selRadius
initCordinates()
}
var expandOnSelect: Boolean = false
set(expandOnSelect) {
field = expandOnSelect
initCordinates()
}
private fun scanForActivity(context: Context?): Activity? {
return when (context) {
null -> null
is Activity -> context
is ContextWrapper -> scanForActivity(context.baseContext)
else -> null
}
}
private fun updateSelectedDot()
{
if (isSingleDir) {
selectedDotPos++
if (selectedDotPos > noOfDots) {
selectedDotPos = 1
}
} else {
if (isFwdDir) {
selectedDotPos++
if (selectedDotPos == noOfDots) {
isFwdDir = !isFwdDir
}
} else {
selectedDotPos--
if (selectedDotPos == 1) {
isFwdDir = !isFwdDir
}
}
}
(scanForActivity(context))?.runOnUiThread {
invalidate()
}
}
private inner class DotsTimerTask: TimerTask(){
override fun run() {
updateSelectedDot()
}
}
}
The answer is to fix racing between cancelling and instantiate/schedule Timer.
To add cancel before each Timer instantiation and starting should be enough.
private fun scheduleTimer() {
timer?.cancel() // fix
timer = Timer()
val dotsTimerTask = DotsTimerTask()
timer?.scheduleAtFixedRate(dotsTimerTask, 0, animDur.toLong())
}
I opened issue on repo of this code:
https://github.com/agrawalsuneet/DotLoadersPack-Android/issues/44
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?
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.
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.
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'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
}
}