Android : Animate Rating Bar to expand and shrink each star

Viewed 275

I have a rating bar that shows 5 stars that I want to animate by filing from 0 to 5 depending on the value of the rating passed into the view and for each star to expand in size and then shrink back to its original size per star that has been colored/filled in.

For example, all 4 stars out of 5 should expand and shrink back down on a star rating of 3.5 as an example.

I have got it to fill from 0 to 3.5 fine with the code below but have no idea how to expand each star that has some filled applied to it?

Is this possible?

Here is what I have so far:

ObjectAnimator.ofFloat(starRating, "rating", 0f, 3.5f).apply {
    duration = animDuration
    addListener(onEachStarFilled?/onEachFrame?? = {
        //todo expand each star? how?
    })
    start()
}
2 Answers

Add a container to hold five image view with default image.

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:id="@+id/start_ratting"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent">
    <ImageView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@android:color/transparent"
        android:scaleType="fitCenter"
        android:src="@drawable/empty"
        android:id="@+id/star_1"/>
    <ImageView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@android:color/transparent"
        android:src="@drawable/empty"
        android:scaleType="fitCenter"
        android:id="@+id/star_2"/>
    <ImageView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@android:color/transparent"
        android:scaleType="fitCenter"
        android:src="@drawable/empty"
        android:id="@+id/star_3"/>
    <ImageView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:background="@android:color/transparent"
        android:scaleType="fitCenter"
        android:src="@drawable/empty"
        android:id="@+id/star_4"/>
    <ImageView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:src="@drawable/empty"
        android:background="@android:color/transparent"
        android:scaleType="fitCenter"
        android:id="@+id/star_5"/>
</LinearLayout>

Then add animator

private fun expandHorizontal(v: ImageView, image: Drawable) {
    val width: Int = v.width
    v.visibility = View.VISIBLE
    v.layoutParams.width = 0
    v.setImageDrawable(image)
    val valueAnimator = ValueAnimator.ofInt(0, width)
    valueAnimator.addUpdateListener { animation ->
        v.layoutParams.width = animation.animatedValue as Int
        v.requestLayout()
    }
    valueAnimator.interpolator = DecelerateInterpolator()
    valueAnimator.duration = VIEW_TRANSITION_TIME
    valueAnimator.start()
}

Call set star value when ever needed

 private fun setStarValue(ratting: Double){
    star1.visibility = View.INVISIBLE
    star2.visibility = View.INVISIBLE
    star3.visibility = View.INVISIBLE
    star4.visibility = View.INVISIBLE
    star5.visibility = View.INVISIBLE
    var i = 0
    for(i in 0..5){
        when(i){
            0->{
                val res = ratting - i;
                if(res >= 1){
                    Log.d(TAG, "start 1")
                    getDrawable(R.drawable.full)?.let { expandHorizontal(star1, it) }
                }
                else if(res > 0 && res < 1){
                    Log.d(TAG, "start half")
                    getDrawable(R.drawable.half)?.let { expandHorizontal(star1, it) }
                }
                else{
                    Log.d(TAG, "no start")
                    getDrawable(R.drawable.empty)?.let { expandHorizontal(star1, it) }
                }
            }
            1 -> {
                val res = ratting - i;
                if(res >= 1){
                    Log.d(TAG, "start 2")
                    getDrawable(R.drawable.full)?.let { expandHorizontal(star2, it) }
                }
                else if(res > 0 && res < 1){
                    Log.d(TAG, "start two and half")
                    getDrawable(R.drawable.half)?.let { expandHorizontal(star2, it) }
                }
                else{
                    Log.d(TAG, "no start")
                    getDrawable(R.drawable.empty)?.let { expandHorizontal(star2, it) }
                }
            }
            2->{
                val res = ratting - i;
                if(res >= 1){
                    Log.d(TAG, "start 3")
                    getDrawable(R.drawable.full)?.let { expandHorizontal(star3, it) }
                }
                else if(res > 0 && res < 1){
                    Log.d(TAG, "start three and half")
                    getDrawable(R.drawable.half)?.let { expandHorizontal(star3, it) }
                }
                else{
                    Log.d(TAG, "no start")
                    getDrawable(R.drawable.empty)?.let { expandHorizontal(star3, it) }
                }
            }
            3 -> {
                val res = ratting - i;
                if(res >= 1){
                    Log.d(TAG, "start 4")
                    getDrawable(R.drawable.full)?.let { expandHorizontal(star4, it) }
                }
                else if(res > 0 && res < 1){
                    Log.d(TAG, "start 4 and half")
                    getDrawable(R.drawable.half)?.let { expandHorizontal(star4, it) }
                }
                else{
                    Log.d(TAG, "no start")
                    getDrawable(R.drawable.empty)?.let { expandHorizontal(star4, it) }
                }
            }
            4 ->{
                val res = ratting - i;
                if(res >= 1){
                    Log.d(TAG, "start 5")
                    getDrawable(R.drawable.full)?.let { expandHorizontal(star5, it) }
                }
                else if(res > 0 && res < 1){
                    Log.d(TAG, "start three and half")
                    getDrawable(R.drawable.half)?.let { expandHorizontal(star5, it) }
                }
                else{
                    Log.d(TAG, "no start")
                    getDrawable(R.drawable.empty)?.let { expandHorizontal(star5, it) }
                }
            }
        }
    }
}

You can chose animation in expandHorizontal

enter image description here

Here is a little hackish way you can do it . A layer with background color appears on top of the stars you want to hide . I tried changing size of rating bar but the rating shows up wrong .

activity_main.xml

<FrameLayout
    android:layout_width="wrap_content"
    android:layout_height="60dp"
    android:layout_marginTop="20dp">

    <androidx.appcompat.widget.AppCompatRatingBar
        android:id="@+id/ratingBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:isIndicator="true"
        android:rating="3.5"
        android:stepSize="0.1" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="#ffffff"
        android:id="@+id/hideView"
        android:orientation="horizontal"
        android:layout_gravity="end"/>

</FrameLayout>


<Button
    android:id="@+id/button3_5Stars"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="20dp"
    android:text="Set rating to 3.5 star" />

</LinearLayout>

MainActivity.kt

package com.example.test

import android.os.Bundle
import android.widget.Button
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatRatingBar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.ceil

class MainActivity : AppCompatActivity() {
    var oneStarWidth:Int?=null
    lateinit var ratingBar: AppCompatRatingBar
    lateinit var hideView: LinearLayout
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ratingBar=findViewById(R.id.ratingBar)
        hideView=findViewById(R.id.hideView)
        getSingleStarWidth()
        findViewById<Button>(R.id.button3_5Stars).setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                ratingBar.rating=3.5F
                expandTogivenRating(ratingBar.rating)
                delay(1000)
                collapseToSingleStar()
            }
        }
    }


    fun getSingleStarWidth(){
        if(oneStarWidth==null){
            ratingBar.post {
                oneStarWidth=ratingBar.width/5
                collapseToSingleStar()
            }
        }
    }

    fun collapseToSingleStar(){
        expandTogivenRating(1.0f)
    }

    fun expandTogivenRating(rating:Float){

        val ratingInt= ceil(rating.toDouble())

        val layoutParams=hideView.layoutParams
        layoutParams.width= ((5- ratingInt) * (oneStarWidth?:0)).toInt()
        hideView.layoutParams=layoutParams

    }
}

Output:-

enter image description here

You can to improve the naming , animation , handle the disposable from coroutines . This should give you a rough idea .

Related