自定义view-58同城数据加载动画

95 阅读3分钟

学习来源地址

一、效果展示

ezgif.com-video-to-gif.gif

自定义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>