一、效果展示
自定义view组成:
1.可变化的图形(等边三角形/正方形/圆形)
2.可缩放的阴影
3.固定文本TextView
动画分析:
1.图形下落时,阴影缩小,并且速度越来越快,下落完毕后,执行上抛动画。
2.图形上抛时,阴影放大,并且速度越来越慢,上抛完毕后,执行下落动画。
3.图形旋转:正方形旋转180度,三角形120 (在图形下落完毕后,执行图形变化)
注意:当视图不可见时,应当停止动画。
二、代码实现
1.自定义可变化的图片view
public class ShapeChangeView extends View {
/** 当前形状*/
private Shape mCurrentShape = Shape.CIRCLE;
private Paint mPaint;
private Path mPath;
public ShapeChangeView(Context context) {
this(context, null);
}
public ShapeChangeView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ShapeChangeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPath = new Path();
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);
// 取宽高的最小值,作为view的尺寸
setMeasuredDimension(Math.min(width, height), Math.min(width, height));
}
@Override
protected void onDraw(Canvas canvas) {
switch (mCurrentShape) {
case CIRCLE:
// 1.画圆形
int center = getWidth() / 2;
mPaint.setColor(ContextCompat.getColor(getContext(), R.color.circle));
canvas.drawCircle(center, center, center, mPaint);
break;
case SQUARE:
// 2.正方形
mPaint.setColor(ContextCompat.getColor(getContext(), R.color.rect));
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
break;
case TRIANGLE:
// 3.等边三角形
mPaint.setColor(ContextCompat.getColor(getContext(), R.color.triangle));
// 正方形的上边的中点
mPath.moveTo(getWidth() * 0.5f, 0);
double radian = Math.toRadians(60);
int height = (int) (Math.sin(radian) * getWidth());
mPath.lineTo(getWidth(), height);
mPath.lineTo(0, height);
// 闭合
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:
mCurrentShape = Shape.CIRCLE;
break;
default:
break;
}
// 设置试图无效,重新绘制
invalidate();
}
public Shape getCurrentShape() {
return mCurrentShape;
}
public enum Shape {
CIRCLE, SQUARE, TRIANGLE;
}
}
2.LoadingView
public class LoadingView extends LinearLayout {
private Context mContext;
/** 变化的形状View*/
private ShapeChangeView mShapeChangeView;
/** 阴影View*/
private ImageView mIvIndicator;
private int mTranslationDistance;
private final int mAnimatorDuration = 350;
private boolean mIsStop = 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);
mContext = context;
// xml布局里设置了mShapeChangeView距离底部阴影是82dp,所以这里可以设置80dp
mTranslationDistance = dip2px(80);
initLayout();
}
private void initLayout() {
inflate(mContext, R.layout.loading_view, this);
mShapeChangeView = findViewById(R.id.shape_change_view);
mIvIndicator = findViewById(R.id.iv_indication);
// 在onCreate()方法中执行,所以改用在post调用
//startFallAnimator();
post(new Runnable() {
@Override
public void run() {
// view绘制流程执行完毕之后执行
startFallAnimator();
}
});
}
private void startFallAnimator() {
// 经过测试,如果自定义view所在的页面关闭,即使调用了setVisibility方法里的清除逻辑,还是会继续执行动画,所以这里增加mIsStop拦截停止动画
if (mIsStop) {
return;
}
Log.d("loadingView", "startFallAnimator " + hashCode());
// 图形下落,速度逐渐变快
ObjectAnimator translationAnimator = ObjectAnimator.ofFloat(mShapeChangeView, "translationY", 0, mTranslationDistance);
// 阴影缩小
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(mIvIndicator, "scaleX", 1f, 0.3f);
AnimatorSet set = new AnimatorSet();
set.setDuration(mAnimatorDuration);
set.setInterpolator(new AccelerateInterpolator());
set.playTogether(translationAnimator, scaleAnimator);
// 监听动画执行完毕后,执行上抛动画
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mShapeChangeView.exchange();
startUpAnimator();
}
});
// TODO 需要在设置完监听后调用,否则监听onAnimationStart不会回调到
set.start();
}
private void startUpAnimator() {
if (mIsStop) {
return;
}
Log.d("loadingView", "startUpAnimator " + hashCode());
// 图形上抛,速度逐渐变慢
ObjectAnimator translationAnimator = ObjectAnimator.ofFloat(mShapeChangeView, "translationY", mTranslationDistance, 0);
// 阴影放大
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(mIvIndicator, "scaleX", 0.3f, 1f);
AnimatorSet set = new AnimatorSet();
set.setInterpolator(new DecelerateInterpolator());
set.setDuration(mAnimatorDuration);
set.playTogether(translationAnimator, scaleAnimator);
// 监听动画执行完毕后,执行下落动画
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
// 上抛时伴随旋转动画
startRotationAnimator();
}
@Override
public void onAnimationEnd(Animator animation) {
startFallAnimator();
}
});
// TODO 需要在设置完监听后调用,否则监听onAnimationStart不会回调到
set.start();
}
/***
* ShapeChangeView执行旋转动画
*/
private void startRotationAnimator() {
ObjectAnimator rotationAnimator = null;
switch (mShapeChangeView.getCurrentShape()) {
case SQUARE: {
// 旋转180
rotationAnimator = ObjectAnimator.ofFloat(mShapeChangeView, "rotation", 0, 180);
}
break;
case TRIANGLE: {
// 旋转120
rotationAnimator = ObjectAnimator.ofFloat(mShapeChangeView, "rotation", 0, -120);
}
break;
default:
break;
}
if (rotationAnimator != null) {
rotationAnimator.setDuration(mAnimatorDuration);
rotationAnimator.setInterpolator(new DecelerateInterpolator());
rotationAnimator.start();
}
}
@Override
public void setVisibility(int visibility) {
// 正常应该只显示一次,所以这里直接设置不可见
super.setVisibility(View.INVISIBLE); // INVISIBLE,可避免再次摆放和计算
mIsStop = true;
mShapeChangeView.clearAnimation();
mIvIndicator.clearAnimation();
// 把LoadingView从父布局移除
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) {
parent.removeView(this);
removeAllViews();
}
}
private int dip2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
}
xml布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<com.example.customviewapplication.customView.ShapeChangeView
android:id="@+id/shape_change_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="4dp" />
<ImageView
android:id="@+id/iv_indication"
android:layout_width="23dp"
android:layout_height="3dp"
android:layout_marginTop="82dp"
android:src="@drawable/shadow" />
<TextView
android:id="@+id/tv_prompt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:text="玩命加载中..."
android:textColor="#757575"
android:textSize="14sp" />
</LinearLayout>