簡介
模仿這個gif中SeekBar的功能和效果
由于這個控件的實現(xiàn)耗費了不少時間颈墅,也遇到了一些數(shù)學(xué),漸變相關(guān)的問題雾袱,過程較為復(fù)雜恤筛,所以我在這里整理了一下流程,希望能方便大家了解這個控件的功能和使用芹橡。
文中幾個容易混淆的概念
- 角度和弧度:我們通常用角度degree描述叹俏,但Math中的三角函數(shù)用弧度radians計算。
- 角度轉(zhuǎn)弧度:Math.toRadians --> angdeg / 180.0 * PI
- 弧度轉(zhuǎn)角度:Math.toDegrees --> angrad * 180.0 / PI
- 橢圓和弧線:Canvas中畫弧是按照矩形內(nèi)接橢圓畫出來的僻族。
- 前后左右四段弧線,進度弧線是藍色弧線屡谐。
實現(xiàn)思路
- 首先述么,在Canvas上畫四段弧線,這四段弧線在一個橢圓上愕掏。
- 每一條弧線有一個起始角度startAngle度秘,和一個掃過角度sweepAngle。
- 有進度變化饵撑,在前端的弧線上畫一段藍色的弧線剑梳。
- 藍色弧線的長度也就是角度,根據(jù)當(dāng)前進度和前端弧線的角度計算出來滑潘。
- 拖拽時垢乙,根據(jù)拖拽的角度,修改這四條弧線的起始角度语卤。
- 旋轉(zhuǎn)時追逮,以旋轉(zhuǎn)時長為動畫時間酪刀,旋轉(zhuǎn)角度以動畫距離,使用插值器钮孵,計算出每次步進的角度骂倘,和拖拽一樣,修改起始角度巴席。
自定義View基礎(chǔ):
自定義的屬性存放在attrs.xml中历涝,主要是橢圓線條顏色,粗細漾唉,控制球半徑和顏色荧库,進度顏色,進度漸變起止顏色毡证,旋轉(zhuǎn)動畫時長电爹,具體參考注釋。
<declare-styleable name="OvalSeekBar">
<!--橢圓線條默認(rèn)顏色-->
<attr name="oval_line_color" format="color|reference"></attr>
<!--橢圓線條默認(rèn)粗細-->
<attr name="oval_stroke_width" format="dimension|reference"></attr>
<!--前端線條開始角度料睛,x軸正向是0度丐箩,順時針計算,前面的這段弧線要繪制進度恤煞,所以通過屬性確定下來-->
<attr name="oval_start_angle" format="integer"></attr>
<!--前端線條結(jié)束角度-->
<attr name="oval_end_angle" format="integer"></attr>
<!--進度條漸變顏色初值-->
<attr name="oval_progress_start_color" format="color|reference"></attr>
<!--進度條漸變顏色終值-->
<attr name="oval_progress_end_color" format="color|reference"></attr>
<!--節(jié)目時長文字顏色-->
<attr name="oval_duration_text_color" format="color|reference"></attr>
<!--節(jié)目時長文字大小-->
<attr name="oval_duration_text_size" format="dimension|reference"></attr>
<!--進度球半徑-->
<attr name="oval_progress_thumb_radius" format="dimension|reference"></attr>
<!--進度球顏色-->
<attr name="oval_progress_thumb_color" format="color|reference"></attr>
<!--弧線之間間隔度數(shù)-->
<attr name="oval_arc_gap_angle" format="integer"></attr>
<!--旋轉(zhuǎn)時長屎勘,如果把一次旋轉(zhuǎn)當(dāng)成一個動畫,那就是動畫時長-->
<attr name="oval_rotate_duration" format="integer"></attr>
<!--旋轉(zhuǎn)角度居扒,如果把一次旋轉(zhuǎn)當(dāng)成一個動畫概漱,那就是動畫距離,
建議是90度喜喂,因為有4條弧線同時旋轉(zhuǎn)瓤摧,90度正好是一次位置替換-->
<attr name="oval_rotate_angle" format="integer"></attr>
</declare-styleable>
在view的構(gòu)造方法中讀取這些屬性,保存在我們view的成員變量中玉吁。
初始化各種畫筆照弥,這些畫筆包括:
- 前端(mPaint)
- 左側(cè)(mLeftPaint)
- 后側(cè)(mBackPaint)
- 右側(cè)(mRightPaint)
- 進度(mProgressPaint)
- 進度球(mThumbPaint)
- 開始時長文字(mDurationStartPaint)
- 調(diào)試(mDebugPaint)
畫筆初始化值得一提的是,給畫筆添加了LightingColorFilter, 如果不加這個进副,則畫出來的線顯得灰暗这揣,像是陷下去了,加了這個就會有發(fā)光效果影斑,顯得飽滿有光澤给赞。
在onMeasure中,確定一些關(guān)鍵變量:
- view的寬高和它的padding (mWidth, mHeight)
- 根據(jù)view的寬高和padding計算出橢圓的寬高(mOvalRectWith, mOvalRectHeight)
- 進而計算出橢圓的區(qū)域(mOvalRectLeft, mOvalRectTop, mOvalRectRight, mOvalRectBottom)矫户,對應(yīng)矩形變量(mOvalRect)
- 計算出橢圓的長半徑和短半徑(mA, mB). 橢圓方程:
x^2 / a^2 + y^2 / b^2 = 1 (a > b > 0)
- 計算出控制球拖拽區(qū)域片迅,因為用戶不會沿著弧線拖,還是會沿著直線拖皆辽,我們設(shè)定一個矩形的區(qū)域障涯,只有在這個區(qū)域的觸摸事件當(dāng)成seek,計算這個區(qū)域的代碼是:
mSeekRect = new RectF(
(float) (mA + mA * Math.cos(Math.toRadians(mEndAngle)) - touchExpendDelta) + getPaddingLeft(),
mB + mB / 2 + touchExpendDelta,
(float) (mA + mA * Math.cos(Math.toRadians(mStartAngle)) + touchExpendDelta + getPaddingLeft()),
mHeight);
- 初始進度是0罐旗, 計算橢圓圓心點(mOvalRect),例如橢圓矩形左頂點x是20, 寬是120唯蝶, 則橢圓中心的x在(120 + 20) / 2 = 70
mOvalCenter.x = (mOvalRectRight + mOvalRectLeft) / 2;
mOvalCenter.y = (mOvalRectBottom + mOvalRectTop) / 2;
弧線角度相關(guān)計算
計算4條弧線的開始角度和掃過角度九秀,對應(yīng)Canvas的drawArc方法的startAngle和sweepAngle:
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
@NonNull Paint paint) {
super.drawArc(oval, startAngle, sweepAngle, useCenter, paint);
}
這4條弧線分別是被縫隙隔開的前后左右4條弧(mFrontArc, mLeftArc, mBackArc, mRightArc)我們定義了一個非常簡單的內(nèi)部類封裝startAngle和sweepAngle:
protected class ArcData {
float startAngle, sweepAngle;
ArcData(float startAngle, float sweepAngle) {
this.startAngle = startAngle;
this.sweepAngle = sweepAngle;
}
void setValue(float startAngle, float sweepAngle) {
this.startAngle = startAngle;
this.sweepAngle = sweepAngle;
}
}
還定義了拖拽時的弧線信息(mRotateFrontArc, mRotateLeftArc, mRotateBackArc, mRotateRightArc)以及旋轉(zhuǎn)時的弧線信息(mDragFrontArc, mDragLeftArc, mDragBackArc, mDragRightArc)
這里的計算用到另外一個橢圓方程,主要是根據(jù)角度計算橢圓上具體一點的坐標(biāo)粘我,也就是橢圓參數(shù)方程鼓蜒。方程是:
x = a * cosθ
y = b * sinθ
a是橢圓長軸,b是橢圓短軸征字,θ計算比較特殊都弹,但我們這里把角度轉(zhuǎn)成弧度以后,用Math的三角函數(shù)就可以了]例如計算前端弧線的起始角度和掃過角度, 起始角度是布局文件中指定的角度加上縫隙角度的二分之一匙姜,掃過角度是結(jié)束角度減去縫隙的二分之一再減去起始角度:
int startAngle = mStartAngle + mArcGapAngle / 2;
int sweepAngle = mEndAngle - mArcGapAngle / 2 - mStartAngle;
mFrontArc = new ArcData(startAngle, sweepAngle);
然后計算前端這段弧線起始位置和結(jié)束位置的坐標(biāo)畅厢,起始位置在第四象限,結(jié)束位置在第三象限氮昧。x, y 是按照橢圓圓心為原點計算出來的位置框杜,View的原點在左上頂點。如果對canvas做平移袖肥,也就是Canvas.translate,這個坐標(biāo)可以直接使用咪辱,但這里并沒有做,所以要用這個坐標(biāo)和View的寬高進行換算椎组,得到它在View中的坐標(biāo)油狂。因為橢圓的圓心也是view的中心,所以這個點在view中的坐標(biāo)是:橫向坐標(biāo)是寬度的一半減去或者加上x寸癌,縱向坐標(biāo)是高度的一半減去或者加上y专筷,借助正弦/余弦三角函數(shù)的周期性,可以寫成下面的樣子蒸苇。
- 起始位置坐標(biāo):
double x = mA * Math.cos(Math.toRadians(mStartAngle));
double y = mB * Math.sin(Math.toRadians(mStartAngle));
mFrontStartX = x + mWidth / 2;
mFrontStartY = y + mHeight / 2;
- 結(jié)束位置坐標(biāo):
x = mA * Math.cos(Math.toRadians(mEndAngle));
y = mB * Math.sin(Math.toRadians(mEndAngle));
mFrontEndX = x + mWidth / 2;
mFrontEndY = y + mHeight / 2;
進度對應(yīng)的角度可以根據(jù)前端弧線對應(yīng)的總時長和前端弧線的總度數(shù)計算出來磷蛹, 例如前端弧線是80度,時長是4分鐘填渠,現(xiàn)在進度是2分鐘,則進度對應(yīng)的角度是 80 / 4 * 2 = 40度鸟辅,當(dāng)然這里是按毫秒計算的
mProgressAngle = (float) progress / mPlayDuration * (mEndAngle - mStartAngle);
因為進度球是從第三象限向第四象限移動氛什,也就是從左往右,所以用結(jié)束角度減去進度角度匪凉。同理枪眉,進度球的位置:
float arcStart = mEndAngle - mProgressAngle;
x = mA * Math.cos(Math.toRadians(arcStart));
y = mB * Math.sin(Math.toRadians(arcStart));
mThumbX = (int) x + mWidth / 2;
mThumbY = (int) y + mHeight / 2;
一個較為繁瑣的計算是漸變的起止位置,用左側(cè)弧線的漸變舉例說明再层,因為左側(cè)弧線開頭的部分不透明度高贸铜,結(jié)尾的部分不透明度低堡纬,漸變開始的坐標(biāo)按它的開始角度算出來,結(jié)束坐標(biāo)按它的開始角度和掃過角度求和計算出來蒿秦。右側(cè)漸變和左側(cè)漸變略有不同烤镐。下面是左側(cè)漸變計算的詳細過程:
float la = startAngle;
double lx = mA * Math.cos(Math.toRadians(la));
double ly = mB * Math.sin(Math.toRadians(la));
float lsx = (int) lx + mWidth / 2;
float lsy = (int) ly + mHeight / 2;
la = sweepAngle + startAngle;
lx = mA * Math.cos(Math.toRadians(la));
ly = mB * Math.sin(Math.toRadians(la));
float lex = (int) lx + mWidth / 2;
float ley = (int) ly + mHeight / 2;
mLeftLineGradient = new LinearGradient(
(int) lsx, (int) lsy, lex, ley,
mLeftArcGradientColors, null, Shader.TileMode.CLAMP);
下面介紹靜止?fàn)顟B(tài)下進度球位置計算, 我給這個計算寫了詳細的注釋,理解這個計算就理解了我們這個進度條的計算過程棍鳖,因為進度的變化會導(dǎo)致進度弧線藍色漸變的變化炮叶,所以還增加了一點漸變的計算《纱Γ可以打開對應(yīng)的debug開關(guān)镜悉,在進度變化的時候關(guān)注log的輸出,找到mThumbX和mThumbY的變化規(guī)律:
/*
* 計算進度球的x医瘫, y坐標(biāo)侣肄,同時根據(jù)進度的長度設(shè)計進度漸變的寬度.
*
*/
private void calculateThumbPoint(int progress) {
if (progress == 0 || mPlayDuration == 0 || mEndAngle - mStartAngle == 0) {
mProgressAngle = 0;
} else {
mProgressAngle = (float) progress / mPlayDuration * (mEndAngle - mStartAngle);
}
//修改旋轉(zhuǎn)弧線的sweep角度,這樣拖拽開始時醇份,控制球的初始位置和進度條上的位置一致稼锅,
mRotateProgressArc.sweepAngle = (int) mProgressAngle;
//修改拖拽弧線的sweep角度,這樣拖拽開始時被芳,控制球的初始位置和進度條上的位置一致缰贝,
mDragProgressArc.sweepAngle = (int) mProgressAngle;
//橢圓標(biāo)準(zhǔn)方程, x = a * cos(弧度), y = b * sin(弧度)畔濒,首先求角度剩晴,然后將角度轉(zhuǎn)弧度
//角度求法:以3點鐘方向為x正向坐標(biāo),9點鐘方向為180度侵状,由于進度是從結(jié)束角度mEndAngle開始赞弥,
//所以球的位置與x軸的初始夾角是mEndAngle - mProgressAngle,
// 簡單來看是180 - mEndAngle + mProgressAngle,但由于余弦函數(shù)的周期性趣兄,我們可以直接寫成
//mEndAngle - mProgressAngle, 如果有進度角绽左,這個夾角會變大進度角度數(shù)
float arcStart = mEndAngle - mProgressAngle;
double x = mA * Math.cos(Math.toRadians(arcStart));
double y = mB * Math.sin(Math.toRadians(arcStart));
//位置從布局左上為原點,上面計算出來的x艇潭, y是以橢圓圓心為原點的值拼窥,所以要用畫布一半的寬度減去x
mThumbX = (int) x + mWidth / 2; //相當(dāng)于mWidth / 2 - x
mThumbY = (int) y + mHeight / 2;
//進度漸變
if (mProgressAngle > 0) {
mProgressGradient = new LinearGradient(
(int) mDragDurationStartX, (int) mDragDurationStartY, mThumbX, mThumbY,
mProgressGradientColors, null, Shader.TileMode.CLAMP);
if (DEBUG_PROCESS) {
Log.d(TAG, "LinearGradient mDragDurationStartX: " + mDragDurationStartX
+ ", mDragDurationStartY: " + mDragDurationStartY + ", mThumbX: "
+ mThumbX + ", mThumbY: " + mThumbY);
}
}
if (DEBUG_PROCESS) {
Log.d(TAG, "calculateThumbPoint progress: " + progress
+ ", mProgressAngle: " + mProgressAngle);
}
if (DEBUG_PROCESS) {
Log.d(TAG, "calculateThumbPoint mThumbX: " + mThumbX
+ ", mThumbY: " + mThumbY);
}
}
靜止?fàn)顟B(tài)下的繪制就已經(jīng)差不多了,我們有畫筆蹋凝,有橢圓矩形鲁纠,有角度,文字根據(jù)進度和時長計算一下鳍寂,文字的位置就是前端弧線的開始和結(jié)束位置改含,進度球位置也根據(jù)進度計算出來了,半徑通過讀取自定義屬性初始化了迄汛。繪制在drawIdle方法中進行捍壤,當(dāng)然骤视,如果想看參考線,可以打開debug開關(guān)和DEBUG_REFER_LINE開關(guān)鹃觉。
處理進度Seek
由于是SeekBar专酗,那么就要能seek,所以需要重寫onTouchEvent帜慢,在onTouchEvent這個方法中笼裳,首先在down事件中判斷觸摸是不是落在進度球范圍之內(nèi),如果不在粱玲,說明用戶沒有點進度球躬柬,所以不想seek,我們return false抽减,不再接收后面的事件允青。如果在,我們標(biāo)記一下卵沉,準(zhǔn)備seek:
case MotionEvent.ACTION_DOWN:
float xd = event.getX(), yd = event.getY();
boolean onThumb = eventDownOnThumb(xd, yd);
if (onThumb) {
startTrackingTouch();
return true;
} else {
return false;
}
在move事件中颠锉,如果事件發(fā)生在我們前面提到的Seek 矩形區(qū)域,認(rèn)為是用戶在做Seek操作史汗,否則琼掠,說明用戶已經(jīng)離開了seek區(qū)域, 如果在做Seek操作我們就根據(jù)拖拽的距離更新進度:
float xm = event.getX(), ym = event.getY();
boolean onSeekArea = eventMoveOnProgressBar(xm, ym);
if (onSeekArea) {
int progress = getSeekProgress(xm, ym);
int nowProgress = mProgress + progress;
if (DEBUG_TOUCH) {
Log.d(TAG, "----onTouchEvent, nowProgress " + nowProgress);
}
setProgress(mProgress + progress, true);
return true;
} else {
stopTrackingTouch();
return false;
}
這里根據(jù)距離計算進度的實現(xiàn)比較粗糙,但它能正常工作停撞,根據(jù)事件的橫坐標(biāo)瓷蛙,減去進度球的橫坐標(biāo),得到一個偏移量戈毒,然后根據(jù)Seek 矩形的長度和總的進度計算seek的進度:
private int getSeekProgress(float x, float y) {
float deltaX = x - mThumbX;
float progress = deltaX / (mSeekRect.right - mSeekRect.left) * mPlayDuration;
if (DEBUG_PROCESS) {
Log.d(TAG, "getSeekProgress deltaX: " + deltaX + ", progress: " + progress);
}
return (int) progress;
}
處理旋轉(zhuǎn)
相比拖拽艰猬,旋轉(zhuǎn)容易一些,所以先處理埋市,理解旋轉(zhuǎn)的邏輯以后冠桃,拖拽就容易多了,因為拖拽可以理解成可控制的旋轉(zhuǎn)道宅,并且拖拽引起的角度變化不會大于旋轉(zhuǎn)的角度食听。我們在onMeasure的時候,用靜止的角度變量初始化了旋轉(zhuǎn)弧線的初值:
private void setRotateInitialData() {
mRotateFrontArc = new ArcData(mFrontArc.startAngle, mFrontArc.sweepAngle);
mRotateLeftArc = new ArcData(mLeftArc.startAngle, mLeftArc.sweepAngle);
mRotateBackArc = new ArcData(mBackArc.startAngle, mBackArc.sweepAngle);
mRotateRightArc = new ArcData(mRightArc.startAngle, mRightArc.sweepAngle);
mRotateProgressArc = new ArcData(mEndAngle, (int) mProgressAngle);
}
旋轉(zhuǎn)通過public void rotate(boolean clockwise) 方法觸發(fā)污茵,boolean參數(shù)表示是否是順時針旋轉(zhuǎn)樱报。一般切換下一頁,順時針轉(zhuǎn)一次省咨,反之逆時針轉(zhuǎn)一次肃弟。在旋轉(zhuǎn)之前玷室,我們修改一下進度球的旋轉(zhuǎn)初始位置零蓉,否則就會出現(xiàn)跳動的問題笤受。如果不是拖拽引起的旋轉(zhuǎn),則正常進度確定的位置就是旋轉(zhuǎn)開始的初始位置,然后我們開始旋轉(zhuǎn)動畫:
public void rotate(boolean clockwise) {
if (bIsRotating) {
//如果正在旋轉(zhuǎn)敌蜂,則忽略新的旋轉(zhuǎn)調(diào)用
return;
}
if (bIsDragging) {
mRotateThumbX = mDragThumbX;
mRotateThumbY = mDragThumbY;
} else {
mRotateThumbX = mThumbX;
mRotateThumbY = mThumbY;
}
startRotateAnimation(clockwise);
}
這個動畫實際上不是一個動畫箩兽,而是反復(fù)的invalidate產(chǎn)生的動畫效果。為了產(chǎn)生和動畫相同的效果章喉,我們創(chuàng)建了一個ValueAnimator汗贫,并且給他一個減速插值器DecelerateInterpolator,并創(chuàng)建一個ValueAnimator.AnimatorUpdateListener監(jiān)聽動畫事件秸脱,在onAnimationUpdate中繪制新弧線落包。
在旋轉(zhuǎn)動畫完成后,重置旋轉(zhuǎn)弧線的變量值:
@Override
public void onAnimationEnd(Animator animation) {
//旋轉(zhuǎn)結(jié)束時將狀態(tài)還原
restoreAfterRotate();
}
在onAnimationUpdate中摊唇,我們拿到動畫的值咐蝇,然后和前值(mAnimatorPreValue)比較,得到偏移量巷查,本次拿到的值變成前值.然后根據(jù)偏移量旋轉(zhuǎn)有序,旋轉(zhuǎn)代碼非常簡單,就是不停地增加或者減小(mRotateFrontArc, mRotateLeftArc, mRotateBackArc, mRotateRightArc)這幾個旋轉(zhuǎn)弧線的開始角度岛请,再計算一下進度球的旋轉(zhuǎn)過程中的坐標(biāo)旭寿,然后invalidate一下view就可以了,startAngle的變化不用說了崇败,簡單介紹一下旋轉(zhuǎn)過程中進度球坐標(biāo)的計算盅称。首先是確定角度,旋轉(zhuǎn)過程中改變的只是起始角度僚匆,掃過角度是保持不變的微渠,所以用起始角度減去掃過角度,代入橢圓標(biāo)準(zhǔn)方程的三角函數(shù)就可以了:
private void calculateRotateThumbXY() {
float arcStart = mRotateProgressArc.startAngle - mRotateProgressArc.sweepAngle;
double x = mA * Math.cos(Math.toRadians(arcStart));
double y = mB * Math.sin(Math.toRadians(arcStart));
mRotateThumbX = (int) x + mWidth / 2;
mRotateThumbY = (int) y + mHeight / 2;
if (DEBUG_TOUCH) {
Log.d(TAG, "calculateRotateThumbXY mRotateThumbX: " + mRotateThumbX
+ ", mRotateThumbY: " + mRotateThumbY);
}
transferThumbPaintAlpha(mThumbPaint.getAlpha() - mThumbPaintAlphaStep);
}
動畫結(jié)束后咧擂,執(zhí)行restoreAfterRotate逞盆,將旋轉(zhuǎn)變量值還原,并將進度設(shè)置為0松申,時長設(shè)置為0云芦。否則,由于上一次遺留的進度贸桶,會使得旋轉(zhuǎn)完畢以后舅逸,立即產(chǎn)生一個不合適的藍色進度線。
這樣皇筛,動畫每更新一次琉历,我們就計算一次,旋轉(zhuǎn)弧線的值就確定了,調(diào)用drawRotating畫一下就可以了旗笔。
處理拖拽
相比旋轉(zhuǎn)的處理彪置,拖拽較為復(fù)雜一些,主要是我們要根據(jù)ViewPager或者RecyclerView的變化而變化蝇恶,先不考慮和這些控件聯(lián)動的問題拳魁,我們假設(shè)橢圓向左或者向右拖拽了一個角度,應(yīng)該怎么處理呢撮弧,其實和旋轉(zhuǎn)過程中的一個片段是一樣的潘懊,對比代碼也非常相似,只是旋轉(zhuǎn)的時候修改的是mRotate開頭的弧線變量贿衍,這里修改的是mDrag開頭的弧線變量授舟。在拖拽過程中修改旋轉(zhuǎn)弧線起始角度的原因是,在拖拽完成以后需要旋轉(zhuǎn)時贸辈,直接接著拖拽的角度旋轉(zhuǎn)岂却,而不是跑到初始位置旋轉(zhuǎn)。由于拖拽的過程中裙椭,文字是跟著動的躏哩,所以比旋轉(zhuǎn)多了一個計算拖拽的文字位置的調(diào)用。
我們在一開始就用靜止?fàn)顟B(tài)下的弧線起始角度和掃過角度初始化拖拽弧線相應(yīng)角度:
private void setDragInitialData() {
mDragFrontArc = new ArcData(mFrontArc.startAngle, mFrontArc.sweepAngle);
mDragLeftArc = new ArcData(mLeftArc.startAngle, mLeftArc.sweepAngle);
mDragBackArc = new ArcData(mBackArc.startAngle, mBackArc.sweepAngle);
mDragRightArc = new ArcData(mRightArc.startAngle, mRightArc.sweepAngle);
mDragProgressArc = new ArcData(mEndAngle, (int) mProgressAngle);
mDragAngle = 0;
mDragDurationStartX = mFrontEndX;
mDragDurationStartY = mFrontEndY;
}
private void calculateDragThumbXY() {
mDragProgressArc = new ArcData(mDragProgressArc.startAngle, mDragProgressArc.sweepAngle);
float arcStart = mDragProgressArc.startAngle - mDragProgressArc.sweepAngle;
double x = mA * Math.cos(Math.toRadians(arcStart));
double y = mB * Math.sin(Math.toRadians(arcStart));
mDragThumbX = (int) x + mWidth / 2;
mDragThumbY = (int) y + mHeight / 2;
mRotateThumbX = mDragThumbX;
mRotateThumbY = mDragThumbY;
if (DEBUG_PROCESS) {
Log.d(TAG, "calculateDragThumbXY mDragThumbX: " + mDragThumbX
+ ", mDragThumbY: " + mDragThumbY);
}
}
計算完成以后invalidate一下揉燃,就會按照新的結(jié)果進行繪制扫尺,為了簡化繪制的處理情況,我們定義了三種繪制方法炊汤,分別是idle正驻,drag, rotate抢腐,對應(yīng)靜止姑曙,拖拽和旋轉(zhuǎn)的繪制,各種繪制互不干擾:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (bIsDragging) { // 拖拽
drawDragging(canvas);
} else if (bIsRotating) { // 旋轉(zhuǎn)
drawRotating(canvas);
} else { // 靜止
drawIdle(canvas);
}
}
這樣拖拽效果的處理就差不多了迈倍,接下來就是把拖拽和ViewPager或者recyclerview結(jié)合起來伤靠。為了簡化代碼結(jié)構(gòu),我們分兩個類各自繼承當(dāng)前類, 分別配合ViewPager或者RecyclerView使用啼染,這樣做的原因是:卡片之前是用ViewPager實現(xiàn)的宴合,后來改成了RecyclerView了,提供兩個實現(xiàn)迹鹅,方便卡片選擇合適的控件:
public class ViewPagerOvalSeekBar extends OvalSeekBar
public class RecyclerViewOvalSeekBar extends OvalSeekBar
這里用RecyclerViewOvalSeekBar說明一下聯(lián)動的過程卦洽。我們通過getRecyclerViewListener返回一個RecyclerView.OnScrollListener對象,把這個對象通過RecyclerView.addOnScrollListener添加給控件斜棚,我們就能獲取到RecyclerView的滑動事件了阀蒂,在收到onScrollStateChanged的SCROLL_STATE_DRAGGING的事件中该窗,我們標(biāo)記為開始拖拽,然后在onScrolled中調(diào)用dragging(dx):
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (DEBUG_TOUCH) {
Log.d(TAG, "onScrolled dx: " + dx + ", dy: " + dy);
}
if (!bIsRotating) {
if (bIsDragging) {
dragging(dx);
}
}
}
這里如何將拖拽的距離換算成橢圓旋轉(zhuǎn)的弧度了蚤霞,正確的做法應(yīng)該是尋找dx的變化規(guī)律挪捕,然后通過三角函數(shù)計算,但這樣比較復(fù)雜争便。我們假設(shè)偏移角度在某一個數(shù)值左右波動,所以這里沒有計算断医,用2試了一下滞乙,感覺表現(xiàn)得還好。所以就這樣做了:
/**
* 橫向拖拽
*
* @param dx
*/
public void dragging(int dx) {
if (DEBUG_TOUCH) {
Log.d(TAG, "--drag-- dragging dx: " + dx);
}
boolean left;
if (dx > 0) {
left = true;
} else {
left = false;
}
dragInner(left, 2);
}
}
我們還可以根據(jù)拖拽的角度換算出一個alpha鉴嗤,把它用在拖拽時前端弧線和進度球上斩启,使它們在拖拽時有漸變的效果:
float alpha = Math.abs((mDragFrontArc.startAngle - mStartAngle)) / mRotateAngle;
if (alpha > 1f) {
alpha = 1f;
}
拖拽時,進度弧線漸變區(qū)域也會發(fā)生變化醉锅,所以需要重新創(chuàng)建, 無論向左還是向右兔簇,可以用mDragProgressArc.startAngle計算出漸變開始的坐標(biāo),用mDragProgressArc.startAngle - mDragProgressArc.sweepAngle計算出漸變結(jié)束的坐標(biāo)硬耍,我們順便還給它做了個debug的參考線
if (mProgressAngle > 0) {
int[] colors = new int[2];
float da = mDragProgressArc.startAngle;
double dx = mA * Math.cos(Math.toRadians(da));
double dy = mB * Math.sin(Math.toRadians(da));
float dsx = (int) dx + mWidth / 2;
float dsy = (int) dy + mHeight / 2;
da = mDragProgressArc.startAngle - mDragProgressArc.sweepAngle;
dx = mA * Math.cos(Math.toRadians(da));
dy = mB * Math.sin(Math.toRadians(da));
float dex = (int) dx + mWidth / 2;
float dey = (int) dy + mHeight / 2;
mDragAlpha = (float) Math.max(1 - alpha, 0.4); //避免變成0
colors[0] = adjustAlpha(mProgressGradientColors[0], mDragAlpha);
colors[1] = adjustAlpha(mProgressGradientColors[1], mDragAlpha);
mDragProgressGradient = new LinearGradient((int) dsx, (int) dsy, dex, dey,
colors, null, Shader.TileMode.CLAMP);
if (DEBUG_REFER_LINE) {
debugDragProcessStartX = dsx;
debugDragProcessStartY = dsy;
debugDragProcessEndX = dex;
debugDragProcessEndY = dey;
Log.d(TAG, "dragInner drag process mDragProgressArc.startAngle:" +
mDragProgressArc.startAngle + ", mDragProgressArc.sweepAngle: " +
mDragProgressArc.sweepAngle + ", mDragProgressArc.endAngle: " +
(mDragProgressArc.startAngle - mDragProgressArc.sweepAngle));
Log.d(TAG, "dragInner process debugDragProcessStartX: " + debugDragProcessStartX
+ ", debugDragProcessStartY: " + debugDragProcessStartY + ", debugDragProcessEndX: "
+ debugDragProcessEndX + ", debugDragProcessEndY: " + debugDragProcessEndY
+ " alpha: " + alpha);
}
if (DEBUG_PROCESS) {
Log.d(TAG, "mDragProgressGradient dsx: " + dsx + ", dsy: " + dsy + ", dex: "
+ dex + ", dey: " + dey);
}
}
如何使用:
這是一個繼承View的自定義控件垄琐,用法和普通View一樣,但由于沒有提供設(shè)置自定義屬性的set方法经柴,所以只能在布局文件里面使用狸窘,并指定那些屬性:
<com.kaolafm.widget.RecyclerViewOvalSeekBar
android:id="@+id/oval_progress_bar"
android:layout_width="@dimen/x840"
android:layout_height="@dimen/y200"
android:layout_above="@+id/player_audio_simple_info_layout"
android:layout_centerHorizontal="true"
android:layout_marginBottom="@dimen/y30"
android:paddingLeft="10dip"
android:paddingRight="10dip"
android:paddingTop="20dip"
android:visibility="invisible"
kaola:oval_arc_gap_angle="6"
kaola:oval_duration_text_size="@dimen/m20"
kaola:oval_end_angle="135"
kaola:oval_line_color="#AA3F4A6B"
kaola:oval_progress_end_color="#FF06BDDB"
kaola:oval_progress_start_color="#FF4C5BFE"
kaola:oval_progress_thumb_color="#FF06BDDB"
kaola:oval_progress_thumb_radius="@dimen/x6"
kaola:oval_rotate_angle="90"
kaola:oval_rotate_duration="600"
kaola:oval_start_angle="45"
kaola:oval_stroke_width="@dimen/x4" />
讓它根據(jù)翻頁旋轉(zhuǎn):
int offset = realItem - position;
boolean rotatingClockwise = offset > 0 ? false : true;
mSeekBar.rotate(rotatingClockwise);
代碼下載:
github:
OvalSeekBar
結(jié)語
以上是這個控件的總體實現(xiàn)過程,還有一些點沒有提到坯认,例如旋轉(zhuǎn)完成以后翻擒,進度球漸變出現(xiàn)的實現(xiàn),進度變化的監(jiān)聽牛哺,播放時長的格式轉(zhuǎn)換陋气,Log輸出的控制,參考線的繪制細節(jié)等引润,但這些都不難理解巩趁。當(dāng)然還有很多細節(jié)需要提高,比如拖拽和旋轉(zhuǎn)過程中的漸變效果淳附,使用自己的估值器(TypeEvaluator)使得動畫的緩動更加明顯(這個需要找一個數(shù)學(xué)公式晶渠,比較難),還可以考慮適配更多的卡片控件等燃观。