您的位置:首页 > 其它

知乎安卓客户端关注和取消关注的这个按钮点击特效是怎么实现的?

2016-08-23 09:43 871 查看
点击打开链接

作者:轩辕

链接:http://www.zhihu.com/question/41586221/answer/107630865

来源:知乎

著作权归作者所有,转载请联系作者获得授权。

一个周末就600多赞了,让我这个知乎小透明心里小小的激动啊。多谢大家的认可

我才不会说一觉醒来过百赞了,其实我是一只盯着的O(∩_∩)O哈哈~

回答下评论的问题:

我年芳18,未婚,无不良癖好,谢谢

说正经的,评论中 @冯子力说的,这里也写出来,大家一起学习下:

<img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/64cd6d209ec87dd635b04598e9cfabdf.png" data-rawwidth="535" data-rawheight="153" class="origin_image zh-lightbox-thumb" width="535" data-original="https://pic3.zhimg.com/9d2e80a73213ec2e8bda38fd794c889a_r.png">第一点应该很多人都知道,Android官方给出的概念:Android流畅运行,需要运行60帧/秒,
则需要每帧的处理时间不超过16ms。

第一点应该很多人都知道,Android官方给出的概念:Android流畅运行,需要运行60帧/秒, 则需要每帧的处理时间不超过16ms。

这里所谓的每帧就是一个onDraw方法的运行时间,所以一切耗时操作都不能放在onDraw里面,更可以说对onDraw的限制应该是苛刻的。

另外,因为onDraw再刷新的时候会被大量调用,所以onDraw中也不能出现声明对象的地方,因为对象会被大量声明,并且不能及时释放,有可能会造成OOM。

关于第二点,这个说法我是第一次听说,但听起来就很高级,有没有,所以查了一下资料,国内资料很少,有兴趣的朋友可以google上查一下,这里贴个国内写的比较全的资料吧

Android Project Butter分析

深入分析UI 上层事件处理核心机制 Choreographer

其实按照我的理解,屏幕硬件刷新有一个频率,android系统UI刷新有一个频率,这两个频率必须对应起来。也就是说系统在刷新的时候,硬件也要刷新,不然不管系统刷新的多块,如果和硬件对应不起来也会出现丢帧的现象。

而这一套体系在android底层已经实现了,就是Choreographer体系,反应在上层编码,就是Animator,对于invalidate方法, 冯子力 给出的的解释是这样的,我觉得有道理,但是还没有详细调查过。所以只在这里引用一下,并不给出结论。

<img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/ad228e80448eeeb517d86cb520e3645b.png" data-rawwidth="529" data-rawheight="171" class="origin_image zh-lightbox-thumb" width="529" data-original="https://pic2.zhimg.com/0d4225f353e548989b95b7edd01ef821_r.png">


另外一点,值得注意的是,知乎的代码里就是用了Animator实现屏幕刷新,具体是不是因为choreographer的原因,不知道,但很可能是。

代码已经更新了,手动的invalidate换成了Animator实现,大家可以看一下。

看了 @冯子力的评论,真的感觉又打开了一扇窗,这里也表达一下我崇高的敬意。

=======================万能的分割线,以下是正文======================

谢不邀

先说明一下,项目代码已上传至github,不想看长篇大论的也可以先去下代码,对照代码,哪里不懂点哪里。

代码在这GitHub - zgzczzw/ZHFollowButton: 模仿知乎关注按钮的点击效果

前几天发现知乎关注的点击效果确实赞,查了一下实现方式,刚好看到这个问题,花了一天时间终于把这个效果实现了,现在来回答一下,很不幸,楼上各位的答案都不全对,且听我一一道来。

首先,我先详细观察了一些知乎的效果,如 @Ben Zhang所说,其中有一个很神奇的地方,如图:

<img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/3cbb38b28da11c0a6cf3753050b5b290.png" data-rawwidth="355" data-rawheight="199" class="content_image" width="355">

<img
src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/198be06c0e220a54386b1f5d308910af.png" data-rawwidth="289" data-rawheight="189" class="content_image" width="289">

<img
src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/fe992d9af640f20861a280673036765b.png" data-rawwidth="407" data-rawheight="218" class="content_image" width="407">


<img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/610dff11bae14c3208849e711911ea9b.png" data-rawwidth="307" data-rawheight="165" class="content_image" width="307">注意看第二张图,这个圆形在扩散的时候,圆形底下的字还在,而且新的字也在圆形上,就这个效果实现起来最难。

注意看第二张图,这个圆形在扩散的时候,圆形底下的字还在,而且新的字也在圆形上,就这个效果实现起来最难。

首先看一下楼上各位的回答,归纳来说,一共有2种实现方式,ripple效果和用paint在canvas上手动画圆

ripple:

ripple即波纹效果,是android API 21以后引入的一种material design的元素,是触摸反馈的一种,也就是说点击的时候会出现水波扩散的样式,demo(见最后)中第一个按钮就是用了ripple效果。

实现方式很简单,实现一个这样的drawable

<img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/b6e060294b98f7c5da248f6890e4b0cf.png" data-rawwidth="670" data-rawheight="253" class="origin_image zh-lightbox-thumb" width="670" data-original="https://pic2.zhimg.com/6ebfaaf537962918b6d5fd56a1ed378d_r.png">

第一个color是波纹颜色,item里面指定background正常的颜色,可以是一个shape,也可以是一个drawable,还可以是一个selector。

设置为按钮的background即可

<img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/f6d2a803049ebd25573730d31d014676.png" data-rawwidth="664" data-rawheight="177" class="origin_image zh-lightbox-thumb" width="664" data-original="https://pic4.zhimg.com/11d226893dcdaeb7b65c15b247ef39a3_r.png">


如果整个程序的theme用了meterial,那基本所有的带点击效果的控件,比如button都自带这个波纹效果。不过需要注意的是这一套API是21以后才提供的,所以需要做兼容处理。

效果如下:

<img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/8307c4e04e8fb530b12c9adfa0ced072.png" data-rawwidth="306" data-rawheight="178" class="content_image" width="306">


<img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/c11d8cf731ead098568704c9efbb0e7c.png" data-rawwidth="359" data-rawheight="197" class="content_image" width="359">


从图中可以看出即使我设置了波纹为红色(#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();
}
}
}


效果如图:

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/42c7da6658441f8d778ab050f931adc2.png" data-rawwidth="282" data-rawheight="129" class="content_image" width="282"&gt;

&lt;img
src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/841e4dfa3f6b01888bbf625347f33022.png" data-rawwidth="306" data-rawheight="140" class="content_image" width="306"&gt;

&lt;img
src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/b2666f53181575fe8deddafa3b82a0f4.png" data-rawwidth="243" data-rawheight="118" class="content_image" width="243"&gt;

&lt;img
src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/fc62a47f7096f3eff7f34a6464e6470b.png" data-rawwidth="241" data-rawheight="121" class="content_image" width="241"&gt;

&lt;img
src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/0836426e5536570a7a8314364de3f8b8.png" data-rawwidth="280" data-rawheight="122" class="content_image" width="280"&gt;

本来觉得差不多就是这样,但是跟知乎的效果比较一下,还是能发现差别的。用paint画圆能实现的是在点击的地方画一个圆,然后半径慢慢变大慢慢扩散。但是问题在于,画的这个圆会盖住显示的内容,而且画的圆上也不能显示内容。我试过用drawText,也实现不了字和圆一起的效果,解决方法只有,

画的过程中改背景色和上面文字。

然后,画完圆之后把圆擦掉,把下面的背景色和文字显示出来。

这样就会出现一次文字闪烁的问题,首先文字会消失掉,然后画完圆之后才显示出来。因为圆在扩散的时候是看不到文字的,只有等圆消失了,文字才能显示出来。而知乎的效果是文字和圆一起刷出来,而且底下的文字还在,中间也没有文字闪烁的问题,整个过程行云流水,看起来很顺畅,好像用圆形揭开了幕布一样。

综上所述,楼上所有的答案都是答主们看到这个效果后第一反应的实现,其实如果不是我自己实现了一下,真的以为第二种方法就是知乎采用的,但是目前看来,很遗憾,知乎采用了一种更好的方式来实现这个效果。

那怎么办呢,我也没什么思路,怎么才能在画圆的时候把字也画在圆上,然后圆下面的背景也还有呢。没什么思路,看看知乎的代码吧,反编译。

反编译的过程我简单说一下:

到知乎官网下载最新的知乎apk

用apktool反编译apk,得到资源文件

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/8b2b5a8b4c80e096850a71c9ef0e9c34.png" data-rawwidth="558" data-rawheight="536" class="origin_image zh-lightbox-thumb" width="558" data-original="https://pic3.zhimg.com/9e7f206dce11d80a1d6eac689c0311b6_r.png"&gt;

在资源文件中搜索follow,这里一开始我搜的是ripple,因为我觉得这个效果总归应该和ripple有关,没结果,于是搜了follow,没想到还真搜出来了。

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/0d03665be847a7e8d62e0c98e1137aec.png" data-rawwidth="558" data-rawheight="188" class="origin_image zh-lightbox-thumb" width="558" data-original="https://pic2.zhimg.com/718d9c47d6fb0b39beb98e9de3989f49_r.png"&gt;

RevealFollowButton这明显就是我们要的波纹展开的控件,这就好说了,下一步就是去代码里找到这个控件了。这里要记一下,这个控件的位置com.zhihu.android.app.ui.widget.RevealFollowButton。

反编译代码

将apk改名成rar,打开,可以找到里面的class文件

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/45587cb6ad98002c83e94fc3ed69ba65.png" data-rawwidth="558" data-rawheight="309" class="origin_image zh-lightbox-thumb" width="558" data-original="https://pic4.zhimg.com/fb732f22a1f96d6d8c3c30465f8a5433_r.png"&gt;

知乎用了multidex,所以会有两个class文件,都拖出来放在dex2jar里反编译一下,就能生成两个jar包了,把jar包放在GUI里看一下,就能看到代码了,虽然代码被混淆过,但是基本逻辑还是能看出来的。

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/c4d1e0bf0c0ec5670e286c386e9235aa.png" data-rawwidth="558" data-rawheight="349" class="origin_image zh-lightbox-thumb" width="558" data-original="https://pic1.zhimg.com/2f93a494ae21de94016b4139c9c5b624_r.png"&gt;

然后根据前面xml里的路径找到RevelFollowButton的位置,打开代码看就可以了。

这是类的继承关系,RevealFollowButton继承自RevealFrameLayout,然后继承自ZHFrameLayout,这个ZHFrameLayout的父类就是FrameLayout了,从名字我们能看出,RevelFollowButton和RevealFrameLayout就是这个效果实现的两个类了。

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/1adf4bda063cb5c2227c72cf025e1804.png" data-rawwidth="419" data-rawheight="63" class="content_image" width="419"&gt;


&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/8589c11f1775d88c7fd487f26e5a116e.png" data-rawwidth="418" data-rawheight="68" class="content_image" width="418"&gt;

看到这个效果的实现是基于Framelayout,我就知道我们之前讨论的方法其实都走错了方向,如果告诉你用framelayout来实现这个效果,你会怎么做?

我的想法是加入两个TextView到这个layout里,然后一个Visible一个gone,如此切换,后来看过代码后,也证明我的这个想法是对的。

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/b0dfd2354fde84bf1ce2b5a98de9a029.png" data-rawwidth="436" data-rawheight="133" class="origin_image zh-lightbox-thumb" width="436" data-original="https://pic4.zhimg.com/7c31400370d73671d53caf1839f70713_r.png"&gt;


看,这里有两个TextView。如此的话,其实切换TextView是很容易实现的,问题是怎么实现波纹切换的效果,那第一件事就是看onDraw函数了,对于GroupView来说是drawChild方法。

RevealFollowButton的drawChild方法没什么内容,基本是调用了父类,那么我们来看RevealFrameLayout的drawChild方法。

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/78ef4daf0ba825cf8a9a045bea2ebbd1.png" data-rawwidth="558" data-rawheight="237" class="origin_image zh-lightbox-thumb" width="558" data-original="https://pic2.zhimg.com/0a528e94e76b0e60b99f6e63b8344101_r.png"&gt;


这里有两部分逻辑,如果满足一个条件,就做第一部分,一开始我也不知道这个条件是什么,混淆后的代码能看懂大逻辑,像这种小逻辑只能走一步看一步了。所以假设这个条件永远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里面跟定时器有关的就是这句了

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/d003f8c5562007f5844e00d14a6fb186.png" data-rawwidth="782" data-rawheight="206" class="origin_image zh-lightbox-thumb" width="782" data-original="https://pic2.zhimg.com/28f1dc87898b731d8276e8c3e10b6f61_r.png"&gt;


一个Animator对象,其实这句代码我是没看懂的,但逻辑很简单,设置一个Animator,定时500ms,在这个过程中修改圆形半径,然后刷新。
Math.hypot(getWidth(), getHeight()))

其中这个方法是根据勾股定理获取三角形的斜边长度,想想我们所要绘制的圆形半径最长是多少,没错,就是TextView的对角线长度。所以,整个逻辑就很简单了。

我搞了下代码,就这样吧

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/f33697c48570486db3244073813bba69.png" data-rawwidth="663" data-rawheight="325" class="origin_image zh-lightbox-thumb" width="663" data-original="https://pic4.zhimg.com/f395814e2a099fd4e241913a1d32b9f7_r.png"&gt;


整个方法的代码如下吧,还包括控制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显示出来,然后设置一个定时器不断扩大所要绘制圆的半径,根据这个半径裁剪画布成一个渐渐变大的圆形,然后内容就渐渐显示出来了。

这个效果实现出来之后,试着运行一下,还不错,但是总觉得有地方不对,于是细细观察,终于发现了,知乎的那个效果在刷新的时候,底下的背景不是白色的,还是之前的状态,比如要变成关注的时候,背景中的未关注还是在的,而我们实现的这个,刷新的时候背景是白色的。

这是知乎的

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/198be06c0e220a54386b1f5d308910af.png" class="content_image"&gt;


这是我的

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/a855aa5fbc0f3869d5c8af831822815e.png" data-rawwidth="295" data-rawheight="180" class="content_image" width="295"&gt;


所以还是没有知乎那么行云流水,所以我们是少了什么吗。这时候想起来了,之前在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;
}


至此,整个效果就和知乎完全一样了,刷新过程行云流水,非常赞。效果如下

&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/ced46eab6bdefe64a92e54dd503b5cb9.png" data-rawwidth="607" data-rawheight="301" class="origin_image zh-lightbox-thumb" width="607" data-original="https://pic3.zhimg.com/fe4bab3826766ed46fec80fffc6f1772_r.png"&gt;


&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/49afa61dcdd9c284e954acb48b0b25ac.png" data-rawwidth="429" data-rawheight="247" class="origin_image zh-lightbox-thumb" width="429" data-original="https://pic2.zhimg.com/6d2fe37adf3e3640261642bde6008119_r.png"&gt;


&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/87b488842cf3d517975d63e966ee8c4c.png" data-rawwidth="474" data-rawheight="282" class="origin_image zh-lightbox-thumb" width="474" data-original="https://pic3.zhimg.com/59e4f366fab8c62fb526b360032b75e2_r.png"&gt;


&lt;img src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/5af079bf9b2a59abe7e810c7d9b04122.png" data-rawwidth="489" data-rawheight="241" class="origin_image zh-lightbox-thumb" width="489" data-original="https://pic4.zhimg.com/2b69b4cff8536e8264bfac335c2fd98f_r.png"&gt;

&lt;img
src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/c8473de1273cc5a4c714c733a4d361a2.png" data-rawwidth="566" data-rawheight="322" class="origin_image zh-lightbox-thumb" width="566" data-original="https://pic4.zhimg.com/d15c92db4f841d74ab26da796889231b_r.png"&gt;

&lt;img
src="https://oscdn.geek-share.com/Uploads/Images/Content/201608/f73ee499114999816a26545d9790d186.png" data-rawwidth="540" data-rawheight="275" class="origin_image zh-lightbox-thumb" width="540" data-original="https://pic1.zhimg.com/d6a2ff5271131a834e5a02971e8c2d0c_r.png"&gt;


实现代码已上传至github:

GitHub - zgzczzw/ZHFollowButton: 模仿知乎关注按钮的点击效果
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐