Android 自定義圓形進(jìn)度條總結(jié)

版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。
微博:厲圣杰
微信公眾號(hào):牙鍋?zhàn)樱ɑ静桓?br> 源碼:CircleProgress
文中如有紕漏,歡迎大家留言指出。

最近擼了一個(gè)圓形進(jìn)度條的開(kāi)源項(xiàng)目,算是第一次完完整整的使用自定義 View 。在此對(duì)項(xiàng)目開(kāi)發(fā)思路做個(gè)小結(jié),歡迎大家 Star 和 Fork

該項(xiàng)目總共實(shí)現(xiàn)了三種圓形進(jìn)度條效果

  1. CircleProgress:圓形進(jìn)度條,可以實(shí)現(xiàn)仿 QQ 健康計(jì)步器的效果,支持配置進(jìn)度條背景色、寬度、起始角度,支持進(jìn)度條漸變
  2. DialProgress:類似 CircleProgress,但是支持刻度
  3. WaveProgress:實(shí)現(xiàn)了水波紋效果的圓形進(jìn)度條,不支持漸變和起始角度配置,如需此功能可參考 CircleProgress 自行實(shí)現(xiàn)。

先上效果圖,有圖才好說(shuō)。
CircleProgress 效果圖


a w300

DialProgress 和 WaveProgress 效果圖


b w300

恩,那么接下來(lái),就來(lái)講講怎么實(shí)現(xiàn)以上自定義進(jìn)度條的效果。

圓形進(jìn)度條

圓形進(jìn)度條是第一個(gè)實(shí)現(xiàn)的進(jìn)度條效果,用了我大半天的時(shí)間,實(shí)現(xiàn)起來(lái)并不復(fù)雜。

其思路主要可以分為以下幾步:

  1. View 的測(cè)量
  2. 計(jì)算繪制 View 所需參數(shù)
  3. 圓弧的繪制及漸變的實(shí)現(xiàn)
  4. 文字的繪制
  5. 動(dòng)畫(huà)效果的實(shí)現(xiàn)

首先,我們要測(cè)量出所繪制 View 的大小,即重寫(xiě) onMeasure() 方法,代碼如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   setMeasuredDimension(MiscUtil.measure(widthMeasureSpec, mDefaultSize),
           MiscUtil.measure(heightMeasureSpec, mDefaultSize));
}

由于其他兩個(gè)進(jìn)度條類都需要實(shí)現(xiàn) View 的測(cè)量,這里對(duì)代碼進(jìn)行了封裝:

/**
* 測(cè)量 View
*
* @param measureSpec
* @param defaultSize View 的默認(rèn)大小
* @return 測(cè)量出來(lái)的 View 大小
*/
public static int measure(int measureSpec, int defaultSize) {
   int result = defaultSize;
   int specMode = View.MeasureSpec.getMode(measureSpec);
   int specSize = View.MeasureSpec.getSize(measureSpec);

   if (specMode == View.MeasureSpec.EXACTLY) {
       result = specSize;
   } else if (specMode == View.MeasureSpec.AT_MOST) {
       result = Math.min(result, specSize);
   }
   return result;
}

關(guān)于 View 測(cè)量可以看下這篇博客 Android 自定義View 中的onMeasure的用法

接下來(lái),在 onSizeChanged() 中計(jì)算繪制圓及文字所需的參數(shù),考慮到屏幕旋轉(zhuǎn)的情況,故未直接在 onMeasure() 方法中直接計(jì)算。這里以下面草圖來(lái)講解繪制計(jì)算過(guò)程中的注意事項(xiàng),圖丑,勿怪~

WechatIMG2 w300

圖中,外面藍(lán)色矩形為 View,里面黑色矩形為圓的外接矩形,藍(lán)色矩形和黑色矩形中間空白的地方為 View 的內(nèi)邊距(padding)。兩個(gè)藍(lán)色的圓其實(shí)是一個(gè)圓,代表圓的粗細(xì),這是因?yàn)?Android 在繪制圓或者圓弧的時(shí)候是圓的邊寬的中心與外接矩形相交,所以在計(jì)算的時(shí)候要考慮到內(nèi)邊距(padding) 和 圓與外接矩形的相交。

默認(rèn)不考慮圓弧的寬度,繪制出來(lái)的效果如下:


device-2017-03-02-071101 w300
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
   super.onSizeChanged(w, h, oldw, oldh);
   Log.d(TAG, "onSizeChanged: w = " + w + "; h = " + h + "; oldw = " + oldw + "; oldh = " + oldh);
   //求圓弧和背景圓弧的最大寬度
   float maxArcWidth = Math.max(mArcWidth, mBgArcWidth);
   //求最小值作為實(shí)際值
   int minSize = Math.min(w - getPaddingLeft() - getPaddingRight() - 2 * (int) maxArcWidth,
           h - getPaddingTop() - getPaddingBottom() - 2 * (int) maxArcWidth);
   //減去圓弧的寬度,否則會(huì)造成部分圓弧繪制在外圍
   mRadius = minSize / 2;
   //獲取圓的相關(guān)參數(shù)
   mCenterPoint.x = w / 2;
   mCenterPoint.y = h / 2;
   //繪制圓弧的邊界
   mRectF.left = mCenterPoint.x - mRadius - maxArcWidth / 2;
   mRectF.top = mCenterPoint.y - mRadius - maxArcWidth / 2;
   mRectF.right = mCenterPoint.x + mRadius + maxArcWidth / 2;
   mRectF.bottom = mCenterPoint.y + mRadius + maxArcWidth / 2;
   //計(jì)算文字繪制時(shí)的 baseline
   //由于文字的baseline、descent、ascent等屬性只與textSize和typeface有關(guān),所以此時(shí)可以直接計(jì)算
   //若value、hint、unit由同一個(gè)畫(huà)筆繪制或者需要?jiǎng)討B(tài)設(shè)置文字的大小,則需要在每次更新后再次計(jì)算
   mValueOffset = mCenterPoint.y - (mValuePaint.descent() + mValuePaint.ascent()) / 2;
   mHintOffset = mCenterPoint.y * 2 / 3 - (mHintPaint.descent() + mHintPaint.ascent()) / 2;
   mUnitOffset = mCenterPoint.y * 4 / 3 - (mUnitPaint.descent() + mUnitPaint.ascent()) / 2;
   updateArcPaint();
   Log.d(TAG, "onSizeChanged: 控件大小 = " + "(" + w + ", " + h + ")"
           + "圓心坐標(biāo) = " + mCenterPoint.toString()
           + ";圓半徑 = " + mRadius
           + ";圓的外接矩形 = " + mRectF.toString());
}

關(guān)于 Android 中文字繪制可以參考以下兩篇文章:

  1. Android 自定義View學(xué)習(xí)(三)——Paint 繪制文字屬性
  2. measureText() vs .getTextBounds()

以上,已經(jīng)基本完成了 View 繪制所需全部參數(shù)的計(jì)算。接下來(lái)就是繪制圓弧及文字了。

繪制圓弧需要用到 Canvas 的

// oval 為 RectF 類型,即圓弧顯示區(qū)域
// startAngle 和 sweepAngle  均為 float 類型,分別表示圓弧起始角度和圓弧度數(shù)。3點(diǎn)鐘方向?yàn)?度,順時(shí)針遞增
// 如果 startAngle < 0 或者 > 360,則相當(dāng)于 startAngle % 360
// useCenter:如果為 true 時(shí),在繪制圓弧時(shí)將圓心包括在內(nèi),通常用來(lái)繪制扇形
// 繪制圓弧的畫(huà)筆
drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint);

為了方便計(jì)算,繪制圓弧的時(shí)候使用了 Canvas 的 rotate() 方法,對(duì)坐標(biāo)系進(jìn)行了旋轉(zhuǎn)

private void drawArc(Canvas canvas) {
   // 繪制背景圓弧
   // 從進(jìn)度圓弧結(jié)束的地方開(kāi)始重新繪制,優(yōu)化性能
   canvas.save();
   float currentAngle = mSweepAngle * mPercent;
   canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);
   // +2 是因?yàn)槔L制的時(shí)候出現(xiàn)了圓弧起點(diǎn)有尾巴的問(wèn)題
   canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle + 2, false, mBgArcPaint);
   canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint);
   canvas.restore();
}

恩,圓環(huán)已經(jīng)繪制完成,那么接下來(lái)就是實(shí)現(xiàn)圓環(huán)的漸變,這里使用 SweepGradient 類。SweepGradient 可以實(shí)現(xiàn)從中心放射性漸變的效果,如下圖:

1344993412_1866

SweepGradient 類有兩個(gè)構(gòu)造方法,

/**
 * @param cx 渲染中心點(diǎn)x坐標(biāo)
 * @param cy 渲染中心點(diǎn)y坐標(biāo)
 * @param colors 圍繞中心渲染的顏色數(shù)組,至少要有兩種顏色值
 * @param positions 相對(duì)位置的顏色數(shù)組,可為null,  若為null,可為null,顏色沿漸變線均勻分布。一般不需要設(shè)置該參數(shù)
 /
public SweepGradient(float cx, float cy, int[] colors, float[] positions)

/**
 * @param cx 渲染中心點(diǎn)x坐標(biāo)
 * @param cy 渲染中心點(diǎn)y坐標(biāo)
 * @param color0 起始渲染顏色
 * @param color1 結(jié)束渲染顏色
 /
public SweepGradient(float cx, float cy, int color0, int color1)

這里我們選擇第一個(gè)構(gòu)造方法。由于設(shè)置漸變需要每次都創(chuàng)建一個(gè)新的 SweepGradient 對(duì)象,所以最好不要放到 onDraw 方法中去更新,最好在初始化的時(shí)候就設(shè)置好,避免頻繁創(chuàng)建導(dǎo)致內(nèi)存抖動(dòng)。

private void updateArcPaint() {
   // 設(shè)置漸變
   int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED};
   mSweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, null);
   mArcPaint.setShader(mSweepGradient);
}

這里還有一個(gè)值得注意的地方,草圖如下


WechatIMG3 w300

假設(shè),漸變顏色如下:

int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED, Color.BLUE};

因?yàn)?SweepGradient 漸變是 360 度的,所以如果你繪制的圓弧只有 270度,則藍(lán)色部分(圖中黑色陰影部分)的漸變就會(huì)不可見(jiàn)。

接下來(lái),就是文字的繪制了。文字繪制在上述提到的文章中已經(jīng)進(jìn)行了詳細(xì)的講解,這里就不再贅述。代碼如下:

private void drawText(Canvas canvas) {
   canvas.drawText(String.format(mPrecisionFormat, mValue), mCenterPoint.x, mValueOffset, mValuePaint);

   if (mHint != null) {
       canvas.drawText(mHint.toString(), mCenterPoint.x, mHintOffset, mHintPaint);
   }

   if (mUnit != null) {
       canvas.drawText(mUnit.toString(), mCenterPoint.x, mUnitOffset, mUnitPaint);
   }
}

最后,我們來(lái)實(shí)現(xiàn)進(jìn)度條的動(dòng)畫(huà)效果。這里我們使用 Android 的屬性動(dòng)畫(huà)來(lái)實(shí)現(xiàn)進(jìn)度更新。

private void startAnimator(float start, float end, long animTime) {
   mAnimator = ValueAnimator.ofFloat(start, end);
   mAnimator.setDuration(animTime);
   mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
       @Override
       public void onAnimationUpdate(ValueAnimator animation) {
           mPercent = (float) animation.getAnimatedValue();
           mValue = mPercent * mMaxValue;
           if (BuildConfig.DEBUG) {
               Log.d(TAG, "onAnimationUpdate: percent = " + mPercent
                       + ";currentAngle = " + (mSweepAngle * mPercent)
                       + ";value = " + mValue);
           }
           invalidate();
       }
   });
   mAnimator.start();
}

這里有兩個(gè)注意點(diǎn):

  1. 不要在 ValueAnimator.AnimatorUpdateListener 中輸出 Log,特別是動(dòng)畫(huà)調(diào)用頻繁的情況下,因?yàn)檩敵?Log 頻繁會(huì)生成大量 String 對(duì)象造成內(nèi)存抖動(dòng),當(dāng)然也可以使用 StringBuilder 來(lái)優(yōu)化。
  2. 關(guān)于 invalidate()postInvalidate() 兩者最本質(zhì)的前者只能在 UI 線程中使用,而后者可以在非 UI 線程中使用,其實(shí) postInvalidate() 內(nèi)部也是使用 Handler 實(shí)現(xiàn)的。

關(guān)于 Android 屬性動(dòng)畫(huà)可以參考:

  1. Android 屬性動(dòng)畫(huà)(Property Animation) 完全解析 (上)
  2. Android 屬性動(dòng)畫(huà)(Property Animation) 完全解析 (下)

補(bǔ)充:同一個(gè)屬性如何支持顏色和顏色數(shù)組

考慮到圓弧設(shè)置單色和漸變的區(qū)別,即單色只需要提供一種色值,而漸變至少需要提供兩種色值。可以有以下幾種解決方案:

  1. 定義兩個(gè)屬性,漸變的優(yōu)先級(jí)高于單色的。
  2. 定義一個(gè) format 為 string 屬性,以 #FFFFFF|#000000 形式提供色值
  3. 定義一個(gè) format 為 color|reference 的屬性,其中 reference 屬性指代漸變色的數(shù)組。

這里選用第三種方案,實(shí)現(xiàn)如下:

<!-- 圓形進(jìn)度條 -->
<declare-styleable name="CircleProgressBar">
    <!-- 圓弧顏色, -->
    <attr name="arcColors" format="color|reference" />
</declare-styleable>

<!-- colors.xml -->
<color name="green">#00FF00</color>
<color name="blue">#EE9A00</color>
<color name="red">#EE0000</color>
<!-- 漸變顏色數(shù)組 -->
<integer-array name="gradient_arc_color">
   <item>@color/green</item>
   <item>@color/blue</item>
   <item>@color/red</item>
</integer-array>

<!-- 布局文件中使用 -->
<!-- 使用漸變 -->
<com.littlejie.circleprogress.DialProgress
    android:id="@+id/dial_progress_bar"
    android:layout_width="300dp"
    android:layout_height="300dp"
    app:arcColors="@array/gradient_arc_color" />
<!-- 使用單色 -->    
<com.littlejie.circleprogress.DialProgress
    android:id="@+id/dial_progress_bar"
    android:layout_width="300dp"
    android:layout_height="300dp"
    app:arcColors="@color/green" />

代碼中讀取 xml 中配置:

int gradientArcColors = typedArray.getResourceId(R.styleable.CircleProgressBar_arcColors, 0);
   if (gradientArcColors != 0) {
       try {
           int[] gradientColors = getResources().getIntArray(gradientArcColors);
           if (gradientColors.length == 0) {//如果漸變色為數(shù)組為0,則嘗試以單色讀取色值
               int color = getResources().getColor(gradientArcColors);
               mGradientColors = new int[2];
               mGradientColors[0] = color;
               mGradientColors[1] = color;
           } else if (gradientColors.length == 1) {//如果漸變數(shù)組只有一種顏色,默認(rèn)設(shè)為兩種相同顏色
               mGradientColors = new int[2];
               mGradientColors[0] = gradientColors[0];
               mGradientColors[1] = gradientColors[0];
           } else {
               mGradientColors = gradientColors;
           }
       } catch (Resources.NotFoundException e) {
           throw new Resources.NotFoundException("the give resource not found.");
       }
   }

帶刻度進(jìn)度條

前面,詳細(xì)講了 CircleProgress 的繪制思路,接下來(lái)講 DialProgress。

實(shí)話說(shuō),DialProgress 與 CircleProgress 的實(shí)現(xiàn)極其相似,因?yàn)閮烧咧g其實(shí)就差了一個(gè)刻度,但考慮到擴(kuò)展以及類職責(zé)的單一,所以將兩者分開(kāi)。

這里主要講一下刻度的繪制。刻度繪制主要使用 Canvas 類的 save()、rotate()restore() 方法,當(dāng)然你也可以使用 translate() 方法對(duì)坐標(biāo)系進(jìn)行平移,方便計(jì)算。

/**
 * 用來(lái)保存Canvas的狀態(tài)。save之后,可以調(diào)用Canvas的平移、放縮、旋轉(zhuǎn)、錯(cuò)切、裁剪等操作。
 */
public void save()

/**
 * 旋轉(zhuǎn)一定的角度繪制圖像
 * @param degrees 旋轉(zhuǎn)角度
 * @param x 旋轉(zhuǎn)中心點(diǎn)x軸坐標(biāo)
 * @param y 旋轉(zhuǎn)中心點(diǎn)y軸坐標(biāo)
 */
public void rotate(float degrees, float x, float y)

/**
 * 在當(dāng)前的坐標(biāo)上平移(x,y)個(gè)像素單位
 * 若dx <0 ,沿x軸向上平移; dx >0  沿x軸向下平移
 * 若dy <0 ,沿y軸向上平移; dy >0  沿y軸向下平移
 */
public void translate(float dx, float dy)

/**
 * 用來(lái)恢復(fù)Canvas之前保存的狀態(tài)。防止save后對(duì)Canvas執(zhí)行的操作對(duì)后續(xù)的繪制有影響。
 */
public void restore()
private void drawDial(Canvas canvas) {
   int total = (int) (mSweepAngle / mDialIntervalDegree);
   canvas.save();
   canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);
   for (int i = 0; i <= total; i++) {
       canvas.drawLine(mCenterPoint.x + mRadius, mCenterPoint.y, mCenterPoint.x + mRadius + mArcWidth, mCenterPoint.y, mDialPaint);
       canvas.rotate(mDialIntervalDegree, mCenterPoint.x, mCenterPoint.y);
   }
   canvas.restore();
}

關(guān)于 Canvas 的畫(huà)布操作可以參考這篇文章:安卓自定義View進(jìn)階-Canvas之畫(huà)布操作

水波紋效果的進(jìn)度條

水波紋效果的進(jìn)度條實(shí)現(xiàn)需要用到貝塞爾曲線,主要難點(diǎn)在于 繪制區(qū)域的計(jì)算波浪效果 的實(shí)現(xiàn),其余的邏輯跟上述兩種進(jìn)度條相似。

這里使用了 Path 類,該類在 Android 2D 繪圖中是非常重要的,Path 不僅能夠繪制簡(jiǎn)單圖形,也可以繪制這些比較復(fù)雜的圖形。也可以對(duì)多個(gè)路徑進(jìn)行布爾操作,類似設(shè)置 Paint 的 setXfermode() ,具體使用可以參考這篇博客:安卓自定義View進(jìn)階-Path基本操作。這里就不再贅述,有機(jī)會(huì)自己也會(huì)對(duì) Android 自定義 View 的知識(shí)進(jìn)行總結(jié),不過(guò),感覺(jué)應(yīng)該了了無(wú)期。

繼續(xù)上示意圖,請(qǐng)叫我靈魂畫(huà)手~


WechatIMG1

圖中黑色的圓為我們要繪制的進(jìn)度條圓,黑色的曲線為初始狀態(tài)的的波浪,該波浪使用貝塞爾曲線繪制,其中奇數(shù)的點(diǎn)為貝塞爾曲線的起始點(diǎn),偶數(shù)的點(diǎn)為貝塞爾曲線的控制點(diǎn)。例如:1——>2——>3就為一條貝塞爾曲線,1 是起點(diǎn),2 是控制點(diǎn),3 是終點(diǎn)。從圖中可以看到波浪在園內(nèi)圓外各一個(gè)(1—>5 和 5->9),通過(guò)對(duì)波浪在 x 軸上做平移,即圖中藍(lán)色實(shí)線,來(lái)實(shí)現(xiàn)波浪的動(dòng)態(tài)效果,所以一個(gè)波浪的完整動(dòng)畫(huà)效果需要有兩個(gè)波浪來(lái)實(shí)現(xiàn)。同理,通過(guò)控制 y 軸的偏移量,即圖中藍(lán)色虛線,可以實(shí)現(xiàn)波浪隨進(jìn)度的上漲下降。

貝塞爾曲線上起始點(diǎn)和控制點(diǎn)的計(jì)算如下:

/**
 * 計(jì)算貝塞爾曲線上的起始點(diǎn)和控制點(diǎn)
 * @param waveWidth 一個(gè)完整波浪的寬度
 */
private Point[] getPoint(float waveWidth) {
   Point[] points = new Point[mAllPointCount];
   //第1個(gè)點(diǎn)特殊處理,即數(shù)組的中心
   points[mHalfPointCount] = new Point((int) (mCenterPoint.x - mRadius), mCenterPoint.y);
   //屏幕內(nèi)的貝塞爾曲線點(diǎn)
   for (int i = mHalfPointCount + 1; i < mAllPointCount; i += 4) {
       float width = points[mHalfPointCount].x + waveWidth * (i / 4 - mWaveNum);
       points[i] = new Point((int) (waveWidth / 4 + width), (int) (mCenterPoint.y - mWaveHeight));
       points[i + 1] = new Point((int) (waveWidth / 2 + width), mCenterPoint.y);
       points[i + 2] = new Point((int) (waveWidth * 3 / 4 + width), (int) (mCenterPoint.y + mWaveHeight));
       points[i + 3] = new Point((int) (waveWidth + width), mCenterPoint.y);
   }
   //屏幕外的貝塞爾曲線點(diǎn)
   for (int i = 0; i < mHalfPointCount; i++) {
       int reverse = mAllPointCount - i - 1;
       points[i] = new Point(points[mHalfPointCount].x - points[reverse].x,
               points[mHalfPointCount].y * 2 - points[reverse].y);
   }
   return points;
}

以上,我們已經(jīng)獲取到繪制貝塞爾曲線所需的路徑點(diǎn)。接下來(lái),我們就需要來(lái)計(jì)算出繪制區(qū)域,即使用 Path 類。

WechatIMG1

紫色區(qū)域?yàn)樨惾麪柷€需要繪制的整體區(qū)域。

WechatIMG1

紅色區(qū)域?yàn)樯蠄D紫色區(qū)域與圓的交集,也就是波浪要顯示的區(qū)域

代碼如下:

//該方法必須在 Android 19以上的版本才能使用(Path.op())
@TargetApi(Build.VERSION_CODES.KITKAT)
private void drawWave(Canvas canvas, Paint paint, Point[] points, float waveOffset) {
   mWaveLimitPath.reset();
   mWavePath.reset();
   //lockWave 用于判斷波浪是否隨進(jìn)度條上漲下降
   float height = lockWave ? 0 : mRadius - 2 * mRadius * mPercent;
   //moveTo和lineTo繪制出水波區(qū)域矩形
   mWavePath.moveTo(points[0].x + waveOffset, points[0].y + height);

   for (int i = 1; i < mAllPointCount; i += 2) {
       mWavePath.quadTo(points[i].x + waveOffset, points[i].y + height,
               points[i + 1].x + waveOffset, points[i + 1].y + height);
   }
   mWavePath.lineTo(points[mAllPointCount - 1].x, points[mAllPointCount - 1].y + height);
   //不管如何移動(dòng),波浪與圓路徑的交集底部永遠(yuǎn)固定,否則會(huì)造成上移的時(shí)候底部為空的情況
   mWavePath.lineTo(points[mAllPointCount - 1].x, mCenterPoint.y + mRadius);
   mWavePath.lineTo(points[0].x, mCenterPoint.y + mRadius);
   mWavePath.close();
   mWaveLimitPath.addCircle(mCenterPoint.x, mCenterPoint.y, mRadius, Path.Direction.CW);
   //取該圓與波浪路徑的交集,形成波浪在圓內(nèi)的效果
   mWaveLimitPath.op(mWavePath, Path.Op.INTERSECT);
   canvas.drawPath(mWaveLimitPath, paint);
}

以上,就實(shí)現(xiàn)了水波動(dòng)態(tài)的效果,當(dāng)然,你也可以通過(guò)配置,來(lái)設(shè)定水波是否隨進(jìn)度上漲下降。為了實(shí)現(xiàn)更好的效果,可以設(shè)置一個(gè)淺色的水波并支持設(shè)置水波的走向(R2L 和 L2R),通過(guò)設(shè)置淺色波浪和深色波浪動(dòng)畫(huà)的時(shí)間,從而實(shí)現(xiàn)長(zhǎng)江后浪推前浪的效果,恩,效果很自然的~自己腦補(bǔ)從右至左波浪的實(shí)現(xiàn)和貝塞爾點(diǎn)的計(jì)算。

對(duì)獲取坐標(biāo)點(diǎn)的代碼進(jìn)行優(yōu)化:

/**
* 從左往右或者從右往左獲取貝塞爾點(diǎn)
*
* @return
*/
private Point[] getPoint(boolean isR2L, float waveWidth) {
   Point[] points = new Point[mAllPointCount];
   //第1個(gè)點(diǎn)特殊處理,即數(shù)組的中點(diǎn)
   points[mHalfPointCount] = new Point((int) (mCenterPoint.x + (isR2L ? mRadius : -mRadius)), mCenterPoint.y);
   //屏幕內(nèi)的貝塞爾曲線點(diǎn)
   for (int i = mHalfPointCount + 1; i < mAllPointCount; i += 4) {
       float width = points[mHalfPointCount].x + waveWidth * (i / 4 - mWaveNum);
       points[i] = new Point((int) (waveWidth / 4 + width), (int) (mCenterPoint.y - mWaveHeight));
       points[i + 1] = new Point((int) (waveWidth / 2 + width), mCenterPoint.y);
       points[i + 2] = new Point((int) (waveWidth * 3 / 4 + width), (int) (mCenterPoint.y + mWaveHeight));
       points[i + 3] = new Point((int) (waveWidth + width), mCenterPoint.y);
   }
   //屏幕外的貝塞爾曲線點(diǎn)
   for (int i = 0; i < mHalfPointCount; i++) {
       int reverse = mAllPointCount - i - 1;
       points[i] = new Point((isR2L ? 2 : 1) * points[mHalfPointCount].x - points[reverse].x,
               points[mHalfPointCount].y * 2 - points[reverse].y);
   }
   //對(duì)從右向左的貝塞爾點(diǎn)數(shù)組反序,方便后續(xù)處理
   return isR2L ? MiscUtil.reverse(points) : points;
}

至此,自定義圓形進(jìn)度條相關(guān)的思路已全部講述完成。代碼已全部上傳至 Git ,歡迎大家 Star 和 Fork,傳送門(mén):CircleProgress。

如有不清楚或者錯(cuò)誤的地方,歡迎指出~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容