绚丽的loading动效的实现

最近看到有个gif动画效果挺不错的,可以拿来当项目的LoadingView,所以就花点时间做了下。先来看下效果图:

分析

从效果上看,我们可以将其拆分成以下几部分:

(1)底部框:带有黄色边框的圆角矩形和右边的圆形,为了方便,整个底部框切了,不需要我们去绘制圆角矩形和圆形了;

(2)进度框:带有进度值和色值的圆角矩形框,特殊的是它的圆角只有左上角和左下角是,另外的两个角是直角;

(3)风扇:在底部框的圆形位置中,绘制风扇,它可以旋转,直至进度加载完毕;

(4)叶子:从风扇处飘出,有多个叶子,按照一定的曲线和频率飘荡,遇到了进度框,看起来似乎融入进去了。

我们需要考虑的有几个问题

1:叶子是随机产生的;

2:叶子遇到进度框,像是融进去了,不在显示了;

3:叶子是随着一条正余弦曲线移动;

4:叶子飘出的角度是不一样的,而且移动的振幅也不一样,比较有美感。

这样子,我们需要处理的有以下几部分:

一是,绘制底部框;

二是,不断往前绘制的进度条;

三是,不断旋转的风扇;

最后,不断飘出的叶子。

=========================================================

我们先处理第一部分

private void drawBackground(Canvas canvas){
        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.leaf_kuang);
        mPicWidth = bitmap.getWidth();
        mPicHeight = bitmap.getHeight();
        canvas.drawBitmap(bitmap,0,0,mBgPaint);

        mTotalProgressWidth = mPicWidth - 76;
    } 

第二部分

利用Path的addRoundRect方法绘制可定制圆角的圆角矩形。绘制进度值,它的宽度的计算是进度值/100*总进度的宽度。因为,我们刚刚先绘制了底部框,然后绘制进度框,两者会有相交的地方,遵循上层覆盖下层原则,会出现相交的部分显示在底部框之上,不符合我们的效果。所以给它设置下图片混合效果DST_OVER,就可以了。

private void drawProgress(Canvas canvas){
        mProgressWidth = mProgress/100 * mTotalProgressWidth;
        RectF rectF = new RectF();
        rectF.left = 16;
        rectF.top = 16;
        rectF.right = mProgressWidth;
        rectF.bottom = mPicHeight-16;

        float[] radius = new float[8];
        radius[0] = 40;
        radius[1] = 40;
        radius[2] = 0;
        radius[3] = 0;
        radius[4] = 0;
        radius[5] = 0;
        radius[6] = 40;
        radius[7] = 40;

        Path path = new Path();
        path.addRoundRect(rectF,radius, Path.Direction.CW);
        //SRC 上层 DST 下层
        mProgressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));//设置图片混合显示效果
        canvas.drawPath(path,mProgressPaint);
    }

第三部分

我们通过Matrix矩阵操作图片,可以让图片旋转,缩放。首先,风扇图片是显示在底部框最右边的圆形位置(称为R位置),所以需要先位移坐标。原来的坐标原点是在(0,0),现在图片是要在R位置旋转,缩放,所以通过setTranslate设置图片的坐标位置,然后在进度未达到100%时,让图片需要不停的旋转;达到95%以上,这时图片会进行缩放;达到100%时,就显示文本。

private void drawFan(Canvas canvas){
        int centerX = (int) mTotalProgressWidth;
        int centerY = 8;
        if(mProgress == 100){
            String text = "100%";
            canvas.drawText(text,centerX,mPicHeight/2+getTextHeight(text)/2,mFanPaint);
        }else{
            Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.fengshan);
            int bitmapWidth = bitmap.getWidth();
            int bitmapHeight = bitmap.getHeight();

            Matrix matrix = new Matrix();
            matrix.setTranslate(centerX, centerY);     //设置图片的原点坐标
            if (this.mProgress >= 95 && this.mProgress < 100){
                float scale = Math.abs(this.mProgress - 100) * 0.2f;
                //缩放 参数1:X轴缩放倍数,参数2:Y轴缩放倍数 参数3,4:缩放中心点
                matrix.preScale(scale,scale,(float)bitmapWidth/2, (float)bitmapHeight/2);
            }else{
                //旋转 参数1:角度,参数2,3:旋转中心点
                matrix.preRotate(mAngle, (float)bitmapWidth/2, (float)bitmapHeight/2);
            }
            canvas.drawBitmap(bitmap, matrix, mFanPaint);
            if (this.mProgress != 100){
                mAngle += 60;
            }
        }
    }

最后一部分

首先根据效果情况基本确定出曲线函数,标准函数方程为:y = A(wx+Q)+h,其中w影响周期,A影响振幅 ,周期T= 2 * Math.PI/w;

根据效果可以看出,周期T大致为总进度长度,所以确定w=(float) ((float) 2 * Math.PI /mTotalProgressWidth);

由以上的分析可知,叶子是有位置(x,y),有振幅的幅度type(低等幅度,中等幅度,高等幅度),有旋转的角度和方向rotateAngle和rotateDirection,它是随机产生的,有个起始时间startTime。

叶子Leaf类就此产生:

private enum StartType {
        LITTLE, MIDDLE, BIG
    }

    /**
     * 叶子对象,用来记录叶子主要数据
     *
     */
    private class Leaf {
        // 在绘制部分的位置
        float x, y;
        // 控制叶子飘动的幅度
        StartType type;
        // 旋转角度
        int rotateAngle;
        // 旋转方向--0代表顺时针,1代表逆时针
        int rotateDirection;
        // 起始时间(ms)
        long startTime;
    }

叶子是可以随机产生一个或多个的,所以我们提供了一个LeafFactory类来生产叶子:

private class LeafFactory {
        private static final int MAX_LEAFS = 8;
        Random random = new Random();

        // 生成一个叶子信息
        public Leaf generateLeaf() {
            Leaf leaf = new Leaf();
            int randomType = random.nextInt(3);
            // 随时类型- 随机振幅
            StartType type = StartType.MIDDLE;
            switch (randomType) {
                case 0:
                    break;
                case 1:
                    type = StartType.LITTLE;
                    break;
                case 2:
                    type = StartType.BIG;
                    break;
                default:
                    break;
            }
            leaf.type = type;
            // 随机起始的旋转角度
            leaf.rotateAngle = random.nextInt(360);
            // 随机旋转方向(顺时针或逆时针)
            leaf.rotateDirection = random.nextInt(2);
            // 为了产生交错的感觉,让开始的时间有一定的随机性
            mLeafFloatTime = mLeafFloatTime <= 0 ? LEAF_FLOAT_TIME : mLeafFloatTime;
            mAddTime += random.nextInt((int) (mLeafFloatTime));
            leaf.startTime = System.currentTimeMillis() + mAddTime;
            return leaf;
        }

        // 根据最大叶子数产生叶子信息
        public List<Leaf> generateLeafs() {
            return generateLeafs(MAX_LEAFS);
        }

        // 根据传入的叶子数量产生叶子信息
        public List<Leaf> generateLeafs(int leafSize) {
            List<Leaf> leafs = new LinkedList<Leaf>();
            for (int i = 0; i < leafSize; i++) {
                leafs.add(generateLeaf());
            }
            return leafs;
        }
    }

接下来,我们就可以获取到叶子的Y坐标了:

// 通过叶子信息获取当前叶子的Y值
    private int getLocationY(Leaf leaf) {
        // y = A(wx+Q)+h
        float w = (float) ((float) 2 * Math.PI / mTotalProgressWidth);
        float a = mMiddleAmplitude;
        switch (leaf.type) {
            case LITTLE:
                // 小振幅 = 中等振幅 - 振幅差
                a = mMiddleAmplitude - mAmplitudeDisparity;
                break;
            case MIDDLE:
                a = mMiddleAmplitude;
                break;
            case BIG:
                // 小振幅 = 中等振幅 + 振幅差
                a = mMiddleAmplitude + mAmplitudeDisparity;
                break;
            default:
                break;
        }
        return (int) (a * Math.sin(w * leaf.x)) + 40 * 2 / 3;//40是圆角半径
    }

接下来,开始绘制叶子:

 /**
     * 绘制叶子
     *
     * @param canvas
     */
    private void drawLeaf(Canvas canvas) {
        mLeafRotateTime = mLeafRotateTime <= 0 ? LEAF_ROTATE_TIME : mLeafRotateTime;
        long currentTime = System.currentTimeMillis();
        for (int i = 0; i < mLeafInfos.size(); i++) {
            Leaf leaf = mLeafInfos.get(i);
            if (currentTime > leaf.startTime && leaf.startTime != 0) {
                // 绘制叶子--根据叶子的类型和当前时间得出叶子的(x,y)
                getLeafLocation(leaf, currentTime);
                // 根据时间计算旋转角度
                canvas.save();
                // 通过Matrix控制叶子旋转
                Matrix matrix = new Matrix();
                float transX = leaf.x;
                float transY = leaf.y;
                Log.e("(x,y)=","("+transX+","+transY+")");
                if (transX > mProgressWidth) {//叶子遇到进度框,就融入了不再显示
                    matrix.postTranslate(transX, transY);
                    // 通过时间关联旋转角度,则可以直接通过修改LEAF_ROTATE_TIME调节叶子旋转快慢
                    float rotateFraction = ((currentTime - leaf.startTime) % mLeafRotateTime)
                            / (float) mLeafRotateTime;
                    int angle = (int) (rotateFraction * 360);
                    // 根据叶子旋转方向确定叶子旋转角度
                    int rotate = leaf.rotateDirection == 0 ? angle + leaf.rotateAngle : -angle
                            + leaf.rotateAngle;
                    matrix.postRotate(rotate, transX
                            + mLeafWidth / 2, transY + mLeafHeight / 2);
                    canvas.drawBitmap(mLeafBitmap, matrix, mLeafPaint);
                    canvas.restore();
                }
            } else {
                continue;
            }
        }
    }
/**
     * 获取叶子的x,y
     * @param leaf
     * @param currentTime
     */
    private void getLeafLocation(Leaf leaf, long currentTime) {
        long intervalTime = currentTime - leaf.startTime;
        mLeafFloatTime = mLeafFloatTime <= 0 ? LEAF_FLOAT_TIME : mLeafFloatTime;
        if (intervalTime < 0) {
            return;
        } else if (intervalTime > mLeafFloatTime) {
            leaf.startTime = System.currentTimeMillis()
                    + new Random().nextInt((int) mLeafFloatTime);
        }

        float fraction = (float) intervalTime / mLeafFloatTime;
        leaf.x = (int) (mTotalProgressWidth - mTotalProgressWidth * fraction);
        leaf.y = getLocationY(leaf);
    }

最后,向外暴露几个方法:

/**
     * 设置进度
     * @param progress
     */
    public void setProgress(float progress){
        mProgress = progress;
        invalidate();
    }

    /**
     * 进度框颜色
     * @param color
     */
    public void setProgressColor(int color){
        this.mProgressColor = color;
    }

    /**
     * 设置中等幅度值
     * @param amplitude
     */
    public void setAmplitude(int amplitude){
        this.mMiddleAmplitude = amplitude;
    }

    /**
     * 设置幅度差
     * @param amplitudeDisparity
     */
    public void setAmplitudeDisparity(int amplitudeDisparity){
        this.mAmplitudeDisparity = amplitudeDisparity;
    }

使用方式

(1)Activity:

public class MainActivity extends AppCompatActivity {
    private final int REFRESH_PROGRESS = 1000;
    private float mProgress = 0;
    //利用handler实现动画
    private Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case REFRESH_PROGRESS:
                    if (mProgress <= 100) {
                        mProgress += 1;
                        // 随机100ms以内刷新一次
                        mHandler.sendEmptyMessageDelayed(REFRESH_PROGRESS,
                                100);
                        mLoadingView.setProgress(mProgress);
                    }
                    break;

                default:
                    break;
            }
        };
    };

    private LoadingLeafView mLoadingView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findControl();
    }

    private void findControl(){
        mLoadingView  = (LoadingLeafView) findViewById(R.id.loading_leaf_view);
//        mLoadingView.setProgressColor(Color.RED);
//        mLoadingView.setAmplitude(16);
//        mLoadingView.setAmplitudeDisparity(10);
        mHandler.sendEmptyMessageDelayed(REFRESH_PROGRESS, 100);
    }

  (2)layout:

 <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <com.ha.cjy.myproject.view.widget.LoadingLeafView
            android:id="@+id/loading_leaf_view"
            android:layout_marginTop="48dp"
            android:layout_marginLeft="24dp"
            android:layout_width="302dp"
            android:layout_height="61dp"/>
    </LinearLayout>

Demo下载地址:https://github.com/hacjy/LeafLoadingView

posted @ 2017-08-22 16:10  ha_cjy  阅读(527)  评论(0编辑  收藏  举报