Android进阶:9、自定义View之手写Loading动效

这是一个很简单的动画效果,使用属性动画便可实现,但愿对读者学习动画能达到抛砖引玉的效果java

一.自定义动画效果——Loading效果

如上是咱们须要作的一个Loading动画。Loading效果是很常见的一种动画,最简单的实现让设计画个动态图便可,或者画个静态图而后使用帧动画也能够实现。可是今天咱们用纯代码实现,不用任何图片资源。
canvas




大体思路
咱们自定义一个View,继承View类,而后画两个不一样半径的弧形,转动不一样的角度便可实现。

绘制两个不一样半径的弧形
首先初始化外圆和内园的Recf();性能优化

private RectF mOuterCircleRectF = new RectF();
    private RectF mInnerCircleRectF = new RectF();
复制代码

而后在onDraw方法绘制圆弧:架构

//获取View的中心
        float centerX = getWidth() / 2;
        float centerY = getHeight() / 2;

        if (lineWidth > centerX) {
            throw new IllegalArgumentException("lineWidth值太大了");
        }
        //外圆半径,由于咱们的弧形是有宽度的,因此计算半径的时候应该把这部分减去,否则会有切割的效果
        float outR = centerX - lineWidth;

        //小圆半径
        float inR = outR * 0.6f - lineWidth;
        //设置弧形的距离上下左右的距离,也就是包围园的矩形。
        mOuterCircleRectF.set(centerX - outR, centerY - outR, centerX + outR, centerY + outR);
        mInnerCircleRectF.set(centerX - inR, centerY - inR, centerX + inR, centerY + inR);
        //绘制外圆
        canvas.drawArc(mOuterCircleRectF, mRotateAngle % 360, OUTER_CIRCLE_ANGLE, false, mStrokePaint);
        //绘制内圆
        canvas.drawArc(mInnerCircleRectF, 270 - mRotateAngle % 360, INTER_CIRCLE_ANGLE, false, mStrokePaint);
复制代码

代码很简单,就像注释同样:ide

  • 获取整个loadView的宽高,而后计算loadview的中心
  • 利用中心计算外圆和内园的半径,由于圆弧的弧边有宽度,因此应该减去这部分宽度,否则上下左右会有被切割的效果。
  • 在Recf中设置以圆半径为边长的矩形
  • 在画布中以矩形的数据绘制圆弧便可,这里设置了角度,使圆形有缺角,只要不是360度的圆都是有缺角的。

绘制圆的过程应该放在onDraw方法中,这样咱们能够不断的重绘,也能够获取view的真实的宽高性能

固然,咱们还需设置一个画笔来画咱们的圆学习

mStrokePaint = new Paint();
        mStrokePaint.setStyle(Paint.Style.STROKE);
        mStrokePaint.setStrokeWidth(lineWidth);
        mStrokePaint.setColor(color);
        mStrokePaint.setAntiAlias(true);
        mStrokePaint.setStrokeCap(Paint.Cap.ROUND);
        mStrokePaint.setStrokeJoin(Paint.Join.ROUND);
复制代码

二.设置属性动画

圆弧画好了,而后利用属性动画便可实现动画效果。这里采用的是ValueAnimator,值属性动画,咱们能够设置一个值范围,而后让他在这个范围内变化。优化

mFloatValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        mFloatValueAnimator.setRepeatCount(Animation.INFINITE);
        mFloatValueAnimator.setDuration(ANIMATION_DURATION);
        mFloatValueAnimator.setStartDelay(ANIMATION_START_DELAY);
        mFloatValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
复制代码

这个设置很简单,设置值得范围,这是无线循环,设置动画执行的时间,这只动画循环时延迟的时间,设置插值器。动画

三.弧形动起来

让弧形动起来的原理,就是监听值属性动画的值变化,而后在这个变化的过程当中不断的改变弧形的角度,而后让它重绘便可。ui

咱们让咱们的loadview实现ValueAnimator.AnimatorUpdateListener接口,而后在onAnimationUpdate监听动画的变化。咱们初始化值属性动画的时候设置了值得范围为float型,因此这里能够获取这个变化的值。而后利用这个值能够改变绘制圆的角度大小,再调用重绘方法,便可实现:

@Override
    public void onAnimationUpdate(ValueAnimator animation) {
        mRotateAngle = 360 * (float)animation.getAnimatedValue();
        invalidate();
    }
复制代码

整个思路大体就是这样。完整代码以下:

public class LoadingView extends View implements Animatable, ValueAnimator.AnimatorUpdateListener {
    private static final long ANIMATION_START_DELAY = 200;
    private static final long ANIMATION_DURATION = 1000;
    private static final int OUTER_CIRCLE_ANGLE = 270;
    private static final int INTER_CIRCLE_ANGLE = 90;

    private ValueAnimator mFloatValueAnimator;
    private Paint mStrokePaint;
    private RectF mOuterCircleRectF;
    private RectF mInnerCircleRectF;

    private float mRotateAngle;


    public LoadingView (Context context) {
        this(context, null);
    }

    public LoadingView (Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public LoadingView (Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, -1);
    }

    public LoadingView (Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView(context, attrs);
    }

    float lineWidth;

    private void initView(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomLoadingView);
        lineWidth = typedArray.getFloat(R.styleable.MyCustomLoadingView_lineWidth, 10.0f);
        int color = typedArray.getColor(R.styleable.MyCustomLoadingView_viewColor, context.getColor(R.color.colorAccent));
        typedArray.recycle();
        initAnimators();
        mOuterCircleRectF = new RectF();
        mInnerCircleRectF = new RectF();
        //初始化画笔
        initPaint(lineWidth, color);
        //旋转角度
        mRotateAngle = 0;
    }

    private void initAnimators() {
        mFloatValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        mFloatValueAnimator.setRepeatCount(Animation.INFINITE);
        mFloatValueAnimator.setDuration(ANIMATION_DURATION);
        mFloatValueAnimator.setStartDelay(ANIMATION_START_DELAY);
        mFloatValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
    }

    /** * 初始化画笔 */
    private void initPaint(float lineWidth, int color) {
        mStrokePaint = new Paint();
        mStrokePaint.setStyle(Paint.Style.STROKE);
        mStrokePaint.setStrokeWidth(lineWidth);
        mStrokePaint.setColor(color);
        mStrokePaint.setAntiAlias(true);
        mStrokePaint.setStrokeCap(Paint.Cap.ROUND);
        mStrokePaint.setStrokeJoin(Paint.Join.ROUND);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float centerX = getWidth() / 2;
        float centerY = getHeight() / 2;

        //最大尺寸
        if (lineWidth > centerX) {
            throw new IllegalArgumentException("lineWidth值太大了");
        }
        float outR = centerX - lineWidth;
        //小圆尺寸
        float inR = outR * 0.6f;
        mOuterCircleRectF.set(centerX - outR, centerY - outR, centerX + outR, centerY + outR);
        mInnerCircleRectF.set(centerX - inR, centerY - inR, centerX + inR, centerY + inR);
        //先保存画板的状态
        canvas.save();
        //外圆
        canvas.drawArc(mOuterCircleRectF, mRotateAngle % 360, OUTER_CIRCLE_ANGLE, false, mStrokePaint);
        //内圆
        canvas.drawArc(mInnerCircleRectF, 270 - mRotateAngle % 360, INTER_CIRCLE_ANGLE, false, mStrokePaint);
        //恢复画板的状态
        canvas.restore();
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startLoading();
    }

    public void startLoading() {
        start();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stopLoading();
    }

    public void stopLoading() {
        stop();
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        mRotateAngle = 360 * (float)animation.getAnimatedValue();
        invalidate();
    }

    protected void computeUpdateValue(float animatedValue) {
        mRotateAngle = (int) (360 * animatedValue);
    }

    @Override
    public void start() {
        if (mFloatValueAnimator.isStarted()) {
            return;
        }
        mFloatValueAnimator.addUpdateListener(this);
        mFloatValueAnimator.setRepeatCount(Animation.INFINITE);
        mFloatValueAnimator.setDuration(ANIMATION_DURATION);
        mFloatValueAnimator.start();
    }

    @Override
    public void stop() {
        mFloatValueAnimator.removeAllUpdateListeners();
        mFloatValueAnimator.removeAllListeners();
        mFloatValueAnimator.setRepeatCount(0);
        mFloatValueAnimator.setDuration(0);
        mFloatValueAnimator.end();
    }

    @Override
    public boolean isRunning() {
        return mFloatValueAnimator.isRunning();
    }
}
复制代码

attr文件代码以下:

<declare-styleable name="LoadingView">
        <attr name="lineWidth" format="float" />
        <attr name="viewColor" format="color" />
    </declare-styleable>复制代码

若是喜欢个人文章,想与一群资深开发者一块儿交流学习的话,欢迎加入个人合做群Android Senior Engineer技术交流群。有flutter—性能优化—移动架构—资深UI工程师 —NDK相关专业人员和视频教学资料,后面也有和本篇文章想对应的视频资料分享
群号:925019412

相关文章
相关标签/搜索