动态加载的Button(学习自定义View从造轮子开始)

前言:学习Android有一段时间了,但是一直都没有突破这个自定义View的关口,因为不知从何下手,网上有很多大神写的博文都很好,但是如果没有真的去实践的话,隔一段时间就会忘记.这两个星期我就下定决心,要突破自定义View这个关口,从造轮子开始.

一 学习次博文我们能学习到什么:

  • 1我们能大概知道一个自定义View是按什么流程写出来的;

  • 2学习到自定义View中的一些:View大小的确定;绘制一些图形;PathMeasure的应用;Animator的应用;

  • 3我们直接来看预想图和效果图,预测一下我们的绘制过程.
    预想图.gif
    制作图.gif

先别喷我~虽然制作出来的配色很丑,但是麻雀虽丑,五脏俱全.下面来看看我们的制作过程.文末的配色会让这个控件的13格瞬间提升.

二 把轮子拆开看看要什么零件

我盯着预想图半天,结合自身的水平,写出了自己要怎么去写这个自定义View.(只有把轮子拆开了,我们才知道这个轮子里面藏着什么乾坤)

  • ①实现点击事件,我这里没有用传统的回调监听,而是用了内部方法调用,这样就可以控制View的加载过程.

  • ②在画布上绘制一个长方形(或者带圆角的矩形),然后把长方形变成一个长圆形,为什么是长圆形呢?因为在矩形(圆角矩形)变成圆形过程中,是要有一个过度的,这个过渡期我们就用下面图示的长圆形来代替.

  • ③长圆形缩成圆形. ②和③的操作就如图所示


    2-3.jpg
  • ④圆内实现滚轮加载

  • ⑤加载完成绘制对勾和圆圈(这是我的版本,原版的话就把圆向上平移再绘制对勾就好了,没有④这一步)
    上述的五步过程就可以让我们把这个轮子给造出来.下面就一步一步来实现

三 开始组装我们自己的轮子

按照上面的流程,我们一步一步来实现.可以看一下我的代码,里面的注释写得比较清楚.但是还是建议一步一步去实现,我在写的时候没有一步一步去录制gif,所以就没有一步一步来分析了.下次会记得记录一下!!

public class YoungButton extends View {
    private static String TAG = YoungButton.class.getSimpleName();

    private float mWidth, mHeight;
    private String strBtn = "确定";
    private float mCircleRadius;//绘制圆的半径为控件高度的一半


    //画笔设置
    private Paint mPaint;
    private Paint mTestPaint;
    private Paint mLoadingPaint;
    private int PAINT_COLOR = Color.RED;
    private int PAINT_LOADING_COLOR = Color.WHITE;
    private int TEST_COLOR = Color.WHITE;
    private float PAINT_WIDTH = 5f;
    private float LOADING_WIDTH = 8f;
    private float TEST_WIDTH = 20f;

    //画布设置
    private int COLOR_BG = 0xff00ff00;

    //形状路径设置
    private PathMeasure mPathMeasure = new PathMeasure();
    private Path mOKPath = new Path();
    private Path mOKDst = new Path();
    private Path mLoadingPath = new Path(); //加载的实时路径
    private Path mLoadingDst = new Path();
    private RectF rectF;
    private float circleAngle = 30f;//按钮角度


    //动画的状态
    private static final int STATE_NORMAL = 510;//普通状态
    private static final int STATE_B_LOADING = 194;//加载之前
    private static final int STATE_LOADING = 20;//正在加载
    private static final int STATE_COMP = 666;//完成
    private int nowState = STATE_NORMAL;

    //动画监听器
    private float currentValue;//加载进度
    private float currentValueRTC;//矩形到长圆形动画变换的进度
    private float currentValueCTC;//长圆形变成圆型动画变换进度
    private int ANIM_DURATION = 500;//动画时间
    private ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener;  //动画的变换  0.00-1.00
    private ValueAnimator mRectToCircleAnimator; //矩形变成长圆形动画监听
    private ValueAnimator mCircleToCircleAnimator; //长圆形变成圆形动画监听
    private ValueAnimator mLoadingAnimator;//加载动画
    private ValueAnimator mCompleteAnimator;//完成动画
    private AnimatorSet mAnimatorSet = new AnimatorSet();


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

    public YoungButton(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void initView() {
        mPaint = new Paint();
        mPaint.setColor(PAINT_COLOR);
        mPaint.setAntiAlias(true);//反锯齿
        mPaint.setStrokeWidth(PAINT_WIDTH);
        mPaint.setStyle(Paint.Style.FILL);

        mTestPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTestPaint.setTextSize(30);
        mTestPaint.setColor(TEST_COLOR);
        mTestPaint.setAntiAlias(true);//反锯齿
        mTestPaint.setStrokeWidth(TEST_WIDTH);
        mTestPaint.setTextAlign(Paint.Align.CENTER);

        mLoadingPaint = new Paint();
        mLoadingPaint.setColor(PAINT_LOADING_COLOR);
        mLoadingPaint.setAntiAlias(true);//反锯齿
        mLoadingPaint.setStrokeWidth(LOADING_WIDTH);
        mLoadingPaint.setStyle(Paint.Style.STROKE);
    }



    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mHeight = h;
        mWidth = w;
        resetData();
        initPath();
        initListener();
    }

    //重新设置值
    private void resetData() {
        mCircleRadius = mHeight / 2;
        currentValueCTC = mWidth;
        mTestPaint.setAlpha(255);
    }

    private void initPath() {
        rectF = new RectF();

        //加载Path
        RectF loadingRectF = new RectF(-mCircleRadius*17/20, -mCircleRadius*17/20, mCircleRadius*17/20, mCircleRadius*17/20);
        mLoadingDst.addArc(loadingRectF, -90, 359.9999f);//不要设置360°内部会自动优化,测量不能取到需要的数值

        mOKDst.moveTo(-mCircleRadius/3,0);
        mOKDst.lineTo(-mCircleRadius/12,mCircleRadius/4);
        mOKDst.lineTo(mCircleRadius/3,-mCircleRadius/4);

    }

    //动画监听
    private void initListener() {
        //通用的updateListener
        ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentValue = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        };

        //矩形到长圆形动画
        mRectToCircleAnimator = ValueAnimator.ofFloat(circleAngle, mCircleRadius).setDuration(ANIM_DURATION);
        mRectToCircleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentValueRTC = (float) animation.getAnimatedValue(); //circleAngle-mCircleRadius
                postInvalidate();//  !!!一定要加上这句提醒更新
            }
        });


        //长圆形到圆形动画
        mCircleToCircleAnimator = ValueAnimator.ofFloat(mWidth, mHeight).setDuration(ANIM_DURATION);
        mCircleToCircleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentValueCTC = (float) animation.getAnimatedValue();

                int alpha = (int) (255 - 255 * (mWidth - currentValueCTC) / (mWidth - mHeight));//设置文字透明度
                mTestPaint.setAlpha(alpha);

                postInvalidate();
            }
        });
        mCircleToCircleAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                nowState = STATE_LOADING;
                mLoadingAnimator.start();
            }
        });

        //加载动画
        mLoadingAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIM_DURATION * 4);
        mLoadingAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mLoadingAnimator.addUpdateListener(animatorUpdateListener);
        mLoadingAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                Log.i(TAG, "onAnimationEnd: ");
                nowState = STATE_COMP;
                mCompleteAnimator.start();
            }
        });

        //完成动画
        mCompleteAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIM_DURATION * 3);
        mCompleteAnimator.addUpdateListener(animatorUpdateListener);

        mAnimatorSet.play(mCircleToCircleAnimator)
                .with(mRectToCircleAnimator);
        mAnimatorSet.setDuration(ANIM_DURATION*2);
    }

    /**
     * 不能直接在onDraw中实例RectF,所以在之前实例之后,
     * 在这里来设置Reccanvas.drawRoundRect(rectF,currentValueRTC,currentValueRTC,mPaint);
     * tF的值,从而改变参数的时候可以改变形状
     *
     * @param canvas 画布
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(mWidth / 2, mHeight / 2);
        canvas.drawColor(Color.GREEN);
        switch (nowState) {
            case STATE_NORMAL:
                rectF.left = -mWidth / 2;
                rectF.top = -mHeight / 2;
                rectF.right = mWidth / 2;
                rectF.bottom = mHeight / 2;
                canvas.drawRoundRect(rectF, circleAngle, circleAngle, mPaint);
                if (mLoadingAnimator.isRunning()||mCompleteAnimator.isRunning()){
                    mLoadingAnimator.end();
                    mCompleteAnimator.end();
                }
                break;
            case STATE_B_LOADING:
                beforeLoad(canvas);
                break;
            case STATE_LOADING:
                onLoad(canvas);
                break;
            case STATE_COMP:
                onComplete(canvas);
                break;
        }

        drawTest(canvas);
    }

    //加载完成
    private void onComplete(Canvas canvas) {
        canvas.drawRoundRect(rectF, currentValueRTC, currentValueRTC, mPaint); //背景圆

        mLoadingPath.reset();//必须重置
        mOKPath.reset();//必须重置

        //点击一下就开始搜索
        mPathMeasure.setPath(mLoadingDst, true);
        float stop = mPathMeasure.getLength()*currentValue;
        mPathMeasure.getSegment(0,stop,mLoadingPath,true);
        canvas.drawPath(mLoadingPath, mLoadingPaint);


        mPathMeasure.setPath(mOKDst,false);
        mPathMeasure.getSegment(0,mPathMeasure.getLength()*currentValue,mOKPath,true);
        canvas.drawPath(mOKPath,mLoadingPaint);

    }

    //正在加载
    private void onLoad(Canvas canvas) {
        mLoadingPath.reset();//必须重置
        //点击一下就开始搜索
        mPathMeasure.setPath(mLoadingDst, false);
        float stop = mPathMeasure.getLength()*currentValue;
        float start = (float) (stop - ((0.5 - Math.abs(currentValue - 0.5)) * mCircleRadius * 4));//abs为绝对值  公式为经验值
        mPathMeasure.getSegment(start,stop,mLoadingPath,true);
        canvas.drawRoundRect(rectF, currentValueRTC, currentValueRTC, mPaint);
        canvas.drawPath(mLoadingPath, mLoadingPaint);
    }

    //加载之前的绘制
    private void beforeLoad(Canvas canvas) {
        rectF.left = -currentValueCTC / 2;
        rectF.top = -mHeight / 2;
        rectF.right = currentValueCTC / 2;
        rectF.bottom = mHeight / 2;
        canvas.drawRoundRect(rectF, currentValueRTC, currentValueRTC, mPaint);
    }

    /**
     * 绘制画布中的文字
     *
     * @param canvas 画布
     */
    private void drawTest(Canvas canvas) {
        Paint.FontMetrics fontMetrics = mTestPaint.getFontMetrics();
        float top = fontMetrics.top;//为基线到字体上边框的距离
        float bottom = fontMetrics.bottom;//为基线到字体下边框的距离
        int baseLine = (int) (rectF.centerY() - (top + bottom) / 2);//基线中心点y坐标
        canvas.drawText(strBtn, rectF.centerX(), baseLine, mTestPaint);
    }

    /**
     * 开始加载
     */
    public void startLoading() {
        Log.i(TAG, "startLoading: ");
        nowState = STATE_B_LOADING;
        mAnimatorSet.start();
    }

    /**
     * 停止加载
     */
    public void stopLoading() {
        Log.i(TAG, "stopLoading: ");
    }

    /**
     * 回到初始状态
     */
    public void backRect() {
        Log.i(TAG, "backRect: ");
        resetData();
        nowState = STATE_NORMAL;
        postInvalidate();
    }

    /**
     * 加载成功
     */
    public void completeLoading() {
        Log.i(TAG, "completeLoading: ");
        mLoadingAnimator.end();
    }
}

参考的资料
本文主要参考学习的一篇文章://www.greatytc.com/p/3eb9777f6ab7
GcsSloop大神的博文(学习自定义View零件的好地方):http://www.gcssloop.com/customview/CustomViewIndex/
Canvas里面的绘制的字怎么居中:http://blog.csdn.net/zly921112/article/details/50401976
PathMeasure实现加载动画: http://blog.csdn.net/eclipsexys/article/details/51992473

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,116评论 25 707
  • 《杀人不难》 by 小时了了 杀人不难。 因你也知, 杀猪不难, 杀鸡不难, 杀牛不难, 杀狗不难, 所以杀人怎么...
    Ping_小时了了阅读 356评论 0 0
  • 望着无名指上的那颗痣,我在想这是否真是你前世留在我掌中的泪,这滴泪是为了今生更好的相遇,更好的相爱,亦或是心碎...
    林汐的时尚阅读 169评论 0 2
  • 这十几天坚持每天记录写点什么,也没有特别设计写什么,怎么写。只是灵机一动想到什么就写了什么。每次写完大概看看就发出...
    慧好聊吧阅读 189评论 0 1
  • 《非暴力沟通》 沟通无处不在,沟通能力强,会说话的人在哪里都受欢迎,简直像外挂一样的存在啊,一直在学习关注如何和人...
    明荒阅读 112评论 0 0