Android UI-自定义Drawable(一)

概述

有了前面几篇博客的预备知识,现在就可以来学习下自定义Drawble了。这篇主要是介绍一个开源项目的自定义Drawble的实现,主要是没有看到效果无法讲清楚原理。下一篇再介绍原理。

开源项目

开元项目地址
https://github.com/dinuscxj/LoadingDrawable
项目效果


CircleRotateDrawable.gif

这篇会介绍右上角的代码,注释都会写在代码中。先来看下XML中的入口,自定义了一个View。

        

看下自定义的View,创建了Drawable和Render,这是是先动画效果最终要的类。其中Drawble是android自带的,BitmapDrawble就是其子类,主要是绘制的用处。Render是作者自定义的。用来实现效果。

public class LoadingView extends ImageView {
    private LoadingDrawable mLoadingDrawable;

    public LoadingView(Context context) {
        super(context);
    }

    public LoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initAttrs(context, attrs);
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        try {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
            int loadingRendererId = ta.getInt(R.styleable.LoadingView_loading_renderer, 0);
            // 创建一个Render,这个是动画的主要实现类
            LoadingRenderer loadingRenderer = LoadingRendererFactory.createLoadingRenderer(context, loadingRendererId);
            // 创建Drawable,并关联Drawable
            setLoadingRenderer(loadingRenderer);
            ta.recycle();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void setLoadingRenderer(LoadingRenderer loadingRenderer) {
        mLoadingDrawable = new LoadingDrawable(loadingRenderer);
        setImageDrawable(mLoadingDrawable);
    }

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

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

    @Override
    protected void onVisibilityChanged(View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        if (visibility == View.VISIBLE) {
            startAnimation();
        } else {
            stopAnimation();
        }
    }

    // 让Drawable开始冻哈
    private void startAnimation() {
        if (mLoadingDrawable != null) {
            mLoadingDrawable.start();
        }
    }
    
    // 让Drawbale停止动画
    private void stopAnimation() {
        if (mLoadingDrawable != null) {
            mLoadingDrawable.stop();
        }
    }
}

看下Drawble,这个类代码比较少,继承了Drawable,实现了Animatable接口。然后就是重写了一些方法。mCallback是Render通知Drawble重新绘制用的。start()和stop()方法中可以看到,实现动画效果的是Render。

public class LoadingDrawable extends Drawable implements Animatable {
    private final LoadingRenderer mLoadingRender;

    // 传递给Render的回调
    private final Callback mCallback = new Callback() {
        @Override
        public void invalidateDrawable(Drawable d) {
            invalidateSelf();
        }

        @Override
        public void scheduleDrawable(Drawable d, Runnable what, long when) {
            scheduleSelf(what, when);
        }

        @Override
        public void unscheduleDrawable(Drawable d, Runnable what) {
            unscheduleSelf(what);
        }
    };

    public LoadingDrawable(LoadingRenderer loadingRender) {
        this.mLoadingRender = loadingRender;
        // 设置回调
        this.mLoadingRender.setCallback(mCallback);
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);
        // 改变绘制区域
        this.mLoadingRender.setBounds(bounds);
    }

    @Override
    public void draw(Canvas canvas) {
        if (!getBounds().isEmpty()) {
            // 调用Render的绘制方法
            this.mLoadingRender.draw(canvas);
        }
    }

    @Override
    public void setAlpha(int alpha) {
        // 设置透明度
        this.mLoadingRender.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        // 设置过滤器
        this.mLoadingRender.setColorFilter(cf);
    }

    // 获取不透明策略
    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    // 开始
    @Override
    public void start() {
        this.mLoadingRender.start();
    }

    // 结束
    @Override
    public void stop() {
        this.mLoadingRender.stop();
    }

    // 是否在运行
    @Override
    public boolean isRunning() {
        return this.mLoadingRender.isRunning();
    }

    // 获取默认的高度
    @Override
    public int getIntrinsicHeight() {
        return (int) this.mLoadingRender.mHeight;
    }

    // 获取默认的宽度
    @Override
    public int getIntrinsicWidth() {
        return (int) this.mLoadingRender.mWidth;
    }
}

看下Render,这是一个抽象类。实现了主要的逻辑,就是创建了属性动画ValueAnimator ,实现开始和结束属性动画的方法。mAnimatorUpdateListener实现了属性动画的动画进度的监听。但是方法computeRender是在子类中去实现的。

// 所有着色器的父类
public abstract class LoadingRenderer {
    // 动画持续时间
    private static final long ANIMATION_DURATION = 1333;
    // 默认图形的大小
    private static final float DEFAULT_SIZE = 56.0f;

    // 插值器的监听器
    private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener
            = new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            // 计算,子类实现
            computeRender((float) animation.getAnimatedValue());
            // 通知Drawble重新绘制
            invalidateSelf();
        }
    };

    /**
     * Whenever {@link LoadingDrawable} boundary changes mBounds will be updated.
     * More details you can see {@link LoadingDrawable#onBoundsChange(Rect)}
     */
    protected final Rect mBounds = new Rect();

    // 回调,通知Drawable
    private Drawable.Callback mCallback;
    // 属性动画,用来控制动画的进度
    private ValueAnimator mRenderAnimator;
    // 动画时长
    protected long mDuration;
    // 长宽
    protected float mWidth;
    protected float mHeight;

    public LoadingRenderer(Context context) {
        // 初始化参数
        initParams(context);
        // 设置动画
        setupAnimators();
    }

    @Deprecated
    protected void draw(Canvas canvas, Rect bounds) {
    }

    // 子类去实现
    protected void draw(Canvas canvas) {
        draw(canvas, mBounds);
    }
    // 子类去实现
    protected abstract void computeRender(float renderProgress);
    // 子类去实现
    protected abstract void setAlpha(int alpha);
    // 子类去实现
    protected abstract void setColorFilter(ColorFilter cf);

    protected abstract void reset();
    // 设置监听
    protected void addRenderListener(Animator.AnimatorListener animatorListener) {
        mRenderAnimator.addListener(animatorListener);
    }

    // 开始属性动画
    void start() {
        reset();
        mRenderAnimator.addUpdateListener(mAnimatorUpdateListener);

        mRenderAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mRenderAnimator.setDuration(mDuration);
        mRenderAnimator.start();
    }

    // 结束属性动画
    void stop() {
        // if I just call mRenderAnimator.end(),
        // it will always call the method onAnimationUpdate(ValueAnimator animation)
        // why ? if you know why please send email to me ([email protected])
        mRenderAnimator.removeUpdateListener(mAnimatorUpdateListener);

        mRenderAnimator.setRepeatCount(0);
        mRenderAnimator.setDuration(0);
        mRenderAnimator.end();
    }

    boolean isRunning() {
        return mRenderAnimator.isRunning();
    }

    // 设置与Drawbale相关的回调
    void setCallback(Drawable.Callback callback) {
        this.mCallback = callback;
    }

    // 当bounds改变时,改变render的bounds
    void setBounds(Rect bounds) {
        mBounds.set(bounds);
    }

    private void initParams(Context context) {
        // 设置长宽
        mWidth = DensityUtil.dip2px(context, DEFAULT_SIZE);
        mHeight = DensityUtil.dip2px(context, DEFAULT_SIZE);
        // 1.333秒
        mDuration = ANIMATION_DURATION;
    }

    // 初始化属性动画
    private void setupAnimators() {
        mRenderAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        mRenderAnimator.setRepeatCount(Animation.INFINITE);
        mRenderAnimator.setRepeatMode(Animation.RESTART);
        mRenderAnimator.setDuration(mDuration);
        //fuck you! the default interpolator is AccelerateDecelerateInterpolator
        mRenderAnimator.setInterpolator(new LinearInterpolator());
        mRenderAnimator.addUpdateListener(mAnimatorUpdateListener);
    }

    // 调用Drawbale中的invalidate
    private void invalidateSelf() {
        mCallback.invalidateDrawable(null);
    }
}

看下具体实现类MaterialLoadingRenderer。 draw方法中的canvas.drawArc(mTempBounds, mStartDegrees, mSwipeDegrees, false, mPaint);用绘制圆弧的方法实现了旋转的效果。逻辑是这样,首先根据属性动画的Update监听返回fraction值,然后根据这个值计算出圆弧的动画策略。如果进度在50%下,那么圆弧的头前进。如果大于50%,那么圆弧的尾前进。同时还自带一个缓慢旋转的效果。等到执行了一个周期(这里是5次旋转回到起点)就重新开始。还会在每一次动画效果中改变颜色。每一次计算后,将通知Drawble去重新绘制,随后会调用draw方法绘制。

public class MaterialLoadingRenderer extends LoadingRenderer {
    private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();

    // 360
    private static final int DEGREE_360 = 360;
    // 循环周期里有5此swipe
    private static final int NUM_POINTS = 5;

    // 一次swipe的角度
    private static final float MAX_SWIPE_DEGREES = 0.8f * DEGREE_360;
    // 一个周期总的角度
    private static final float FULL_GROUP_ROTATION = 3.0f * DEGREE_360;
    // 进度的80%
    private static final float COLOR_START_DELAY_OFFSET = 0.8f;
    // 进度的100%
    private static final float END_TRIM_DURATION_OFFSET = 1.0f;
    // 进度的50%
    private static final float START_TRIM_DURATION_OFFSET = 0.5f;

    // 半径
    private static final float DEFAULT_CENTER_RADIUS = 12.5f;
    // 圈的宽度
    private static final float DEFAULT_STROKE_WIDTH = 2.5f;

    // 预设的颜色
    private static final int[] DEFAULT_COLORS = new int[]{
            Color.RED, Color.GREEN, Color.BLUE
    };

    // 画笔
    private final Paint mPaint = new Paint();
    // 绘制区域的矩形
    private final RectF mTempBounds = new RectF();

    // 设置动画的监听
    private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
        @Override
        public void onAnimationRepeat(Animator animator) {
            super.onAnimationRepeat(animator);
            // 重新设置初始值(mOriginEndDegrees,mOriginStartDegrees)
            storeOriginals();
            // 获取颜色数组的下一个颜色
            goToNextColor();
            // 重置开始值
            mStartDegrees = mEndDegrees;
            // 重置开始次数
            mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
        }

        @Override
        public void onAnimationStart(Animator animation) {
            super.onAnimationStart(animation);
            // 开始时设置次数为0
            mRotationCount = 0;
        }
    };

    // 颜色数组
    private int[] mColors;
    // 当前颜色的index
    private int mColorIndex;
    // 当前颜色
    private int mCurrentColor;
    // 需要拓展或者搜索的宽度
    private float mStrokeInset;

    // 一个周期内循环的次数,如再次回到起点需要5此次
    private float mRotationCount;
    // 在周期循环的基础上,还要加上不停的转动
    private float mGroupRotation;

    // 结束的角度
    private float mEndDegrees;
    // 开始的角度
    private float mStartDegrees;
    // 结束-开始
    private float mSwipeDegrees;
    // 初始的结束角度
    private float mOriginEndDegrees;
    // 初始的开始角度
    private float mOriginStartDegrees;
    // 圈的粗细
    private float mStrokeWidth;
    // 内径
    private float mCenterRadius;

    private MaterialLoadingRenderer(Context context) {
        super(context);
        // 初始化参数
        init(context);
        // 初始化画笔和绘制模式
        setupPaint();
        // 设置监听
        addRenderListener(mAnimatorListener);
    }

    private void init(Context context) {
        // 从dp变换成px
        mStrokeWidth = DensityUtil.dip2px(context, DEFAULT_STROKE_WIDTH);
        mCenterRadius = DensityUtil.dip2px(context, DEFAULT_CENTER_RADIUS);
        // 默认颜色数组
        mColors = DEFAULT_COLORS;
        // 设置数组中为0的颜色
        setColorIndex(0);
        // 计算需要扩大或者收缩的宽度
        initStrokeInset(mWidth, mHeight);
    }

    private void setupPaint() {
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(mStrokeWidth);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
    }

    @Override
    protected void draw(Canvas canvas) {
        // 将Canvas当前状态保存在堆栈
        int saveCount = canvas.save();
        // 设置绘制区域
        mTempBounds.set(mBounds);
        // 设置需要扩大或者收缩绘制区域
        mTempBounds.inset(mStrokeInset, mStrokeInset);
        // 画笔旋转一定角度,这样看上去会像是在转动
        canvas.rotate(mGroupRotation, mTempBounds.centerX(), mTempBounds.centerY());
        // 如果swipe角度不为0,绘制
        if (mSwipeDegrees != 0) {
            mPaint.setColor(mCurrentColor);
            canvas.drawArc(mTempBounds, mStartDegrees, mSwipeDegrees, false, mPaint);
        }
        // 恢复为之前堆栈保存的Canvas状态,即旋转前的状态
        canvas.restoreToCount(saveCount);
    }

    // 根据属性动画的Update来绘制
    @Override
    protected void computeRender(float renderProgress) {
        // 刷新
        updateRingColor(renderProgress);
        // Moving the start trim only occurs in the first 50% of a single ring animation
        // 如果进度还小于50%,那么改变开始的角度
        if (renderProgress <= START_TRIM_DURATION_OFFSET) {
            // 计算百分比
            float startTrimProgress = renderProgress / START_TRIM_DURATION_OFFSET;
            // 开始角度+一次swipe的最大角度*快出慢进插值器计算出来的值
            mStartDegrees = mOriginStartDegrees + MAX_SWIPE_DEGREES
                    * MATERIAL_INTERPOLATOR.getInterpolation(startTrimProgress);
        }

        // Moving the end trim starts after 50% of a single ring animation completes
        // 如果进度还大于50%,那么改变结束的角度
        if (renderProgress > START_TRIM_DURATION_OFFSET) {
            float endTrimProgress = (renderProgress - START_TRIM_DURATION_OFFSET)
                    / (END_TRIM_DURATION_OFFSET - START_TRIM_DURATION_OFFSET);
            mEndDegrees = mOriginEndDegrees + MAX_SWIPE_DEGREES
                    * MATERIAL_INTERPOLATOR.getInterpolation(endTrimProgress);
        }
        // 计算swipe的值
        if (Math.abs(mEndDegrees - mStartDegrees) > 0) {
            mSwipeDegrees = mEndDegrees - mStartDegrees;
        }
        // 计算滚动角度
        mGroupRotation = ((FULL_GROUP_ROTATION / NUM_POINTS) * renderProgress)
                + (FULL_GROUP_ROTATION * (mRotationCount / NUM_POINTS));
    }

    @Override
    protected void setAlpha(int alpha) {
        // 设置透明度
        mPaint.setAlpha(alpha);
    }

    @Override
    protected void setColorFilter(ColorFilter cf) {
        // 设置颜色过滤器
        mPaint.setColorFilter(cf);
    }

    @Override
    protected void reset() {
        // 重置
        resetOriginals();
    }

    private void setColorIndex(int index) {
        // 设置当前颜色
        mColorIndex = index;
        mCurrentColor = mColors[mColorIndex];
    }

    private int getNextColor() {
        // 获取下一个颜色
        return mColors[getNextColorIndex()];
    }

    private int getNextColorIndex() {
        return (mColorIndex + 1) % (mColors.length);
    }

    private void goToNextColor() {
        // 跳到下一个颜色
        setColorIndex(getNextColorIndex());
    }

    private void initStrokeInset(float width, float height) {
        // 长宽中的最小值
        float minSize = Math.min(width, height);
        // 从长宽最小值中计算需要插入的宽度
        float strokeInset = minSize / 2.0f - mCenterRadius;
        // 从画笔粗细中计算
        float minStrokeInset = (float) Math.ceil(mStrokeWidth / 2.0f);
        // 取较大值
        mStrokeInset = strokeInset < minStrokeInset ? minStrokeInset : strokeInset;
    }

    private void storeOriginals() {
        mOriginEndDegrees = mEndDegrees;
        mOriginStartDegrees = mEndDegrees;
    }

    private void resetOriginals() {
        mOriginEndDegrees = 0;
        mOriginStartDegrees = 0;

        mEndDegrees = 0;
        mStartDegrees = 0;
    }

    private int getStartingColor() {
        return mColors[mColorIndex];
    }

    private void updateRingColor(float interpolatedTime) {
        // 如果进度已经超过0.8,那么计算新的颜色
        if (interpolatedTime > COLOR_START_DELAY_OFFSET) {
            // 在剩余的20%中重新计算百分比,然后从当前颜色渐变到下一个颜色
            mCurrentColor = evaluateColorChange((interpolatedTime - COLOR_START_DELAY_OFFSET)
                    / (1.0f - COLOR_START_DELAY_OFFSET), getStartingColor(), getNextColor());
        }
    }

    // 计算过度颜色
    private int evaluateColorChange(float fraction, int startValue, int endValue) {
        int startA = (startValue >> 24) & 0xff;
        int startR = (startValue >> 16) & 0xff;
        int startG = (startValue >> 8) & 0xff;
        int startB = startValue & 0xff;

        int endA = (endValue >> 24) & 0xff;
        int endR = (endValue >> 16) & 0xff;
        int endG = (endValue >> 8) & 0xff;
        int endB = endValue & 0xff;

        return ((startA + (int) (fraction * (endA - startA))) << 24)
                | ((startR + (int) (fraction * (endR - startR))) << 16)
                | ((startG + (int) (fraction * (endG - startG))) << 8)
                | ((startB + (int) (fraction * (endB - startB))));
    }

    private void apply(Builder builder) {
        this.mWidth = builder.mWidth > 0 ? builder.mWidth : this.mWidth;
        this.mHeight = builder.mHeight > 0 ? builder.mHeight : this.mHeight;
        this.mStrokeWidth = builder.mStrokeWidth > 0 ? builder.mStrokeWidth : this.mStrokeWidth;
        this.mCenterRadius = builder.mCenterRadius > 0 ? builder.mCenterRadius : this.mCenterRadius;

        this.mDuration = builder.mDuration > 0 ? builder.mDuration : this.mDuration;

        this.mColors = builder.mColors != null && builder.mColors.length > 0 ? builder.mColors : this.mColors;

        setColorIndex(0);
        setupPaint();
        initStrokeInset(this.mWidth, this.mHeight);
    }

    public static class Builder {
        private Context mContext;

        private int mWidth;
        private int mHeight;
        private int mStrokeWidth;
        private int mCenterRadius;

        private int mDuration;

        private int[] mColors;

        public Builder(Context mContext) {
            this.mContext = mContext;
        }

        public Builder setWidth(int width) {
            this.mWidth = width;
            return this;
        }

        public Builder setHeight(int height) {
            this.mHeight = height;
            return this;
        }

        public Builder setStrokeWidth(int strokeWidth) {
            this.mStrokeWidth = strokeWidth;
            return this;
        }

        public Builder setCenterRadius(int centerRadius) {
            this.mCenterRadius = centerRadius;
            return this;
        }

        public Builder setDuration(int duration) {
            this.mDuration = duration;
            return this;
        }

        public Builder setColors(int[] colors) {
            this.mColors = colors;
            return this;
        }

        public MaterialLoadingRenderer build() {
            MaterialLoadingRenderer loadingRenderer = new MaterialLoadingRenderer(mContext);
            loadingRenderer.apply(this);
            return loadingRenderer;
        }
    }
}

总结

这篇主要是一个开源框架的讲解,介绍了Drawable的实现。如果可能的话,那么下一篇将介绍自定义Drawable的实现步骤。

你可能感兴趣的:(Android UI-自定义Drawable(一))