自定义进度条PictureProgressBar——从开发到开源发布全过程

自定义进度条PictureProgressBar——从开发到开源发布全过程

出处
炎之铠邮箱:yanzhikai_yjk@qq.com
本文原创,转载请注明本出处!
本项目JCenter地址:https://bintray.com/yanzhikaijky/CustomViewRepository/PictureProgressbar/
本项目GitHub地址:https://github.com/totond/PictureProgressBar
欢迎 Star or Fork和在Issue里提出意见建议!
*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

前言

上一篇文章掌握了ProgressBar的自定义样式和它的扩展ProgressDialog,但是没有封装,这一次就继承View从零开始做了一个自定义进度条——PictureProgressBar,并发布到Github和JCenter上,下面就开始一步一步介绍这个过程。

PS:JCenter是一个Android的代码库,把代码放上去,就可以在AS项目里的Gradle文件里compile 'xxx'这样来引入你的代码了。

本文涉及到:

  • 一个继承自View的自定义ProgressBar实现全过程
  • 一个项目开源的全过程:使用AndroidStudio上传代码到GitHub、JCenter的过程,添加开源协议的过程等。

实现

PictureProgressBar是一个可以带图片和动画效果的进度条,可以先看看它的效果,如下图:


实现的逻辑并不复杂,看看流程图:


  主要的逻辑是在onDraw()方法实现,里面大量利用到Canvas,Canvas的使用可以参考下我以前这篇笔记。

1.初始化属性

由于前面的属性定义太多了,所以这里不列出来,后面要用到的属性会有介绍,想详细了解的可以看GitHub的介绍文档,那里有个表详细介绍。这里定义初始化方法,用来配置画笔和设置Gradient渐变器,由于Gradient需要进度条的宽高,所以要在Measure过程之后才配置:

    //初始化
    private void init() {
        //初始化画笔
        paintPicture = new Paint();

        paintBackGround = new Paint();
        paintBackGround.setColor(backGroundColor);

        paintBar = new Paint();
        paintBar.setColor(barColor);

        
        if (isGradient) {
            //在PreDraw时获取View属性,因为在初始化的时候View还没进行Measure
            getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    getViewTreeObserver().removeOnPreDrawListener(this);
                    linearGradient = new LinearGradient(0, progressHeight / 2, progressWidth, progressHeight / 2, gradientStartColor, gradientEndColor, Shader.TileMode.CLAMP);
                    paintBar.setShader(linearGradient);
                    return false;
                }
            });
        }
    }

2.画进度条

首先就是要画好进度条,Android源码自带的ProgressBar是基于事件机制来刷新View的,也就是每当有进度改变才会调用刷新View的方法,但是因为我这里要实现动画而且对怎么实现事件机制不是很熟(后面学),所以采用了定时刷新的方法,先把进度条画出来:

    //画进度条
    private void drawBar(Canvas canvas){
        if (isRound) {
            //画圆角矩形
            rectFBG.set(0, y - progressHeight / 2 + progressHeightOffset,
                    progressWidth, y + progressHeight / 2 + progressHeightOffset);
            canvas.drawRoundRect(rectFBG, roundX, roundY, paintBackGround);
            rectFPB.set(0, y - progressHeight / 2 + progressHeightOffset,
                    x, y + progressHeight / 2 + progressHeightOffset);
            canvas.drawRoundRect(rectFPB, roundX, roundY, paintBar);
        } else {
            //画矩形
            rectFBG.set(0, 0, getWidth(), getHeight());
            canvas.drawRect(rectFBG, paintBackGround);
            canvas.drawRect(0, 0, x, getHeight(), paintBar);
        }
    }

简单说一下上面的一些属性:

  • isRound是决定进度条是否圆角的boolean变量,由于觉得不是圆角的进度条有点难看,所以就最终发布时默认初始设置是true。
  • progressWidth和progressHeight是进度条的宽高,而不是整个View的宽高,因为View是包括进度条和图片,要为图片的显示预留空间,所以进度条宽高会在onMeasure()根据属性设置来定义大小(具体怎么定义后面说)。
  • x和y是当前进度的中心点坐标位置。
  • progressHeightOffset是进度条的所处高度偏移量,负数为向上偏移,正数为向下偏移。前面就说过progressHeight是进度条的宽高不一定是整个View的宽高,所以进度条可以处于一个自定义的位置(目前仅仅是高度,因为一般都不用设置宽度)。具体的效果可以看前面demo效果的第一个,进度条就是向下偏移而实现了被可爱的丘比龙踩在脚下的效果。


3.画图片Drawable

接下来是画图片Drawable的方法,这个Drawable可以是图片或者是Shape,根据当前进度的中心点坐标x、y和图片的半宽高属性halfDrawableWidth、halfDrawableHeight来实现,其中drawableHeightOffset是图片的高度偏移量:

    //画图
    private void drawPicture(Canvas canvas) {
        if (drawable == null && animMode != ANIM_NULL){
            Log.e(TAG,"drawable is null");
            return;
        }
        drawable.setBounds(x - halfDrawableWidth,
                getHeight() / 2 - halfDrawableHeight + drawableHeightOffset,
                x + halfDrawableWidth,
                getHeight() / 2 + halfDrawableHeight + drawableHeightOffset);
        drawable.draw(canvas);
    }

4.画动画:

先对是否开启动画,当前动画模式做出判断,实现5个动画模式:

animMode模式 意义
ANIM_NULL 无动画模式
ANIM_ROTATE 旋转动画模式
ANIM_SCALE 缩放动画模式
ANIM_ROTATE_SCALE 旋转加缩放动画模式
ANIM_FRAME 帧动画模式
    //画动画
    private void drawAnimPicture(Canvas canvas) {
        if (isAnimRun) {
            switch (animMode) {
                case ANIM_NULL:
                    drawPicture(canvas);
                    break;
                case ANIM_ROTATE:
                    rotateCanvas(canvas);
                    drawPicture(canvas);
                    break;
                case ANIM_SCALE:
                    scaleCanvas(canvas);
                    drawPicture(canvas);
                    break;
                case ANIM_ROTATE_SCALE:
                    rotateCanvas(canvas);
                    scaleCanvas(canvas);
                    drawPicture(canvas);
                    break;
                case ANIM_FRAME:
                    drawable = getResources().getDrawable(drawableIds[frameIndex]);
                    drawPicture(canvas);
                    if (frameIndex >= drawableIds.length - 1){
                        frameIndex = 0;
                    }else {
                        frameIndex++;
                    }
                    break;
            }
        } else {
                drawPicture(canvas);
        }
    }

实现帧动画是通过轮播图片的方法实现。
  实现旋转,缩放的效果,是采用操纵画布Canvas的方法来实现:

    //旋转画布
    private void rotateCanvas(Canvas canvas) {
        canvas.rotate(rotateDegree % 360, x, y + drawableHeightOffset);
        rotateDegree += rotateRate;
    }

    //伸缩画布
    private void scaleCanvas(Canvas canvas) {
        if (scaleLevel >= scaleMax) {
            isScaleIncrease = false;
        } else if (scaleLevel <= scaleMin) {
            isScaleIncrease = true;
        }
        if (isScaleIncrease) {
            scaleLevel += scaleRate;
        } else {
            scaleLevel -= scaleRate;
        }
        canvas.scale(scaleLevel, scaleLevel, x, y + drawableHeightOffset);
    }

由于drawAnimPicture()方法之后并没有其他使用Canvas的方法了,所以这里不用Canvas.save()和Canvas.restore()来使Canvas恢复到初始状态了,这里说明一下,免得后面有功能拓展的需要加代码时候忘了。

5.重写onMeasure()

重写onMeasure()的意义:让View支持wrap_content,还有设置了进度条的宽高(前面说过,进度条的宽高不一定等于整个View的宽高):

    //重写onMeasure,以自定义获取进度条的宽高
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);


        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            //在这里实现计算需要wrap_content时需要的宽
            width = halfDrawableWidth * 2;
        }
        if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            //在这里实现计算需要wrap_content时需要的高
            height = halfDrawableHeight * 2;
        }

        progressWidth = width;
        //如果不是自定义设置进度条高度,就直接把高度当作进度条高度
        if (!isSetBar) {
            progressHeight = height;
        }

        //如果有图片,就为图片预留空间
        if (drawable != null) {
            progressWidth = width - halfDrawableWidth;
        }

        //传入处理后的宽高
        setMeasuredDimension(width, height);
    }

6.封装

自定义属性

为了让自定义View的属性能直接通过XML设置,需要用到自定义属性,在res/value文件夹里新建一个attrs.xml(名字随便,建立位置对就行),定义自己所需的属性和相应类型:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PictureProgressBar">
        <attr name="backGroundColor" format="color"/>
        <attr name="barColor" format="color"/>
        <attr name="drawable" format="reference"/>
        <attr name="halfDrawableWidth" format="dimension"/>
        <attr name="halfDrawableHeight" format="dimension"/>
        <attr name="drawableHeightOffset" format="dimension"/>
        <attr name="isRound" format="boolean"/>
        <attr name="roundX" format="dimension"/>
        <attr name="roundY" format="dimension"/>
        <attr name="progress" format="integer"/>
        <attr name="max" format="integer"/>
        <attr name="isSetBar" format="boolean"/>
        <attr name="progressHeight" format="dimension"/>
        <attr name="progressHeightOffset" format="dimension"/>
        <attr name="refreshTime" format="integer"/>
        <attr name="animMode" format="enum">
            <enum name="ANIM_NULL" value="0"/>
            <enum name="ANIM_ROTATE" value="1"/>
            <enum name="ANIM_SCALE" value="2"/>
            <enum name="ANIM_ROTATE_SCALE" value="3"/>
            <enum name="ANIM_FRAME" value="4"/>
        </attr>
        <attr name="rotateRate" format="integer"/>
        <attr name="rotateDegree" format="integer"/>
        <attr name="scaleMax" format="float"/>
        <attr name="scaleMin" format="float"/>
        <attr name="scaleRate" format="float"/>
        <attr name="isGradient" format="boolean"/>
        <attr name="gradientStartColor" format="color"/>
        <attr name="gradientEndColor" format="color"/>
    </declare-styleable>
</resources>

一些set、get方法和其他

有了自定义属性,只是能在XML上使用,想要在Java代码上设置属性,还需要弄一些set、get方法,还有一些特殊的属性,在xml设置不了,如帧动画的图片id数组、线性渐变器、进度监听器等,也需要set方法,因为有太多,下面只列举一些特殊的出来:


    //设置进度
    public void setProgress(int progress) {
        if (progress <= max) {
            this.progress = progress;
        } else if (progress < 0){
            this.progress = 0;
        }
        else {
            this.progress = max;
        }
        doProgressRefresh();
    }
    
    //进行进度改变之后的操作
    private synchronized void doProgressRefresh() {
        if (onProgressChangeListener != null) {
            onProgressChangeListener.onOnProgressChange(progress);
            if (progress >= max) {
                onProgressChangeListener.onOnProgressFinish();
            }
        }
    }
    

    //设置动画开关
    public void setAnimRun(boolean isAnimRun) {
        this.isAnimRun = isAnimRun;
    }

    //设置帧动画时要传入的图片ID数组
    public void setDrawableIds(int[] drawableIds) {
        this.drawableIds = drawableIds;
    }

    //设置图片
    public void setPicture(int id) {
        drawable = getResources().getDrawable(id);
    }

    //设置颜色渐变器
    public void setLinearGradient(LinearGradient linearGradient) {
        this.linearGradient = linearGradient;
    }

     //设置进度监听器
    public void setOnProgressChangeListener(OnProgressChangeListener onProgressChangeListener) {
        this.onProgressChangeListener = onProgressChangeListener;
    }

    //进度监听器
    public interface OnProgressChangeListener {
        //进度改变时的回调
        public void onOnProgressChange(int progress);
        //进度完成时的回答
        public void onOnProgressFinish();
    }

7.发布

发布到GitHub

这个我相信大家都很熟悉,我这里就简单的列出一下我把这个项目上传到GitHub(我是使用AndroidStudio上自带的功能,简单方便)的步骤吧:

  1. 首先是本地要安装git,没有的可以下载
  2. 在AndroidStudio的File的Setting选项配置Git信息


  3. 在Setting配置你的GitHub账号信息,没有的可以注册,那里有按钮(我这个是AndroidStudio2.2版本,不同的版本可能里面的内容有一点不同),输入完账号密码按Test连接一下,失败的确认好账号密码多试几次,把timeout设置长点


  4. 在VCS里点击如下(一个项目第一次提交时用这个),然后按提示操作commit就行了(因为我这个不是第一次提交就不演示下去了):


  5. 后面更新项目就是点击Commit按钮了,然后push了:


      其实到了第4步,在GitHub上就能看到我们的项目了(免得太多图就不贴了):https://github.com/totond/PictureProgressBar

发布到JCenter

我们开发有时候会用到一些第三方库,有一些库很方便,在AS的Gradle里面compile 'xxx'就可以引入了,我们把项目提交到JCenter,也可以让别人很方便地引入了。发布到JCenter有很多方法,本方法是参考鸿洋_大神的bintray-release插件方法,之后亲自试了很多坑之后才成功的(鸿洋大神这篇方法有很多细节没说,具体看它下面的评论),可以说根据我这个步骤是肯定能成功的,下面我们来看看步骤:
1. 首先是在 https://bintray.com/signup/oss 进行注册,不要直接在官网注册,那是注册企业试用版的,到时候你还要自己点击右上角的倒计时取消企业版切换回个人版。注意注册的邮箱不能是qq邮箱,163邮箱(不知道是不是所有中国的都不行)。
2. 注册完之后首先就是查看自己的API,复制下来备用,后面的上传都要用到:


3. 建立一个仓库,用来装代码(填好Name,里面的Type选择Maven,其他的都不用管了,点Create就行):


4. 新建一个Module,把想要上传的代码放进去,上传就是上传这个Module(不这样做的话会在上传的时候报Error:Could not get unknown property 'main' for SourceSet container.)在这个Module的Gradle文件里面添加:

apply plugin: 'com.novoda.bintray-release'//最上方添加

        publish {
            userOrg = 'yanzhikaijky'        //Binary用户名
            repoName = 'CustomViewRepository'   //Repository的名字
            groupId = 'com.yanzhikaijky'        //包名
            artifactId = 'PictureProgressbar'   //项目名
            publishVersion = '1.1.0'            //版本号
            desc = 'a picture progressbar'      //description说明,随便写
            website = 'https://github.com/totond/PictureProgressBar'//VCS地址,这里最好写GitHub的,我试过不写然后上传不了
        }

** 5. 在项目的Gradle文件里添加(弄完就build一下project):**

//在dependencies里面加
classpath 'com.novoda:bintray-release:0.3.4'//添加bintray-release插件

//下面这个整个加进去
allprojects {
    repositories {
        jcenter()
    }
    //防止中文注释出错
    tasks.withType(Javadoc) {
        options {
            encoding "UTF-8"
            charSet 'UTF-8'
            links "http://docs.oracle.com/javase/7/docs/api"
        }
    }
}

** 6. 在AndroidStudio的Terminal里输入这些命令Key里面的星号的内容实际上应该是我们的API KEY,我这里屏蔽掉而已。注意每行命令之间用一个空格**隔开,这个很重要),最后回车提交:

gradlew.bat clean build bintrayUpload 
-PbintrayUser=yanzhikaijky 
-PbintrayKey=***************************** 
-PdryRun=false

  第一次提交会比较慢(翻墙可能会提高速度?),给点耐心,如果报错里面会显示出来。

7. 成功了之后,可以在网页上看到你的项目,左下可以看到如何添加依赖,右下角会有一个Add To jcenter(因为这个项目早已经add了,所以借用一下鸿洋大神的图片),本项目JCenter地址:https://bintray.com/yanzhikaijky/CustomViewRepository/PictureProgressbar

8.添加开源协议和说明文档README

添加开源协议

开源项目可以选择一个开源协议来表示自己这个项目的许可声明。关于开源协议的选择,我这里也引用阮一峰老师的一篇文章里面的一幅图来大概说明一下:


  最后我选择了Apache v2 License。至于如何添加,一个方法是在GitHub创建Repository的时候选择添加(这个我一开始就忘了),第二个是使用官方推荐的 www.addalicense.com 来给当前存在的Repository添加,然而

  这东西我用不了,只能采用第三种方法手动添加了:可以查看GitHub关于这个的帮助文档,看不懂英文没关系,隔壁有图说明:

  成功之后主页会多了一个表示开源协议的栏目:

添加说明文档Readme

说明文档就是告诉别人你的开源项目如何使用的文档,可以在Android Project的根目录下添加README.md文件然后Push上去或者直接在项目GitHub上Add a README。

这个就是PictureProgressBar的README.md

9.测试

在APP的Gradle里添加依赖项之后,就可以使用了:

    compile 'com.yanzhikaijky:PictureProgressbar:1.1.0'

这里的测试就是demo用了属性动画来控制进度变化(想了解属性动画的可以看下我这篇笔记),想详细看demo代码的,这里再次给出地址和效果:

后话

欢迎大家Star or Fork,使用Gradle依赖很方便,也可以clone来试着按自己想法修改一下,改一下gradlewrapper文件就可以直接在你们的AS上运行了,欢迎提出意见和建议。
  这个PictureProgressBar的后续可能会加入一些其他的动画和文字,目前没有什么好的创意,如果大家有什么创意和意见,欢迎互相交流进步。

更新

  • version 1.1.1:2017/07/07修复bug:
      加入了progressPercentage属性来表示进度条进度比例,修改了setProgress()内容,在里面加入
progressPercentage = progress/max;

防止在输入较大int数值的时候,计算进度操作导致int类型溢出的情况,如下载场景下的进度数值。

  • version 1.1.2:2017/09/07修复bug:
      progress/max得到0的结果,醉了过了两个月才发现。。。

  • version 1.2.0:2017/09/11更新:

    • 1.修复非圆角进度条宽度设置失效问题。
    • 2.进度条左方预留空间给图片,不会出现进度0%但是还是显示有加载了部分进度的情况。
    • 3.新增进度条的图片设置,类似官方ProgressBar的图片平铺设置功能。具体效果可看README。

最新版本

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,081评论 25 707
  • 冥想是一种古老的艺术。关于冥想可以追溯到存在任何历史记录之前的时期。几千年以来,各个宗教的圣贤都在不断地为它注入新...
    陈晓莲阅读 305评论 0 2
  • 每天大约都是8点多就醒了 只是今天突然不想起床 一赖就到了11点 刷牙洗脸绑头发 诶 昨晚没洗头诶 这是我19年来...
    倾卿已久阅读 99评论 0 0