Android自定義View實(shí)現(xiàn)鐘擺效果進(jìn)度條PendulumView
在網(wǎng)上看到了一個(gè)IOS組件PendulumView,實(shí)現(xiàn)了鐘擺的動(dòng)畫效果。由于原生的進(jìn)度條確實(shí)是不好看,所以想可以自定義View實(shí)現(xiàn)這樣的效果,以后也可以用于加載頁(yè)面的進(jìn)度條。
廢話不多說(shuō),先上效果圖
底部黑邊是錄制時(shí)不小心錄上的,可以忽略。
既然是自定義View我們就按標(biāo)準(zhǔn)的流程來(lái),第一步,自定義屬性
自定義屬性
建立屬性文件
在Android項(xiàng)目的res->values目錄下新建一個(gè)attrs.xml文件,文件內(nèi)容如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="PendulumView"> <attr name="globeNum" format="integer"/> <attr name="globeColor" format="color"/> <attr name="globeRadius" format="dimension"/> <attr name="swingRadius" format="dimension"/> </declare-styleable> </resources>
其中declare-styleable的name屬性用于在代碼中引用該屬性文件。name屬性,一般情況下寫的都是我們自定義View的類名,較為直觀。
使用styleale,系統(tǒng)可以為我們完成很多常量(int[]數(shù)組,下標(biāo)常量)等的編寫,簡(jiǎn)化我們的開(kāi)發(fā)工作,例如下面代碼中用到的R.styleable.PendulumView_golbeNum等就是系統(tǒng)為我們自動(dòng)生成的。
globeNum屬性表示小球數(shù)量,globeColor表示小球顏色,globeRadius表示小球半徑,swingRadius表示擺動(dòng)半徑
讀取屬性值
在自定view的構(gòu)造方法中通過(guò)TypedArray讀取屬性值
通過(guò)AttributeSet同樣可以獲取屬性值,但是如果屬性值是引用類型,則得到的只是ID,仍需繼續(xù)通過(guò)解析ID獲取真正的屬性值,而TypedArray直接幫助我們完成了上述工作。
public PendulumView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //使用TypedArray讀取自定義的屬性值 TypedArray ta = context.getResources().obtainAttributes(attrs, R.styleable.PendulumView); int count = ta.getIndexCount(); for (int i = 0; i < count; i++) { int attr = ta.getIndex(i); switch (attr) { case R.styleable.PendulumView_globeNum: mGlobeNum = ta.getInt(attr, 5); break; case R.styleable.PendulumView_globeRadius: mGlobeRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics())); break; case R.styleable.PendulumView_globeColor: mGlobeColor = ta.getColor(attr, Color.BLUE); break; case R.styleable.PendulumView_swingRadius: mSwingRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics())); break; } } ta.recycle(); //避免下次讀取時(shí)出現(xiàn)問(wèn)題 mPaint = new Paint(); mPaint.setColor(mGlobeColor); }
重寫OnMeasure()方法
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //高度為小球半徑+擺動(dòng)半徑 int height = mGlobeRadius + mSwingRadius; //寬度為2*擺動(dòng)半徑+(小球數(shù)量-1)*小球直徑 int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius; //如果測(cè)量模式為EXACTLY,則直接使用推薦值,如不為EXACTLY(一般處理wrap_content情況),使用自己計(jì)算的寬高 setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize : height); }
其中
int height = mGlobeRadius + mSwingRadius;
<pre name="code" class="java">int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;
用于處理測(cè)量模式為AT_MOST的情況,一般是自定義View的寬高設(shè)置為了wrap_content,此時(shí)通過(guò)小球的數(shù)量,半徑,擺動(dòng)的半徑等計(jì)算View的寬高,如下圖:
以小球個(gè)數(shù)5為例,View的大小為下圖紅色矩形區(qū)域
重寫onDraw()方法
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //繪制除左右兩個(gè)小球外的其他小球 for (int i = 0; i < mGlobeNum - 2; i++) { canvas.drawCircle(mSwingRadius + (i + 1) * 2 * mGlobeRadius, mSwingRadius, mGlobeRadius, mPaint); } if (mLeftPoint == null || mRightPoint == null) { //初始化最左右兩小球坐標(biāo) mLeftPoint = new Point(mSwingRadius, mSwingRadius); mRightPoint = new Point(mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1), mSwingRadius); //開(kāi)啟擺動(dòng)動(dòng)畫 startPendulumAnimation(); } //繪制左右兩小球 canvas.drawCircle(mLeftPoint.x, mLeftPoint.y, mGlobeRadius, mPaint); canvas.drawCircle(mRightPoint.x, mRightPoint.y, mGlobeRadius, mPaint); }
onDraw()方法是自定義View的關(guān)鍵所在,在該方法體內(nèi)繪制View的顯示效果。代碼首先繪制了除去最左邊最右邊小球以外的其他小球,然后對(duì)左右兩小球的坐標(biāo)值進(jìn)行判斷,如果是第一次繪制,坐標(biāo)值均為空,則初始化兩小球坐標(biāo),并且開(kāi)啟動(dòng)畫。最后通過(guò)mLeftPoint,mRightPoint的x,y值,繪制左右兩個(gè)小球。
其中mLeftPoint,mRightPoint均是android.graphics.Point對(duì)象,僅是使用它們來(lái)存放左右兩小球的x,y坐標(biāo)信息。
使用屬性動(dòng)畫
public void startPendulumAnimation() { //使用屬性動(dòng)畫 final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { //參數(shù)fraction用于表示動(dòng)畫的完成度,我們根據(jù)它來(lái)計(jì)算當(dāng)前的動(dòng)畫值 double angle = Math.toRadians(90 * fraction); int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle)); int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle)); Point point = new Point(x, y); return point; } }, new Point(), new Point()); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { Point point = (Point) animation.getAnimatedValue(); //獲得當(dāng)前的fraction值 float fraction = anim.getAnimatedFraction(); //判斷是否是fraction先減小后增大,即是否處于即將向上擺動(dòng)狀態(tài) //在每次即將向上擺動(dòng)時(shí)切換小球 if (lastSlope && fraction > mLastFraction) { isNext = !isNext; } //通過(guò)不斷改動(dòng)左右小球的x,y坐標(biāo)值實(shí)現(xiàn)動(dòng)畫效果 //利用isNext來(lái)判斷應(yīng)該是左邊小球動(dòng),還是右邊小球動(dòng) if (isNext) { //當(dāng)左邊小球擺動(dòng)時(shí),右邊小球置于初始位置 mRightPoint.x = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1); mRightPoint.y = mSwingRadius; mLeftPoint.x = mSwingRadius - point.x; mLeftPoint.y = mGlobeRadius + point.y; } else { //當(dāng)右邊小球擺動(dòng)時(shí),左邊小球置于初始位置 mLeftPoint.x = mSwingRadius; mRightPoint.y = mSwingRadius; mRightPoint.x = mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + point.x; mRightPoint.y = mGlobeRadius + point.y; } invalidate(); lastSlope = fraction < mLastFraction; mLastFraction = fraction; } }); //設(shè)置永久循環(huán)播放 anim.setRepeatCount(ValueAnimator.INFINITE); //設(shè)置循環(huán)模式為倒序播放 anim.setRepeatMode(ValueAnimator.REVERSE); anim.setDuration(200); //設(shè)置補(bǔ)間器,控制動(dòng)畫的變化速率 anim.setInterpolator(new DecelerateInterpolator()); anim.start(); }
其中使用ValueAnimator.ofObject方法是為了可以對(duì)Point對(duì)象進(jìn)行操作,更為形象具體。還有就是通過(guò)ofObject方法使用了自定義的TypeEvaluator對(duì)象,由此得到了fraction值,該值是一個(gè)從0-1變化的小數(shù)。所以該方法的后兩個(gè)參數(shù)startValue(new Point()),endValue(new Point())并沒(méi)有實(shí)際意義,也可以直接不寫,此處寫上主要是為了便于理解。同樣道理也可以直接使用ValueAnimator.ofFloat(0f, 1f)方法獲取到一個(gè)從0-1變化的小數(shù)。
final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { //參數(shù)fraction用于表示動(dòng)畫的完成度,我們根據(jù)它來(lái)計(jì)算當(dāng)前的動(dòng)畫值 double angle = Math.toRadians(90 * fraction); int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle)); int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle)); Point point = new Point(x, y); return point; } }, new Point(), new Point());
通過(guò)fraction,我們計(jì)算得到小球擺動(dòng)時(shí)的角度變化值,0-90度
mSwingRadius-mGlobeRadius表示的值是圖中綠色直線的長(zhǎng)度,擺動(dòng)的路線,小球圓心的路線是一個(gè)以(mSwingRadius-mGlobeRadius)為半徑的弧線,變化的X值為(mSwingRadius-mGlobeRadius)*sin(angle),變化的y值為(mSwingRadius-mGlobeRadius)*cos(angle)
對(duì)應(yīng)的小球?qū)嶋H的圓心坐標(biāo)為(mSwingRadius-x,mGlobeRadius+y)
右邊小球運(yùn)動(dòng)路線與左邊類似,僅僅是方向不同。右邊小球?qū)嶋H的圓心坐標(biāo)(mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + x,mGlobeRadius+y)
可見(jiàn)左右兩邊小球的縱坐標(biāo)是相同的,僅橫坐標(biāo)不同。
float fraction = anim.getAnimatedFraction(); //判斷是否是fraction先減小后增大,即是否處于即將向上擺動(dòng)狀態(tài) //在每次即將向上擺動(dòng)時(shí)切換小球 if (lastSlope && fraction > mLastFraction) { isNext = !isNext; } //記錄上一次fraction是否不斷減小 lastSlope = fraction < mLastFraction; //記錄上一次的fraction mLastFraction = fraction;
這兩段代碼用于計(jì)算何時(shí)切換運(yùn)動(dòng)的小球,本動(dòng)畫設(shè)置了循環(huán)播放,且循環(huán)模式為倒序播放,所以動(dòng)畫的一個(gè)周期即為小球拋起加上小球落下的過(guò)程。在該過(guò)程中fraction的值先有0變?yōu)?,再由1變?yōu)?。那么何時(shí)是動(dòng)畫新一輪周期的開(kāi)始呢?就是在小球即將拋起的時(shí)候,在這個(gè)時(shí)候切換運(yùn)動(dòng)的小球,即可實(shí)現(xiàn)左邊小球落下后右邊小球拋起,右邊小球落下后左邊小球拋起的動(dòng)畫效果。
那么如何捕捉到這個(gè)時(shí)間點(diǎn)呢?
小球拋起時(shí)fraction值不斷增大,小球落下時(shí)fraction值不斷減小。小球即將拋起的時(shí)刻,就是fraction從不斷減小轉(zhuǎn)變?yōu)椴粩嘣龃蟮臅r(shí)刻。代碼中記錄上一次fraction是否在不斷減小,然后比較這一次fraction是否在不斷增大,若兩個(gè)條件均成立則切換運(yùn)動(dòng)的小球。
anim.setDuration(200); //設(shè)置補(bǔ)間器,控制動(dòng)畫的變化速率 anim.setInterpolator(new DecelerateInterpolator()); anim.start();
設(shè)置動(dòng)畫的持續(xù)時(shí)間為200毫秒,讀者可以通過(guò)更改該值而達(dá)到修改小球擺動(dòng)速度的目的。
設(shè)置動(dòng)畫的補(bǔ)間器,由于小球拋起是一個(gè)逐漸減速的過(guò)程,落下是一個(gè)逐漸加速的過(guò)程,所以使用DecelerateInterpolator實(shí)現(xiàn)減速效果,在倒序播放時(shí)為加速效果。
啟動(dòng)動(dòng)畫,鐘擺效果的自定義View進(jìn)度條就實(shí)現(xiàn)了!趕快運(yùn)行,看看效果吧!
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android view自定義實(shí)現(xiàn)動(dòng)態(tài)進(jìn)度條
- Android 自定義view和屬性動(dòng)畫實(shí)現(xiàn)充電進(jìn)度條效果
- Android自定義View基礎(chǔ)開(kāi)發(fā)之圖片加載進(jìn)度條
- Android自定義View之圓形進(jìn)度條式按鈕
- Android自定義View實(shí)現(xiàn)帶數(shù)字的進(jìn)度條實(shí)例代碼
- android自定義進(jìn)度條漸變色View的實(shí)例代碼
- Android實(shí)現(xiàn)環(huán)形進(jìn)度條的實(shí)例
- Android自定義漂亮的圓形進(jìn)度條
- Android實(shí)現(xiàn)環(huán)形進(jìn)度條代碼
- Android 仿支付寶中的余額寶收益進(jìn)度條
- Android自定義帶水滴的進(jìn)度條樣式(帶漸變色效果)
- Android編程基于自定義View實(shí)現(xiàn)絢麗的圓形進(jìn)度條功能示例
相關(guān)文章
Android RadioGroup和RadioButton控件簡(jiǎn)單用法示例
這篇文章主要介紹了Android RadioGroup和RadioButton控件簡(jiǎn)單用法,結(jié)合實(shí)例形式分析了Android單選按鈕控件的基本定義、布局與功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-07-07Android開(kāi)發(fā)之如何自定義數(shù)字鍵盤詳解
這篇文章主要給大家介紹了關(guān)于Android開(kāi)發(fā)之如何自定義數(shù)字鍵盤的相關(guān)資料,本文語(yǔ)言是基于kotlin實(shí)現(xiàn)的,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-09-09Android程序開(kāi)發(fā)之UIScrollerView里有兩個(gè)tableView
這篇文章主要介紹了UIScrollerView里有兩個(gè)tableView 的相關(guān)資料,需要的朋友可以參考下2016-04-04Android安裝apk文件并適配Android 7.0詳解
這篇文章主要介紹了Android安裝apk文件并適配Android 7.0詳解的相關(guān)資料,需要的朋友可以參考下2017-05-05android自定義view實(shí)現(xiàn)數(shù)字進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了android自定義view實(shí)現(xiàn)數(shù)字進(jìn)度條,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12Android 圓角邊框的實(shí)現(xiàn)方式匯總
這篇文章主要介紹了Android 圓角邊框的實(shí)現(xiàn)方式匯總的相關(guān)資料,需要的朋友可以參考下2016-03-03Android下Button實(shí)現(xiàn)圖文混排效果
這篇文章主要為大家詳細(xì)介紹了Android下Button實(shí)現(xiàn)圖文混排效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08Android通過(guò)BLE傳輸文件遇到問(wèn)題解決
這篇文章主要為大家介紹了Android通過(guò)BLE傳輸文件遇到問(wèn)題解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04Android開(kāi)發(fā)兩個(gè)activity之間傳值示例詳解
這篇文章主要為大家介紹了Android開(kāi)發(fā)兩個(gè)activity之間傳值示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07