1. 说明
我们前边讲解了MD之后,那么从这节课开始我们就开始我们新的内容 - 属性动画。那么关于动画我在前边已经讲解了我们常用的一些动画,不太清楚的可以看下。那么我们今天所要讲解的一个效果其实很简单,就是一个早期的58同城数据加载动画,一般会用于我们加载一个ListView、RecyclerView的列表数据之前的一个动画,当数据还没加载成功时候显示动画,当数据加载成功之后隐藏动画,效果如下:
2. 分析
那么我们现在就来分析下,如何实现这个效果,其实只要分析好了过程,也没有那么难。分解图片如下:
2.1 加载显示组合控件,即就是一个布局文件;
布局文件分为3个部分 = ShapeView + View(阴影背景 shape = "oval" 表示椭圆) + TextView(固定文字)
自定义一个 LoadingView,将这个布局文件加载到自定义View中,动态添加 布局文件到 自定义View中有2种方式;
第一种:
View loadView = inflate(getContext(),R.layout.ui_loading_view ,null);
addView(loadView) ;
第二种:
inflate(getContext(),R.layout.ui_loading_view ,this);
此处的this就表示 把ui_loading_view加载到 LoadingView中
2.2 分析实现动画;
2.3 实现形状切换的view;
千万不要直接把ui_loading_view布局文件直接使用include引入到要加载数据的页面,因为这样的话你需要去findViewById()去找到很多控件然后也需要写很多代码,工作量很大,所以在这里我们需要自定义 View,在需要使用的时候我们去用就行。
3. 代码如下
3.1 ShapeView.class —— 3个动画不断切换代码如下:
/**
* Email [email protected]
* Created by JackChen on 2018/1/26.
* Version 1.0
* Description: 3个形状不断切换
*/
public class ShapeView extends View {
private Shape mCurrentShape = Shape.Circle;
Paint mPaint;
private Path mPath;
public ShapeView(Context context) {
this(context, null);
}
public ShapeView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ShapeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 只保证是正方形
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(Math.min(width, height), Math.min(width, height));
}
@Override
protected void onDraw(Canvas canvas) {
switch (mCurrentShape) {
case Circle:
// 画圆形
int center = getWidth() / 2;
mPaint.setColor(ContextCompat.getColor(getContext(),R.color.circle));
canvas.drawCircle(center, center, center, mPaint);
break;
case Square:
// 画正方形
mPaint.setColor(ContextCompat.getColor(getContext(),R.color.rect));
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
break;
case Triangle:
// 画三角 Path 画路线
mPaint.setColor(ContextCompat.getColor(getContext(),R.color.triangle));
if (mPath == null) {
// 画路径
mPath = new Path();
mPath.moveTo(getWidth() / 2, 0);
mPath.lineTo(0, (float) ((getWidth()/2)*Math.sqrt(3)));
mPath.lineTo(getWidth(), (float) ((getWidth()/2)*Math.sqrt(3)));
// path.lineTo(getWidth()/2,0);
mPath.close();// 把路径闭合
}
canvas.drawPath(mPath, mPaint);
break;
}
}
/**
* 改变形状
*/
public void exchange() {
switch (mCurrentShape) {
case Circle:
mCurrentShape = Shape.Square;
break;
case Square:
mCurrentShape = Shape.Triangle;
break;
case Triangle:
// 画三角 Path 画路线
mCurrentShape = Shape.Circle;
break;
}
// 不断重新绘制形状 如果是圆就绘制成正方形;如果是正方形就绘制成三角;如果是三角形就绘制成圆
invalidate();
}
public enum Shape {
Circle, Square, Triangle
}
public Shape getCurrentShape() {
return mCurrentShape;
}
}
3.2 LoadingView.class ——仿58同城数据加载动画,代码如下:
/**
* Created by JackChen on 2018/3/7.
* Decription : 仿58同城数据加载动画
*/
public class LoadingView extends LinearLayout {
// 上边的形状
private ShapeView mShapeView;
// 中间的阴影
private View mShadowView;
// 下落的高度
private int mTranslationDistance ;
// 动画执行的时间
private final long ANIMATOR_DURATION = 350;
// 是否停止动画
private boolean mIsStopAnimator = false;
public LoadingView(Context context) {
this(context,null);
}
public LoadingView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public LoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTranslationDistance = dp2px(80) ;
// 初始化加载布局
initLayout() ;
}
/**
* dp - px
* @param dip
* @return
*/
private int dp2px(int dip) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP , dip , getResources().getDisplayMetrics());
}
/**
* 初始化加载布局
*/
private void initLayout() {
// 1. 加载写好的 ui_loading_view
// 1.1 实例化view
// View loadView = inflate(getContext() , R.layout.ui_loading_view , null) ;
// // 1.2 添加到该View中
// addView(loadView);
// 找一下 插件式换肤资源加载那一节内容
// this就代表把 ui_loading_view 加载到 LoadingView中
inflate(getContext() , R.layout.ui_loading_view , this) ;
mShapeView = (ShapeView) findViewById(R.id.shape_view);
mShadowView = (View) findViewById(R.id.shadow_view);
post(new Runnable() {
@Override
public void run() {
// onResume()之后 view绘制流程执行完毕之后 开始动画
startFallAnimation() ;
}
}) ;
}
/**
* 开始下落动画
*/
private void startFallAnimation() {
// 如果动画正在进行,就让其停止
if (mIsStopAnimator){
return;
}
// 下落的位移动画 translationY表示位移动画 Y方向
// 参数一:动画作用在谁身上 参数二:动画名称 参数三:起点位置 参数四:高度
ObjectAnimator translationAnimator = ObjectAnimator.ofFloat(mShapeView , "translationY", 0 , mTranslationDistance) ;
// 配合中间阴影缩小 scaleX表示缩放动画 X方向
ObjectAnimator scaleAnimatator = ObjectAnimator.ofFloat(mShadowView , "scaleX" , 1f , 0.3f) ;
AnimatorSet animatorSet = new AnimatorSet();
// 时长
animatorSet.setDuration(ANIMATOR_DURATION);
// 下落的速度应该是越来越快
animatorSet.setInterpolator(new AccelerateInterpolator());
// 多个动画一起执行
animatorSet.playTogether(translationAnimator , scaleAnimatator);
// 按顺序执行 先执行translationAnimator 后执行scaleAnimatator
// animatorSet.playSequentially(translationAnimator , scaleAnimatator);
animatorSet.start();
// 下落完之后就开始上抛,监听动画执行完毕
animatorSet.addListener(new AnimatorListenerAdapter() {
// 动画执行完之后
@Override
public void onAnimationEnd(Animator animation) {
// super.onAnimationEnd(animation);
// 改变形状
mShapeView.exchange();
// 下落完之后就开始上抛了
startUpAnimator() ;
}
});
}
/**
* 开始执行上抛动画
*/
private void startUpAnimator() {
// 如果动画正在进行,就让其停止
if(mIsStopAnimator){
return;
}
// 下落的位移动画 translationY 位移动画 Y方向
//上抛动画
ObjectAnimator transtationAnimator = ObjectAnimator.ofFloat(mShapeView , "translationY" , mTranslationDistance , 0) ;
// 配合中间阴影放大
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(mShadowView , "scaleX" , 0.3f , 1f) ;
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(ANIMATOR_DURATION) ;
// 上抛的时候越来越慢
animatorSet.setInterpolator(new DecelerateInterpolator()); //差值器
animatorSet.playTogether(transtationAnimator , scaleAnimator);
// 上抛完之后就开始下落,监听动画执行完毕
animatorSet.addListener(new AnimatorListenerAdapter() {
// 动画执行完毕
@Override
public void onAnimationEnd(Animator animation) {
// 上抛完之后就开始下落了
startFallAnimation();
}
// 动画开始执行
@Override
public void onAnimationStart(Animator animation) {
// 开始旋转
startRotationAnimator() ;
}
});
// 开始动画
animatorSet.start();
}
private void startRotationAnimator() {
ObjectAnimator rotationAnimator = null ;
switch (mShapeView.getCurrentShape()){
case Circle:
case Square:
// 180
rotationAnimator = ObjectAnimator.ofFloat(mShapeView,"rotation",0,180);
break;
case Triangle:
// 120
rotationAnimator = ObjectAnimator.ofFloat(mShapeView,"rotation",0,-120);
break;
}
rotationAnimator.setDuration(ANIMATOR_DURATION) ;
rotationAnimator.setInterpolator(new DecelerateInterpolator());
rotationAnimator.start();
}
// 设置控件的隐藏
@Override
public void setVisibility(int visibility) {
super.setVisibility(View.INVISIBLE); //不要再去摆放和计算,少走一些系统的源码, 参考View的绘制流程
// 清理动画
mShapeView.clearAnimation();
mShadowView.clearAnimation();
// 把LoadingView从父布局中移除
ViewGroup parent = (ViewGroup) getParent();
if (parent != null){
parent.removeView(this);
removeAllViews();
}
mIsStopAnimator=true;
}
}
3.3 ui_loading_view.xml——布局文件如下:
具体使用,可直接在activity_main.xml文件中直接引用 LoadingView即可,activity_main.xml布局文件如下:
就是这样,代码拷贝进去可直接使用,这里在MainActivity中测试,让其显示3秒,3秒后可让其隐藏,然后让自己对应的 ListView或RecyclerView的数据显示即可;
public class MainActivity extends AppCompatActivity {
private LoadingView loading_view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loading_view = (LoadingView) findViewById(R.id.loading_view);
// 3秒之后让动画隐藏
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
loading_view.setVisibility(View.GONE);
}
} , 3000) ;
}
}
4. 注意
在这里一定要注意:
在自定义View的LoadingView中的最后这段代码:
/**
* 针对于动画涉及到内存泄露的处理情况
* @param visibility
*/
@Override
public void setVisibility(int visibility) {
// 不要再去摆放和计算,少走一些系统源码
super.setVisibility(View.INVISIBLE);
// 清理动画
mLeftView.clearAnimation();
mMiddleView.clearAnimation();
mRightView.clearAnimation();
// 把LoadingView从父布局中移除
ViewGroup parent = (ViewGroup) getParent();
if (parent != null){
parent.removeView(this);
removeAllViews();
}
}
这段代码意思就是 :
凡是动画,都可能会涉及到内存泄露,这个时候采用这种方式去清除动画,可以解决内存泄露问题,总共分为3步:
4.1 首先清除所有的子View的动画,这里有3个子View,分别为mLeftView、mMiddleView、mRightView;
4.2 获取父布局,并判断如果父布局如果不为空,首先移除父布局中的子View,最后移除所有的View即可。
这里的处理和花束直播数据加载效果 处理方式是一样的
具体代码已上传至github:
https://github.com/shuai999/View_day21.git