自定義進度條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閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,723評論 25 708
  • 冥想是一種古老的藝術。關于冥想可以追溯到存在任何歷史記錄之前的時期。幾千年以來,各個宗教的圣賢都在不斷地為它注入新...
    陳曉蓮閱讀 324評論 0 2
  • 每天大約都是8點多就醒了 只是今天突然不想起床 一賴就到了11點 刷牙洗臉綁頭發 誒 昨晚沒洗頭誒 這是我19年來...
    傾卿已久閱讀 106評論 0 0