Android 自訂View之BounceProgressBar

來源:互聯網
上載者:User

標籤:android   style   blog   http   io   ar   color   os   使用   

之前幾天下載了很久沒用了的案頭版酷狗來用用的時候,發現其中載入歌曲的等待進度條的效果不錯(個人感覺),如下:




然後趁著這周末兩天天氣較冷,窩在宿舍放下成堆的作業系統作業(目測要抄一節多課的一堆堆文字了啊...啊..)毅然決定把它鼓搗出來,最終的效果如下(總感覺有點不和諧啊·):




對比能看出來的就是多了形狀的選擇還有使用圖片了,那麼接下來就是它的實現過程。

對自訂View實現還不明白的建議看下郭神的部落格(View系列4篇): Android LayoutInflater原理分析,帶你一步步深入瞭解View(一) 和大苞米的這篇:ANDROID自訂視圖——onMeasure,MeasureSpec源碼 流程 思路詳解


自訂屬性

自訂View一般都要用到view本身的屬性了,重寫現有的控制項則不用。額,然後我們的這個BounceProgressBar需要什麼特有的屬性呢?首先要明確的是這裡BounceProgressBar沒有提供具體進度表現的實現的。再具體想想:它需要每個映像的大小,叫singleSrcSize,類型就是dimension了;上下跳動的速度,叫speed,類型為integer;形狀,叫shape,類型為枚舉類型,提供這幾個形狀的實現,original、circle、pentagon、rhombus、heart都是見名知意的了;最後是需要的圖片資源,叫src,類型為reference|color,即可以是drawable裡的圖片或顏色值。

有了需要的屬性後,在values檔案夾下建個資源檔(名字隨意,見名知意就好)來定義這些屬性了,如下,代碼可能有些英文,而且水平有些渣,不過一般前面都會解釋了的:

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="BounceProgressBar">        <!-- the single child size -->        <attr name="singleSrcSize" format="dimension" />        <!-- the bounce animation one-way duration -->        <attr name="speed" format="integer" />        <!-- the child count ,本來還想能自訂個數的,但是暫時個人實現起來有些麻煩,所以先不加這個-->        <!-- <attr name="count" format="integer" min="1" /> -->        <!-- the progress child shape -->        <attr name="shape" format="enum">            <enum name="original" value="0" />            <enum name="circle" value="1" />            <enum name="pentagon" value="2" />            <enum name="rhombus" value="3" />            <enum name="heart" value="4" />        </attr>        <!-- the progress drawable resource -->        <attr name="src" format="reference|color"></attr>    </declare-styleable></resources>

然後先把BounceProgressBar類寫出來如下:

public class BounceProgressBar extends View {//...}

現在就可以在布局裡用我們的BounceProgressBar了,這裡需要注意的是,我們需要加上下面代碼第二行命名空間才能使用我們的屬性,也可以把它放到根項目的屬性裡。

        <org.roc.bounceprogressbar.BounceProgressBar            xmlns:bpb="http://schemas.android.com/apk/res-auto"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_centerHorizontal="true"            android:layout_centerVertical="true"            bpb:shape="circle"            bpb:singleSrcSize="8dp"            bpb:speed="250"            bpb:src="#6495ED" />

自訂了屬性最後我們要做的就是在代碼裡去擷取它了,在哪裡擷取呢,當然是BounceProgressBar類的構造方法裡了,相關代碼如下:

public BounceProgressBar(Context context) {this(context, null, 0);}public BounceProgressBar(Context context, AttributeSet attrs) {this(context, attrs, 0);}public BounceProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(attrs);}private void init(AttributeSet attrs) {if (null == attrs) {return;}TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.BounceProgressBar);speed = a.getInt(R.styleable.BounceProgressBar_speed, 250);size = a.getDimensionPixelSize(R.styleable.BounceProgressBar_singleSrcSize, 50);shape = a.getInt(R.styleable.BounceProgressBar_shape, 0);src = a.getDrawable(R.styleable.BounceProgressBar_src);a.recycle();}

得到屬性還是比較簡單的,記得把TypedArray回收掉。首先是獲得我們定義的TypedArray,然後是一個一個的去get屬性 值。然後可能有人要說了,我明明沒定義R.styleable.BounceProgressBar_xxx這些東西啊,其實呢這是Android自動給 我們產生的declare-styleable裡的每個屬性的在TypedArray裡的index對應位置的,你是找不到類似 R.styleable.speed這種東西存在的,它又是怎麼對應的呢,點進去看一下R檔案就知道 了,R.styleable.BounceProgressBar_speed的值是1,因為speed是第2個屬性(0,1..),所以你確定屬性的位 置直接寫a.getInt(1, 250)也是可以的。第二個參數是預設值。

圖形的形狀

得到屬性值後,我們就可以去做相應的處理操作了,這裡是圖形形狀的擷取,用到了shapesrcsize屬性,speed和size在下一點中也會講到。

首先我們觀察到三個圖片是有些漸層的效果的,我這裡只是簡單地做透明度處理,即一次變透明,效果是可以在處理好一點,可能之後再最佳化了。從src得到的圖片資源是Drawable的,無論是ColorDrawable或是BitmapDrawable。我們需要先把它轉換成size大小的Bitmap,再用canvas對它進行形狀裁剪操作。至於為什麼要先轉Bitmap呢,這是我的做法,再看完下面的操作後如果有更好的方式希望可以交流一下。

/** * Drawable → Bitmap(the size is "size") */private Bitmap drawable2Bitmap(Drawable drawable) {Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);drawable.setBounds(0, 0, size, size);drawable.draw(canvas);return bitmap;}

Bitmap得到了,形狀呢我們就可以進行操作了,我們先說圓形circle、菱形rhombus、五角星pentagon,再說心形heart,因為處理方式有些不同。像其它ShapeImageView我看到好像喜歡用svg來處理,看了他們的代碼,例如這個:https://github.com/siyamed/android-shape-imageview  貌似有些麻煩,相比之下我的處理比較簡單。


圓形circle、菱形rhombus、五角星pentagon

這些形狀都可以使用ShapeDrawable來得到。我們需要BitmapShader渲染器,這是ShapeDrawable的Paint畫筆需要的,再需要一個空的位元影像Bitmap,再一個 Canvas。如下:

BitmapShader bitmapShader = new BitmapShader(srcBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);Canvas canvas = new Canvas(bitmap);Path path;ShapeDrawable shapeDrawable = new ShapeDrawable();shapeDrawable.getPaint().setAntiAlias(true);shapeDrawable.getPaint().setShader(bitmapShader);shapeDrawable.setBounds(0, 0, size, size);shapeDrawable.setAlpha(alpha);

Canvas是ShapeDrawable上的畫 布,BitmapShader是ShapeDrawable畫筆Paint的的渲染器,用來渲染處理圖形(由src的drawable轉換得到的 bitmap),渲染模式選用了CLAMP,意思是 如果渲染器超出原始邊界範圍,會複製範圍內邊緣染色。

圓形呢,我們直接用現成的就可以:

shapeDrawable.setShape(new OvalShape());

這個ShapeDrawable畫出來的就是圓形了,當然要調用shapeDrawable.draw(canvas);方法了,這樣bitmap就會變成圓形的srcBitmap(方法傳進的參數)了,這方法的完整代碼後面給出。

菱形呢,我們則這樣子:

path = new Path();path.moveTo(size / 2, 0);path.lineTo(0, size / 2);path.lineTo(size / 2, size);path.lineTo(size, size / 2);path.close();shapeDrawable.setShape(new PathShape(path, size, size));

就是邊長為 size的正方形,取每條邊的中點,四個點連起來就是了。我們知道Android的座標一般都是螢幕左上方頂點為座標原點的,座標點找到了我們把path 串連起來即close。這樣PathShape就是一個菱形了。多邊形差不多都可以這麼畫的,下面的五角形也是一樣。說明:這裡所有圖形的繪製都是在邊長size的正方形裡。


五角形的原理也是用PathShape,只是它需要的座標點有點多啊,需要仔細計算慢慢調試。

path = new Path();// The Angle of the pentagramfloat radian = (float) (Math.PI * 36 / 180);float radius = size / 2;// In the middle of the radius of the pentagonfloat radius_in = (float) (radius * Math.sin(radian / 2) / Math.cos(radian));// The starting point of the polygonpath.moveTo((float) (radius * Math.cos(radian / 2)), 0);path.lineTo((float) (radius * Math.cos(radian / 2) + radius_in * Math.sin(radian)),(float) (radius - radius * Math.sin(radian / 2)));path.lineTo((float) (radius * Math.cos(radian / 2) * 2),(float) (radius - radius * Math.sin(radian / 2)));path.lineTo((float) (radius * Math.cos(radian / 2) + radius_in * Math.cos(radian / 2)),(float) (radius + radius_in * Math.sin(radian / 2)));path.lineTo((float) (radius * Math.cos(radian / 2) + radius * Math.sin(radian)),(float) (radius + radius * Math.cos(radian)));path.lineTo((float) (radius * Math.cos(radian / 2)), (float) (radius + radius_in));path.lineTo((float) (radius * Math.cos(radian / 2) - radius * Math.sin(radian)),(float) (radius + radius * Math.cos(radian)));path.lineTo((float) (radius * Math.cos(radian / 2) - radius_in * Math.cos(radian / 2)),(float) (radius + radius_in * Math.sin(radian / 2)));path.lineTo(0, (float) (radius - radius * Math.sin(radian / 2)));path.lineTo((float) (radius * Math.cos(radian / 2) - radius_in * Math.sin(radian)),(float) (radius - radius * Math.sin(radian / 2)));path.close();// Make these points closed polygonsshapeDrawable.setShape(new PathShape(path, size, size));

連線果然有點多啊。。這裡的繪製五角形是先根據指定的五角形的角的角度還有半徑,然後確定連線起點,再連下一點...最後封閉,一不小心就不知道連到哪去了。。

心形heart

path來畫心形就不能連直線實現了,剛開始是使用path的quadTo(x1, y1, x2, y2)方法來畫貝茲路徑來實現的,發現畫出來的形狀不飽滿,更像一個錐形(腦補),所以就放棄這種方式了。然後找到了這篇關於畫心形的介紹Heart Curve,然後就採用他的第四種方法(如),即採用兩個橢圓形狀來裁剪實現。



1、畫一個橢圓形狀

</pre><pre name="code" class="java">
</pre><pre name="code" class="java">   //canvas bitmap bitmapshader等,上面代碼已有   path = new Path();   Paint paint = new Paint();   paint.setAntiAlias(true);   paint.setShader(bitmapShader);   Matrix matrix = new Matrix(); //控制旋轉   Region region = new Region();//裁剪一段繪圖區域   RectF ovalRect = new RectF(size / 4, 0, size - (size / 4), size);   path.addOval(ovalRect, Path.Direction.CW);

 

</pre></p><p><img src="http://img.blog.csdn.net/20141118132526861"  /></p><p></p>2、旋轉圖形,大概45度左右<p><pre name="code" class="java">matrix.postRotate(42, size / 2, size / 2);path.transform(matrix, path);



3、選取旋轉後的右半部分圖形,並用cancas畫出這半邊的心形

path.transform(matrix, path);region.setPath(path, new Region((int) size / 2, 0, (int) size, (int) size));canvas.drawPath(region.getBoundaryPath(), paint);



4、重複1、2、3同時改變方向角度和裁剪的地區

matrix.reset();path.reset();path.addOval(ovalRect, Path.Direction.CW);matrix.postRotate(-42, size / 2, size / 2);path.transform(matrix, path);region.setPath(path, new Region(0, 0, (int) size / 2, (int) size));canvas.drawPath(region.getBoundaryPath(), paint);

這樣我們便完成心形圖片的裁剪工作了,得到的bitmap就變成心形了:

    這個心可以見人了。。
畫完心就該下一步了。



View的繪製

說到view的繪製過程就需要下面三部曲了:

  1. 繪製——onDraw():如何繪製這個View。


    測量

    對於BounceProgressBar控制項的測量還是比較簡單的,當wrap_content時高度和寬度分別為size的5倍和4倍,其它情況時就指定寬高為具體測量到的值就好。然後決定三個圖形在控制項之中的水平位置:

    @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);int modeWidth = MeasureSpec.getMode(widthMeasureSpec);int modeHeight = MeasureSpec.getMode(heightMeasureSpec);setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? mWidth = sizeWidth : mWidth,(modeHeight == MeasureSpec.EXACTLY) ? mHeight = sizeHeight : mHeight);firstDX = mWidth / 4 - size / 2;//第一個圖形的水平位置secondDX = mWidth / 2 - size / 2;//...thirdDX = 3 * mWidth / 4 - size / 2;//...}
    當有指定了具體值的寬高時,mWidth和mHeight也設定應為測量到的sizeWidth和sizeHeight。

    布局

    說到布局時先明確一點的是映像的跳動是通過屬性動畫來控制的,屬性動畫是什嗎?我一句話說一下就是:可以以動畫的效果形式去更改一個對象的某個屬性。還不太瞭解的可以先找找資料看一下。

    布局這裡就決定視圖裡的各種位置的操作了,作為單個控制項時一般不怎麼用到,我在這裡進行動畫的初始化並開始的操作了。可以看到我們的BounceProgressBar是三個圖形在跳動的。

    三個屬性的封裝如下:

    /** * firstBitmapTop‘s Property. The change of the height through canvas is * onDraw() method. */private Property<BounceProgressBar, Integer> firstBitmapTopProperty = new Property<BounceProgressBar, Integer>(Integer.class, "firstDrawableTop") {@Overridepublic Integer get(BounceProgressBar obj) {return obj.firstBitmapTop;}public void set(BounceProgressBar obj, Integer value) {obj.firstBitmapTop = value;invalidate();};};/** * secondBitmapTop‘s Property. The change of the height through canvas is * onDraw() method. */private Property<BounceProgressBar, Integer> secondBitmapTopProperty = new Property<BounceProgressBar, Integer>(Integer.class, "secondDrawableTop") {@Overridepublic Integer get(BounceProgressBar obj) {return obj.secondBitmapTop;}public void set(BounceProgressBar obj, Integer value) {obj.secondBitmapTop = value;invalidate();};};/** * thirdBitmapTop‘s Property. The change of the height through canvas is * onDraw() method. */private Property<BounceProgressBar, Integer> thirdBitmapTopProperty = new Property<BounceProgressBar, Integer>(Integer.class, "thirdDrawableTop") {@Overridepublic Integer get(BounceProgressBar obj) {return obj.thirdBitmapTop;}public void set(BounceProgressBar obj, Integer value) {obj.thirdBitmapTop = value;invalidate();};};
    onLayout部分的代碼如下:
    @Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);if (bouncer == null || !bouncer.isRunning()) {ObjectAnimator firstAnimator = initDrawableAnimator(firstBitmapTopProperty, speed, size / 2,mHeight - size);ObjectAnimator secondAnimator = initDrawableAnimator(secondBitmapTopProperty, speed, size / 2,mHeight - size);secondAnimator.setStartDelay(100);ObjectAnimator thirdAnimator = initDrawableAnimator(thirdBitmapTopProperty, speed, size / 2,mHeight - size);thirdAnimator.setStartDelay(200);bouncer = new AnimatorSet();bouncer.playTogether(firstAnimator, secondAnimator, thirdAnimator);bouncer.start();}}private ObjectAnimator initDrawableAnimator(Property<BounceProgressBar, Integer> property, int duration,int startValue, int endValue) {ObjectAnimator animator = ObjectAnimator.ofInt(this, property, startValue, endValue);animator.setDuration(duration);animator.setRepeatCount(Animation.INFINITE);animator.setRepeatMode(ValueAnimator.REVERSE);animator.setInterpolator(new AccelerateInterpolator());return animator;}
    動畫的值變換是從size到mHeight-size的,要減去size的原因是在canvas中,大於(mHeight, mHeight)的左邊已經view本身的大小範圍了。


    繪製

    繪製這裡做的工作不是很多,就是根據每個映像的水平位置,和通過屬性動畫控制的高度來去繪製bitmap在畫布上。

    @Overrideprotected synchronized void onDraw(Canvas canvas) {/* draw three bitmap */firstBitmapMatrix.reset();firstBitmapMatrix.postTranslate(firstDX, firstBitmapTop);secondBitmapMatrix.reset();secondBitmapMatrix.setTranslate(secondDX, secondBitmapTop);thirdBitmapMatrix.reset();thirdBitmapMatrix.setTranslate(thirdDX, thirdBitmapTop);canvas.drawBitmap(firstBitmap, firstBitmapMatrix, mPaint);canvas.drawBitmap(secondBitmap, secondBitmapMatrix, mPaint);canvas.drawBitmap(thirdBitmap, thirdBitmapMatrix, mPaint);}
    位置是通過Matrix來控制的,因為當時還考慮到落地的變形,但現在給去掉先了。

    總的來說繪製的流程是通過屬性動畫來控制每個映像在畫布上的位置,在屬性更改時調用invalidate()方法去通知重繪就行了,看起來就是跳動的效果了,跳動速度的變化則是給動畫設定插值器來完成。

Android 自訂View之BounceProgressBar

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.