知乎安卓客戶端關注和取消關注的這個按鈕點擊特效是怎麼實現的?

能從按鈕的點擊的位置向外擴散,是canvas動態繪製的嗎?求大神指點一下思路,表示是第一次見過


一個周末就600多贊了,讓我這個知乎小透明心裡小小的激動啊。多謝大家的認可
我才不會說一覺醒來過百贊了,其實我是一隻盯著的O(∩_∩)O哈哈~

回答下評論的問題:
我年芳18,未婚,無不良癖好,謝謝

說正經的,評論中 @馮子力說的,這裡也寫出來,大家一起學習下:

第一點應該很多人都知道,Android官方給出的概念:Android流暢運行,需要運行60幀/秒, 則需要每幀的處理時間不超過16ms。

這裡所謂的每幀就是一個onDraw方法的運行時間,所以一切耗時操作都不能放在onDraw裡面,更可以說對onDraw的限制應該是苛刻的。

另外,因為onDraw再刷新的時候會被大量調用,所以onDraw中也不能出現聲明對象的地方,因為對象會被大量聲明,並且不能及時釋放,有可能會造成OOM。

關於第二點,這個說法我是第一次聽說,但聽起來就很高級,有沒有,所以查了一下資料,國內資料很少,有興趣的朋友可以google上查一下,這裡貼個國內寫的比較全的資料吧

Android Project Butter分析

深入分析UI 上層事件處理核心機制 Choreographer

其實按照我的理解,屏幕硬體刷新有一個頻率,android系統UI刷新有一個頻率,這兩個頻率必須對應起來。也就是說系統在刷新的時候,硬體也要刷新,不然不管系統刷新的多塊,如果和硬體對應不起來也會出現丟幀的現象。

而這一套體系在android底層已經實現了,就是Choreographer體系,反應在上層編碼,就是Animator,對於invalidate方法, 馮子力 給出的的解釋是這樣的,我覺得有道理,但是還沒有詳細調查過。所以只在這裡引用一下,並不給出結論。

另外一點,值得注意的是,知乎的代碼里就是用了Animator實現屏幕刷新,具體是不是因為choreographer的原因,不知道,但很可能是。

代碼已經更新了,手動的invalidate換成了Animator實現,大家可以看一下。

看了 @馮子力的評論,真的感覺又打開了一扇窗,這裡也表達一下我崇高的敬意。

=======================萬能的分割線,以下是正文======================

謝不邀

先說明一下,項目代碼已上傳至github,不想看長篇大論的也可以先去下代碼,對照代碼,哪裡不懂點哪裡。
代碼在這GitHub - zgzczzw/ZHFollowButton: 模仿知乎關注按鈕的點擊效果

前幾天發現知乎關注的點擊效果確實贊,查了一下實現方式,剛好看到這個問題,花了一天時間終於把這個效果實現了,現在來回答一下,很不幸,樓上各位的答案都不全對,且聽我一一道來。

首先,我先詳細觀察了一些知乎的效果,如 @Ben Zhang所說,其中有一個很神奇的地方,如圖:

注意看第二張圖,這個圓形在擴散的時候,圓形底下的字還在,而且新的字也在圓形上,就這個效果實現起來最難。

首先看一下樓上各位的回答,歸納來說,一共有2種實現方式,ripple效果和用paint在canvas上手動畫圓

ripple:

ripple即波紋效果,是android API 21以後引入的一種material design的元素,是觸摸反饋的一種,也就是說點擊的時候會出現水波擴散的樣式,demo(見最後)中第一個按鈕就是用了ripple效果。

實現方式很簡單,實現一個這樣的drawable

第一個color是波紋顏色,item裡面指定background正常的顏色,可以是一個shape,也可以是一個drawable,還可以是一個selector。

設置為按鈕的background即可

如果整個程序的theme用了meterial,那基本所有的帶點擊效果的控制項,比如button都自帶這個波紋效果。不過需要注意的是這一套API是21以後才提供的,所以需要做兼容處理。

效果如下:

從圖中可以看出即使我設置了波紋為紅色(#FF0000),點擊後的效果也是淡紅色,我猜測因為是水波紋效果,為了不影響按鈕本身展示的內容,android系統自動做了透明度的處理,另外從圖中也可以明顯的看出,水波紋和顯示的內容是上下兩層的,互不影響,水波紋是在background層面上。這個效果做普通的點擊反饋還不錯,但絕對實現不出知乎這種用波紋刷新出內容的效果。所以很容易能看出知乎的點擊效果不是用ripple做出來的。

Paint在canvas上畫圓

@chaossss 所說的用 paint在點擊的地方畫圓形,然後讓畫的圓形半徑慢慢變大,實現出擴散出去的樣式,我實現了一下,代碼如下:

@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);
}//設置畫筆顏色
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint);

if (mRevealRadius &<= mMaxRadius) { //一定時間後再刷新 postInvalidateDelayed(INVALIDATE_DURATION); } else { if (mIsPressed) { setTextColor(Color.WHITE); this.setBackgroundColor(Color.RED); } else { setTextColor(Color.BLACK); this.setBackgroundColor(Color.WHITE); } mShouldDoAnimation = false; invalidate(); } } }

效果如圖:

本來覺得差不多就是這樣,但是跟知乎的效果比較一下,還是能發現差別的。用paint畫圓能實現的是在點擊的地方畫一個圓,然後半徑慢慢變大慢慢擴散。但是問題在於,畫的這個圓會蓋住顯示的內容,而且畫的圓上也不能顯示內容。我試過用drawText,也實現不了字和圓一起的效果,解決方法只有,

  1. 畫的過程中改背景色和上面文字。
  2. 然後,畫完圓之後把圓擦掉,把下面的背景色和文字顯示出來。

這樣就會出現一次文字閃爍的問題,首先文字會消失掉,然後畫完圓之後才顯示出來。因為圓在擴散的時候是看不到文字的,只有等圓消失了,文字才能顯示出來。而知乎的效果是文字和圓一起刷出來,而且底下的文字還在,中間也沒有文字閃爍的問題,整個過程行雲流水,看起來很順暢,好像用圓形揭開了幕布一樣。

綜上所述,樓上所有的答案都是答主們看到這個效果後第一反應的實現,其實如果不是我自己實現了一下,真的以為第二種方法就是知乎採用的,但是目前看來,很遺憾,知乎採用了一種更好的方式來實現這個效果。


那怎麼辦呢,我也沒什麼思路,怎麼才能在畫圓的時候把字也畫在圓上,然後圓下面的背景也還有呢。沒什麼思路,看看知乎的代碼吧,反編譯。


反編譯的過程我簡單說一下:

  • 到知乎官網下載最新的知乎apk
  • 用apktool反編譯apk,得到資源文件

  • 在資源文件中搜索follow,這裡一開始我搜的是ripple,因為我覺得這個效果總歸應該和ripple有關,沒結果,於是搜了follow,沒想到還真搜出來了。

RevealFollowButton這明顯就是我們要的波紋展開的控制項,這就好說了,下一步就是去代碼里找到這個控制項了。這裡要記一下,這個控制項的位置com.zhihu.android.app.ui.widget.RevealFollowButton。

  • 反編譯代碼

將apk改名成rar,打開,可以找到裡面的class文件

知乎用了multidex,所以會有兩個class文件,都拖出來放在dex2jar里反編譯一下,就能生成兩個jar包了,把jar包放在GUI里看一下,就能看到代碼了,雖然代碼被混淆過,但是基本邏輯還是能看出來的。

然後根據前面xml里的路徑找到RevelFollowButton的位置,打開代碼看就可以了。

這是類的繼承關係,RevealFollowButton繼承自RevealFrameLayout,然後繼承自ZHFrameLayout,這個ZHFrameLayout的父類就是FrameLayout了,從名字我們能看出,RevelFollowButton和RevealFrameLayout就是這個效果實現的兩個類了。

看到這個效果的實現是基於Framelayout,我就知道我們之前討論的方法其實都走錯了方向,如果告訴你用framelayout來實現這個效果,你會怎麼做?

我的想法是加入兩個TextView到這個layout里,然後一個Visible一個gone,如此切換,後來看過代碼後,也證明我的這個想法是對的。

看,這裡有兩個TextView。如此的話,其實切換TextView是很容易實現的,問題是怎麼實現波紋切換的效果,那第一件事就是看onDraw函數了,對於GroupView來說是drawChild方法。

RevealFollowButton的drawChild方法沒什麼內容,基本是調用了父類,那麼我們來看RevealFrameLayout的drawChild方法。

這裡有兩部分邏輯,如果滿足一個條件,就做第一部分,一開始我也不知道這個條件是什麼,混淆後的代碼能看懂大邏輯,像這種小邏輯只能走一步看一步了。所以假設這個條件永遠false吧,看第二部分,看到這裡瞬間明白了,原來是採用切割畫布的方式,把畫布切成一個圓的,就能做到顯示的內容也在圓上,而不是內容被覆蓋在圓下面了。然後同理,把這個圓形區域不斷擴大,然後不斷刷新,就是實現波形刷出內容的效果了。代碼如下吧


protected boolean drawChild(Canvas canvas, View paramView, long paramLong) {
int i = canvas.save();
mPath.reset();
//mCenterX mCenterY是點擊的位置,在onTouchEvent里設置
//mRevealRadius是圓的半徑,會漸漸變大
mPath.addCircle(mCenterX, mCenterY, mRevealRadius, Path.Direction.CW);
canvas.clipPath(this.mPath);
boolean bool2 = super.drawChild(canvas, paramView, paramLong);
canvas.restoreToCount(i);
return bool2;
}

按照上面說的,肯定還有一個類似於定時器的東西,能不斷改變圓形的半徑,然後刷新,其實這個在代碼里找找很容易就找到了。RevealFrameLayout里除了這個drawChild,沒有別的代碼了。所以我們來看RevealFollowButton。


RevealFollowButton裡面跟定時器有關的就是這句了

一個Animator對象,其實這句代碼我是沒看懂的,但邏輯很簡單,設置一個Animator,定時500ms,在這個過程中修改圓形半徑,然後刷新。

Math.hypot(getWidth(), getHeight()))


其中這個方法是根據勾股定理獲取三角形的斜邊長度,想想我們所要繪製的圓形半徑最長是多少,沒錯,就是TextView的對角線長度。所以,整個邏輯就很簡單了。

我搞了下代碼,就這樣吧

整個方法的代碼如下吧,還包括控制FollowTv和unFollowTv哪個顯示

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();
}
}

根據當前狀態把Follow的Textview或UnFollow的TextView顯示出來,然後設置一個定時器不斷擴大所要繪製圓的半徑,根據這個半徑裁剪畫布成一個漸漸變大的圓形,然後內容就漸漸顯示出來了。

這個效果實現出來之後,試著運行一下,還不錯,但是總覺得有地方不對,於是細細觀察,終於發現了,知乎的那個效果在刷新的時候,底下的背景不是白色的,還是之前的狀態,比如要變成關注的時候,背景中的未關注還是在的,而我們實現的這個,刷新的時候背景是白色的。


這是知乎的

這是我的

所以還是沒有知乎那麼行雲流水,所以我們是少了什麼嗎。這時候想起來了,之前在RevealFrameLayout的drawChild里有一個判斷條件,當時我們不知道它的邏輯是幹什麼的,現在看來。那部分邏輯就是處理這個的,畫子控制項的時候,要畫兩個,FollowTextView和UnFollowTextView,要隨圓形刷出的控制項我們採用裁剪畫布的方式慢慢畫出。那作為背景的另一個控制項就不需要慢慢畫出,只要完全畫出來就行了。所以,猜想這裡這個判斷條件就是判斷當前控制項是不是要隨圓形刷出的控制項,如果不是,就直接畫出來就行了。所以修改代碼如下:


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;
}

至此,整個效果就和知乎完全一樣了,刷新過程行雲流水,非常贊。效果如下

實現代碼已上傳至github:

GitHub - zgzczzw/ZHFollowButton: 模仿知乎關注按鈕的點擊效果


恩,看到這,差不多該小手一抖,點個贊了哈


public static Animator createCircularReveal (View view, int centerX, int centerY, float startRadius, float endRadius);

文檔看這裡(API21+) -&> https://developer.android.com/intl/zh-cn/reference/android/view/ViewAnimationUtils.html#createCircularReveal(android.view.View,%20int,%20int,%20float,%20float)

Update .
Github 上有一個褲子 CircularReveal 兼容到2.3+
https://github.com/ozodrukh/CircularReveal


-----------------------------------
莫名其妙被三無用戶噴我也是無語,伸手黨要代碼很光榮很值得驕傲么?思路都告訴你了,實際代碼量100行都沒有的東西。
-----------------------------------
思路:

手動實現的話,在onDraw方法裡面手動繪製吧。具體操作:

1.點擊時,開始動畫
2.動畫動態改變你要顯示的圓的半徑,在onDraw()方法裡面重繪
3.動畫顯示完畢將半徑設為初始值

ps:用插值器+Animator方便很多,高版本有API

-----------------------------------

部分代碼:

我就不給你們全部代碼,不服啊?順著網線過來打死我啊!

if(mTargetView == null || !mShouldDoAnimation || mTargetWidth &<= 0)
return;

if(mRevealRadius &> mMinBetweenWidthAndHeight / 2)
mRevealRadius += mRevealRadiusGap * 4;
else
mRevealRadius += mRevealRadiusGap;

int[] location = new int[2];
this.getLocationOnScreen(mLocation);
mTargetView.getLocationOnScreen(location);

int top = location[1] - mLocation[1];
int left = location[0] - mLocation[0];
int right = left + mTargetView.getMeasuredWidth();
int bottom = top + mTargetView.getMeasuredHeight();

canvas.save();
canvas.clipRect(left, top, right, bottom);
canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint);
canvas.restore();

if(mRevealRadius &<= mMaxRadius)
postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
else if(!mIsPressed){
mShouldDoAnimation = false;
postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
}


看了很多人說是ripple,我仔細看了下,其實這個按鈕特效的定位邏輯跟原生的&是有區別的,以下是知乎關注按鈕點擊狀態改變過程中的截圖:

而ripple的表現如下(網易雲音樂為例):

(好吧我懶沒上虛擬機,暫時沒找到響應那麼快的截圖工具,所以其實是點了很多次的但是我保證點的大致都是同一個地方,就是藍色的點的位置,而紅色點標註的是大致的圓心)

可以看出來其實ripple的圓心是在變化的,隨著時間從手指觸摸的地方移動到View的中心,從而達到圓形邊緣同時觸及到View的四個corner的效果。因此,在ripple中是不可能出現上面知乎截圖中半邊按鈕已經被填滿了而右邊還空著的現象,這應該是知乎自己的實現,chaossss的實現應該類似於這個效果。

之前玩過一段時間Android,不是特別深入,輕噴。

(PS 你見我截圖那麼辛苦的也忍心不點個贊么&>^&<)
去看看Google的 material dragon的OG吧,你要是覺得這效果好的話整個界面都會迷醉的。。


android5.0之後有自帶,你在drawable-v21文件下添加按鈕資源文件的xml,是以&&開頭結尾格式。
網上也有兼容包可以實現在5.0以下顯示波紋效果,但是就只是為了這個效果沒必要


這個在國外很多設計中已經應用非常普遍了,怕你不夠用,提供10個漣漪效果和框架,有APP原生的也有web的, 框架都很成熟易用了,如果你不是專門的開發者用來學習或者其它,只是項目需要,完全無需專門去寫個新的。
https://design.google.com/
Material-UI
appspot.com
Button Ripple Effect
android - Material effect on button with background color
Ripple Animation
Rippler · Effect that spreads like a wave in touch or click.
Button · rey5137/material Wiki · GitHub
Waves - Materialize!
Simple Ripple + Reveal + Elevation tutorial

最後,我自己如果需要會使用一個最簡單的辦法,就是使用CSS3的類似這樣的動畫即可(僅限web):

.ripple-demo {
-webkit-animation: spread .6s linear;
animation: spread .6s linear;
}

@-webkit-keyframes spread {
100% {
-webkit-transform: scale(2);
-ms-transform: scale(2);
transform: scale(2);
opacity: 0;
}
}
@keyframes spread {
100% {
-webkit-transform: scale(2);
-ms-transform: scale(2);
transform: scale(2);
opacity: 0;
}
}


5.0material design的水波紋效果,api21以下,可以試試github上的rippleview。


我最先想到的竟然是ae_(:з)∠)_


Waves


知乎的關注動畫跟 ripple 還不一樣(我的手機是4.4的,不知道知乎有沒有設置drawable-v21 中用ripple)。知乎4.4的動畫效果是手指鬆開後有一個類似於波紋的效果,v21以後的ripple是手指一按上去就有波紋(手指一直按著波紋才會慢慢擴散)。
至於實現,給你一個簡單的.xml示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
&

&
&
&
&
&
&
&
&
&
&
&
&


你去看看推特的點贊,更厲害!


操作系統實現,你用你也有


5.0後design support包里有一個circular reveal可以實現這種效果


哈哈被你說了以後這個關注鍵我能玩一年


知乎客戶端是個IONIC的H5應用


android 5.0開始加入的波紋效果,有帶邊界的就像這個關注按鈕,也有不帶邊界的borderless你看見的只是字,點擊擴散開是個圓。20以下的sdk應該是沒有這個效果的,得仿造!


非常自覺的點了關注,又取消了


不自覺的點了關注,又點了取消關注


自定義view,點擊的時候以點擊的點為圓心,以半徑為0到最大寬度不斷畫圓畫圓


推薦閱讀:

有沒有拍照濾鏡的第三方SDK?
如何評價《第一行代碼》一書?
知乎網頁登錄背景的動畫是怎麼做出來的?
QQ 未讀消息的拖拽動態效果是如何實現的?

TAG:Android開發 | 交互設計 | Android | MaterialDesign |