Android 自定义加载动画LoadingView

Posted 惜许

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 自定义加载动画LoadingView相关的知识,希望对你有一定的参考价值。

前言

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

最终效果

整体思路

绘制部分分析:

  • 整体加载动画由三部分组成:
    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一点点进行拆分成每一步的实现,就会变的十分容易,同时通过这个例子体会到属性动画的重要性,它真的是无所不能,无处不在。

结语

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

UITableView 的自定义加载动画

【中文标题】UITableView 的自定义加载动画【英文标题】:Custom loading animation for UITableView 【发布时间】:2014-10-16 17:41:06 【问题描述】:

我正在尝试创建一个 UITableViewController,其中 tableview 上的加载动画应该使项目从侧面滑入。单元格的高度比默认单元格高,大约大 5-7 倍。我想要一个像 Google+ 加载动画这样的动画,可以在 android 上看到。 (卡片动画)。

我想是继承 TableView 类,重写一个加载方法,当它们被引入显示器时显示新项目,然后使用弹簧动画同时旋转单元格,同时它的 x 和 y 值逐渐改变.

我在this answer 中尝试过这段代码,但并没有完全发挥作用。对该答案的详细说明和解释也将非常有帮助。代码:

- (void)animate

[[self.tableView visibleCells] enumerateObjectsUsingBlock:^(UITableViewCell *cell, NSUInteger idx, BOOL *stop) 
    [cell setFrame:CGRectMake(320, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)];
    [UIView animateWithDuration:1 animations:^
        [cell setFrame:CGRectMake(0, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)];
    ];
];

任何帮助将不胜感激! 埃里克

【问题讨论】:

【参考方案1】:

这是一个功能齐全的示例(我相信)您所描述的内容。我打开了一个空的单视图项目,删除了情节提要中的视图控件,然后简单地创建了一个 UITableViewController 并将其类设置为我在下面创建的 UITableViewController 子类的类。

头文件中没有添加任何内容,所以我将其排除在外

#import "weeeTables.h"

@interface weeeTables ()


@property (nonatomic, strong) NSMutableSet *shownIndexes;
@property (nonatomic, assign) CATransform3D initialTransform;

@end

shownIndexes 将包含已显示视图的索引,因此我们不会重复向上滚动的动画。 initialTransform 属性是单元格初始状态的转换(在它出现在屏幕上之前你想要它的位置)

@implementation weeeTables

- (void)viewDidLoad 
[super viewDidLoad];

CGFloat rotationAngleDegrees = -30;
CGFloat rotationAngleRadians = rotationAngleDegrees * (M_PI/180);
CGPoint offsetPositioning = CGPointMake(-20, -20.0);

CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DRotate(transform, rotationAngleRadians, -50.0, 0.0, 1.0);
transform = CATransform3DTranslate(transform, offsetPositioning.x, offsetPositioning.y, -50.0);
_initialTransform = transform;

_shownIndexes = [NSMutableSet set];

在 ViewDidLoad 中,我们设置了初始变换的值,我对这些值进行了一些调整,并鼓励您这样做以获得您正在寻找的效果,但单元格从左下角开始动画目前。

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 
return 1;


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
return 20;


- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

return 385;





- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"weeCell" forIndexPath:indexPath];


return cell;

上面块中的所有内容对于 UITableViewController 子类来说都是非常典型的,并且与添加动画没有特别的关系,我只是设置了一些静态数字来添加大量要滚动的单元格,这里唯一的唯一值是单元格标识符,如您所见,它有一个经过深思熟虑的名称。

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell  forRowAtIndexPath:(NSIndexPath *)indexPath


if (![self.shownIndexes containsObject:indexPath]) 

    [self.shownIndexes addObject:indexPath];
    UIView *weeeeCell = [cell contentView];

    weeeeCell.layer.transform = self.initialTransform;
    weeeeCell.layer.opacity = 0.8;

    [UIView animateWithDuration:.65 delay:0.0 usingSpringWithDamping:.85 initialSpringVelocity:.8 options:0 animations:^
        weeeeCell.layer.transform = CATransform3DIdentity;
        weeeeCell.layer.opacity = 1;
     completion:^(BOOL finished) ];


@end

这是真正将所有内容放在一起的人,首先我们检查我们要显示的单元格是否已经在显示索引列表中,如果不是,我们添加它,然后创建一个局部变量当前单元格内容视图。

然后将该 contentView 上的转换设置为我们的初始转换(我们在 ViewDidLoad 中设置)。这会在用户可见之前将单元格移到我们的起始位置,然后我们将该变换动画回身份变换。我也有不透明的地方,只是为了它。

希望对你有帮助!

【讨论】:

目前在学校,今晚试试;) 像魅力一样工作 :) 有什么办法可以改变这个动画吗?有没有其他选择?我看到我们设置了 CATransform3DIdentity,但我可以将其更改为其他会产生不同动画的东西吗? 当然,这只是我在此示例中选择的路线,因为它具有灵活性。主要思想是一个两步过程,将单元格(或者在这种情况下,单元格内部的内容视图)放置在您想要开始动画的位置,然后将该单元格设置为动画位置。这两个步骤都将在 willDisplayCell 方法中完成。 @james_womack 的回应,这也是将单元格设置为动画位置的一种完全有效的方式。他只是将整个单元格框架移出屏幕,然后使用 UIView 动画 api 将其设置为动画。 你好,我的代码有个小问题。在动画完全完成之前,我无法点击任何行。这意味着用户可能需要多次点击它——这很烦人。关于如何解决这个问题的任何想法? 晚了两年,但我想这回答了你的问题@Erik,你可以使用UIViewAnimationOptionAllowUserInteraction 作为动画中的选项。希望这对某人有所帮助。【参考方案2】:

我会尝试使用tableView:willDisplayCell:forRowAtIndexPath:

您可以将 UITableViewCell 设置为初始的离屏帧,然后异步初始化该单元格的动画。

我已经对此进行了测试,它使用 Xcode Master/Detail 模板工作

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) 
        let frame = CGRectMake(-cell.frame.size.width, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)
        cell.frame = frame

        UIView.animateWithDuration(1, delay: 0.1, options: UIViewAnimationOptions.CurveEaseInOut, animations:  () -> Void in
            let endFrame = CGRectMake(100, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)
            cell.frame = endFrame
        )  (foo:Bool) -> Void in
            //
        
    

您只需要为不同的演示案例添加特殊的外壳。有时,当您再次返回视图时,或者在单元格从视图中滚动并重新打开后,将调用此方法。

【讨论】:

以上是关于Android 自定义加载动画LoadingView的主要内容,如果未能解决你的问题,请参考以下文章

Android自定义View实战之仿百度加载动画,一种优雅的Loading方式

为啥自定义进度条在android中没有动画?

Android技术分享| 自习室自定义View代替通知动画

Android技术分享| 自习室自定义View代替通知动画

自定义View——仿爱奇艺加载动画...

自定义View——仿爱奇艺加载动画...