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
- 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的主要内容,如果未能解决你的问题,请参考以下文章