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

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

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

如上是我们需要做的一个Loading动画。Loading效果是很常见的一种动画,最简单的实现让设计画个动态图即可,或者画个静态图然后使用帧动画也可以实现。但是今天我们用纯代码实现,不用任何图片资源。


大致思路
我们自定义一个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);

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

  • 获取整个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());

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


三.弧形动起来

让弧形动起来的原理,就是监听值属性动画的值变化,然后在这个变化的过程中不断的改变弧形的角度,然后让它重绘即可。
我们让我们的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>

原文地址:https://blog.51cto.com/14295695/2387329

时间: 2024-10-08 16:41:50

Android进阶:九、自定义View之手写Loading动效的相关文章

Android进阶之自定义View实战(二)九宫格手势解锁实现

一.引言 在上篇博客Android进阶之自定义View实战(一)仿iOS UISwitch控件实现中我们主要介绍了自定义View的最基本的实现方法.作为自定义View的入门篇,仅仅介绍了Canvas的基本使用方法,而对用户交互层面仅仅处理了单击事件接口,在实际的业务中,常常涉及到手势操作,本篇博客以九宫格手势解锁View为例,来说明自定义View如何根据需求处理用户的手势操作.虽然九宫格手势解锁自定义View网上资料有很多,实现原理大同小异,但这里我只是根据自己觉得最优的思路来实现它,目的是让更

自定义View之实现日出日落太阳动效

以前也很羡慕网上大神随手写写就是一个很漂亮的自定义控件,所以我下决心也要学着去写,刚好最近复习了Android View的绘制流程知识,看来看去就是那些个知识点,没点产出总感觉很迷.现在个人呢用的是华为荣耀8手机,碰巧在看自带的天气APP时,滑到最下面看到那个动效图:日出时间和日落时间上边是一个半圆,白天任意的时刻(在日出和日落时间之间)都有对应一个太阳从日出时刻沿着半圆弧做动画特效,个人第一感觉就是:就拿这个来练练手啦!于是拿着笔和纸,画了模型图,甚至求什么sin.cos函数,有点过分了哈,还

自定义View之圆形水波扩散动效

这个效果做出来以后,真的美极了!放在你的应用中,无疑增添了光彩! 效果图    其实,第一种效果,才是产品的需求要的效果.第三种效果,是不是很熟悉?支付宝的咻一咻!哈哈,无意中,我就写出来了. 实现步骤 1.attrs.xml定义属性 <declare-styleable name="WaveView"> <!--圆颜色--> <attr name="wave_color" format="color"/> &

Android进阶:自定义视频播放器开发(下)

上一篇文章我们主要讲了视频播放器开发之前需要准备的一个知识,TextureView,用于对图像流的处理.这篇文章开始构建一个基础的视频播放器. 一.准备工作 在之前的文章已经说过了,播放器也是一个view,我们要在这个view上播放视频流.所以我们要自定义一个简单的viewgroup,比如继承FrameLayout.还出就是布局简单,其他控件可以往上面添加.大家见过的视频播放器的控制器都是放在视频的上方的.这样就是用FrameLayout布局是最好的. class SmallVideoPlaye

【android自定义控件】自定义View属性

1.自定义View的属性 2.在View的构造方法中获得我们自定义的属性 3.重写onMesure 4.重写onDraw 3这个步骤不是必须,当然了大部分情况下还是需要重写的. 1.自定义View的属性,首先在res/values/  下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式. <?xml version="1.0" encoding="utf-8"?> <resources> <attr name=&

iOS动画进阶 - 实现炫酷的上拉刷新动效

移动端访问不佳,请访问我的个人博客 最近撸了一个上拉刷新的小轮子,只要遵循一个协议就能自定义自己动效的上拉刷新和加载,我自己也写了几个动效进去,下面是一个比较好的动效的实现过程 先上效果图和github地址,有其他好的动效大家也可以交流~ 动效的原地址,在uimovement网站上看到这个动效时感觉特别6,就想自己实现一下,费了很长时间,换了几种方案终于实现出来了,下面是实现的步骤: 分析动效 写一个动效的第一步就应该仔细的去分析它,把它的每一帧展开来看,找一个最合适的方式来实现它,下面是我分析

iOS动画进阶 - 实现炫酷的上拉刷新动效(二)

最近撸了一个上拉刷新的小轮子,只要遵循一个协议就能自定义自己动效的上拉刷新和加载,我自己也写了几个动效进去,下面是一个比较好的动效的实现过程 先上效果图和github地址,完整代码个demo和进入查看,有其他好的动效大家也可以学习交流~ 分析动效 写一个动效的第一步就应该仔细的去分析它,把它的每一帧展开来看,找一个最合适的方式来实现它,我们可以把以上动画分解成以下三个步骤: 箭头的绘制和动效 圆环的绘制和小点的旋转 对勾的绘制和动画 以下是会用到主要的类: CAShapeLayer UIBezi

Android 高手进阶之自定义View,自定义属性(带进度的圆形进度条)

转载请注明地址:http://blog.csdn.net/xiaanming/article/details/10298163 很多的时候,系统自带的View满足不了我们功能的需求,那么我们就需要自己来自定义一个能满足我们需求的View,自定义View我们需要先继承View,添加类的构造方法,重写父类View的一些方法,例如onDraw,为了我们自定义的View在一个项目中能够重用,有时候我们需要自定义其属性,举个很简单的例子,我在项目中的多个界面使用我自定义的View,每个界面该自定义View

自定义进阶2——自定义View显示超大图片

上一节(自定义初学5--自定义View显示图片)已经说了如何自定义View显示图片.做android时,加载图片是避免不了的,加载网络图片还需要异步加载,最烦人的就是经常出现OOM,为了避免这样的问题,我们一般这样解决: 根据图片控件的大小对图片进行压缩显示. 如果图片数量非常多,则会使用LruCache等缓存机制,将所有图片占据的内容维持在一个范围内. 有时加载图片还会遇到特殊情况--就是单个图片非常巨大,还不允许压缩.那么对于这种需求,该如何做呢? 首先不压缩,按照原图尺寸加载,那么屏幕肯定