Android贝塞尔曲线实现消息拖拽消失
写在前头
写消息拖拽效果的文章不少,但是大部分都把自定义view写死了,我们要实现的是传入一个view,每个view都可以实现拖拽消失爆炸的效果,当然我也是站在巨人的肩膀上来学习的。但个人觉得程序员本就应该敢于学习和借鉴。
源码地址:源码github地址
效果图
分析(用到的知识点):
(1)valueanimator (数值生成器) 用于生成数值,可以设置差值器来改变数字的变化幅度。
(2)objectanimator (动画生成器) 用于生成各种属性,布局动画,同样也可以设置差值器来改变效果。
(3)贝塞尔一阶曲线
(4)自定义view的基础知识
(5)windowmanager 使view拖拽能显示在整个屏幕的任何地方,而不是局限于父布局内
具体实现方法
一、首先我们要实现基础效果
基础效果是点击屏幕任意一点能出现消息拖拽的效果,但是此时我们不用管我们拖动的view,只需要完成大致模型。该部分的难点在于贝塞尔一阶曲线的怎么实现。
基础效果图
分析:
(1)点击任意一点画出两个圆,和一个有贝塞尔曲线组成的path路径
(2)随着拖动距离的增加原点的圆半径逐渐缩小,当距离达到一定大以后原点的圆和贝塞尔曲线组成的path不再显示
贝塞尔曲线的画法
首先我们需要求出角a的大小,根据角a来求到a,b,c,d的坐标位子,然后求到控制点e点的坐标,通过path.quadto()方法来连接a,b和c,d两条贝塞尔曲线。
各点坐标
a(c1.x+sina*c1半径,c1.y-cina*c1半径)
b(c2.x+sina*c2半径,c2.y-cina*c2半径)
c(c2.x-sina*c1半径,c2.y+cina*c1半径)
d(c1.x-sina*c2半径,c1.y+cina*c2半径)
e ((c1.x+c2.x)/2,(c1.y+c2.y)/2)
贝塞尔曲线的path代码
private path getbezeierpath() { double distance = getdistance(mbigcirclepoint,mlittlecirclepoint); mlittlecircleradius = (int) (mlittlecircleradiusmax - distance / 10); if (mlittlecircleradius < mlittlecircleradiusmin) { // 超过一定距离 贝塞尔和固定圆都不要画了 return null; } path bezeierpath = new path(); // 求角 a // 求斜率 float dy = (mbigcirclepoint.y-mlittlecirclepoint.y); float dx = (mbigcirclepoint.x-mlittlecirclepoint.x); float tana = dy/dx; // 求角a double arctana = math.atan(tana); // a float ax = (float) (mlittlecirclepoint.x + mlittlecircleradius*math.sin(arctana)); float ay = (float) (mlittlecirclepoint.y - mlittlecircleradius*math.cos(arctana)); // b float bx = (float) (mbigcirclepoint.x + mbigcircleradius*math.sin(arctana)); float by = (float) (mbigcirclepoint.y - mbigcircleradius*math.cos(arctana)); // c float cx = (float) (mbigcirclepoint.x - mbigcircleradius*math.sin(arctana)); float cy = (float) (mbigcirclepoint.y + mbigcircleradius*math.cos(arctana)); // d float dx = (float) (mlittlecirclepoint.x - mlittlecircleradius*math.sin(arctana)); float dy = (float) (mlittlecirclepoint.y + mlittlecircleradius*math.cos(arctana)); // 拼装 贝塞尔的曲线路径 bezeierpath.moveto(ax,ay); // 移动 // 两个点 pointf controlpoint = getcontrolpoint(); // 画了第一条 第一个点(控制点,两个圆心的中心点),终点 bezeierpath.quadto(controlpoint.x,controlpoint.y,bx,by); // 画第二条 bezeierpath.lineto(cx,cy); // 链接到 bezeierpath.quadto(controlpoint.x,controlpoint.y,dx,dy); bezeierpath.close(); return bezeierpath; }
二、完善代码
这部分我们需要完善所有代码,实现代码的分离,使得所用view都能被拖动,且需要创建一个监听器来监听view是否拖动结束了,结束后调用回调方法以便需要做其他处理。
需要完成的功能:
(1)将传入的view画出来
(2)在手指抬起时判断是爆炸还是回弹
(3)完成回弹和爆炸的代码部分
(4)回弹或者爆炸结束后调用回调通知动画结束
(5)使用windowmanager把自定义拖拽view加进去,隐藏原来得view实现view在任意地方拖动
完整代码部分
(1)自定义view的代码
public class msgdrafitingview extends view{ private pointf mlittlecirclepoint; private pointf mbigcirclepoint; private paint mpaint; //大圆半径 private int mbigcircleradius = 10; //小圆半径 private int mlittlecircleradiusmax = 10; private int mlittlecircleradiusmin = 2; private int mlittlecircleradius; private bitmap dragbitmap; private ontoucnuplistener montoucnuplistener; public msgdrafitingview(context context) { this(context,null); } public msgdrafitingview(context context, @nullable attributeset attrs) { this(context, attrs,0); } public msgdrafitingview(context context, @nullable attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); mbigcircleradius = dip2px(mbigcircleradius); mlittlecircleradiusmax = dip2px(mlittlecircleradiusmax); mlittlecircleradiusmin = dip2px(mlittlecircleradiusmin); mpaint = new paint(); mpaint.setcolor(color.red); mpaint.setantialias(true); mpaint.setdither(true); } private int dip2px(int dip) { return (int) typedvalue.applydimension(typedvalue.complex_unit_dip,dip,getresources().getdisplaymetrics()); } @override protected void ondraw(canvas canvas) { if (mbigcirclepoint == null || mlittlecirclepoint == null) { return; } //画大圆 canvas.drawcircle(mbigcirclepoint.x, mbigcirclepoint.y, mbigcircleradius, mpaint); //获得贝塞尔路径 path bezeierpath = getbezeierpath(); if (bezeierpath!=null) { // 小到一定层度就不见了(不画了) canvas.drawcircle(mlittlecirclepoint.x, mlittlecirclepoint.y, mlittlecircleradius, mpaint); // 画贝塞尔曲线 canvas.drawpath(bezeierpath, mpaint); } // 画图片 if (dragbitmap != null) { canvas.drawbitmap(dragbitmap, mbigcirclepoint.x - dragbitmap.getwidth() / 2, mbigcirclepoint.y - dragbitmap.getheight() / 2, null); } } private path getbezeierpath() { double distance = getdistance(mbigcirclepoint,mlittlecirclepoint); mlittlecircleradius = (int) (mlittlecircleradiusmax - distance / 10); if (mlittlecircleradius < mlittlecircleradiusmin) { // 超过一定距离 贝塞尔和固定圆都不要画了 return null; } path bezeierpath = new path(); // 求角 a // 求斜率 float dy = (mbigcirclepoint.y-mlittlecirclepoint.y); float dx = (mbigcirclepoint.x-mlittlecirclepoint.x); float tana = dy/dx; // 求角a double arctana = math.atan(tana); // a float ax = (float) (mlittlecirclepoint.x + mlittlecircleradius*math.sin(arctana)); float ay = (float) (mlittlecirclepoint.y - mlittlecircleradius*math.cos(arctana)); // b float bx = (float) (mbigcirclepoint.x + mbigcircleradius*math.sin(arctana)); float by = (float) (mbigcirclepoint.y - mbigcircleradius*math.cos(arctana)); // c float cx = (float) (mbigcirclepoint.x - mbigcircleradius*math.sin(arctana)); float cy = (float) (mbigcirclepoint.y + mbigcircleradius*math.cos(arctana)); // d float dx = (float) (mlittlecirclepoint.x - mlittlecircleradius*math.sin(arctana)); float dy = (float) (mlittlecirclepoint.y + mlittlecircleradius*math.cos(arctana)); // 拼装 贝塞尔的曲线路径 bezeierpath.moveto(ax,ay); // 移动 // 两个点 pointf controlpoint = getcontrolpoint(); // 画了第一条 第一个点(控制点,两个圆心的中心点),终点 bezeierpath.quadto(controlpoint.x,controlpoint.y,bx,by); // 画第二条 bezeierpath.lineto(cx,cy); // 链接到 bezeierpath.quadto(controlpoint.x,controlpoint.y,dx,dy); bezeierpath.close(); return bezeierpath; } /** * 获得控制点距离 */ public pointf getcontrolpoint() { return new pointf((mlittlecirclepoint.x+mbigcirclepoint.x)/2,(mlittlecirclepoint.y+mbigcirclepoint.y)/2); } /** * 获得两点之间的距离 */ private double getdistance(pointf point1, pointf point2) { return math.sqrt((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y)); } /** * 绑定view */ public static void attach(view view, msgdrafitinglistener.bubbledisappearlistener disappearlistener) { view.setontouchlistener(new msgdrafitinglistener(view.getcontext(),disappearlistener)); } public void initpoint(float x, float y) { mbigcirclepoint = new pointf(x,y); mlittlecirclepoint = new pointf(x,y); } public void updatepoint(float x,float y) { mbigcirclepoint.x = x; mbigcirclepoint.y = y; invalidate(); } public void setdragbitmap(bitmap dragbitmap) { this.dragbitmap = dragbitmap; } public void setontoucnuplistener(ontoucnuplistener listener) { montoucnuplistener = listener; } public interface ontoucnuplistener { // 还原 void restore(); // 消失爆炸 void dismiss(pointf pointf); } /** * 处理手指抬起后的操作 */ public void ontouchup() { if (mlittlecircleradius > mlittlecircleradiusmin) { // 回弹 valueanimator 值变化的动画 0 变化到 1 valueanimator animator = objectanimator.offloat(1); animator.setduration(250); final pointf start = new pointf(mbigcirclepoint.x, mbigcirclepoint.y); final pointf end = new pointf(mlittlecirclepoint.x, mlittlecirclepoint.y); animator.addupdatelistener(new valueanimator.animatorupdatelistener() { @override public void onanimationupdate(valueanimator animation) { float percent = (float) animation.getanimatedvalue();// 0 - 1 pointf pointf = utils.getpointbypercent(start, end, percent); //更新位子 updatepoint(pointf.x, pointf.y); } }); // 设置一个差值器 在结束的时候回弹 animator.setinterpolator(new overshootinterpolator(3f)); animator.start(); // 还要通知 touchlistener animator.addlistener(new animatorlisteneradapter() { @override public void onanimationend(animator animation) { if(montoucnuplistener != null){ montoucnuplistener.restore(); } } }); } else { // 爆炸 if(montoucnuplistener != null){ montoucnuplistener.dismiss(mbigcirclepoint); } } } }
(2)自定义ontouchlistenner的代码
public class msgdrafitinglistener implements view.ontouchlistener { private windowmanager mwindowmanager; private windowmanager.layoutparams params; private msgdrafitingview mmsgdrafitingview; private context context; // 爆炸动画 private framelayout mbombframe; private imageview mbombimage; private bubbledisappearlistener mdisappearlistener; public msgdrafitinglistener(context context,bubbledisappearlistener disappearlistener) { mwindowmanager = (windowmanager) context.getsystemservice(context.window_service); params = new windowmanager.layoutparams(); mmsgdrafitingview = new msgdrafitingview(context); //背景透明 params.format = pixelformat.transparent; this.context = context; mbombframe = new framelayout(context); mbombimage = new imageview(context); mbombimage.setlayoutparams(new framelayout.layoutparams(utils.dip2px(30,context), utils.dip2px(30,context))); mbombframe.addview(mbombimage); this.mdisappearlistener = disappearlistener; } @override public boolean ontouch(final view view, motionevent motionevent) { switch (motionevent.getaction()) { case motionevent.action_down: //隐藏自己 view.setvisibility(view.invisible); mwindowmanager.addview(mmsgdrafitingview,params); int[] location = new int[2]; view.getlocationonscreen(location); bitmap bitmap = getbitmapbyview(view); //y轴需要减去状态栏的高度 mmsgdrafitingview.initpoint(location[0] + view.getwidth() / 2, location[1]+view.getheight()/2 -utils.getstatusbarheight(context)); // 给消息拖拽设置一个bitmap mmsgdrafitingview.setdragbitmap(bitmap); //设置ontouchuplistener mmsgdrafitingview.setontoucnuplistener(new msgdrafitingview.ontoucnuplistener() { @override public void restore() { //还原位子 // 把消息的view移除 mwindowmanager.removeview(mmsgdrafitingview); // 把原来的view显示 view.setvisibility(view.visible); } @override public void dismiss(pointf pointf) { //爆炸效果 // 要去执行爆炸动画 (帧动画) //移除拖拽的view mwindowmanager.removeview(mmsgdrafitingview); // 要在 mwindowmanager 添加一个爆炸动画 mwindowmanager.addview(mbombframe,params); mbombimage.setbackgroundresource(r.drawable.anim_bubble_pop); animationdrawable drawable = (animationdrawable) mbombimage.getbackground(); mbombimage.setx(pointf.x-drawable.getintrinsicwidth()/2); mbombimage.sety(pointf.y-drawable.getintrinsicheight()/2); drawable.start(); // 等它执行完之后我要移除掉这个 爆炸动画也就是 mbombframe mbombimage.postdelayed(new runnable() { @override public void run() { mwindowmanager.removeview(mbombframe); // 通知一下外面该消失 if(mdisappearlistener != null){ mdisappearlistener.dismiss(view); } } },getanimationdrawabletime(drawable)); } }); break; case motionevent.action_move: mmsgdrafitingview.updatepoint(motionevent.getrawx(), motionevent.getrawy() - utils.getstatusbarheight(context)); break; case motionevent.action_up: mmsgdrafitingview.ontouchup(); break; } return true; } private bitmap getbitmapbyview(view view) { view.builddrawingcache(); bitmap bitmap = view.getdrawingcache(); return bitmap; } public interface bubbledisappearlistener { void dismiss(view view); } /** * 获取爆炸动画画的时间 * @param drawable * @return */ private long getanimationdrawabletime(animationdrawable drawable) { int numberofframes = drawable.getnumberofframes(); long time = 0; for (int i=0;i<numberofframes;i++){ time += drawable.getduration(i); } return time; } }
(3)view的调用代码
public class msgdrafitingviewactivity extends appcompatactivity{ private button mbutton; private textview mtext; @override protected void oncreate(@nullable bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.qq_msg_drafitingview_activity); mbutton = findviewbyid(r.id.mbtn); mtext = findviewbyid(r.id.mtext); msgdrafitingview.attach(mbutton, new msgdrafitinglistener.bubbledisappearlistener() { @override public void dismiss(view view) { } }); msgdrafitingview.attach(mtext, new msgdrafitinglistener.bubbledisappearlistener() { @override public void dismiss(view view) { } }); } }
源码地址:源码github地址
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: PostgreSQL基本用法讲解