Android仿知乎客戶端關(guān)注和取消關(guān)注的按鈕點(diǎn)擊特效實(shí)現(xiàn)思路詳解
先說(shuō)明一下,項(xiàng)目代碼已上傳至github,不想看長(zhǎng)篇大論的也可以先去下代碼,對(duì)照代碼,哪里不懂點(diǎn)哪里。
代碼在這https://github.com/zgzczzw/ZHFollowButton
前幾天發(fā)現(xiàn)知乎關(guān)注的點(diǎn)擊效果確實(shí)贊,查了一下實(shí)現(xiàn)方式,剛好看到這個(gè)問(wèn)題,花了一天時(shí)間終于把這個(gè)效果實(shí)現(xiàn)了,現(xiàn)在來(lái)回答一下,很不幸,樓上各位的答案都不全對(duì),且聽(tīng)我一一道來(lái)。
首先,我先詳細(xì)觀察了一些知乎的效果,其中有一個(gè)很神奇的地方,如圖:
注意看第二張圖,這個(gè)圓形在擴(kuò)散的時(shí)候,圓形底下的字還在,而且新的字也在圓形上,就這個(gè)效果實(shí)現(xiàn)起來(lái)最難。
首先看一下樓上各位的回答,歸納來(lái)說(shuō),一共有2種實(shí)現(xiàn)方式,ripple效果和用paint在canvas上手動(dòng)畫(huà)圓
ripple:
ripple即波紋效果,是android API 21以后引入的一種material design的元素,是觸摸反饋的一種,也就是說(shuō)點(diǎn)擊的時(shí)候會(huì)出現(xiàn)水波擴(kuò)散的樣式,demo(見(jiàn)最后)中第一個(gè)按鈕就是用了ripple效果。
實(shí)現(xiàn)方式很簡(jiǎn)單,實(shí)現(xiàn)一個(gè)這樣的drawable
第一個(gè)color是波紋顏色,item里面指定background正常的顏色,可以是一個(gè)shape,也可以是一個(gè)drawable,還可以是一個(gè)selector。
設(shè)置為按鈕的background即可
如果整個(gè)程序的theme用了meterial,那基本所有的帶點(diǎn)擊效果的控件,比如button都自帶這個(gè)波紋效果。不過(guò)需要注意的是這一套API是21以后才提供的,所以需要做兼容處理。
效果如下:
從圖中可以看出即使我設(shè)置了波紋為紅色(#FF0000),點(diǎn)擊后的效果也是淡紅色,我猜測(cè)因?yàn)槭撬y效果,為了不影響按鈕本身展示的內(nèi)容,android系統(tǒng)自動(dòng)做了透明度的處理,另外從圖中也可以明顯的看出,水波紋和顯示的內(nèi)容是上下兩層的,互不影響,水波紋是在background層面上。這個(gè)效果做普通的點(diǎn)擊反饋還不錯(cuò),但絕對(duì)實(shí)現(xiàn)不出知乎這種用波紋刷新出內(nèi)容的效果。所以很容易能看出知乎的點(diǎn)擊效果不是用ripple做出來(lái)的。
Paint在canvas上畫(huà)圓
@chaossss 所說(shuō)的用 paint在點(diǎn)擊的地方畫(huà)圓形,然后讓畫(huà)的圓形半徑慢慢變大,實(shí)現(xiàn)出擴(kuò)散出去的樣式,我實(shí)現(xiàn)了一下,代碼如下:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mShouldDoAnimation) { mMaxRadius = getMeasuredWidth() + 50; if (mRevealRadius > mMinBetweenWidthAndHeight / 2) mRevealRadius += mRevealRadiusGap * 4; else mRevealRadius += mRevealRadiusGap;//半徑變大 Paint mPaint = new Paint(); if (!mIsPressed) { mPaint.setColor(Color.WHITE); } else { mPaint.setColor(Color.RED); }//設(shè)置畫(huà)筆顏色 mPaint.setStyle(Paint.Style.FILL); canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint); if (mRevealRadius <= mMaxRadius) { //一定時(shí)間后再刷新 postInvalidateDelayed(INVALIDATE_DURATION); } else { if (mIsPressed) { setTextColor(Color.WHITE); this.setBackgroundColor(Color.RED); } else { setTextColor(Color.BLACK); this.setBackgroundColor(Color.WHITE); } mShouldDoAnimation = false; invalidate(); } } }
效果如圖:
本來(lái)覺(jué)得差不多就是這樣,但是跟知乎的效果比較一下,還是能發(fā)現(xiàn)差別的。用paint畫(huà)圓能實(shí)現(xiàn)的是在點(diǎn)擊的地方畫(huà)一個(gè)圓,然后半徑慢慢變大慢慢擴(kuò)散。但是問(wèn)題在于,畫(huà)的這個(gè)圓會(huì)蓋住顯示的內(nèi)容,而且畫(huà)的圓上也不能顯示內(nèi)容。我試過(guò)用drawText,也實(shí)現(xiàn)不了字和圓一起的效果,解決方法只有,畫(huà)的過(guò)程中改背景色和上面文字。
然后,畫(huà)完圓之后把圓擦掉,把下面的背景色和文字顯示出來(lái)。
這樣就會(huì)出現(xiàn)一次文字閃爍的問(wèn)題,首先文字會(huì)消失掉,然后畫(huà)完圓之后才顯示出來(lái)。因?yàn)閳A在擴(kuò)散的時(shí)候是看不到文字的,只有等圓消失了,文字才能顯示出來(lái)。而知乎的效果是文字和圓一起刷出來(lái),而且底下的文字還在,中間也沒(méi)有文字閃爍的問(wèn)題,整個(gè)過(guò)程行云流水,看起來(lái)很順暢,好像用圓形揭開(kāi)了幕布一樣。
綜上所述,樓上所有的答案都是答主們看到這個(gè)效果后第一反應(yīng)的實(shí)現(xiàn),其實(shí)如果不是我自己實(shí)現(xiàn)了一下,真的以為第二種方法就是知乎采用的,但是目前看來(lái),很遺憾,知乎采用了一種更好的方式來(lái)實(shí)現(xiàn)這個(gè)效果。
那怎么辦呢,我也沒(méi)什么思路,怎么才能在畫(huà)圓的時(shí)候把字也畫(huà)在圓上,然后圓下面的背景也還有呢。沒(méi)什么思路,看看知乎的代碼吧,反編譯。
反編譯的過(guò)程我簡(jiǎn)單說(shuō)一下:
到知乎官網(wǎng)下載最新的知乎apk
用apktool反編譯apk,得到資源文件
在資源文件中搜索follow,這里一開(kāi)始我搜的是ripple,因?yàn)槲矣X(jué)得這個(gè)效果總歸應(yīng)該和ripple有關(guān),沒(méi)結(jié)果,于是搜了follow,沒(méi)想到還真搜出來(lái)了。
RevealFollowButton這明顯就是我們要的波紋展開(kāi)的控件,這就好說(shuō)了,下一步就是去代碼里找到這個(gè)控件了。這里要記一下,這個(gè)控件的位置com.zhihu.android.app.ui.widget.RevealFollowButton。
反編譯代碼
將apk改名成rar,打開(kāi),可以找到里面的class文件
知乎用了multidex,所以會(huì)有兩個(gè)class文件,都拖出來(lái)放在dex2jar里反編譯一下,就能生成兩個(gè)jar包了,把jar包放在GUI里看一下,就能看到代碼了,雖然代碼被混淆過(guò),但是基本邏輯還是能看出來(lái)的。
然后根據(jù)前面xml里的路徑找到RevelFollowButton的位置,打開(kāi)代碼看就可以了。
這是類(lèi)的繼承關(guān)系,RevealFollowButton繼承自RevealFrameLayout,然后繼承自ZHFrameLayout,這個(gè)ZHFrameLayout的父類(lèi)就是FrameLayout了,從名字我們能看出,RevelFollowButton和RevealFrameLayout就是這個(gè)效果實(shí)現(xiàn)的兩個(gè)類(lèi)了。
看到這個(gè)效果的實(shí)現(xiàn)是基于Framelayout,我就知道我們之前討論的方法其實(shí)都走錯(cuò)了方向,如果告訴你用framelayout來(lái)實(shí)現(xiàn)這個(gè)效果,你會(huì)怎么做?
我的想法是加入兩個(gè)TextView到這個(gè)layout里,然后一個(gè)Visible一個(gè)gone,如此切換,后來(lái)看過(guò)代碼后,也證明我的這個(gè)想法是對(duì)的。
看,這里有兩個(gè)TextView。如此的話,其實(shí)切換TextView是很容易實(shí)現(xiàn)的,問(wèn)題是怎么實(shí)現(xiàn)波紋切換的效果,那第一件事就是看onDraw函數(shù)了,對(duì)于GroupView來(lái)說(shuō)是drawChild方法。
RevealFollowButton的drawChild方法沒(méi)什么內(nèi)容,基本是調(diào)用了父類(lèi),那么我們來(lái)看RevealFrameLayout的drawChild方法。
這里有兩部分邏輯,如果滿足一個(gè)條件,就做第一部分,一開(kāi)始我也不知道這個(gè)條件是什么,混淆后的代碼能看懂大邏輯,像這種小邏輯只能走一步看一步了。所以假設(shè)這個(gè)條件永遠(yuǎn)false吧,看第二部分,看到這里瞬間明白了,原來(lái)是采用切割畫(huà)布的方式,把畫(huà)布切成一個(gè)圓的,就能做到顯示的內(nèi)容也在圓上,而不是內(nèi)容被覆蓋在圓下面了。然后同理,把這個(gè)圓形區(qū)域不斷擴(kuò)大,然后不斷刷新,就是實(shí)現(xiàn)波形刷出內(nèi)容的效果了。代碼如下吧
protected boolean drawChild(Canvas canvas, View paramView, long paramLong) { int i = canvas.save(); mPath.reset(); //mCenterX mCenterY是點(diǎn)擊的位置,在onTouchEvent里設(shè)置 //mRevealRadius是圓的半徑,會(huì)漸漸變大 mPath.addCircle(mCenterX, mCenterY, mRevealRadius, Path.Direction.CW); canvas.clipPath(this.mPath); boolean bool2 = super.drawChild(canvas, paramView, paramLong); canvas.restoreToCount(i); return bool2; }
按照上面說(shuō)的,肯定還有一個(gè)類(lèi)似于定時(shí)器的東西,能不斷改變圓形的半徑,然后刷新,其實(shí)這個(gè)在代碼里找找很容易就找到了。RevealFrameLayout里除了這個(gè)drawChild,沒(méi)有別的代碼了。所以我們來(lái)看RevealFollowButton。
RevealFollowButton里面跟定時(shí)器有關(guān)的就是這句了
一個(gè)Animator對(duì)象,其實(shí)這句代碼我是沒(méi)看懂的,但邏輯很簡(jiǎn)單,設(shè)置一個(gè)Animator,定時(shí)500ms,在這個(gè)過(guò)程中修改圓形半徑,然后刷新。
Math.hypot(getWidth(), getHeight()))
其中這個(gè)方法是根據(jù)勾股定理獲取三角形的斜邊長(zhǎng)度,想想我們所要繪制的圓形半徑最長(zhǎng)是多少,沒(méi)錯(cuò),就是TextView的對(duì)角線長(zhǎng)度。所以,整個(gè)邏輯就很簡(jiǎn)單了。
我搞了下代碼,就這樣吧
整個(gè)方法的代碼如下吧,還包括控制FollowTv和unFollowTv哪個(gè)顯示
protected void setFollowed(boolean isFollowed, boolean needAnimate) {
mIsFollowed = isFollowed;
if (isFollowed) {
mUnFollowTv.setVisibility(View.VISIBLE);
mFollowTv.setVisibility(View.VISIBLE);
mFollowTv.bringToFront();
} else {
mUnFollowTv.setVisibility(View.VISIBLE);
mFollowTv.setVisibility(View.VISIBLE);
mUnFollowTv.bringToFront();
}
if (needAnimate) {
ValueAnimator animator = ObjectAnimator.ofFloat(mFollowTv, "empty", 0.0F, (float) Math.hypot(getMeasuredWidth(), getMeasuredHeight()));
animator.setDuration(500L);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mRevealRadius = (Float) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
}
根據(jù)當(dāng)前狀態(tài)把Follow的Textview或UnFollow的TextView顯示出來(lái),然后設(shè)置一個(gè)定時(shí)器不斷擴(kuò)大所要繪制圓的半徑,根據(jù)這個(gè)半徑裁剪畫(huà)布成一個(gè)漸漸變大的圓形,然后內(nèi)容就漸漸顯示出來(lái)了。
這個(gè)效果實(shí)現(xiàn)出來(lái)之后,試著運(yùn)行一下,還不錯(cuò),但是總覺(jué)得有地方不對(duì),于是細(xì)細(xì)觀察,終于發(fā)現(xiàn)了,知乎的那個(gè)效果在刷新的時(shí)候,底下的背景不是白色的,還是之前的狀態(tài),比如要變成關(guān)注的時(shí)候,背景中的未關(guān)注還是在的,而我們實(shí)現(xiàn)的這個(gè),刷新的時(shí)候背景是白色的。
這是知乎的
這是我的
所以還是沒(méi)有知乎那么行云流水,所以我們是少了什么嗎。這時(shí)候想起來(lái)了,之前在RevealFrameLayout的drawChild里有一個(gè)判斷條件,當(dāng)時(shí)我們不知道它的邏輯是干什么的,現(xiàn)在看來(lái)。那部分邏輯就是處理這個(gè)的,畫(huà)子控件的時(shí)候,要畫(huà)兩個(gè),F(xiàn)ollowTextView和UnFollowTextView,要隨圓形刷出的控件我們采用裁剪畫(huà)布的方式慢慢畫(huà)出。那作為背景的另一個(gè)控件就不需要慢慢畫(huà)出,只要完全畫(huà)出來(lái)就行了。所以,猜想這里這個(gè)判斷條件就是判斷當(dāng)前控件是不是要隨圓形刷出的控件,如果不是,就直接畫(huà)出來(lái)就行了。所以修改代碼如下:
protected boolean drawChild(Canvas canvas, View paramView, long paramLong) {
if (drawBackground(paramView)) {
return super.drawChild(canvas, paramView, paramLong);
}
int i = canvas.save();
mPath.reset();
mPath.addCircle(mCenterX, mCenterY, mRevealRadius, Path.Direction.CW);
canvas.clipPath(this.mPath);
boolean bool2 = super.drawChild(canvas, paramView, paramLong);
canvas.restoreToCount(i);
return bool2;
}
判斷的方法如下:
private boolean drawBackground(View paramView) {
if (mIsFollowed && paramView == mUnFollowTv) {
return true;
} else if (!mIsFollowed && paramView == mFollowTv) {
return true;
}
return false;
}
至此,整個(gè)效果就和知乎完全一樣了,刷新過(guò)程行云流水,非常贊。效果如下
實(shí)現(xiàn)代碼已上傳至github:
https://github.com/zgzczzw/ZHFollowButton
以上所述是小編給大家介紹的Android仿知乎客戶端關(guān)注和取消關(guān)注的按鈕點(diǎn)擊特效實(shí)現(xiàn)思路詳解,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
- Android懸浮按鈕點(diǎn)擊返回頂部FloatingActionButton
- Android點(diǎn)擊按鈕返回頂部實(shí)現(xiàn)代碼
- Android Button按鈕的四種點(diǎn)擊事件
- android中在Activity中響應(yīng)ListView內(nèi)部按鈕的點(diǎn)擊事件的兩種方法
- Android模擬開(kāi)關(guān)按鈕點(diǎn)擊打開(kāi)動(dòng)畫(huà)(屬性動(dòng)畫(huà)之平移動(dòng)畫(huà))
- Android防止按鈕過(guò)快點(diǎn)擊造成多次事件的解決方法
- 基于Android實(shí)現(xiàn)點(diǎn)擊某個(gè)按鈕讓菜單選項(xiàng)從按鈕周?chē)付ㄎ恢脧棾?/a>
- 實(shí)例詳解Android解決按鈕重復(fù)點(diǎn)擊問(wèn)題
- Android實(shí)現(xiàn)點(diǎn)擊AlertDialog上按鈕時(shí)不關(guān)閉對(duì)話框的方法
- Android實(shí)現(xiàn)按鈕點(diǎn)擊效果
相關(guān)文章
詳談Android中onTouch與onClick事件的關(guān)系(必看)
下面小編就為大家?guī)?lái)一篇詳談Android中onTouch與onClick事件的關(guān)系(必看)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03Android中SurfaceView用法簡(jiǎn)單實(shí)例
這篇文章主要介紹了Android中SurfaceView用法,以一個(gè)簡(jiǎn)單的圖形繪制及改變位置實(shí)現(xiàn)方法分析了SurfaceView的使用技巧,需要的朋友可以參考下2015-10-10Dagger2 Android依賴注入學(xué)習(xí)筆記
這篇文章主要介紹了Dagger2 Android依賴注入學(xué)習(xí)筆記,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06關(guān)于Android中drawable必知的一些規(guī)則
drawable這個(gè)東西相信大家天天都在使用,每個(gè)Android開(kāi)發(fā)者都再熟悉不過(guò)了,但可能還有一些你所不知道的規(guī)則,那今天我們就來(lái)一起探究一下這些規(guī)則。2016-08-08Android使用DocumentFile讀寫(xiě)外置存儲(chǔ)的問(wèn)題
大家好,本篇文章主要講的是Android使用DocumentFile讀寫(xiě)外置存儲(chǔ)的問(wèn)題,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話記得收藏一下2021-12-12Android自定義流式布局實(shí)現(xiàn)淘寶搜索記錄
這篇文章主要為大家詳細(xì)介紹了Android自定義流式布局實(shí)現(xiàn)淘寶搜索記錄,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10Android 中 Tweened animation的實(shí)例詳解
這篇文章主要介紹了Android 中 Tweened animation的實(shí)例詳解的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家理解掌握這部分內(nèi)容,需要的朋友可以參考下2017-09-09Android 判斷SIM卡屬于哪個(gè)移動(dòng)運(yùn)營(yíng)商詳解及實(shí)例
這篇文章主要介紹了Android 判斷SIM卡屬于哪個(gè)移動(dòng)運(yùn)營(yíng)商詳解相關(guān)資料,并附實(shí)例代碼,具有一定參考價(jià)值,需要的朋友可以參考下2016-11-11