Android 自定义加载动画LoadingView

前言

本文参考辉哥的博客属性动画 - 58同城数据加载动画,用来学习属性动画相关知识非常合适。

最终效果

LoadingView加载动画

整体思路

绘制部分分析:

  • 整体加载动画由三部分组成:
    1.上方的正方形、圆形以及三角形,需要进行旋转以及下落动画;
    2.中间的阴影部分,需要进行缩放;
    3.最下方的文字;

  • 针对上方的形状变化我们先自定义一个ShapeView,主要绘制正方形、圆形、三角形,同时支持形状切换;

  • 针对中间阴影部分,我们就通过给View设置背景,然后进行x轴缩放即可;

动画部分分析:

  • 一进来先进行下落动画,同时伴随着阴影部分缩小动画;
  • 下落结束后进行上升动画,阴影部分进行放大动画,同时ShapeView进行旋转动画;
  • 上升结束后继续进行下落动画,如此反复,即可实现效果。

具体实现

将整体思路理清楚,我们便可以开始自定义我们的View

  • 自定义ShapeView
package com.crystal.view.animation
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import kotlin.math.sqrt

/**
* 圆、三角形、正方形变换
* on 2022/11/9
*/
class ShapeView : View {
   /**
    * 画笔
    */
   private var paint: Paint = Paint()

   /**
    * 圆形颜色
    */
   private val CIRCLE_COLOR = Color.parseColor("#AA738FFE")

   /**
    * 正方形颜色
    */
   private val SQUARE_COLOR = Color.parseColor("#AAE84E40")

   /**
    * 三角形颜色
    */
   private val TRIANGLE_COLOR = Color.parseColor("#AA72D572")

   /**
    * 当前形状
    */
   private var currentShape = Shape.CIRCLE

   /**
    * 正方形Rect
    */
   private var squareRect = Rect()

   /**
    * 三角形path
    */
   private var path = Path()


   constructor(context: Context) : this(context, null)
   constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
   constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
       context, attrs, defStyleAttr
   ) {
       paint.isAntiAlias = true
       paint.style = Paint.Style.FILL_AND_STROKE
       paint.isDither = true
   }

   override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec)
       //设置宽高始终一致
       var widthSize = MeasureSpec.getSize(widthMeasureSpec)
       var heightSize = MeasureSpec.getSize(heightMeasureSpec)
       if (widthSize > heightSize) {
           widthSize = heightSize
       } else {
           heightSize = widthSize
       }
       setMeasuredDimension(widthSize, heightSize)
   }

   override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
       super.onLayout(changed, left, top, right, bottom)
       //设置正方形rect
       squareRect.left = 0
       squareRect.top = 0
       squareRect.right = width
       squareRect.bottom = height

   }


   override fun onDraw(canvas: Canvas) {
       when (currentShape) {
           Shape.SQUARE -> {
               paint.color = SQUARE_COLOR
               canvas.drawRect(squareRect, paint)
           }
           Shape.CIRCLE -> {
               paint.color = CIRCLE_COLOR
               canvas.drawCircle(width / 2f, height / 2f, width / 2f, paint)
           }

           Shape.TRIANGLE -> {
               paint.color = TRIANGLE_COLOR
               path.moveTo(width / 2f, 0f)
               path.lineTo(0f, (sqrt(3.0) * width / 2f).toFloat())
               path.lineTo(width.toFloat(), (sqrt(3.0) * width / 2f).toFloat())
               path.close()
               canvas.drawPath(path, paint)
           }
       }
   }

   fun exchangeShape() {
       currentShape = when (currentShape) {
           Shape.CIRCLE -> {
               Shape.SQUARE
           }
           Shape.SQUARE -> {
               Shape.TRIANGLE
           }
           Shape.TRIANGLE -> {
               Shape.CIRCLE
           }
       }
       postInvalidate()
   }

   /**
    * 获取当前形状
    */
   fun getCurrentShape(): Shape {
       return currentShape
   }

   enum class Shape {
       CIRCLE, SQUARE, TRIANGLE
   }

}
  • 自定义最终的LoadingView
  1. LoadingView布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!--上方形状-->
    <com.crystal.view.animation.ShapeView
        android:id="@+id/shape_view"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_marginBottom="82dp"
        app:layout_constraintBottom_toTopOf="@+id/shadow_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

    <View
        android:id="@+id/shadow_view"
        android:layout_width="40dp"
        android:layout_height="5dp"
        android:background="@drawable/shape_shadow_bg"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="玩命加载中..."
        android:textSize="16sp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/shadow_view" />
</androidx.constraintlayout.widget.ConstraintLayout>

2.阴影shape文件

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
   android:shape="oval">
   <solid android:color="#33000000" />
</shape>

3.自定义最终的LoadingView

package com.crystal.view.animation

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import com.crystal.view.R

/**
 * 自定义加载动画
 * on 2022/11/9
 */
class LoadingView : LinearLayout {
    /**
     * 上方形状
     */
    private var shapeView: ShapeView

    /**
     * 下方阴影缩放
     */
    private var shadowView: View

    /**
     * 下落高度
     */
    private var translationYValue = 0f

    /**
     * 动画时长
     */
    private val DURATION_TIME = 500L

    /**
     * 用于判断动画是否已停止
     */
    private var isStopLoading = false

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context, attrs, defStyleAttr
    ) {
        //就布局添加到当前ViewGroup中
        inflate(getContext(), R.layout.layout_loading_view, this)
        shapeView = findViewById(R.id.shape_view)
        shadowView = findViewById(R.id.shadow_view)
        translationYValue =
            TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 80f, resources.displayMetrics)
        post {
            //1.进来就先下落,同时阴影进行缩放,当view绘制完成后开启动画
            downAnimation()
        }
    }

    private fun downAnimation() {
        if (isStopLoading) {
            return
        }
        //shapeView 下降
        val shapeViewAnimation =
            ObjectAnimator.ofFloat(shapeView, "translationY", 0f, translationYValue)
        //shadowView缩小
        val shadowViewAnimation = ObjectAnimator.ofFloat(shadowView, "scaleX", 1f, 0.3f)
        val downAnimatorSet = AnimatorSet()
        downAnimatorSet.playTogether(shapeViewAnimation, shadowViewAnimation)
        downAnimatorSet.duration = DURATION_TIME
        downAnimatorSet.start()
        downAnimatorSet.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                //监听动画结束后,进行上升操作,同时伴随着shapeView旋转
                upAnimation()
            }
        })
    }

    /**
     * 向上动画
     */
    private fun upAnimation() {
        //shapeView上升
        val shapeViewAnimation =
            ObjectAnimator.ofFloat(shapeView, "translationY", translationYValue, 0f)
        //shadowView放大
        val shadowViewAnimation = ObjectAnimator.ofFloat(shadowView, "scaleX", 0.3f, 1f)
        shapeView.exchangeShape()
        val upAnimatorSet = AnimatorSet()
        upAnimatorSet.playTogether(shapeViewAnimation, shadowViewAnimation, remoteShapeView())
        upAnimatorSet.duration = DURATION_TIME
        upAnimatorSet.start()
        upAnimatorSet.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                //监听动画结束后,进行下降操作
                downAnimation()
            }
        })
    }

    /**
     * shapeView旋转动画
     */
    private fun remoteShapeView(): ObjectAnimator {
        return when (shapeView.getCurrentShape()) {
            ShapeView.Shape.CIRCLE, ShapeView.Shape.SQUARE -> {
                //旋转180度
                ObjectAnimator.ofFloat(shapeView, "rotation", 0f, 180f)
            }
            else -> {
                //旋转120度
                ObjectAnimator.ofFloat(shapeView, "rotation", 0f, 120f)
            }
        }

    }

    fun stopAnimation() {
        isStopLoading = true
        //清除动画
        shapeView.clearAnimation()
        shadowView.clearAnimation()
        if (parent != null) {
            //移除当前View
            (parent as ViewGroup).removeView(this)
            removeAllViews()
        }
    }


}

总结

自定义View看似很难,其实将View一点点进行拆分成每一步的实现,就会变的十分容易,同时通过这个例子体会到属性动画的重要性,它真的是无所不能,无处不在。

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android中实现过程动画可以使用ProgressBar或者自定义View来实现。以下是两种实现方式: 1. 使用ProgressBar ProgressBar是Android系统自带的控件,可以实现过程动画。可以通过以下代码实现: ``` <ProgressBar android:id="@+id/progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:indeterminate="true" /> ``` 其中,android:indeterminate="true"表示ProgressBar是一个不确定进度的动画。 2. 自定义View 可以通过自定义View来实现更个性化的过程动画。以下是一个简单的示例代码: ``` public class LoadingView extends View { private Paint mPaint; private RectF mRectF; private float mStartAngle = 0; private float mSweepAngle = 45; private int mWidth; private int mHeight; public LoadingView(Context context) { super(context); init(); } public LoadingView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public LoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mPaint = new Paint(); mPaint.setColor(Color.RED); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(10); mPaint.setAntiAlias(true); mRectF = new RectF(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mRectF.set(0, 0, mWidth, mHeight); canvas.drawArc(mRectF, mStartAngle, mSweepAngle, false, mPaint); mStartAngle += 5; if (mStartAngle >= 360) { mStartAngle = 0; } postInvalidateDelayed(10); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = MeasureSpec.getSize(widthMeasureSpec); mHeight = MeasureSpec.getSize(heightMeasureSpec); } } ``` 该自定义View会绘制一个旋转的圆弧,可以通过改变mStartAngle和mSweepAngle的值来改变动画效果。在使用时,直接将该View入布局即可: ``` <com.example.myapplication.LoadingView android:layout_width="100dp" android:layout_height="100dp" android:layout_centerInParent="true" /> ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值