一些“跳過(guò)”按鈕及緩沖框示例

前言

??最近自定義控件的實(shí)踐相對(duì)多一些,看到了別人app上實(shí)現(xiàn)的效果就想自己動(dòng)手嘗試下,看自己能不能做到。本文是對(duì)一些app第一個(gè)頁(yè)面的“跳過(guò)”按鈕及一些緩沖框的實(shí)現(xiàn)。一個(gè)控件就詳細(xì)寫(xiě)一篇文,未免過(guò)于麻煩,所以這里是做了一個(gè)匯總,只寫(xiě)核心思路及相關(guān)偽代碼,幾個(gè)控件寫(xiě)成一篇。后面會(huì)給出完整代碼。

1. 矩形倒計(jì)時(shí)“跳過(guò)”

??1. 先來(lái)看一下最終效果:
progressSet0.gif
??2. 基本思路及相關(guān)代碼

??首先,我們看到“跳過(guò)”這兩個(gè)字的背景是一個(gè)圓角矩形,而控件的形狀一般是矩形,這時(shí),我們把控件的背景設(shè)置成透明的,然后在控件上畫(huà)出圓角矩形就可以了。

private int mBgColor = Color.TRANSPARENT;
// 畫(huà)出控件背景色,為透明
canvas.drawColor(mBgColor)

//圓角矩形內(nèi)填充的顏色,默認(rèn)是半透明的黑色
private int mRectFillColor = 0x32000000;
//控件坐標(biāo)系移至控件中心位置
canvas.translate(mWidth / 2, mHeight / 2);
//在其上畫(huà)出圓角矩形,圓角大小由外界決定
mPaint.setColor(mRectFillColor);
RectF rectF = new RectF(-mWidth / 2, -mHeight / 2, mWidth / 2, mHeight / 2);
canvas.drawRoundRect(rectF, mCornerX, mCornerY, mPaint);

??控件坐標(biāo)系原點(diǎn)之所以移到控件的中心,是因?yàn)槲覀兯?huà)的內(nèi)容基本上都是基于控件中心坐標(biāo)的,移過(guò)去之后,我們更好確定我們所畫(huà)內(nèi)容的坐標(biāo)。mBgColor、mRectFillColor、mCornerX、mCornerY都可以通過(guò)設(shè)定其set方法由控件外決定其值。

??其次,“跳過(guò)”這兩個(gè)字以及倒計(jì)時(shí)的數(shù)字,我們希望它們能夠被視為一個(gè)整體,置于控件的中心。那我們要做的就是先計(jì)算出這兩字以及倒計(jì)時(shí)數(shù)字的寬度。另外,注意,一般“跳過(guò)”倆字跟倒計(jì)時(shí)數(shù)字是有間距的。

private int mTextSize = 14;
//估算出字體的寬度
mPaint.setTextSize(getResources().getDisplayMetrics().scaledDensity * mTextSize);
mPaint.setTextAlign(Paint.Align.LEFT);

Rect rect = new Rect();
mPaint.getTextBounds(text, 0, text.length(), rect);
float textWidth = rect.right - rect.left;

private int mNumberTextSize = 12;
//估算出數(shù)字的寬度
mPaint.setTextSize(getResources().getDisplayMetrics().scaledDensity * mNumberTextSize);
mPaint.setTextAlign(Paint.Align.LEFT);

Rect rect1 = new Rect();
mPaint.getTextBounds("3", 0, 1, rect1);
float numberWidth = rect1.right - rect1.left;

??計(jì)算“跳過(guò)”兩個(gè)字寬度、計(jì)算倒計(jì)時(shí)數(shù)字寬度的方法是一樣的,都是給畫(huà)筆設(shè)置了textSize,然后用畫(huà)筆的getTextBounds方法計(jì)算。算出了“跳過(guò)”兩字和倒計(jì)時(shí)數(shù)字的寬度,如果兩者有間距的話就能計(jì)算出這兩者看做一個(gè)整體時(shí),這個(gè)整體的寬度

//字體與數(shù)字之間的間距
private float mDivider = 15;
//總長(zhǎng)度,中間添加間距
float length = textWidth + mDivider + numberWidth;

??有了這個(gè)寬度就可以計(jì)算出,“跳出”和倒計(jì)時(shí)數(shù)字的基線坐標(biāo)了:

mPaint.setTextAlign(Paint.Align.LEFT);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
“跳過(guò)”兩字的基線坐標(biāo)(jumpLoc):(-length / 2,(-fontMetrics.top - fontMetrics.bottom) / 2)

mPaint.setTextAlign(Paint.Align.LEFT);
Paint.FontMetrics fontMetrics1 = mPaint.getFontMetrics();
倒計(jì)時(shí)數(shù)字的基線坐標(biāo)(numberLoc):(-length / 2 + textWidth + mDivider,(-fontMetrics1.top - fontMetrics1.bottom) / 2)

??最后,算出了基線坐標(biāo)就可以分別畫(huà)出“跳出”和倒計(jì)時(shí)數(shù)字了:

mPaint.setColor(mTextColor);
mPaint.setTextSize(getResources().getDisplayMetrics().scaledDensity * mTextSize);
canvas.drawText(text, jumpLoc.x, jumpLoc.y, mPaint);

mPaint.setColor(mNumberTextColor);
mPaint.setTextSize(getResources().getDisplayMetrics().scaledDensity * mNumberTextSize);
canvas.drawText(mDelayTime + "", numberLoc.x, numberLoc.y, mPaint);

??還有一點(diǎn),標(biāo)識(shí)倒計(jì)時(shí)數(shù)字的變量mDelayTime,本例是通過(guò)Timer完成倒計(jì)時(shí)。每過(guò)1秒,就向主線程發(fā)送一個(gè)消息,由主線程去控制mDelayTime的變化并重畫(huà)控件(invalidate)。這部分代碼看源碼吧,在ThreeSecondJump0類中。

2. 圓形倒計(jì)時(shí)“跳過(guò)”

??1. 最終效果如下:
progressSet1.gif
??2. 基本思路及相關(guān)代碼

??如上效果,是看了網(wǎng)易新聞的splash頁(yè)做的。首先,還是需要平移控件坐標(biāo)系,畫(huà)透明背景,把畫(huà)筆設(shè)置成填充模式畫(huà)一個(gè)圓,這些跟上面控件類似,在這里就不重復(fù)說(shuō)了。這里說(shuō)下面幾個(gè)點(diǎn):畫(huà)“跳過(guò)”兩字、畫(huà)動(dòng)態(tài)變化的紅色邊界。

??畫(huà)“跳過(guò)”兩字

??仔細(xì)看“跳過(guò)”兩個(gè)字,會(huì)發(fā)現(xiàn)它的大小基本上是跟圓邊界頂著的,我們需要設(shè)置合適的大小才能夠做到這樣的效果,那怎么才能獲取合適的大小呢?本例是通過(guò)循環(huán)得到的

do {
    mPaint.setTextSize(mTextSize);
    Rect rect = new Rect();
    mPaint.getTextBounds("跳過(guò)", 0, 2, rect);
    textWidth = rect.right - rect.left;
    textHeight = rect.bottom - rect.top;

    if (Math.pow(textWidth / 2, 2) + Math.pow(textHeight / 2, 2) > Math.pow(mCircleRadius, 2)) {
        mTextSize -= 0.5;
    } else {
        break;
    }

} while (true);

??使textSize逐漸縮小,直至字體的寬一半的平方與字體高一半的平方之和不再大于圓半徑的平方。仔細(xì)思考下,前后兩者的值正好相等時(shí),包含字體的矩形正好“頂著”外部的黑色圓邊界。從循環(huán)中跳出之后就正好是頂著圓邊界的textSize了,然后估算出基線坐標(biāo),畫(huà)出“跳出”兩個(gè)字就可以了

//字體的顏色
private int mTextColor = Color.WHITE;

mPaint.setColor(mTextColor);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
canvas.drawText("跳過(guò)", -textWidth / 2, (-fontMetrics.top - fontMetrics.bottom) / 2, mPaint);
??畫(huà)動(dòng)態(tài)變化的紅色邊界

??首先,畫(huà)邊界很簡(jiǎn)單,只需把畫(huà)筆模式設(shè)置為stroke,半徑與上述黑色圓半徑一樣,再設(shè)置上顏色和strokeWidth,基本就可以畫(huà)出這個(gè)紅色邊界了

private int mCircleStrokeColor = Color.RED;

mPaint.setColor(mCircleStrokeColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setStrokeCap(Paint.Cap.ROUND);

Path path = new Path();
path.addCircle(0, 0, mCircleRadius, Path.Direction.CW);
canvas.drawPath(path, mPaint);

??但上述代碼只是能畫(huà)出一個(gè)靜態(tài)的紅色邊界,自己不會(huì)動(dòng)的。我們還需要使用屬性動(dòng)畫(huà)及PathMeasure使之動(dòng)起來(lái)

...//省略上述Paint設(shè)置部分

Path path = new Path();
path.addCircle(0, 0, mCircleRadius, Path.Direction.CW);

//將此path交給一個(gè)PathMeasure對(duì)象
PathMeasure pathMeasure = new PathMeasure(path, false);
float length = pathMeasure.getLength();
Path pathDst = new Path();
//mCurrentValue,即為動(dòng)畫(huà)某一時(shí)刻的值
//getSegment可以獲取此path的片段
pathMeasure.getSegment(mCurrentValue * length, length, pathDst, true);
canvas.drawPath(pathDst, mPaint);

??上述就可以畫(huà)出一個(gè)動(dòng)態(tài)的紅色邊界了,但此邊界的起點(diǎn)是在x軸的正半軸上的,我們希望邊界起點(diǎn)在y軸的負(fù)半軸。也好解決,暫時(shí)將控件坐標(biāo)系逆時(shí)針旋轉(zhuǎn)90度,然后再畫(huà)path,畫(huà)完path之后再恢復(fù)控件坐標(biāo)系即可

canvas.save();

canvas.rotate(-90);

...//省略的內(nèi)容為上述畫(huà)path部分

canvas.restore();

??到此,起點(diǎn)在y軸負(fù)半軸、動(dòng)態(tài)變化的紅色邊界就畫(huà)完了。

3. 仿ios菊花緩沖圖標(biāo)

??最終效果圖如下:

[圖片上傳失敗...(image-28645-1513672793159)]

??基本思路及相關(guān)代碼

??模仿ios的緩沖圖標(biāo)來(lái)寫(xiě)的,但不知道ios那邊具體是如何實(shí)現(xiàn)的。我的實(shí)現(xiàn)效果總感覺(jué)不那么好,不知道是不是因?yàn)閳D片的問(wèn)題。下面是我的基本思路:

??簡(jiǎn)單來(lái)講,就是找了一張圖片,然后給這張圖片一個(gè)動(dòng)畫(huà)讓它不停地旋轉(zhuǎn)。那怎么使圖片不停地旋轉(zhuǎn)呢?本示例使用的Matrix的postRotate。下面一步一步來(lái)講,一開(kāi)始還是給控件畫(huà)透明背景,將控件坐標(biāo)系原點(diǎn)移到控件中心,這里不再多說(shuō)。接下來(lái)就是畫(huà)圖片了,畫(huà)圖片有幾個(gè)重載的方法,因?yàn)槲覀円玫骄仃嚨男D(zhuǎn),所以最終選擇了如下這個(gè)方法:

public void drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, @Nullable Paint paint)

??第一步,先從應(yīng)用資源中獲取所要旋轉(zhuǎn)的圖片

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.loading);

??調(diào)用drawBitmap時(shí),默認(rèn)圖片的左上角是跟坐標(biāo)系原點(diǎn)重合的,而我們是想讓圖片居中顯示,這就需要第二步,平移圖片將圖片中心與坐標(biāo)系原點(diǎn)重合為一點(diǎn)。我們且看drawBitmap的第二個(gè)參數(shù)Matrix,此矩陣會(huì)作用于bitmap的坐標(biāo),使bitmap完成平移、旋轉(zhuǎn)、縮放或者錯(cuò)切的操作。我們現(xiàn)在需要平移,需調(diào)用matrix的translate相關(guān)方法進(jìn)行操作:

Matrix matrix = new Matrix();
//向x軸負(fù)半軸和y軸負(fù)半軸平移寬度的一半、高度的一半
matrix.postTranslate(-bitmapW / 2, -bitmapH / 2);

??平移過(guò)后就可將圖片的中心與坐標(biāo)系的中心置于同一點(diǎn)了。平移之后,還會(huì)有一個(gè)問(wèn)題:控件的寬高是由控件外設(shè)置的,而圖片的大小是固定的,這就可能造成圖片太大在控件內(nèi)顯示不全或圖片太小不能夠很好地顯示在控件中。這就需要第三步,縮放圖片??s放到圖片的較大邊剛好跟控件的較小邊重合,當(dāng)然為了更好一些的顯示效果,我們可以讓這兩個(gè)邊留一些間距

//圖片的寬高是一樣的,所以只獲取一個(gè)寬就可以了
int bitmapWH = bitmap.getWidth();
//找出控件寬高較小的
int temp = mWidth > mHeight ? mHeight : mWidth;
int tempContent = temp - mPadding;

//控件的較小邊比圖片的寬或高大多少倍
float s = tempContent * 1.0f / bitmapWH;

Matrix matrix = new Matrix();
//這是上一步說(shuō)明過(guò)的平移
matrix.postTranslate(-bitmapWH / 2, -bitmapWH / 2);
matrix.postScale(s, s);
canvas.drawBitmap(bitmap, matrix, mPaint);

??計(jì)算出當(dāng)前控件的較小邊是當(dāng)前圖片寬度或高度的多少倍,然后調(diào)用postScale等比例縮放,至此縮放完畢。以上的三步也只是畫(huà)出了一個(gè)靜態(tài)的圖片并將其縮放到合適的寬高,而我們需要的是圖片旋轉(zhuǎn)起來(lái)。使圖片旋轉(zhuǎn)使用的就是matrix.postRotate這個(gè)方法,動(dòng)起來(lái)就需要使用屬性動(dòng)畫(huà)來(lái)不斷改變圖片旋轉(zhuǎn)的角度來(lái)實(shí)現(xiàn)了:

//畫(huà)圖片
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.loading);
//圖片的寬高是一樣的,所以只獲取一個(gè)寬就可以了
int bitmapWH = bitmap.getWidth();
//找出控件寬高較小的
int temp = mWidth > mHeight ? mHeight : mWidth;
int tempContent = temp - mPadding;

//控件的較小邊比圖片的寬或高大多少倍
float s = tempContent * 1.0f / bitmapWH;

Matrix matrix = new Matrix();
matrix.postTranslate(-bitmapWH / 2, -bitmapWH / 2);
matrix.postScale(s, s);
//此處的mCurrentValue為屬性動(dòng)畫(huà)0-1變化時(shí)某一個(gè)時(shí)刻的值
matrix.postRotate(360 * mCurrentValue);
canvas.drawBitmap(bitmap, matrix, mPaint);

??至此,旋轉(zhuǎn)緩沖控件完成。

4. 仿交通銀行緩沖框

??手里有一張交通的卡,然后就下載了交通銀行的app用,覺(jué)得他們的緩沖框挺有意思的,然后就想實(shí)現(xiàn)以下。

??最終效果圖如下
progressSet3.gif
??基本思路及相關(guān)代碼

??基本思路大致是這樣的:先使用Path畫(huà)兩個(gè)圓弧,使用某種方式讓這兩個(gè)圓弧的某一個(gè)點(diǎn)連接,然后為此Path創(chuàng)建一個(gè)PathMeasure對(duì)象,調(diào)用其getSegment方法獲取此path的某一個(gè)片段,最后使用屬性動(dòng)畫(huà)使此片段動(dòng)起來(lái)。

??首先,使用path畫(huà)兩個(gè)幾乎為圓的圓弧,并保證第一個(gè)圓弧的最后一個(gè)點(diǎn)可以跟第二個(gè)圓弧的第一個(gè)點(diǎn)連接。而在畫(huà)出第一個(gè)圓弧之前我們需要得到這些圓弧的半徑

//計(jì)算出單個(gè)圓的半徑
//當(dāng)所畫(huà)內(nèi)容的寬盡量填充控件的寬時(shí)單個(gè)圓的半徑
float radius0 = (mWidth - mPadding) * 1.0f / 4 - mStrokeWidth;
//當(dāng)所畫(huà)內(nèi)容的高盡量填充控件的高時(shí)單個(gè)圓的半徑
float radius1 = (mHeight - mPadding) * 1.0f / 2 - mStrokeWidth;
//較小值即為合適的半徑
float radius = radius0 > radius1 ? radius1 : radius0;

??說(shuō)明下,mPadding為所畫(huà)的內(nèi)容控件邊界的間距,mStrokeWidth為圓弧邊界的寬度。上述代碼的意思是,先計(jì)算出所畫(huà)內(nèi)容的寬高盡量填充控件寬高時(shí)的半徑,為了使所畫(huà)內(nèi)容始終保持在控件內(nèi),選擇這兩個(gè)半徑中的較小半徑。接下來(lái)就是,畫(huà)兩個(gè)圓弧了:

//透明背景
canvas.drawColor(mBgColor);

//控件坐標(biāo)系平移到控件中心
canvas.translate(mWidth / 2, mHeight / 2);

//畫(huà)出背景軌道
mPaint.setColor(mCirCleBgColor);
mPaint.setStrokeWidth(mStrokeWidth);

Path path = new Path();

RectF rectF = new RectF(0, -radius, 2 * radius, radius);
//這里雖然可以設(shè)置成-360,但設(shè)置成-360就會(huì)有問(wèn)題,多了一條線
path.addArc(rectF, 180, -359.99f);


RectF rectF1 = new RectF(-2 * radius, -radius, 0, radius);
path.arcTo(rectF1, 0, 359.99f);

canvas.drawPath(path, mPaint);

??前兩句代碼是畫(huà)出控件的透明背景、將控件坐標(biāo)系的原點(diǎn)移至控件的中心。之后設(shè)置畫(huà)筆的顏色及stroke寬度,接著是畫(huà)兩個(gè)圓弧,第一個(gè)圓弧是從180度開(kāi)始畫(huà)起,逆時(shí)針359.99度,結(jié)合包含此圓弧的矩形坐標(biāo)可知,這個(gè)圓弧的起始點(diǎn)是坐標(biāo)系原點(diǎn),最后一個(gè)點(diǎn)是一個(gè)十分接近坐標(biāo)系原點(diǎn)的點(diǎn),而畫(huà)第二個(gè)圓弧使用的是path.arcTo,這就能保證第二個(gè)圓弧的起始點(diǎn)與第一個(gè)圓弧的最后一個(gè)點(diǎn)連接(arcTo方法的特性)。第二個(gè)圓弧的起始點(diǎn)是坐標(biāo)系原點(diǎn),最后一個(gè)點(diǎn)是十分接近坐標(biāo)系原點(diǎn)的點(diǎn),并沒(méi)有連接在一起。

??這里有一個(gè)插曲,如果添加第一個(gè)圓弧到path時(shí)設(shè)置的不是-359.99f,而是-360f(劃過(guò)的角度是可以設(shè)置成-360f的),就會(huì)莫名其妙是多一條直線。此直線并不是第二個(gè)圓弧的最后一個(gè)點(diǎn)與第一個(gè)圓弧起始點(diǎn)的連線(之所以這么想,是因?yàn)橐詾閜ath自動(dòng)閉合了),所以,就比較詭異,只能設(shè)置成一個(gè)十分靠近-360的值了。

??其次,要做的是根據(jù)此path創(chuàng)建一個(gè)PathMeasure對(duì)象

//畫(huà)動(dòng)態(tài)的變化
//設(shè)置畫(huà)筆的顏色
mPaint.setColor(mCircleColor);

//根據(jù)上述path創(chuàng)建PathMeasure
PathMeasure pathMeasure = new PathMeasure(path, false);
//pathMeasure可以計(jì)算出上述path的總長(zhǎng)度
float length = pathMeasure.getLength();
//mCurrentValue為在0~1之間變化的屬性動(dòng)畫(huà)某一刻的值
//startD,是獲取的path片段的起始點(diǎn),
//隨著mCurrentValue的變化,startD從path的起始點(diǎn)變化到最后一點(diǎn)
float startD = mCurrentValue * length;
Path pathDst = new Path();
//ratio為動(dòng)態(tài)線的長(zhǎng)度
if (mCurrentValue + ratio <= 1) {
    float stopD = (mCurrentValue + ratio) * length;
    pathMeasure.getSegment(startD, stopD, pathDst, true);
} else {
    //先取出startD-length這個(gè)片段
    pathMeasure.getSegment(startD, length, pathDst, true);
    Path pathDst0 = new Path();
    //再?gòu)念^取出不足mCurrentValue + ratio的片段
    pathMeasure.getSegment(0, (mCurrentValue + ratio - 1) * length, pathDst0, true);
    //都結(jié)合到path中
    pathDst.addPath(pathDst0);
}

canvas.drawPath(pathDst, mPaint);

??通過(guò)getSegment獲取path的片段,片段的起始點(diǎn)距離path起始點(diǎn)的距離startD為:mCurrentValue * length,大部分情況下,片段的最后一點(diǎn)距離path起始點(diǎn)的距離stopD為:(mCurrentValue + ratio) * length,這樣寫(xiě)可使動(dòng)態(tài)變化弧線的長(zhǎng)度固定為ratio * length。而少部分情況下,mCurrentValue + ratio已經(jīng)大于1,這時(shí)候就需要先取出start~length這個(gè)片段,然后再?gòu)膒ath的起始點(diǎn)取出另一個(gè)片段,使兩個(gè)片段的長(zhǎng)度之后為ratio * length。

??至此,仿交通銀行的緩沖框已經(jīng)制作完畢。

總結(jié)

??兩個(gè)“跳過(guò)”、兩個(gè)緩沖框,實(shí)現(xiàn)起來(lái)大多都會(huì)用到屬性動(dòng)畫(huà)完成動(dòng)態(tài)效果,定時(shí)器本身也可以被動(dòng)畫(huà)替代。比較有挑戰(zhàn)的,大概就是旋轉(zhuǎn)圖片時(shí)會(huì)用到矩陣的操作,完成一些動(dòng)態(tài)效果時(shí)會(huì)用到PathMeasure來(lái)完成。其它可說(shuō)的并沒(méi)有太多,畢竟只是一些簡(jiǎn)單的效果。

??這里是完整代碼

?著作權(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)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,366評(píng)論 25 708
  • 原文鏈接:https://github.com/opendigg/awesome-github-android-u...
    IM魂影閱讀 33,172評(píng)論 6 472
  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線程,因...
    小菜c閱讀 7,391評(píng)論 0 17
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,838評(píng)論 4 61
  • 昨天那個(gè)句子翻譯的不順,翻譯完了之后沒(méi)有看答案。然后劉安娜指出了錯(cuò)誤,而且翻譯的和參考答案意思一樣。她的簡(jiǎn)書(shū)名字是...
    吳黃龍本人閱讀 386評(píng)論 0 0

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