Android仿QQ圆形头像个性名片
先看看效果图:
中间的圆形头像和光环波形讲解请看:
周围的气泡布局,因为布局ratiolayout是继承自viewgroup,所以布局layout就可以根据自己的需求来布局其子view,view.layout(int l,int t,int r,int b);用于布局子view在父viewgroup中的位置(相对于父容器),所以在ratiolayout中计算所有子view的left,top,right,bottom。那么头像的周围的气泡view是如何计算它的left,top,right,bottom的呢,这些气泡view是坐落在头像外围的圆环上,只要知道这个圆环的半径,然后再根据气泡的个数,计算每个气泡之间的角度,半径加角度就可以计算每个气泡坐落的位置。
/** * 计算气泡的布局位置 * @param textviews */ private void calculateratioframe(list<bubbleview> textviews){ if(textviews.size() == 0) return; mratioframelist.clear(); double angle = 0;//记录每个气泡的角度,正上方的为0° double grad = math.pi * 2 / textviews.size();//梯度,每个textview之间的角度 (math.pi 是数学中的90°) double rightangle = math.pi / 2;//一圈为360°,一共四个方向,每个方向90°,我们按照小于等于90°来计算,然后再放到相应的方向上 //cx,cy是容器的中心点,也是圆形头像的中心点,计算气泡的位置就是已cx,cy为基准来计算的 int cx = mwidth / 2;//容器中心x坐标 int cy = mheight / 2;//容器中心y坐标 int radius = mminsize / 2 / 2 / 2 + mminsize / 2 / 2 ;//动态气泡的组成圆的半径 int left = 0; int top = 0; int right = 0; int bottom = 0; int a = 0,b = 0;//a是基于cx的偏移量,b是基于cy的偏移量, //int r = mminsize / 6 / 2;//气泡半径 for (int i = 0; i < textviews.size(); i++) { int r = textviews.get(i).getmeasuredwidth() / 2;//计算得来//固定死的mminsize / 6 / 2;//气泡半径 if(angle >= 0 && angle < rightangle){ //0 - 90度是计算偏移量 //保持角度在 0 - 90 a = (int)(radius * math.sin(math.abs(angle % rightangle))); b = (int)(radius * math.cos(math.abs(angle % rightangle))); left = cx + a - r;//cx + a为气泡的中心点,要想得到left,还需减去半径r top = cy - b - r; right = left + 2 * r; bottom = top + 2 * r; }else if(angle >= rightangle && angle < rightangle * 2){ // 90 - 180 a = (int)(radius * math.sin(math.abs(angle % rightangle))); b = (int)(radius * math.cos(math.abs(angle % rightangle))); left = cx + b - r; top = cy + a - r; right = left + 2 * r; bottom = top + 2 * r; }else if(angle >= rightangle * 2 && angle < rightangle * 3){ // 180 - 270 a = (int)(radius * math.sin(math.abs(angle % rightangle))); b = (int)(radius * math.cos(math.abs(angle % rightangle))); left = cx - a - r; top = cy + b - r; right = left + 2 * r; bottom = top + 2 * r; }else if(angle >= rightangle * 3 && angle < rightangle * 4){ //270 - 360 a = (int)(radius * math.sin(math.abs(angle % rightangle))); b = (int)(radius * math.cos(math.abs(angle % rightangle))); left = cx - b - r; top = cy - a - r; right = left + 2 * r; bottom = top + 2 * r; } //将计算好的left, top, right,bottom,angle保存起来 mratioframelist.add(new ratioframe(left, top, right,bottom,angle)); //角度再加一个梯度值 angle += grad; } }
计算好气泡的布局left, top, right,bottom,下面就开始布局这起气泡,布局中的代码就简单的多了
@override protected void onlayout(boolean changed, int l, int t, int r, int b) { if(mimageview == null) return; int width = mimageview.getmeasuredwidth();//计算圆形头像的宽 int height = mimageview.getmeasuredheight();//计算圆形头像的高 //计算圆形头像的left, top, right,bottom int left = mwidth / 2 - width / 2; int top = mheight / 2 - height / 2; int right = mwidth / 2 + width / 2; int bottom = mheight / 2 + height / 2; //开始布局 mimageview.layout(left,top,right,bottom); //布局爱心动画 for (int i = 0; i < mlovexinlist.size(); i++) { imageview imageview = mlovexinlist.get(i); left = mwidth / 2 + width / 4 - imageview.getmeasuredwidth() / 2; bottom = mheight / 2 + height / 3; top = bottom - imageview.getmeasuredheight(); right = left + imageview.getmeasuredwidth(); imageview.layout(left,top,right,bottom); } //布局所有气泡 for (int i = 0; i < mtextviews.size(); i++) { textview textview = mtextviews.get(i); //ratioframe ratioframe = mratioframelist.get(i);//无动画时使用 //有动画的时候,执行期间left, top, right,bottom都在变 if(mcurrentratioframelist != null){ //valueanimator执行动画是所产生的所有气泡left, top, right,bottom ratioframe ratioframe = mcurrentratioframelist.get(i); textview.layout(ratioframe.mleft,ratioframe.mtop,ratioframe.mright,ratioframe.mbottom); } } }
好了,静态的气泡排版到这里就好了,下面的问题是,展开时如何使气泡从中心点,以弧形的路径展开,并且气泡的大小也是由小到大变化。这里就用到的动画类valueanimator和scaleanimation,详解请参考:
向外展开的效果我们可以使用view.layout()不断的重新布局气泡view,让其产生一个平移的效果,下面的一个问题就是如何计算平移轨道上面的left, top, right,bottom,然后重新请求布局就可以了,那么下面就解决如何计算这个轨迹,分析
弧形轨迹计算,其实就是在直线轨迹的基础上加上偏移量(movex和movey),就形成了弧形轨迹,直线轨迹很好计算,关键的就是这个偏移量,因为在首位的偏移量小,而在中间的偏移量大,且在不同的方向上,movex和movey的值的正负也不一样。偏移的距离因为是由小到大再由大到小,所以我们用二次函数( -2 * math.pow(fraction,2) + 2 * fraction)来计算距离,用此二次函数得到的值乘以一个设定的最大值,这个最大值的就会是由小到大再由大到小的变化,然后再用不同的角度来计算它的正负
if(endratioframe.mangle >0 && endratioframe.mangle <= rightangle){//(0 < angle <= 90)上移,左移 movex = (int)(temp * math.abs(math.cos(endratioframe.mangle)));//上移就应该在原本的轨迹上减去movex movey = (int)(temp * math.abs(math.sin(endratioframe.mangle))); }else if(endratioframe.mangle > rightangle && endratioframe.mangle <= rightangle * 2){//(90 < angle <= 180)右移,上移 movex = (int)(-temp * math.abs(math.cos(endratioframe.mangle))); movey = (int)(temp * math.abs(math.sin(endratioframe.mangle))); }else if(endratioframe.mangle > rightangle * 2 && endratioframe.mangle <= rightangle * 3){//(180 < angle <= 2700)下移,右移 movex = (int)(-temp * math.abs(math.cos(endratioframe.mangle))); movey = (int)(-temp * math.abs(math.sin(endratioframe.mangle))); }else if(endratioframe.mangle > rightangle * 3 && endratioframe.mangle <= rightangle * 4 || endratioframe.mangle == 0){//(270 < angle <= 360 或者 angle == 0) 左移,下移 movex = (int)(temp * math.abs(math.cos(endratioframe.mangle))); movey = (int)(-temp * math.abs(math.sin(endratioframe.mangle))); }
根据三角函数的变化值,上面的代码可以简化为
movex = (int)(temp * math.cos(endratioframe.mangle)); movey = (int)(temp * math.sin(endratioframe.mangle));
通过上面的计算公式逻辑,就可以得到气泡展开时的类型估算器的实现类,退出气泡就将逻辑反一下就可以了
package com.cj.dynamicavatarview.ratio; import android.animation.typeevaluator; import android.content.context; import android.util.typedvalue; import java.util.arraylist; import java.util.list; /** * created by chenj on 2016/10/19. */ public class enterratioframeevaluator implements typeevaluator { public static final int offset_distance = 80; private context mcontext; private int moffsetdistance; public enterratioframeevaluator(context context){ this.mcontext = context; moffsetdistance = (int)typedvalue.applydimension(typedvalue.complex_unit_dip,offset_distance,mcontext.getresources().getdisplaymetrics()); } @override public object evaluate(float fraction, object startvalue, object endvalue) { list<ratioframe> startratioframelist = (list<ratioframe>) startvalue;//开始值 list<ratioframe> endratioframelist = (list<ratioframe>) endvalue;//结束值 list<ratioframe> ratioframelist = new arraylist<>();//产生的新值 for (int i = 0; i < endratioframelist.size(); i++) { ratioframe endratioframe = endratioframelist.get(i); ratioframe startratioframe = startratioframelist.get(i); //计算left,top,right,bottom double t = ( -2 * math.pow(fraction,2) + 2 * fraction);//倾斜变化率 int temp = (int)((moffsetdistance) * t); double rightangle = math.pi / 2; int movex = 0,movey = 0; //让气泡上、下、左、右平移,形成弧度的平移路线 movex = (int)(temp * math.cos(endratioframe.mangle)); movey = (int)(temp * math.sin(endratioframe.mangle)); //重新得到left ,top,right,bottom int left = (int)(startratioframe.mleft + ((endratioframe.mleft - startratioframe.mleft) * fraction) - movex); int top = (int)(startratioframe.mtop + ((endratioframe.mtop - startratioframe.mtop) * fraction) - movey) ; int right = (int)(startratioframe.mright + ((endratioframe.mright - startratioframe.mright) * fraction) - movex); int bottom = (int)(startratioframe.mbottom + ((endratioframe.mbottom - startratioframe.mbottom) * fraction) - movey) ; ratioframelist.add(new ratioframe(left,top,right,bottom)); } return ratioframelist; } }
下面就可以用valueanimator来实现弧形平移轨迹了
valueanimator manimatorenetr = valueanimator.ofobject(new enterratioframeevaluator(getcontext()), getratioframecenterlist(mratioframecenter,mratioframelist),mratioframelist); manimatorenetr.addupdatelistener(new valueanimator.animatorupdatelistener() { @override public void onanimationupdate(valueanimator animation) { //获取新的布局值 mcurrentratioframelist = (list<ratioframe>) animation.getanimatedvalue(); //请求重新布局 requestlayout(); } }); manimatorenetr.setduration(open_bubble_time); manimatorenetr.start();
好了,从中心点向外展开的弧形动画到这就实现了,然后再加上缩放的动画就可以了,缩放的动画使用view动画就可以实现。
/** * 气泡由小到大缩放 * @param textviews */ private void scalesmalltolarge(list<bubbleview> textviews){ // 以view中心为缩放点,由初始状态缩小到看不间 scaleanimation animation = new scaleanimation( 0.0f, 1.0f,//一点点变小知道看不见为止 0.0f, 1.0f, animation.relative_to_self, 0.5f, animation.relative_to_self, 0.5f//中间缩放 ); animation.setduration(open_bubble_time);//要和平移的时间一致 for (int i = 0; i < textviews.size(); i++) { //再执行动画 textviews.get(i).startanimation(animation); } }
下面解决的就是展开后,气泡开始浮动,点击气泡后停止浮动,滑动手指的之后气泡跟着手指移动,松开手指后气泡返回到原来的位置,返回时的动画效果和气泡展开的动画效果非常的类似,气泡跟着手指移动也很好实现,只需要将气泡view设置ontouch事件,再ontouch中计算滑动的距离,然后重新view.layout()就可以了,所以这里我们解决浮动问题就可以了。浮动是不规则的,并且浮动的距离和速度也是不一样的,我用view动画实现的效果不是很好,然后就改用了属性动画来实现。只需要将view平移x轴和y轴,让其平移的距离和时间都不同,看上去就像无规则的移动,让其反复的做这样的平移就可以实现浮动的效果。
/** * 给指定的view设置浮动效果 * @param view * @return */ private animatorset setanimfloat(view view ){ list<animator> animators = new arraylist<>(); //getrandomdp()得到一个随机的值 objectanimator translationxanim = objectanimator.offloat(view, "translationx", 0f,getrandomdp(),getrandomdp() , 0); translationxanim.setduration(getrandomtime()); translationxanim.setrepeatcount(valueanimator.infinite);//无限循环 translationxanim.setrepeatmode(valueanimator.infinite);// translationxanim.setinterpolator(new linearinterpolator()); translationxanim.start(); animators.add(translationxanim); // objectanimator translationyanim = objectanimator.offloat(view, "translationy", 0f,getrandomdp(),getrandomdp() , 0); translationyanim.setduration(getrandomtime()); translationyanim.setrepeatcount(valueanimator.infinite); translationyanim.setrepeatmode(valueanimator.infinite); translationxanim.setinterpolator(new linearinterpolator()); translationyanim.start(); animators.add(translationyanim); animatorset animatorset = new animatorset(); animatorset.playtogether(animators); //animatorset.setstartdelay(delay); animatorset.start(); return animatorset; }
按住停止浮动,松开的时候先归位,然后再次的浮动,如果animator.end()方法,归位后开始浮动的时候会出现闪动的现象,因为属性动画,虽然可以改变view的位置,但是不会改变view的left,top,right,bottom,所以重新开始浮动的时候会出现闪烁的现象,因为x = mleft + translationx,当重新开始的时候,属性动画是重新创建的,translationx是从0开始的,因此会出现闪烁的现象。
final animatorset animatorset = manimatorsetlist.get(position); for (animator animator : animatorset.getchildanimations()) { //执行到动画最后,恢复到初始位置,不然重新开始浮动的时候,会有一个闪烁的bug if(animator.isrunning()) { animator.end();//执行到动画最后 animator.cancel();//取消动画 } }
到这里流程已经差不多了,但是当气泡移动到圆形头像的里面的时候松开,气泡应当有一个缩放的效果后归位,然后应有一个接口回调,告诉调用者,我到中间了松开了,你可以做一些相应的处理。现在我们看一下如何计算气泡已经移动到头像里了,其实通过圆形头像中心点和气泡的中心点构成一个直接三角形,然后通过勾股定理,计算直角边的长度和圆形头像的半径做比较,如果小于圆形头像的半径,就说明已经到头像里面了。
/** * 判断气泡中心点是否在图片内部 * @param view * @param current 当前移动到的位置 * @param endratioframe 如果在中间,该值用于复位到原本位置 * @return */ private boolean isinpicturecenter(int position,view view,ratioframe current,ratioframe endratioframe){ ratiopoint centerpoint = new ratiopoint(mwidth/2,mheight/2); ratiopoint currentpoint = new ratiopoint(current.mleft + ((current.mright - current.mleft) / 2),current.mtop + ((current.mbottom - current.mtop) / 2)); int x = math.abs(centerpoint.x - currentpoint.x); int y = math.abs(centerpoint.y - currentpoint.y); //通过勾股定理计算两点之间的距离 int edge = (int)math.sqrt(math.pow(x,2) + math.pow(y,2)); int pictureradius = mimageview.getpictureradius(); //然后和内部图片的半斤比较,小于pictureradius,就说明在内部 if(pictureradius > edge){//进入到内部 if(minnercenterlistener != null){ minnercenterlistener.innercenter(position,((textview)view).gettext().tostring()); } //说明到中心了,执行气泡缩放 revesescaleview(position ,view,current,endratioframe); return true; } return false; }
气泡执行缩放
/** * 缩放图片(补间动画) * @param view * @param current 缩放后用于平移的起点 * @param endratioframe 缩放后用于平移的终点 */ public void revesescaleview(final int position , final view view, final ratioframe current, final object endratioframe) { // 以view中心为缩放点,由初始状态缩小到看不间 scaleanimation animation = new scaleanimation( 1.0f, 0.0f,//一点点变小知道看不见为止 1.0f, 0.0f, animation.relative_to_self, 0.5f, animation.relative_to_self, 0.5f//中间缩放 ); animation.setduration(bubble_enter_center_scale_time); animation.setrepeatmode(animation.reverse); animation.setrepeatcount(1); animation.setanimationlistener(new animation.animationlistener() { @override public void onanimationstart(animation animation) { } @override public void onanimationend(animation animation) { //执行完缩放后,让气泡归位,归位结束后,执行接口回调 homingbubbleview(true,position,view, current, endratioframe); } @override public void onanimationrepeat(animation animation) { } }); view.startanimation(animation); }
气泡进入中心的接口回调定义
public interface innercenterlistener{ //进入中心,松开归位后调用 void innercenterhominged(int position, string text); //进入中心,松开时调用 void innercenter(int position, string text); }
下面就剩执行加1操作和播放爱心的动画,这两个动画就是执行两个view动画,这里就不贴出来了,到这里高仿qq个性名片就讲解结束了,如果讲的不好或有问题欢迎留言
源码下载:github
下载2
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。