上一篇博客写了水滴状的下拉刷新头,不过那个只能垂直下拉,也就是一个方向,而且经常用QQ的人也知道,QQ消息来了后,列表右边会有一个红色消息小球,这个小球是可以拉动的,拉起来就像一根皮筋一样,效果很棒,于是我根据那个上篇博客,进行修改,得到一个可以平面拉动的小球,基本近似QQ。
嗯,效果还不错,基本原理和上篇一样,不过因为可以平面拖动,和原来相比,就相当于一维到二维进步,计算难度和运算量基本上了几个数量级,还总是要考虑圆心连线的方向性,头都晕了,数学不好可以绕过了。
连接两个圆的圆心,过一个圆的圆心做垂线,交圆上AB两点,同理在大圆上面也作出CD两点
分别连线,现在要根据两个圆的圆心坐标和两个圆的半径求出ABCD这4点坐标
一开始想用解析几何的方式求4点坐标,做法:
求出圆心连线的斜率,根据垂直直线斜率相乘等于-1,得到AB直线的斜率
AB直线过原点,则AB直线方程可以用点斜式表达出,与圆的方程联解,即可得到AB两点
问题有两个:1,需要考虑到斜率不存在的情况2,需要求解二元二次方程组,很麻烦
于是,想到了三角函数的办法,因为计算机中求取反三角很简单,所以可以完全不顾解析几何那一套,做法:
求出圆心连线的斜率,根据垂直直线斜率相乘等于-1,得到AB直线的斜率
根据斜率k=tanθ,θ是直线AB和X轴的夹角,θ=arctank
AB点都是圆上的点,圆的方程已经说的很清楚了,x=cosθ*R+Rx,y=sinθ*R+Ry,其中Rx,Ry都是圆心坐标
CD点同理可得,下面需要根据这四个点做二次曲线,因此需要借助到中间点E,F。
直线EF是圆心连线的垂直平分线,交A,B两点延生的两条平行于圆心连线于E,F
因此这两个点也可以通过三角函数来求,以A为原点,已知AF的距离(圆心距一半),AF和X轴的夹角(θ+90),很容易求得
于是乎,剩下两个半圆弧的绘制方法,依旧是用那个方法、arcTo,依旧是要注意圆弧的方向,和整条路径的方向,不然很可能无法闭合曲线。
当然,实际情况比这还要复杂的多
你需要考虑这种情况
当圆出现在下面时,AB直线的斜率依旧不变,可是绘制圆弧的方向却完全不一样了,因此需要在代码中加入一个判断
闭合曲线也是麻烦的要死,嗯,说到这里,绘制这部分算是完了
至于触摸事件,摸着改动大圆圆心坐标就是了,然后刷新view
说说回弹,这次用的回弹和上次有所不同,上次用的是匀速返回,这种效果并不是很好
想作出那种皮筋,或者弹簧的效果来,果然,想到弹簧上的小球正好就是物理上面所说的简谐运动
这正是我想要的,于是乎,看原理:
我需要的是小球运动时圆心距的改变,从圆心距推算出大圆的圆心坐标
而我们可以让圆心距改变的大小是一个简谐运动,圆心距x=A*sin(w*t+Ψ)
振幅A不停减少,一直到0停止,t的经历的时间(不是间隔时间)
根据圆心距和角度angle,可及时的计算出运动中的圆心坐标
small球在上面时,即islow=1,x为正,在下面时islow=-1,x为负,需要根据这个计算初相φ,φ=islow*π/2
A=手放开的时候的圆心距
周期T=2π/ω,则ω=2π/T
当A递减到0,回弹线程结束
好了,下面贴代码:
package com.example.kaifa.myapplication; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; /** * TODO: document your custom view class. */ public class DanqiuView extends View { private Paint mPaint; /** * 拉伸进度 */ private float progress = 0; /** * view的宽高 */ private int viewheight, viewwdith; /** * 大圆半径 */ private float GreatCircleRadius = 20; /** * 小圆半径 */ private float SmallCircleRadius = 20; /** * 大圆的圆心 */ private Point GreatCirclePoint; /** * 小圆的圆心 */ private Point SmallCirclePoint; /** * 分别在大圆和小圆上面的4个点 */ private Point A, B, C, D; /** * AB直线与X轴的夹角(AB直线和圆心连线垂直),单位 弧度 */ private double angle; /** * 手指按下接触到的第一个点 */ private Point firstPoint; /** * 两条二次曲线的分别两个中间点 */ private Point ACmiddlePoint, BDmiddlePoint; /** * 绘制路径 */ private Path mPath; /** * 1表示小圆在大圆下面,-1反之 */ private int islow = -1; /** * 两园的圆心距 */ double dance=0; /** * 周期,单位/毫秒 */ float T=2000; public DanqiuView(Context context) { super(context); init(null, 0); } public DanqiuView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs, 0); } public DanqiuView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs, defStyle); } private void init(AttributeSet attrs, int defStyle) { // Load attributes mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(0xff0000ff); mPaint.setStyle(Paint.Style.FILL); mPath = new Path(); GreatCirclePoint = new Point(); SmallCirclePoint = new Point(); A = new Point(); B = new Point(); C = new Point(); D = new Point(); firstPoint = new Point(); ACmiddlePoint = new Point(); BDmiddlePoint = new Point(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); viewwdith = w; viewheight = h; GreatCirclePoint.x = w / 2; GreatCirclePoint.y = h / 2; SmallCirclePoint.x = GreatCirclePoint.x; SmallCirclePoint.y = GreatCirclePoint.y; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //路径重置 mPath.reset(); //若重合,则绘制初始圆 if (GreatCirclePoint.x == SmallCirclePoint.x && GreatCirclePoint.y == SmallCirclePoint.y) { //初始绘制 mPath.addCircle(GreatCirclePoint.x, GreatCirclePoint.y, GreatCircleRadius, Path.Direction.CW); canvas.drawPath(mPath, mPaint); return; } //计算四个点的坐标和角度值 //AB直线和圆心连线垂直,斜率相乘等于-1.因此可以得到AB直线的斜率 float kAB = (GreatCirclePoint.x - SmallCirclePoint.x) / (SmallCirclePoint.y - GreatCirclePoint.y); angle = Math.atan(kAB); Log.v("xingyun", "斜率K=" + kAB + " 角度a=" + angle); A.x = (float) (GreatCirclePoint.x + GreatCircleRadius * Math.cos(angle)); A.y = (float) (GreatCirclePoint.y + GreatCircleRadius * Math.sin(angle)); B.x = (float) (GreatCirclePoint.x + GreatCircleRadius * Math.cos(angle + Math.PI)); B.y = (float) (GreatCirclePoint.y + GreatCircleRadius * Math.sin(angle + Math.PI)); C.x = (float) (SmallCirclePoint.x + SmallCircleRadius * Math.cos(angle)); C.y = (float) (SmallCirclePoint.y + SmallCircleRadius * Math.sin(angle)); D.x = (float) (SmallCirclePoint.x + SmallCircleRadius * Math.cos(angle + Math.PI)); D.y = (float) (SmallCirclePoint.y + SmallCircleRadius * Math.sin(angle + Math.PI)); if (GreatCirclePoint.y < SmallCirclePoint.y) { islow = 1; } else { islow = -1; } dance = Math.sqrt((GreatCirclePoint.x - SmallCirclePoint.x) * (GreatCirclePoint.x - SmallCirclePoint.x) + (GreatCirclePoint.y - SmallCirclePoint.y) * (GreatCirclePoint.y - SmallCirclePoint.y)) / 2; progress = (float) dance * 2 / viewheight; SmallCircleRadius = (float) (20 * (1 - progress * 0.1)); GreatCircleRadius = (float) (20 * (1 - progress)); if (progress < 0.5) { BDmiddlePoint.x = (float) (B.x + dance * Math.cos(angle + islow * Math.PI / 2)); BDmiddlePoint.y = (float) (B.y + dance * Math.sin(angle + islow * Math.PI / 2)); ACmiddlePoint.x = (float) (A.x + dance * Math.cos(angle + islow * Math.PI / 2)); ACmiddlePoint.y = (float) (A.y + dance * Math.sin(angle + islow * Math.PI / 2)); } else { BDmiddlePoint.x = GreatCirclePoint.x / 2 + SmallCirclePoint.x / 2; BDmiddlePoint.y = GreatCirclePoint.y / 2 + SmallCirclePoint.y / 2; ACmiddlePoint.x = BDmiddlePoint.x; ACmiddlePoint.y=BDmiddlePoint.y; } mPath.arcTo(new RectF(GreatCirclePoint.x - GreatCircleRadius, GreatCirclePoint.y - GreatCircleRadius , GreatCirclePoint.x + GreatCircleRadius, GreatCirclePoint.y + GreatCircleRadius), (float) (angle * 180 / Math.PI), islow * (-180)); //从B点到D点,选取其中心点作为渐进点 mPath.quadTo(BDmiddlePoint.x, BDmiddlePoint.y, D.x, D.y); mPath.arcTo(new RectF(SmallCirclePoint.x - SmallCircleRadius, SmallCirclePoint.y - SmallCircleRadius , SmallCirclePoint.x + SmallCircleRadius, SmallCirclePoint.y + SmallCircleRadius), (float) (angle * 180 / Math.PI+180), islow * (-180)); //从A点到C点 mPath.quadTo(ACmiddlePoint.x, ACmiddlePoint.y, A.x, A.y); canvas.drawPath(mPath, mPaint); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstPoint.x = event.getX(); firstPoint.y = event.getY(); break; case MotionEvent.ACTION_MOVE: float dx = event.getX() - firstPoint.x; float dy = event.getY() - firstPoint.y; if (Math.abs(dx) > 1 || Math.abs(dy) > 1) { SmallCirclePoint.x = SmallCirclePoint.x + dx; SmallCirclePoint.y = SmallCirclePoint.y + dy; invalidate(); } firstPoint.x = event.getX(); firstPoint.y = event.getY(); break; case MotionEvent.ACTION_UP: //开启线程匀速返回,回弹 new MyTread().start(); break; } return true; } /** * 回弹的线程,作为简谐运动,则圆心距x=Asin(ωt+φ),振幅A不停减少,一直到0停止,t的经历的时间(不是间隔时间) * 根据圆心距和角度angle,可及时的计算出运动中的圆心坐标 * small球在上面时,即islow=1,x为正,在下面时islow=-1,x为负,需要根据这个计算初相φ,φ=islow*π/2 * A=手放开的时候的圆心距 * 周期T=2π/ω,则ω=2π/T */ class MyTread extends Thread { @Override public void run() { float A=(float)dance*2; float fai=(float)( islow*Math.PI/2); float w=(float)(2* Math.PI/T); long t=0; double angle1=angle; while (A>0){ float x=(float)(A*Math.sin(w*t+fai)); A=A-(float)0.5; t=t+10; Log.v("xingyun","振幅:"+A+" X:"+x); SmallCirclePoint.x=GreatCirclePoint.x+x*(float)Math.cos(angle1+Math.PI/2); SmallCirclePoint.y=GreatCirclePoint.y+x*(float) Math.sin(angle1+Math.PI/2); try { sleep(10); postInvalidate(); } catch (InterruptedException e) { e.printStackTrace(); } } } } private class Point { float x, y; } }
能力有限,只能做到这里啦~(计算量这么大幸好也没出现卡顿的现象,运气好好~)
完~