Android仿餓了么加入購(gòu)物車(chē)旋轉(zhuǎn)控件自帶閃轉(zhuǎn)騰挪動(dòng)畫(huà)的按鈕效果(實(shí)例詳解)
概述
在上文,酷炫Path動(dòng)畫(huà)已經(jīng)預(yù)告了,今天給大家?guī)?lái)的是利用 純自定義View,實(shí)現(xiàn)的仿餓了么加入購(gòu)物車(chē)控件,自帶閃轉(zhuǎn)騰挪動(dòng)畫(huà)的按鈕。
效果圖如下:
圖1 項(xiàng)目中使用的效果,考慮到了View的回收復(fù)用,
并且可以看到在RecyclerView中使用,切換LayoutManager也是沒(méi)有問(wèn)題的,
圖2 Demo效果,測(cè)試各種屬性值
注意,本控件非繼承自ViewGroup,而是純自定義View實(shí)現(xiàn)。理由如下:
- 1 減少布局層級(jí),從而提高性能
- 2 文字和圖形純draw,用到什么draw什么,沒(méi)有其他的額外工作,也間接提高性能。
- 3 純自定義View難度更高,更有實(shí)(裝)踐(B)的意義
1 減少布局層次,很好理解,ViewGroup內(nèi)嵌套幾個(gè)TextView、ImageV這里寫(xiě)代碼片iew也可以實(shí)現(xiàn)這個(gè)效果,然而這會(huì)使布局層次多了一級(jí),并且內(nèi)部要嵌套多個(gè)控件,層級(jí)越多,控件越多,繪制的就越慢,在列表中對(duì)性能的影響更大。
2 別小看了“小小”的TextView和的ImageView,其實(shí)它們有很多的屬性和特性在本例中是不必要的,舉個(gè)例子,查看源碼,TextView有一萬(wàn)多行,ondraw()方法有一百多行, ImageView有1588行,這么多行代碼都是我們需要的嗎?直接使用這些現(xiàn)成的控件嵌套實(shí)現(xiàn),其實(shí)性能不如我們用到什么draw什么。唯一的好處可能就是比較簡(jiǎn)單了。(其實(shí)TextView的性能是不高的)
3 純自定義View,draw出這些需要的元素,并且還要考慮動(dòng)畫(huà),以及點(diǎn)擊各區(qū)域的監(jiān)聽(tīng),實(shí)現(xiàn)起來(lái)還是有一些難度的,但我們多寫(xiě)一些有難度的代碼才能提高水平。
如何使用
伸手黨福利:講解實(shí)現(xiàn)前,先看一下如何使用 以及支持的屬性等。
使用
xml:
<!--使用默認(rèn)UI屬性--> <com.mcxtzhang.lib.AnimShopButton android:id="@+id/btn1" android:layout_width="wrap_content" android:layout_height="wrap_content" app:maxCount="3"/> <!--設(shè)置了兩圓間距--> <com.mcxtzhang.lib.AnimShopButton android:id="@+id/btn2" android:layout_width="wrap_content" android:layout_height="wrap_content" app:count="3" app:gapBetweenCircle="90dp" app:maxCount="99"/> <!--仿餓了么--> <com.mcxtzhang.lib.AnimShopButton android:id="@+id/btnEle" android:layout_width="wrap_content" android:layout_height="wrap_content" app:addEnableBgColor="#3190E8" app:addEnableFgColor="#ffffff" app:hintBgColor="#3190E8" app:hintBgRoundValue="15dp" app:hintFgColor="#ffffff" app:maxCount="99"/>
注意:
加減點(diǎn)擊后,具體的操作,要根據(jù)業(yè)務(wù)的不同來(lái)編寫(xiě)了,設(shè)計(jì)到實(shí)際的購(gòu)物車(chē)可能還有寫(xiě)數(shù)據(jù)庫(kù)操作,或者請(qǐng)求接口等,要操作成功后才執(zhí)行動(dòng)畫(huà)、或者修改count,這一塊代碼每個(gè)人寫(xiě)法可能不同。
使用時(shí),可以重寫(xiě)onDelClick()和onAddClick()
方法,并在合適的時(shí)機(jī)回調(diào)onCountAddSuccess()和onCountDelSuccess()
以執(zhí)行動(dòng)畫(huà)。
效果圖如圖2.
支持的屬性
name | format | description | 中文解釋 |
---|---|---|---|
isAddFillMode | boolean | Plus button is opened Fill mode default is stroke (false) | 加按鈕是否開(kāi)啟fill模式 默認(rèn)是stroke(false) |
addEnableBgColor | color | The background color of the plus button | 加按鈕的背景色 |
addEnableFgColor | color | The foreground color of the plus button | 加按鈕的前景色 |
addDisableBgColor | color | The background color when the button is not available | 加按鈕不可用時(shí)的背景色 |
addDisableFgColor | color | The foreground color when the button is not available | 加按鈕不可用時(shí)的前景色 |
isDelFillMode | boolean | Plus button is opened Fill mode default is stroke (false) | 減按鈕是否開(kāi)啟fill模式 默認(rèn)是stroke(false) |
delEnableBgColor | color | The background color of the minus button | 減按鈕的背景色 |
delEnableFgColor | color | The foreground color of the minus button | 減按鈕的前景色 |
delDisableBgColor | color | The background color when the button is not available | 減按鈕不可用時(shí)的背景色 |
delDisableFgColor | color | The foreground color when the button is not available | 減按鈕不可用時(shí)的前景色 |
radius | dimension | The radius of the circle | 圓的半徑 |
circleStrokeWidth | dimension | The width of the circle | 圓圈的寬度 |
lineWidth | dimension | The width of the line (+ - sign) | 線(xiàn)(+ - 符號(hào))的寬度 |
gapBetweenCircle | dimension | The spacing between two circles | 兩個(gè)圓之間的間距 |
numTextSize | dimension | The textSize of draws the number | 繪制數(shù)量的textSize |
maxCount | integer | max count | 最大數(shù)量 |
count | integer | current count | 當(dāng)前數(shù)量 |
hintText | string | The hint text when number is 0 | 數(shù)量為0時(shí),hint文字 |
hintBgColor | color | The hint background when number is 0 | 數(shù)量為0時(shí),hint背景色 |
hintFgColor | color | The hint foreground when number is 0 | 數(shù)量為0時(shí),hint前景色 |
hingTextSize | dimension | The hint text size when number is 0 | 數(shù)量為0時(shí),hint文字大小 |
hintBgRoundValue | dimension | The background fillet value when number is 0 | 數(shù)量為0時(shí),hint背景圓角值 |
這么多屬性夠你用了吧。
下面看重點(diǎn)的實(shí)現(xiàn)吧,Let's Go!.
實(shí)現(xiàn)解剖
關(guān)于自定義View的基礎(chǔ),這里不再贅述。
如果閱讀時(shí)有不明白的,建議下載源碼邊看邊讀,或者學(xué)習(xí)自定義View基礎(chǔ)知識(shí)后再閱讀本文。
代碼傳送門(mén):喜歡的話(huà),隨手點(diǎn)個(gè)star。多謝
https://github.com/mcxtzhang/AnimShopButton
我們撿重點(diǎn)說(shuō),無(wú)非是繪制。
繪制的重點(diǎn),這里分三塊:
- 靜態(tài)繪制。(分兩塊:加減按鈕和數(shù)量、hint提示文字和背景)
- 第一層。(加減按鈕和數(shù)量)以及它的旋轉(zhuǎn)、位移、透明度動(dòng)畫(huà)
- 第二層。(hint區(qū)域)以及它的伸展收縮動(dòng)畫(huà)
除了繪制以外的重點(diǎn)是:
- 由于采用了完全的自定義View去實(shí)現(xiàn)這么一個(gè)“組合控件效果”,則點(diǎn)擊事件的監(jiān)聽(tīng)需要自己處理。
- 在回收復(fù)用的列表中使用時(shí),列表滑動(dòng),如何正確顯示UI。
靜態(tài)繪制
靜態(tài)繪制就是最基本的自定義View知識(shí),繪制圓圈(Circle)、線(xiàn)段(Line)、數(shù)字(Text)以及圓角矩形(RoundRect),值得注意的是,
要考慮到 避免overDraw和動(dòng)畫(huà)的需求,
我們要繪制的兩層應(yīng)該是互斥關(guān)系。
剝離掉動(dòng)畫(huà)代碼,大致如下(基本都是draw代碼,可以快速閱讀):
@Override protected void onDraw(Canvas canvas) { if (isHintMode) { //hint 展開(kāi) //背景 mHintPaint.setColor(mHintBgColor); RectF rectF = new RectF(mLeft, mTop , mWidth - mCircleWidth, mHeight - mCircleWidth); canvas.drawRoundRect(rectF, mHintBgRoundValue, mHintBgRoundValue, mHintPaint); //前景文字 mHintPaint.setColor(mHintFgColor); // 計(jì)算Baseline繪制的起點(diǎn)X軸坐標(biāo) int baseX = (int) (mWidth / 2 - mHintPaint.measureText(mHintText) / 2); // 計(jì)算Baseline繪制的Y坐標(biāo) int baseY = (int) ((mHeight / 2) - ((mHintPaint.descent() + mHintPaint.ascent()) / 2)); canvas.drawText(mHintText, baseX, baseY, mHintPaint); } else { //左邊 //背景 圓 if (mCount > 0) { mDelPaint.setColor(mDelEnableBgColor); } else { mDelPaint.setColor(mDelDisableBgColor); } mDelPaint.setStrokeWidth(mCircleWidth); mDelPath.reset(); mDelPath.addCircle(mLeft + mRadius, mTop + mRadius, mRadius, Path.Direction.CW); mDelRegion.setPath(mDelPath, new Region(mLeft, mTop, mWidth - getPaddingRight(), mHeight - getPaddingBottom())); canvas.drawPath(mDelPath, mDelPaint); //前景 - if (mCount > 0) { mDelPaint.setColor(mDelEnableFgColor); } else { mDelPaint.setColor(mDelDisableFgColor); } mDelPaint.setStrokeWidth(mLineWidth); canvas.drawLine(-mRadius / 2, 0, +mRadius / 2, 0, mDelPaint); //數(shù)量 //是沒(méi)有動(dòng)畫(huà)的普通寫(xiě)法,x left, y baseLine canvas.drawText(mCount + "", mLeft + mRadius * 2, mTop + mRadius - (mFontMetrics.top + mFontMetrics.bottom) / 2, mTextPaint); //右邊 //背景 圓 if (mCount < mMaxCount) { mAddPaint.setColor(mAddEnableBgColor); } else { mAddPaint.setColor(mAddDisableBgColor); } mAddPaint.setStrokeWidth(mCircleWidth); float left = mLeft + mRadius * 2 + mGapBetweenCircle; mAddPath.reset(); mAddPath.addCircle(left + mRadius, mTop + mRadius, mRadius, Path.Direction.CW); mAddRegion.setPath(mAddPath, new Region(mLeft, mTop, mWidth - getPaddingRight(), mHeight - getPaddingBottom())); canvas.drawPath(mAddPath, mAddPaint); //前景 + if (mCount < mMaxCount) { mAddPaint.setColor(mAddEnableFgColor); } else { mAddPaint.setColor(mAddDisableFgColor); } mAddPaint.setStrokeWidth(mLineWidth); canvas.drawLine(left + mRadius / 2, mTop + mRadius, left + mRadius / 2 + mRadius, mTop + mRadius, mAddPaint); canvas.drawLine(left + mRadius, mTop + mRadius / 2, left + mRadius, mTop + mRadius / 2 + mRadius, mAddPaint); } }
根據(jù)isHintMode 布爾值變量,區(qū)分是繪制第二層(Hint層)或者第一層(加減按鈕層)。
繪制第二層時(shí)沒(méi)啥好說(shuō)的,就是利用canvas.drawRoundRect
,繪制圓角矩形,然后canvas.drawText
繪制hint。
(如果圓角的值足夠大,矩形的寬度足夠小,就變成了圓形。)
繪制第一層時(shí),要根據(jù)當(dāng)前的數(shù)量選擇不同的顏色,注意在繪制加減按鈕的圓圈時(shí),我們是用Path繪制的,這是因?yàn)槲覀冞€需要用Path構(gòu)建Region類(lèi),這個(gè)類(lèi)就是我們監(jiān)聽(tīng)點(diǎn)擊區(qū)域的重點(diǎn)。
點(diǎn)擊事件的監(jiān)聽(tīng)
在講解動(dòng)畫(huà)之前,我們先說(shuō)說(shuō)如何監(jiān)聽(tīng)點(diǎn)擊的區(qū)域,因?yàn)楸究丶膭?dòng)畫(huà)是和加減數(shù)量息息相關(guān)的,而數(shù)量的加減是由點(diǎn)擊相應(yīng)”+ - 按鈕”區(qū)域觸發(fā)的。
所以我們的監(jiān)聽(tīng)按鈕的點(diǎn)擊事件,其實(shí)就是監(jiān)聽(tīng)相應(yīng)的”+ - 按鈕”區(qū)域。
上一節(jié)中,我們?cè)诶L制”+ - 按鈕”區(qū)域時(shí),通過(guò)Path,構(gòu)建了兩個(gè)Region類(lèi),Region類(lèi)有個(gè)contains(int x, int y)方法如下,通過(guò)傳入對(duì)應(yīng)觸摸的x、y坐標(biāo),就可知道知否點(diǎn)擊了相應(yīng)區(qū)域。
/** * Return true if the region contains the specified point */ public native boolean contains(int x, int y);
知道了這一點(diǎn),再寫(xiě)這部分代碼就相當(dāng)簡(jiǎn)單了:
@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: //hint模式 if (isHintMode) { onAddClick(); return true; } else { if (mAddRegion.contains((int) event.getX(), (int) event.getY())) { onAddClick(); return true; } else if (mDelRegion.contains((int) event.getX(), (int) event.getY())) { onDelClick(); return true; } } break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: break; } return super.onTouchEvent(event); }
hint模式時(shí),我們可以認(rèn)為控件所有范圍都是“+”的有效區(qū)域。
而在非hint模式時(shí),根據(jù)上一節(jié)構(gòu)建的mAddRegion和mDelRegion去判斷。
判斷確認(rèn)點(diǎn)擊后,具體的操作,要根據(jù)業(yè)務(wù)的不同來(lái)編寫(xiě)了,設(shè)計(jì)到實(shí)際的購(gòu)物車(chē)可能還有寫(xiě)數(shù)據(jù)庫(kù)操作,或者請(qǐng)求接口等,要操作成功后才執(zhí)行動(dòng)畫(huà)、或者修改count,這一塊代碼每個(gè)人寫(xiě)法可能不同。
使用時(shí),可以重寫(xiě)onDelClick()和onAddClick()
方法,并在合適的時(shí)機(jī)回調(diào)onCountAddSuccess()和onCountDelSuccess()
以執(zhí)行動(dòng)畫(huà)。
本文如下編寫(xiě):
protected void onDelClick() { if (mCount > 0) { mCount--; onCountDelSuccess(); } } protected void onAddClick() { if (mCount < mMaxCount) { mCount++; onCountAddSuccess(); } else { } } /** * 數(shù)量增加成功后,使用者回調(diào) */ public void onCountAddSuccess() { if (mCount == 1) { cancelAllAnim(); mAnimReduceHint.start(); } else { mAnimFraction = 0; invalidate(); } } /** * 數(shù)量減少成功后,使用者回調(diào) */ public void onCountDelSuccess() { if (mCount == 0) { cancelAllAnim(); mAniDel.start(); } else { mAnimFraction = 0; invalidate(); } }
動(dòng)畫(huà)的實(shí)現(xiàn)
這里會(huì)用到兩個(gè)變量:
//動(dòng)畫(huà)的基準(zhǔn)值 動(dòng)畫(huà):減 0~1, 加 1~0 // 普通狀態(tài)下是0 protected float mAnimFraction; //提示語(yǔ)收縮動(dòng)畫(huà) 0-1 展開(kāi)1-0 //普通模式時(shí),應(yīng)該是1, 只在 isHintMode true 才有效 protected float mAnimExpandHintFraction;
依次分析有哪些動(dòng)畫(huà):
Hint動(dòng)畫(huà)
主要是圓角矩形的展開(kāi)、收縮。
固定right、bottom,當(dāng)展開(kāi)時(shí),不斷減少矩形的左起點(diǎn)left坐標(biāo)值,則整個(gè)矩形寬度變大,呈現(xiàn)展開(kāi)。收縮時(shí)相反。
代碼:
//背景 mHintPaint.setColor(mHintBgColor); RectF rectF = new RectF(mLeft + (mWidth - mRadius * 2) * mAnimExpandHintFraction, mTop , mWidth - mCircleWidth, mHeight - mCircleWidth); canvas.drawRoundRect(rectF, mHintBgRoundValue, mHintBgRoundValue, mHintPaint);
減按鈕動(dòng)畫(huà)
看起來(lái)是旋轉(zhuǎn)、位移、透明度。
那么對(duì)于背景的圓圈來(lái)說(shuō),我們只需要位移、透明度。因?yàn)樗旧硎莻€(gè)圓,就不要旋轉(zhuǎn)了。
代碼:
//動(dòng)畫(huà) mAnimFraction :減 0~1, 加 1~0 , //動(dòng)畫(huà)位移Max, float animOffsetMax = (mRadius * 2 +mGapBetweenCircle); //透明度動(dòng)畫(huà)的基準(zhǔn) int animAlphaMax = 255; int animRotateMax = 360; //左邊 //背景 圓 mDelPaint.setAlpha((int) (animAlphaMax * (1 - mAnimFraction))); mDelPath.reset(); //改變圓心的X坐標(biāo),實(shí)現(xiàn)位移 mDelPath.addCircle(animOffsetMax * mAnimFraction + mLeft + mRadius, mTop + mRadius, mRadius, Path.Direction.CW); canvas.drawPath(mDelPath, mDelPaint);
對(duì)于前景的“-”號(hào)來(lái)說(shuō),旋轉(zhuǎn)、位移、透明度都需要做。
這里我們利用canvas.translate() canvas.rotate
做旋轉(zhuǎn)和位移動(dòng)畫(huà),別忘了 canvas.save()
和 canvas.restore()
恢復(fù)畫(huà)布的狀態(tài)。(透明度在上面已經(jīng)設(shè)置過(guò)了。)
//前景 - //旋轉(zhuǎn)動(dòng)畫(huà) canvas.save(); canvas.translate(animOffsetMax * mAnimFraction + mLeft + mRadius, mTop + mRadius); canvas.rotate((int) (animRotateMax * (1 - mAnimFraction))); canvas.drawLine(-mRadius / 2, 0, +mRadius / 2, 0, mDelPaint); canvas.restore();
數(shù)量的動(dòng)畫(huà)
看起來(lái)也是旋轉(zhuǎn)、位移、透明度。同樣是利用canvas.translate() canvas.rotate
做旋轉(zhuǎn)和位移動(dòng)畫(huà)。
//數(shù)量 canvas.save(); //平移動(dòng)畫(huà) canvas.translate(mAnimFraction * (mGapBetweenCircle / 2 - mTextPaint.measureText(mCount + "") / 2 + mRadius), 0); //旋轉(zhuǎn)動(dòng)畫(huà),旋轉(zhuǎn)中心點(diǎn),x 是繪圖中心,y 是控件中心 canvas.rotate(360 * mAnimFraction, mGapBetweenCircle / 2 + mLeft + mRadius * 2 , mTop + mRadius); //透明度動(dòng)畫(huà) mTextPaint.setAlpha((int) (255 * (1 - mAnimFraction))); //是沒(méi)有動(dòng)畫(huà)的普通寫(xiě)法,x left, y baseLine canvas.drawText(mCount + "", mGapBetweenCircle / 2 - mTextPaint.measureText(mCount + "") / 2 + mLeft + mRadius * 2, mTop + mRadius - (mFontMetrics.top + mFontMetrics.bottom) / 2, mTextPaint); canvas.restore();
動(dòng)畫(huà)的定義:
動(dòng)畫(huà)是在View初始化時(shí)就定義好的,執(zhí)行順序:
- 數(shù)量增加,0-1時(shí),先收縮Hint(第二層)mAnimReduceHint執(zhí)行,完畢后執(zhí)行減按鈕(第一層)進(jìn)入的動(dòng)畫(huà)mAnimAdd。
- 數(shù)量減少,1-0時(shí),先執(zhí)行減按鈕退出的動(dòng)畫(huà)mAniDel,再伸展Hint動(dòng)畫(huà)mAnimExpandHint,完畢后,顯示hint文字。
代碼如下:
//動(dòng)畫(huà) + mAnimAdd = ValueAnimator.ofFloat(1, 0); mAnimAdd.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mAnimFraction = (float) animation.getAnimatedValue(); invalidate(); } }); mAnimAdd.setDuration(350); //提示語(yǔ)收縮動(dòng)畫(huà) 0-1 mAnimReduceHint = ValueAnimator.ofFloat(0, 1); mAnimReduceHint.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mAnimExpandHintFraction = (float) animation.getAnimatedValue(); invalidate(); } }); mAnimReduceHint.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mCount == 1) { //然后底色也不顯示了 isHintMode = false; } if (mCount == 1) { Log.d(TAG, "現(xiàn)在還是1 開(kāi)始收縮動(dòng)畫(huà)"); if (mAnimAdd != null && !mAnimAdd.isRunning()) { mAnimAdd.start(); } } } @Override public void onAnimationStart(Animator animation) { if (mCount == 1) { //先不顯示文字了 isShowHintText = false; } } }); mAnimReduceHint.setDuration(350); //動(dòng)畫(huà) - mAniDel = ValueAnimator.ofFloat(0, 1); mAniDel.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mAnimFraction = (float) animation.getAnimatedValue(); invalidate(); } }); //1-0的動(dòng)畫(huà) mAniDel.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mCount == 0) { Log.d(TAG, "現(xiàn)在還是0onAnimationEnd() called with: animation = [" + animation + "]"); if (mAnimExpandHint != null && !mAnimExpandHint.isRunning()) { mAnimExpandHint.start(); } } } }); mAniDel.setDuration(350); //提示語(yǔ)展開(kāi)動(dòng)畫(huà) //分析這個(gè)動(dòng)畫(huà),最初是個(gè)圓。 就是left 不斷減小 mAnimExpandHint = ValueAnimator.ofFloat(1, 0); mAnimExpandHint.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mAnimExpandHintFraction = (float) animation.getAnimatedValue(); invalidate(); } }); mAnimExpandHint.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mCount == 0) { isShowHintText = true; } } @Override public void onAnimationStart(Animator animation) { if (mCount == 0) { isHintMode = true; } } }); mAnimExpandHint.setDuration(350);
針對(duì)復(fù)用機(jī)制的處理
因?yàn)槲覀兊馁?gòu)物車(chē)控件肯定會(huì)用在列表中,不管你用ListView還是RecyclerView,都會(huì)涉及到復(fù)用的問(wèn)題。
復(fù)用給我們帶來(lái)一個(gè)麻煩的地方就是,我們要處理好一些屬性狀態(tài)值,否則UI上會(huì)有問(wèn)題。
可以從兩處下手處理:
onMeasure
列表復(fù)用時(shí),依然會(huì)回調(diào)onMeasure()方法,所以在這里初始化一些UI顯示的參數(shù)。
這里順帶將適配wrap_content 的代碼也一同貼上:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int wMode = MeasureSpec.getMode(widthMeasureSpec); int wSize = MeasureSpec.getSize(widthMeasureSpec); int hMode = MeasureSpec.getMode(heightMeasureSpec); int hSize = MeasureSpec.getSize(heightMeasureSpec); switch (wMode) { case MeasureSpec.EXACTLY: break; case MeasureSpec.AT_MOST: //不超過(guò)父控件給的范圍內(nèi),自由發(fā)揮 int computeSize = (int) (getPaddingLeft() + mRadius * 2 +mGapBetweenCircle + mRadius * 2 + getPaddingRight() + mCircleWidth * 2); wSize = computeSize < wSize ? computeSize : wSize; break; case MeasureSpec.UNSPECIFIED: //自由發(fā)揮 computeSize = (int) (getPaddingLeft() + mRadius * 2 + mGapBetweenCircle + mRadius * 2 + getPaddingRight() + mCircleWidth * 2); wSize = computeSize; break; } switch (hMode) { case MeasureSpec.EXACTLY: break; case MeasureSpec.AT_MOST: int computeSize = (int) (getPaddingTop() + mRadius * 2 + getPaddingBottom() + mCircleWidth * 2); hSize = computeSize < hSize ? computeSize : hSize; break; case MeasureSpec.UNSPECIFIED: computeSize = (int) (getPaddingTop() + mRadius * 2 + getPaddingBottom() + mCircleWidth * 2); hSize = computeSize; break; } setMeasuredDimension(wSize, hSize); //復(fù)用時(shí)會(huì)走這里,所以初始化一些UI顯示的參數(shù) mAnimFraction = 0; initHintSettings(); } /** * 根據(jù)當(dāng)前count數(shù)量 初始化 hint提示語(yǔ)相關(guān)變量 */ private void initHintSettings() { if (mCount == 0) { isHintMode = true; isShowHintText = true; mAnimExpandHintFraction = 0; } else { isHintMode = false; isShowHintText = false; mAnimExpandHintFraction = 1; } }
在改變count時(shí)
一般在onBindViewHolder()或者getView()時(shí),都會(huì)對(duì)本控件重新設(shè)置count值,count改變時(shí),當(dāng)然也是需要根據(jù)count進(jìn)行屬性值的調(diào)整。
且此時(shí)如果View正在做動(dòng)畫(huà),應(yīng)該停止這些動(dòng)畫(huà)。
/** * 設(shè)置當(dāng)前數(shù)量 * @param count * @return */ public AnimShopButton setCount(int count) { mCount = count; //先暫停所有動(dòng)畫(huà) if (mAnimAdd != null && mAnimAdd.isRunning()) { mAnimAdd.cancel(); } if (mAniDel != null && mAniDel.isRunning()) { mAniDel.cancel(); } //復(fù)用機(jī)制的處理 if (mCount == 0) { // 0 不顯示 數(shù)字和-號(hào) mAnimFraction = 1; } else { mAnimFraction = 0; } initHintSettings(); return this; }
總結(jié)
代碼傳送門(mén):喜歡的話(huà),隨手點(diǎn)個(gè)star。多謝
https://github.com/mcxtzhang/AnimShopButton
我在實(shí)現(xiàn)這個(gè)控件時(shí),覺(jué)得難度相對(duì)大的地方在于做動(dòng)畫(huà)時(shí),“-”按鈕和數(shù)量的旋轉(zhuǎn)動(dòng)畫(huà),如何確定正確的坐標(biāo)值。因?yàn)閷ext繪制的居中本身就有一些注意事項(xiàng)在里面,再涉及到動(dòng)畫(huà),難免蒙圈。需要多計(jì)算,多試驗(yàn)。
還有就是觀察餓了么的效果,將hint區(qū)域的動(dòng)畫(huà)利用改變RoundRect的寬度去實(shí)現(xiàn)。起初沒(méi)有想到,也是思考了一會(huì)如何去做。這是屬于分析、拆解動(dòng)畫(huà)遇到的問(wèn)題。
除了繪制以外的重點(diǎn)是:
- 利用Region監(jiān)聽(tīng)區(qū)域點(diǎn)擊事件。
- 復(fù)用的列表,如何正確顯示UI。
- 動(dòng)畫(huà)次序以及考慮到復(fù)用時(shí),在合適的地方取消動(dòng)畫(huà)。
盡情在項(xiàng)目中使用它吧,有問(wèn)題隨時(shí)gayhub給我反饋。
通過(guò)sdk工具查看餓了么,它其實(shí)是用TextView和ImageView組合實(shí)現(xiàn)的。另外我十分懷疑它沒(méi)有封裝成控件,因?yàn)樵诹斜眄?yè)和詳情頁(yè)的交互,以及動(dòng)畫(huà)居然略有不同, 在詳情頁(yè),仔細(xì)看由0-1時(shí),它右邊的 + 按鈕的動(dòng)畫(huà)居然會(huì)閃一下,在列表頁(yè)卻沒(méi)有,很是不解。
好了,本文所述到此結(jié)束。
- Android Tween動(dòng)畫(huà)之RotateAnimation實(shí)現(xiàn)圖片不停旋轉(zhuǎn)效果實(shí)例介紹
- Android開(kāi)發(fā)之圖形圖像與動(dòng)畫(huà)(二)Animation實(shí)現(xiàn)圖像的漸變/縮放/位移/旋轉(zhuǎn)
- Android編程實(shí)現(xiàn)RotateAnimation設(shè)置中心點(diǎn)旋轉(zhuǎn)動(dòng)畫(huà)效果
- Android 3D旋轉(zhuǎn)動(dòng)畫(huà)效果實(shí)現(xiàn)分解
- Android動(dòng)畫(huà)之漸變動(dòng)畫(huà)(Tween Animation)詳解 (漸變、縮放、位移、旋轉(zhuǎn))
- Android酷炫動(dòng)畫(huà)效果之3D星體旋轉(zhuǎn)效果
- Android旋轉(zhuǎn)、平移、縮放和透明度漸變的補(bǔ)間動(dòng)畫(huà)
- Android使用Rotate3dAnimation實(shí)現(xiàn)3D旋轉(zhuǎn)動(dòng)畫(huà)效果的實(shí)例代碼
- Android仿視頻加載旋轉(zhuǎn)小球動(dòng)畫(huà)效果的實(shí)例代碼
- Android實(shí)現(xiàn)簡(jiǎn)單旋轉(zhuǎn)動(dòng)畫(huà)
相關(guān)文章
Android 中 onSaveInstanceState()使用方法詳解
這篇文章主要介紹了Android 中 onSaveInstanceState()使用方法詳解的相關(guān)資料,希望通過(guò)本文大家能夠掌握這部分知識(shí),需要的朋友可以參考下2017-09-09Android使用BroadcastReceiver實(shí)現(xiàn)手機(jī)開(kāi)機(jī)之后顯示畫(huà)面的功能
這篇文章主要介紹了Android使用BroadcastReceiver實(shí)現(xiàn)手機(jī)開(kāi)機(jī)之后顯示畫(huà)面的功能,結(jié)合實(shí)例形式分析了BroadcastReceiver的具體使用技巧及實(shí)現(xiàn)開(kāi)機(jī)畫(huà)面的相關(guān)功能代碼,需要的朋友可以參考下2016-01-01Android中 TeaScreenPopupWindow多類(lèi)型篩選彈框功能的實(shí)例代碼
這篇文章主要介紹了Android TeaScreenPopupWindow多類(lèi)型篩選彈框功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-06-06Android Studio / IDEA kotlin 顯示 var 真實(shí)類(lèi)型操作
這篇文章主要介紹了Android Studio / IDEA kotlin 顯示 var 真實(shí)類(lèi)型操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08Android下拉刷新完全解析,教你如何一分鐘實(shí)現(xiàn)下拉刷新功能(附源碼)
以下是我自己花功夫編寫(xiě)了一種非常簡(jiǎn)單的下拉刷新實(shí)現(xiàn)方案,現(xiàn)在拿出來(lái)和大家分享一下。相信在閱讀完本篇文章之后,大家都可以在自己的項(xiàng)目中一分鐘引入下拉刷新功能2013-07-07Android開(kāi)發(fā)之imageView圖片按比例縮放的實(shí)現(xiàn)方法
這篇文章主要介紹了Android開(kāi)發(fā)之imageView圖片按比例縮放的實(shí)現(xiàn)方法,較為詳細(xì)的分析了Android中ImageView控件的scaleType屬性控制圖片縮放的具體用法,需要的朋友可以參考下2016-01-01Android自定義View實(shí)現(xiàn)等級(jí)滑動(dòng)條的實(shí)例
這篇文章主要介紹了 Android自定義View實(shí)現(xiàn)等級(jí)滑動(dòng)條的實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-04-04Android實(shí)現(xiàn)調(diào)用系統(tǒng)分享功能示例的總結(jié)
這篇文章主要介紹了通過(guò)Android調(diào)用系統(tǒng)分享文本信息、單張圖片、多個(gè)文件和指定分享到微信、QQ,同時(shí)分享圖片和文字的功能示例,小編覺(jué)得挺不錯(cuò),一起跟隨小編過(guò)來(lái)看看吧2018-05-05