I'm implementing a custom view which draws some kind ProgressBar view, taking two views as parameters (origin and destination). Like this:
This is the complete class:
class BarView #JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
private var valueAnimator: ObjectAnimator? = null
private lateinit var path: Path
private val pathMeasure = PathMeasure()
private var pauseProgress: Int = dip(40)
var progress = 0f
set(value) {
field = value.coerceIn(0f, pathMeasure.length)
invalidate()
}
private var originPoint: PointF? = null
private var destinationPoint: PointF? = null
private val cornerEffect = CornerPathEffect(dip(10).toFloat())
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = dip(10f).toFloat()
color = ContextCompat.getColor(context, android.R.color.darker_gray)
pathEffect = cornerEffect
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
if (progress < pathMeasure.length) {
val intervals = floatArrayOf(progress, pathMeasure.length - progress)
val progressEffect = DashPathEffect(intervals, 0f)
linePaint.pathEffect = ComposePathEffect(progressEffect, cornerEffect)
}
canvas.drawPath(path, linePaint)
}
object PROGRESS : Property<BarView, Float>(Float::class.java, "progress") {
override fun set(view: BarView, progress: Float) {
view.progress = progress
}
override fun get(view: BarView) = view.progress
}
private fun startAnimator() {
valueAnimator = ObjectAnimator.ofFloat(this, PROGRESS, 0f, pathMeasure.length).apply {
duration = 500L
interpolator = LinearInterpolator()
}
setPauseListener()
valueAnimator!!.start()
}
fun resume() {
valueAnimator!!.resume()
}
fun reset() {
startAnimator()
}
fun setPoints(originView: View, destinationView: View) {
originPoint = PointF(originView.x + originView.width / 2, 0f)
destinationPoint = PointF(destinationView.x + destinationView.width / 2, 0f)
setPath()
startAnimator()
}
private fun setPath() {
path = Path()
path.moveTo(originPoint!!.x, originPoint!!.y)
path.lineTo(destinationPoint!!.x, destinationPoint!!.y)
pathMeasure.setPath(path, false)
}
private fun setPauseListener() {
valueAnimator!!.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
override fun onAnimationUpdate(valueAnimator: ValueAnimator?) {
val progress = valueAnimator!!.getAnimatedValue("progress") as Float
if (progress > pauseProgress) {
valueAnimator.pause()
this#BarView.valueAnimator!!.removeUpdateListener(this)
}
}
})
}
}
What im trying to do is to pause the animation at a specific progress, 40dp in this case:
private fun setPauseListener() {
valueAnimator!!.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
override fun onAnimationUpdate(valueAnimator: ValueAnimator?) {
val progress = valueAnimator!!.getAnimatedValue("progress") as Float
if (progress > pauseProgress) {
valueAnimator.pause()
this#BarView.valueAnimator!!.removeUpdateListener(this)
}
}
})
}
But the animations have different speeds since the views have different path lengths, and all of them have to finish in 500ms. They are not pausing at the same distance from the origin:
I tried switching from a LinearInterpolator to a AccelerateInterpolator, to make the start of the animation slower, but i'm still not satisfied with the results.
The next step for me, would be to try to implement a custom TimeInterpolator to make the animation start speed the same no matter how long the path is on each view, but I cannot wrap my head arrow the maths to create the formula needed.
valueAnimator = ObjectAnimator.ofFloat(this, PROGRESS, 0f, pathMeasure.length).apply {
duration = 500L
interpolator = TimeInterpolator { input ->
// formula here
}
}
Any help with that would be well received. Any suggestions about a different approach. What do you think?
1) As I mentioned I would use determinate ProgressBar and regulate bar width by maximum amount of progress, assigned to certain view
2) I would use ValueAnimator.ofFloat with custom interpolator and set progress inside it
3) I would extend my custom interpolator from AccelerateInterpolator to make it look smthng like this:
class CustomInterpolator(val maxPercentage: Float) : AccelerateInterpolator(){
override fun getInterpolation(input: Float): Float {
val calculatedVal = super.getInterpolation(input)
return min(calculatedVal, maxPercentage)
}
}
where maxPercentage is a fraction of view width (from 0 to 1) you bar should occupy
Hope it helps
Related
I'm using an alert dialog to show an animation (green tick mark on success api call). But it's causing a memory leak if I press the home button before the animation ends.
To reproduce, I enabled "Finish Activities" in Developer Options.
Attaching the code below for "Tick Animation" and the custom dialog box which shows that animation.
SuccessAnimation.kt
class SuccessAnimation #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null,
) : RelativeLayout(context, attrs) {
private val circleFadeInTime: Long
private val checkAnimationTime: Long
val postAnimationTime: Long
init {
inflate(context, R.layout.success_content, this)
val a = context.obtainStyledAttributes(attrs, R.styleable.SuccessAnimation, 0, 0)
try {
circleFadeInTime = a.getInteger(R.styleable.SuccessAnimation_circleAppearanceTime, DEFAULT_CIRCLE_TIME).toLong()
checkAnimationTime = a.getInteger(R.styleable.SuccessAnimation_checkMarkAnimationTime, DEFAULT_ANIMATION_TIME).toLong()
postAnimationTime = a.getInteger(R.styleable.SuccessAnimation_postAnimationTime, DEFAULT_POST_ANIMATION_TIME).toLong()
} finally {
a.recycle()
}
isClickable = true // Prevent anything else from happening!
}
val animationDuration = circleFadeInTime + checkAnimationTime + postAnimationTime
private val circle: View = findViewById(R.id.green_circle)
private val checkMark = findViewById<AnimatedCheckMark>(R.id.check_mark).apply { setAnimationTime(checkAnimationTime) }
private var onAnimationFinished: (() -> Unit)? = {}
/**
* Set a callback to be invoked when the animation finishes
* #param listener listener to be called
*/
fun setSuccessFinishedListener(listener: (() -> Unit)?) {
this.onAnimationFinished = listener
}
/**
* start the animation, also handles making sure the view is visible.
*/
fun show() {
if (visibility != VISIBLE) {
visibility = VISIBLE
post { show() }
return
}
circle.visibility = VISIBLE
circle.scaleX = 0f
circle.scaleY = 0f
val animator = ValueAnimator.ofFloat(0f, 1f)
animator.addUpdateListener { animation: ValueAnimator ->
val scale = animation.animatedFraction
circle.scaleY = scale
circle.scaleX = scale
circle.invalidate()
}
animator.duration = circleFadeInTime
animator.interpolator = OvershootInterpolator()
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
checkMark!!.visibility = VISIBLE
invalidate()
checkMark.startAnimation { view: AnimatedCheckMark ->
view.postDelayed({
visibility = GONE
checkMark.visibility = INVISIBLE
circle.visibility = INVISIBLE
onAnimationFinished?.invoke()
}, postAnimationTime)
}
}
})
invalidate()
animator.start()
}
companion object{
private const val DEFAULT_CIRCLE_TIME = 300
private const val DEFAULT_ANIMATION_TIME = 500
private const val DEFAULT_POST_ANIMATION_TIME = 500
}
}
SuccessAnimationPopup.kt
class SuccessAnimationPopup(context: Context,
private val callback: () -> Unit) :
AlertDialog(context, android.R.style.Theme_Translucent_NoTitleBar) {
init {
window?.let {
it.setFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
)
it.setFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.success_animation_popup)
setCancelable(false)
val success = findViewById<SuccessAnimation>(R.id.success_animation)
success?.setSuccessFinishedListener {
dismiss()
callback()
}
success?.show()
}
}
It is being used like the following:
SuccessAnimationPopup(view.context) {}.show() I only have "view" here and not the activity.
Been trying to find the root cause. Have also added onDetachedFromWindow lifecycle callback in SuccessAnimation.kt and tried setting the onAnimationListener = null, still doesn't work.
What am I missing here?
Also, the AlertDialog constructor is not accepting a nullable context, hence I wasn't able to pass a WeakReference since it's nullable.
I'm currently working in a custom view, basically its a draggable view that has a pulse effect. In fact I achieved the effect that I want but I it has performance issues.
I don't know if it can be seen well in the gif but at first the effect runs without problems and then it shows performance issues.
Here is my class
class BalanceAndFadeView #JvmOverloads constructor(...) : FrameLayout(...) {
// The thumb view is the draggable view
private var thumb: ImageView
// These are for the pulsing effect
private var pulseView: ImageView
private var pulseView2: ImageView
private var selectedPoint: Point = Point()
private val animators = arrayListOf<Animator>()
private var animatorsSet = AnimatorSet()
private var centerX = 0f
private var centerY = 0f
init {
// Here I create and add the views to the root view
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
centerX = w * 0.5f
centerY = h * 0.5f
animationStart()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_MOVE,
MotionEvent.ACTION_UP -> {
val snapPoint = Point(event.x.toInt(), event.y.toInt())
selectedPoint = snapPoint
setCoordinate(snapPoint.x, snapPoint.y)
true
}
else -> {
false
}
}
}
fun setCoordinate(x: Int, y: Int) {
// Sets the thumb view coordinates to create the draggable view effect
thumb.x = x - thumb.measuredWidth * 0.5f
thumb.y = y - thumb.measuredHeight * 0.5f
// These animators are for the pulsing effect to translate to the center of the view
// I want the start point of the translation to update with the position
// of the thumb view, so I need it to be a dynamic animation
val translationXAnimator =
ObjectAnimator.ofFloat(pulseView, "TranslationX", x.toFloat() - centerX, 0f)
.apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.RESTART
startDelay = 0
}
val translationYAnimator =
ObjectAnimator.ofFloat(pulseView, "TranslationY", y.toFloat() - centerY, 0f)
.apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.RESTART
startDelay = 0
}
val translationXAnimator2 = ObjectAnimator.ofFloat(...).apply { ... }
val translationYAnimator2 = ObjectAnimator.ofFloat(...).apply { ... }
animatorsSet.playTogether(
... // adds new animators
)
// Since I run this method every time the touch event it's troggered,
// the performance gets worse every time I touch the screen
animatorsSet.start()
// But if I call animatorsSet.end() and then animatorsSet.start()
// the pulsing effect does not work as expected since the animation stops
// but in terms of performance it's just normal
}
// These animators are for the pulsing effect
private fun animationStart() {
val scaleXAnimator = ObjectAnimator.ofFloat(pulseView, "ScaleX", 1f, 6f).apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.RESTART
startDelay = 0
}
animators.add(scaleXAnimator)
val scaleYAnimator = ObjectAnimator.ofFloat(pulseView, "ScaleY", 1f, 6f).apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.RESTART
startDelay = 0
}
animators.add(scaleYAnimator)
val alphaAnimator = ObjectAnimator.ofFloat(pulseView, "Alpha", 1f, 0f).apply {
repeatCount = ObjectAnimator.INFINITE
repeatMode = ObjectAnimator.RESTART
startDelay = 0
}
animators.add(alphaAnimator)
val scaleXAnimator2 = ObjectAnimator.ofFloat( ... ).apply { ... }
animators.add(scaleXAnimator2)
val scaleYAnimator2 = ObjectAnimator.ofFloat( ... ).apply { ... }
animators.add(scaleYAnimator2)
val alphaAnimator2 = ObjectAnimator.ofFloat( ... ).apply { ... }
animators.add(alphaAnimator2)
animatorsSet.playTogether(animators)
animatorsSet.interpolator = LinearInterpolator()
animatorsSet.duration = 1000
animatorsSet.start()
}
}
I hope you can help me or recommend me a better approach
I am currently creating an Android view in which, when the use tap it, I will display a sort of ripple around the coordinate of the tap.
But I'm not sure on how to do it. My first idea was to invalidate the cache and just make the circle bigger each time but it doesn't seem appropriate nor efficient to do this like that.
If anyone faced the same problem before and would love the share some tips on how to do it it would be much appreciated.
I finally found out a solution. Not a perfect one but it works for now.
Here is the code I did. Basically when I need it I change a boolean to true so my onDrawfunction knows that it have to execute the drawFingerPrintfunction.
The drawFingerPrint function, in the other end, just draw a circle that's bigger and bigger between each iteration until it reaches the diameter needed
private fun drawFingerPrint(canvas: Canvas) {
canvas.drawCircle(pointerX, pointerY, radius, paint)
if(radius<= 100F){
radius+=10F
invalidate()
}
else{
radius = 0F
drawAroundFinger = false
invalidate()
}
}
I hope someone else will find this useful sometimes!
Matthieu
As already mentioned #Matthieu, you need to draw circle on the canvas and invalidate the view. Here I provide a more complete example.
So we have an open class RippleView:
open class RippleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private var rippleX: Float? = null
private var rippleY: Float? = null
private var rippleRadius: Float? = null
var maxRippleRadius: Float = 100f // Retrieve from resources
var rippleColor: Int = 0x88888888.toInt()
private val ripplePaint = Paint().apply {
color = rippleColor
}
private val animationExpand = object : Runnable {
override fun run() {
rippleRadius?.let { radius ->
if (radius < maxRippleRadius) {
rippleRadius = radius + maxRippleRadius * 0.1f
invalidate()
postDelayed(this, 10L)
}
}
}
}
private val animationFade = object : Runnable {
override fun run() {
ripplePaint.color.let { color ->
if (color.alpha > 10) {
ripplePaint.color = color.adjustAlpha(0.9f)
invalidate()
postDelayed(this, 10L)
} else {
rippleX = null
rippleY = null
rippleRadius = null
invalidate()
}
}
}
}
fun startRipple(x: Float, y: Float) {
rippleX = x
rippleY = y
rippleRadius = maxRippleRadius * 0.15f
ripplePaint.color = rippleColor
animationExpand.run()
}
fun stopRipple() {
if (rippleRadius != null) {
animationFade.run()
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val x = rippleX ?: return
val y = rippleY ?: return
val r = rippleRadius ?: return
canvas.drawCircle(x, y, r, ripplePaint)
}
}
fun Int.adjustAlpha(factor: Float): Int =
(this.ushr(24) * factor).roundToInt() shl 24 or (0x00FFFFFF and this)
inline val Int.alpha: Int
get() = (this shr 24) and 0xFF
Now you can extend any custom view with RippleView and use startRipple method when you get ACTION_DOWN, and stopRipplemethod when you get ACTION_UP:
class ExampleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : RippleView(context, attrs), View.OnTouchListener {
init {
setOnTouchListener(this)
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (isInsideArea(event.x, event.y)) {
startRipple(event.x, event.y)
}
}
MotionEvent.ACTION_UP -> {
stopRipple()
}
}
return false
}
private fun isInsideArea(x: Float, y: Float): Boolean {
TODO("Not yet implemented")
}
}
I want to create a battery level indicator as in the image(which i circled). The green part should fill based on the available battery in the device.
Getting the battery percentage from the device like this
registerReceiver(mBatInfoReceiver, new IntentFilter(
Intent.ACTION_BATTERY_CHANGED));
So in the layout i am able to display the battery percentage.
public class BatteryIndicatorActivity extends Activity {
//Create Broadcast Receiver Object along with class definition
private BroadcastReceiver mBatInfoReceiver = new BroadcastReceiver() {
#Override
//When Event is published, onReceive method is called
public void onReceive(Context c, Intent i) {
//Get Battery %
int level = i.getIntExtra("level", 0);
TextView tv = (TextView) findViewById(R.id.textfield);
//Set TextView with text
tv.setText("Battery Level: " + Integer.toString(level) + "%");
}
};
But how to create a UI for this type of battery indicator. Is their any api to achieve this?, If not how to create such type of UI.
Your help will be appreciated.
Here is my CustomView for display battery level
class BatteryView #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
View(context, attrs, defStyleAttr) {
private var radius: Float = 0f
private var isCharging: Boolean = false
// Top
private var topPaint =
PaintDrawable(Color.WHITE) // I only want to corner top-left and top-right so I use PaintDrawable instead of Paint
private var topRect = Rect()
private var topPaintWidthPercent = 50
private var topPaintHeightPercent = 8
// Border
private var borderPaint = Paint().apply {
color = Color.BLUE
style = Paint.Style.STROKE
}
private var borderRect = RectF()
private var borderStrokeWidthPercent = 8
private var borderStroke: Float = 0f
// Percent
private var percentPaint = Paint()
private var percentRect = RectF()
private var percentRectTopMin = 0f
private var percent: Int = 0
// Charging
private var chargingRect = RectF()
private var chargingBitmap: Bitmap? = null
init {
init(attrs)
chargingBitmap = getBitmap(R.drawable.ic_charging)
}
private fun init(attrs: AttributeSet?) {
val ta = context.obtainStyledAttributes(attrs, R.styleable.BatteryView)
try {
percent = ta.getInt(R.styleable.BatteryView_bv_percent, 0)
isCharging = ta.getBoolean(R.styleable.BatteryView_bv_charging, false)
} finally {
ta.recycle()
}
}
#SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val measureWidth = View.getDefaultSize(suggestedMinimumWidth, widthMeasureSpec)
val measureHeight = (measureWidth * 1.8f).toInt()
setMeasuredDimension(measureWidth, measureHeight)
radius = borderStroke / 2
borderStroke = (borderStrokeWidthPercent * measureWidth).toFloat() / 100
// Top
val topLeft = measureWidth * ((100 - topPaintWidthPercent) / 2) / 100
val topRight = measureWidth - topLeft
val topBottom = topPaintHeightPercent * measureHeight / 100
topRect = Rect(topLeft, 0, topRight, topBottom)
// Border
val borderLeft = borderStroke / 2
val borderTop = topBottom.toFloat() + borderStroke / 2
val borderRight = measureWidth - borderStroke / 2
val borderBottom = measureHeight - borderStroke / 2
borderRect = RectF(borderLeft, borderTop, borderRight, borderBottom)
// Progress
val progressLeft = borderStroke
percentRectTopMin = topBottom + borderStroke
val progressRight = measureWidth - borderStroke
val progressBottom = measureHeight - borderStroke
percentRect = RectF(progressLeft, percentRectTopMin, progressRight, progressBottom)
// Charging Image
val chargingLeft = borderStroke
var chargingTop = topBottom + borderStroke
val chargingRight = measureWidth - borderStroke
var chargingBottom = measureHeight - borderStroke
val diff = ((chargingBottom - chargingTop) - (chargingRight - chargingLeft))
chargingTop += diff / 2
chargingBottom -= diff / 2
chargingRect = RectF(chargingLeft, chargingTop, chargingRight, chargingBottom)
}
override fun onDraw(canvas: Canvas) {
drawTop(canvas)
drawBody(canvas)
if (!isCharging) {
drawProgress(canvas, percent)
} else {
drawCharging(canvas)
}
}
private fun drawTop(canvas: Canvas) {
topPaint.bounds = topRect
topPaint.setCornerRadii(floatArrayOf(radius, radius, radius, radius, 0f, 0f, 0f, 0f))
topPaint.draw(canvas)
}
private fun drawBody(canvas: Canvas) {
borderPaint.strokeWidth = borderStroke
canvas.drawRoundRect(borderRect, radius, radius, borderPaint)
}
private fun drawProgress(canvas: Canvas, percent: Int) {
percentPaint.color = getPercentColor(percent)
percentRect.top = percentRectTopMin + (percentRect.bottom - percentRectTopMin) * (100 - percent) / 100
canvas.drawRect(percentRect, percentPaint)
}
// todo change color
private fun getPercentColor(percent: Int): Int {
if (percent > 50) {
return Color.WHITE
}
if (percent > 30) {
return Color.YELLOW
}
return Color.RED
}
private fun drawCharging(canvas: Canvas) {
chargingBitmap?.let {
canvas.drawBitmap(it, null, chargingRect, null)
}
}
private fun getBitmap(drawableId: Int, desireWidth: Int? = null, desireHeight: Int? = null): Bitmap? {
val drawable = AppCompatResources.getDrawable(context, drawableId) ?: return null
val bitmap = Bitmap.createBitmap(
desireWidth ?: drawable.intrinsicWidth,
desireHeight ?: drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
fun charge() {
isCharging = true
invalidate() // can improve by invalidate(Rect)
}
fun unCharge() {
isCharging = false
invalidate()
}
fun setPercent(percent: Int) {
if (percent > 100 || percent < 0) {
return
}
this.percent = percent
invalidate()
}
fun getPercent(): Int {
return percent
}
}
style.xml
<declare-styleable name="BatteryView">
<attr name="bv_charging" format="boolean" />
<attr name="bv_percent" format="integer" />
</declare-styleable>
drawable/ic_charging.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="368.492"
android:viewportHeight="368.492">
<path
android:fillColor="#FFFFFF"
android:pathData="M297.51,150.349c-1.411,-2.146 -3.987,-3.197 -6.497,-2.633l-73.288,16.498L240.039,7.012c0.39,-2.792 -1.159,-5.498 -3.766,-6.554c-2.611,-1.069 -5.62,-0.216 -7.283,2.054L71.166,217.723c-1.489,2.035 -1.588,4.773 -0.246,6.911c1.339,2.132 3.825,3.237 6.332,2.774l79.594,-14.813l-23.257,148.799c-0.436,2.798 1.096,5.536 3.714,6.629c0.769,0.312 1.562,0.469 2.357,0.469c1.918,0 3.78,-0.901 4.966,-2.517l152.692,-208.621C298.843,155.279 298.916,152.496 297.51,150.349z" />
</vector>
Using
<package.BatteryView
android:id="#+id/battery_view"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:src="#drawable/ic_charging"
app:bv_charging="false"
app:bv_percent="20" />
Github project
Hope it help
There are a lot of ways to do it. Here are few of them:
Use a ProgressBar, make it vertical, make it be drawn in the battery shape
Use a custom View, override onDraw() in it, and draw the battery shape on the canvas
Use a white image with a transparent battery shape. Place it over a view, where you fill a background vertically, or change background view's height.
I've created class that extends PopupView in Kotlin, and trying to animate it's appearing with CircleReveal library. Here is function of my class
fun show(root: View) {
showAtLocation(root, Gravity.CENTER, 0, 0)
val cx = (mainView.left + mainView.right) / 2
val cy = (mainView.top + mainView.bottom) / 2
val dx = Math.max(cx, mainView.width - cx)
val dy = Math.max(cy, mainView.height - cy)
val finalRadius = Math.hypot(dx.toDouble(), dy.toDouble()).toFloat()
with (ViewAnimationUtils.createCircularReveal(mainView, cx, cy, 0f, finalRadius)) {
interpolator = AccelerateDecelerateInterpolator()
duration = 1500
start()
}
}
That code gives me following error
java.lang.IllegalStateException: Cannot start this animator on a detached view!
at android.view.RenderNode.addAnimator(RenderNode.java:817)
at android.view.RenderNodeAnimator.setTarget(RenderNodeAnimator.java:300)
at android.view.RenderNodeAnimator.setTarget(RenderNodeAnimator.java:282)
at android.animation.RevealAnimator.<init>(RevealAnimator.java:37)
at android.view.ViewAnimationUtils.createCircularReveal(ViewAnimationUtils.java:53)
at io.codetail.animation.ViewAnimationUtils.createCircularReveal(ViewAnimationUtils.java:74)
at io.codetail.animation.ViewAnimationUtils.createCircularReveal(ViewAnimationUtils.java:39)
Class initialization
class MemberMenu(ctx: Context, val member: Member): PopupWindow(ctx), View.OnClickListener {
val mainView: View
init {
contentView = LayoutInflater.from(ctx).inflate(R.layout.member_menu_layout, null)
mainView = contentView.findViewById(R.id.member_menu_view)
val size = Helpers.dipToPixels(ctx, 240f)
width = size; height = size
setBackgroundDrawable(ColorDrawable())
isOutsideTouchable = true
isTouchable = true
}
.......
Not sure, if it's correct solution, but I just moved that code to OnAttachStateChangeListener
fun show(root: View) {
showAtLocation(root, Gravity.CENTER, 0, 0)
backgroundView.addOnAttachStateChangeListener(this)
}
override fun onViewAttachedToWindow(v: View?) {
if (v==null) return
with(ViewAnimationUtils.createCircularReveal(v, 500, 500, 0f, 500f)) {
interpolator = AccelerateDecelerateInterpolator()
duration = 2500
start()
}
} override fun onViewDetachedFromWindow(v: View?) {}
Same crash I was getting, and even same library i am using, the problem was the device has its Don't keep activity flag ON, that is why it's crashing.
Solution
Goto-->Settings-->Developer Option-->Don't Keep Activity(Make it Turn OFF)
NOTE: Developer option path may vary with devices
Hope it will help you.