I'm working on Android TextView animation.
Requirement is TextView is at fix location of the screen and every character should be animate with alpha (Lower to higher). I've tried couple of libraries unfortunately it doesn’t work for me.
Reference screenshot:
If anybody has solution for this, kindly provide it. Thanks
After doing lots of research on the same, I'm posting my own answer.
Steps:
Create CustomTextLayout
class CustomTextLayout #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayoutCompat(context, attrs, defStyleAttr) {
private var characterAnimationTime = 100
private var textSize = 22f
private var letterSpacing = 0f
private var animationDuration = 2000L
init {
orientation = HORIZONTAL
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomTextLayout, defStyleAttr, 0)
textSize = typedArray.getFloat(R.styleable.CustomTextLayout_textSize, textSize)
typedArray.recycle()
}
/**
* This function sets the animated alpha text
* #param context Context of Activity / Fragment
* #param text Text string
* #param initialDelay Start animation delay
*/
fun setAnimatedText(context: Context, text: String, initialDelay: Long = 0) {
var textDrawPosition = 0
Handler().postDelayed({
for (char in text) {
val textView = getTextView(char.toString())
textView.visibility = View.GONE
this.addView(textView)
textDrawPosition++
drawAnimatedText(
context,
this,
textView,
textDrawPosition,
text,
(textDrawPosition * characterAnimationTime).toLong()
)
}
}, initialDelay)
}
private fun drawAnimatedText(
context: Context,
parentView: LinearLayoutCompat,
textView: AppCompatTextView,
position: Int,
text: String,
initialDelay: Long
) {
val colorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), Color.WHITE, Color.BLACK)
colorAnimation.startDelay = initialDelay
colorAnimation.duration = animationDuration
colorAnimation.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animator: Animator) {
textView.visibility = View.VISIBLE
}
override fun onAnimationEnd(animator: Animator) {
if (position == text.length) {
val updatedTextView = getTextView(text)
updatedTextView.setTextColor(Color.BLACK)
updatedTextView.visibility = View.VISIBLE
parentView.removeAllViews()
parentView.addView(updatedTextView)
}
}
override fun onAnimationCancel(animator: Animator) {
}
override fun onAnimationRepeat(animator: Animator) {
}
})
colorAnimation.addUpdateListener {
textView.setTextColor(it.animatedValue as Int)
}
colorAnimation.start()
}
private fun getTextView(text: String): AppCompatTextView {
val textView = AppCompatTextView(context)
textView.text = text
textView.textSize = textSize
textView.setTypeface(Typeface.SANS_SERIF, Typeface.ITALIC)
textView.letterSpacing = letterSpacing
return textView
}
Add in layout file
<com.mypackagename.CustomTextLayout
app:textSize="30"
app:letterSpacing="0.1"
android:id="#+id/textLayoutFirst"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
</com.mypackagename.CustomTextLayout>
Add attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomTextLayout">
<attr name="textSize" format="float"/>
<attr name="letterSpacing" format="float"/>
</declare-styleable>
</resources>
Start animation:
textLayoutFirst.setAnimatedText(this, "Some text here")
It's done.
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 want to add a indeterminate progressbar to my custom button.
I'm using add setCompoundDrawablesWithIntrinsicBounds to add it to left.
But somehow progress bar animation doesn't appear.
//CustomButton.kt
fun addProgressbar () {
val progressBar = ProgressBar(context)
progressBar.isIndeterminate = true
progressBar.animate()
progressBar.setBackgroundColor(Color.RED)
progressBar.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
progressBar.visibility = View.VISIBLE
setCompoundDrawablesWithIntrinsicBounds(
progressBar.indeterminateDrawable,
null,
ContextCompat.getDrawable(context, R.drawable.ic_baseline_notifications_black_24),
null
)
}
When I debug progressBar.isAnimating() returns false. I also tried to add a resource drawable with ContextCompat.getDrawable which works.
How can I make it work ?
class MyButton #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AppCompatButton(context, attrs, defStyle) {
private var progressDrawable: Drawable
init {
progressDrawable = ProgressBar(context).indeterminateDrawable.apply {
// apply any customization on drawable. not on progress view
setBounds(0, 0, 24.toPx, 24.toPx)
setTint(Color.WHITE)
}
compoundDrawablePadding = 4.toPx
}
var isLoading: Boolean = false
set(value) {
if (isLoading == value) return
field = value
val (startDrawable, topDrawable, endDrawable, bottomDrawable) = compoundDrawablesRelative
if (value) {
// add progress and keep others
setCompoundDrawablesRelative(
progressDrawable,
topDrawable,
endDrawable,
bottomDrawable
)
(progressDrawable as? Animatable)?.start()
} else {
// remove progress
setCompoundDrawablesRelative(
null,
topDrawable,
endDrawable,
bottomDrawable
)
(progressDrawable as? Animatable)?.stop()
}
}
override fun onDetachedFromWindow() {
(progressDrawable as? Animatable)?.stop()
super.onDetachedFromWindow()
}
}
PS: Slowness is related to recording/encoding. It works normally.
class EllipsizedTextView #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0): AppCompatTextView(context, attrs, defStyleAttr) {
private var ellipsis = getDefaultEllipsis().toString()
private var ellipsisColor = getDefaultEllipsisColor()
private var isEllipsis = false
private var ellipsizedText: CharSequence? = null
private var callbackEllipsized: MoreClickableSpan? = null
init {
attrs?.let { attributeSet ->
context.theme.obtainStyledAttributes(attributeSet, R.styleable.EllipsizedTextView, 0, 0).apply {
ellipsis = getString(R.styleable.EllipsizedTextView_ellipsis) ?: getDefaultEllipsis().toString()
ellipsisColor = getColor(R.styleable.EllipsizedTextView_ellipsisColor, getDefaultEllipsisColor())
recycle()
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
/*
Prepare to set custom ellipsize text
*/
val availableScreenWidth = measuredWidth - compoundPaddingLeft.toFloat() - compoundPaddingRight.toFloat()
var availableTextWidth = availableScreenWidth * maxLines
ellipsizedText = TextUtils.ellipsize(text, paint, availableTextWidth, ellipsize, false) { start, end ->
isEllipsis = start != 0 && end >= 65
}
if (isEllipsis) { // check if current text is ellipsized or not
printLog("isEllipsis: $ellipsizedText ellipsis: $ellipsis, text : $text")
// If the ellipsizedText is different than the original text, this means that it didn't fit and got indeed ellipsized.
// Calculate the new availableTextWidth by taking into consideration the size of the custom ellipsis, too.
availableTextWidth = (availableScreenWidth - paint.measureText(ellipsis)) * maxLines
ellipsizedText = TextUtils.ellipsize(text, paint, availableTextWidth, ellipsize, false){ start, end ->
isEllipsis = start != 0 && end >= 65
}
}
setEllipsizedText(ellipsizedText, isEllipsis)
}
private fun setEllipsizedText(value: CharSequence?, isEllipsized: Boolean){
printLog("setEllipsizedText > $isEllipsized")
if(isEllipsized){
val resultText = "$value$ellipsis"
val startPoint = resultText.indexOf(ellipsis)
val endPoint = startPoint + ellipsis.length
val spannable = SpannableString(resultText).apply {
callbackEllipsized?.let { callback ->
printLog("setEllipsizedText > implement callbackEllipsized")
// set event click on spannable
setSpan(callback, startPoint, endPoint, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// set color on target text while enable to click
setSpan(ForegroundColorSpan(ellipsisColor), startPoint, endPoint, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
//val defaultEllipsisStart = value.indexOf(getDefaultEllipsis())
//val defaultEllipsisEnd = defaultEllipsisStart + 1
//text = spannableStringBuilder.append(ellipsizedText).replace(defaultEllipsisStart, defaultEllipsisEnd, ellipsisSpannable)
text = spannable
movementMethod = LinkMovementMethod.getInstance()
printLog("setEllipsizedText > text : $text")
}
}
private fun getDefaultEllipsis(): Char {
return Typography.ellipsis
}
private fun getDefaultEllipsisColor(): Int {
return textColors.defaultColor
}
fun setActionClickEllipse(callback: MoreClickableSpan){
this.callbackEllipsized = callback
}
fun isReadMore(): Boolean = isEllipsis
#Suppress("unused_parameter")
fun printLog(msg: String) { }
open class MoreClickableSpan : ClickableSpan() {
override fun onClick(widget: View) {}
override fun updateDrawState(ds: TextPaint) {}
}
}
How to implement ellipsized text with rules if more than 65 character? my existing code below like this
Why don't you add this to your TextView in the xml file?It will limit it to 65 chars and it will show ellipse after that
android:maxLength="65"
android:ellipsize="end"
android:maxLines="1"
I'm creating a custom compound view which extends LinearLayout (also tried ConstraintLayout) which contains 2 child views. When the app is running the view lays out correctly, however in the preview window it shows as 0 height when I use wrap_content. When the view was extending ConstraintLayout it was rendering as match_parent when the view was set to wrap_content.
If I override onMeasure and force a size when isInEditMode it is still showing up as 0 height.
Can someone point out what I'm doing wrong?
This is the custom view class:
package com.classdojo.android.nessie
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.annotation.ColorRes
import androidx.annotation.DimenRes
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import com.airbnb.paris.utils.getFont
import com.classdojo.android.nessie.icon.Icon
import kotlinx.android.synthetic.main.nessie_button_view.view.*
class NessieButton : LinearLayout {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(attrs)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(attrs)
}
constructor(context: Context) : super(context) {
init(null)
}
enum class Style(
#DrawableRes
val backgroundRes: Int,
#ColorRes
val textColor: Int
) {
PRIMARY(
backgroundRes = R.drawable.nessie_button_style_primary,
textColor = R.color.nessie_button_style_primary_text_color
),
SECONDARY(
backgroundRes = R.drawable.nessie_button_style_secondary,
textColor = R.color.nessie_button_style_secondary_text_color
),
TERTIARY(
backgroundRes = R.drawable.nessie_button_style_tertiary,
textColor = R.color.nessie_button_style_tertiary_text_color
),
DESTRUCTIVE(
backgroundRes = R.drawable.nessie_button_style_destructive,
textColor = R.color.nessie_button_style_destructive_text_color
),
BEYOND(
backgroundRes = R.drawable.nessie_button_style_beyond,
textColor = R.color.nessie_button_style_beyond_text_color
);
companion object {
fun from(index: Int) = values().getOrElse(index) { Style.PRIMARY }
}
}
enum class Size(
#DimenRes
val horizontalPadding: Int,
#DimenRes
val verticalPadding: Int,
#DimenRes
val textSize: Int
) {
SMALL(
horizontalPadding = R.dimen.nessie_default_size_3x,
verticalPadding = R.dimen.nessie_default_size_2x,
textSize = R.dimen.nessie_textSizeDetail
),
MEDIUM(
horizontalPadding = R.dimen.nessie_default_size_3x,
verticalPadding = R.dimen.nessie_default_size_2x,
textSize = R.dimen.nessie_textSizeAction
),
LARGE(
horizontalPadding = R.dimen.nessie_default_size_4x,
verticalPadding = R.dimen.nessie_default_size_3x,
textSize = R.dimen.nessie_textSizeAction
);
companion object {
fun from(index: Int) = values().getOrElse(index) { Size.LARGE }
}
}
var text: CharSequence?
set(value) {
nessie_button_text_view.text = value
nessie_button_text_view.isVisible = !value.isNullOrBlank()
}
get() = nessie_button_text_view.text
var icon: Icon? = null
set(value) {
field = value
when (value) {
null -> {
nessie_button_icon_view.isVisible = false
}
else -> {
nessie_button_icon_view.icon = value
nessie_button_icon_view.isVisible = true
}
}
}
var style: Style = Style.PRIMARY
set(value) {
field = value
setBackgroundResource(value.backgroundRes)
val color = ResourcesCompat.getColor(resources, value.textColor, null)
nessie_button_icon_view.iconColor = color
nessie_button_text_view.setTextColor(color)
}
var size: Size = Size.LARGE
set(value) {
field = value
val horizontalPadding = resources.getDimensionPixelSize(value.horizontalPadding)
val verticalPadding = resources.getDimensionPixelSize(value.verticalPadding)
setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding)
val fontSize = resources.getDimension(value.textSize)
nessie_button_text_view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize)
nessie_button_icon_view.apply {
iconPixelSize = fontSize.toInt()
layoutParams.apply {
width = fontSize.toInt()
height = fontSize.toInt()
}
}
}
init {
init(null)
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
super.setFocusable(enabled)
val colorStateList = ResourcesCompat.getColorStateList(resources, style.textColor, null)
val viewState = drawableState
val color = colorStateList!!.getColorForState(viewState, 0)
nessie_button_text_view.setTextColor(color)
nessie_button_icon_view.iconColor = color
}
private fun init(attrs: AttributeSet?) {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER
inflate(context, R.layout.nessie_button_view, this)
val styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.nessie_NessieButton)
try {
val iconIndex = styledAttributes.getInteger(R.styleable.nessie_NessieButton_nessie_icon, -1)
icon = when (iconIndex) {
-1 -> null
else -> Icon.from(iconIndex)
}
text = styledAttributes.getString(R.styleable.nessie_NessieButton_android_text)
style = Style.from(styledAttributes.getInteger(R.styleable.nessie_NessieButton_nessie_buttonStyle, Style.PRIMARY.ordinal))
size = Size.from(styledAttributes.getInteger(R.styleable.nessie_NessieButton_nessie_buttonSize, Size.LARGE.ordinal))
isEnabled = styledAttributes.getBoolean(R.styleable.nessie_NessieButton_android_enabled, true)
} finally {
styledAttributes.recycle()
}
nessie_button_text_view.typeface = context.getFont(R.font.nessie_proximanova_bold)
}
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) {
if (child.id == R.id.nessie_button_icon_view || child.id == R.id.nessie_button_text_view) {
super.addView(child, index, params)
} else {
throw IllegalArgumentException("Cannot add more views to a NessieButton")
}
}
}
And this is the view layout:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/root_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:padding="#dimen/nessie_default_size_2x"
tools:background="#drawable/nessie_button_primary_background"
tools:parentTag="android.widget.LinearLayout">
<com.classdojo.android.nessie.icon.IconImageView
android:id="#+id/nessie_button_icon_view"
android:layout_width="#dimen/nessie_default_size_3x"
android:layout_height="#dimen/nessie_default_size_3x"
android:layout_marginEnd="#dimen/nessie_default_size" />
<TextView
android:id="#+id/nessie_button_text_view"
style="#style/nessie_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineHeight="22sp"
android:textStyle="bold"
tools:text="My button"
tools:textColor="#color/nessie_white" />
</merge>
It turns out that it wasn't related to the merge/custom view, but was related to the fact that the editor preview couldn't find the font resource.
Wrapping the instances where the font was loaded in if (!isInEditMode){} resolved the problem.
I knew there was an errors list, but I hadn't used it in a while and couldn't find it. In case someone comes across this question, it's the blue i icon in the top right which shows you the errors hit when rendering the view.
<merge is not a ViewGroup. Hence, the android:attributes on that tag are ignored. This means, that your view does not have an id, width, height, gravity nor does it have padding.
Either add the attributes to where you are using your custom view like so:
<com.classdojo.android.nessie.NessieButton
android:id="#+id/root_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:padding="#dimen/item_spacing_normal"/>
Or, if these attributes should be the same for all NessieButton's, set these properties programatically. Inside your init() methods sounds like the best place.
Be careful with the android:id as well! You probably do not want to set it programatically. But you will almost certainly want to set it, when you actually use your NessieButton.
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