QQ的粘性控件的实现原理
2016-09-06 12:49
316 查看
声明:本文资料来自于传智播客安卓52期
先来张效果图:
话不多说,线上代码,然后逐个分析
自定义粘性控件
几何图形工具类
Activity
GooView 详解:
这里属于自定义View(继承于View)
extends View (GooView extends View)
重写构造方法
进行测量
重写onDraw方法
重写构造方法并初始化一个画笔(Paint对象)
构造如上所示的静态图坐标
重点
onDraw方法( 计算连接点值, 控制点, 固定圆半径)
分析:
float tempStickRadius = getTempStickRadius();
作用是根据两个圆心的距离的变化动态设置固定圆的半径,以保证在拖动过程中固定圆的大小发生变化
获取直线与圆的交点:用于构建贝塞尔曲线(即上图中的P0和P2)
获取控制点坐标(即上图中的P1)
画出界定区域圆圈
画由1,2,3,4构成的封闭区关于类Path的使用详解
画固定圆和拖拽圆:
完成上面的onDraw()函数后,将会得到一个静态效果,如下:
接下来要实现动态变化的效果
动态变化主要通过重写onTouchEvent函数,通过按下,移动,抬起三个事件来改变图形效果
1 。处理按下(MotionEvent.ACTION_DOWN)需要做的事:
根据按下的位置坐标,画出拖动点和由点1,2,3,4构成的封闭图形
2。 处理移动(MotionEvent.ACTION_MOVE:)所需要做的事
移动过程中需要根据固定点和拖拽点的距离来改变固定点的半径,
需要判断固定点和移动点之间的距离是否大于farestDistance(即两点之间的最大限制距离,即控制界限区域的那个圆的半径)。
如果大于:断开两个圆之间的连线(此时通过getTempStickRadius()获取的固定圆的半径为0,所以固定圆将消失);
如果不大于:则动态改变拖拽圆的位置和大小,以及固定圆的大小。
3 。处理手指抬起时需要做的事情
判断拖拽是否超出界限
超出界限:判断两个圆的圆心距离(在拖拽已经超出界限区域还要判断圆心距离,是因为在移动动作中,当用户移出界限区后又移回界限区时isOutofRange没有改变,但圆心距会小于farestDistance)
如果Distance>farestDistance:拖拽超出范围,断开, 松手, 消失
如果小于:拖拽超出范围,断开,放回去了,恢复
没有超出界限区: 松手,弹回去
辅助函数:
对整个自定义粘性控件的总结:
第一步:先自定义描绘控件所需要的参数常量
第二步:先通过onDraw绘画出一个想要的静态view效果图
第三步:通过重写onTouchEvent方法。分析清楚按下,移动,抬起时所需要改变的效果,在这三个动作发生时改变相应的参数并调用invalidate();方法,对view进行重绘
自定义控件暴露事件回调的方法:
1.声明一个接口:
2.定义一个 GooViewListener接口变量:
3.为GooViewListener定义一个监听方法:
先来张效果图:
话不多说,线上代码,然后逐个分析
自定义粘性控件
public class GooView extends View { private static final String TAG = "TAG"; private Paint mPaint; public GooView(Context context) { this(context, null); } public GooView(Context context, AttributeSet attrs) { this(context, attrs , 0); } public GooView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // 做初始化操作 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.RED); } PointF[] mStickPoints = new PointF[]{ new PointF(250f, 250f), new PointF(250f, 350f) }; PointF[] mDragPoints = new PointF[]{ new PointF(50f, 250f), new PointF(50f, 350f) }; PointF mControlPoint = new PointF(150f, 300f); PointF mDragCenter = new PointF(80f, 80f); float mDragRadius = 14f; PointF mStickCenter = new PointF(150f, 150f); float mStickRadius = 12f; private int statusBarHeight; float farestDistance = 80f; private boolean isOutofRange;//是否超出界限控制区 private boolean isDisappear;//拖动圆是否存在 @Override protected void onDraw(Canvas canvas) { // 计算连接点值, 控制点, 固定圆半径 // 1. 获取固定圆半径(根据两圆圆心距离) float tempStickRadius = getTempStickRadius(); // 2. 获取直线与圆的交点 float yOffset = mStickCenter.y - mDragCenter.y; float xOffset = mStickCenter.x - mDragCenter.x; Double lineK = null; if(xOffset != 0){ lineK = (double) (yOffset / xOffset); } // 通过几何图形工具获取交点坐标 mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK); mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius, lineK); // 3. 获取控制点坐标 mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter); // 保存画布状态 canvas.save(); canvas.translate(0, -statusBarHeight); // 画出最大范围(参考用) mPaint.setStyle(Style.STROKE); canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, mPaint); mPaint.setStyle(Style.FILL); if(!isDisappear){ if(!isOutofRange){ // 3. 画连接部分 Path path = new Path(); // 跳到点1 path.moveTo(mStickPoints[0].x, mStickPoints[0].y); // 画曲线1 -> 2 path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y); // 画直线2 -> 3 path.lineTo(mDragPoints[1].x, mDragPoints[1].y); // 画曲线3 -> 4 path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y); path.close(); canvas.drawPath(path, mPaint); // 画附着点(参考用) mPaint.setColor(Color.BLUE); canvas.drawCircle(mDragPoints[0].x, mDragPoints[0].y, 3f, mPaint); canvas.drawCircle(mDragPoints[1].x, mDragPoints[1].y, 3f, mPaint); canvas.drawCircle(mStickPoints[0].x, mStickPoints[0].y, 3f, mPaint); canvas.drawCircle(mStickPoints[1].x, mStickPoints[1].y, 3f, mPaint); mPaint.setColor(Color.RED); // 2. 画固定圆 canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, mPaint); } // 1. 画拖拽圆 canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, mPaint); } // 恢复上次的保存状态 canvas.restore(); } // 获取固定圆半径(根据两圆圆心距离) private float getTempStickRadius() { float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter); // if(distance> farestDistance){ // distance = farestDistance; // } distance = Math.min(distance, farestDistance); // 0.0f -> 1.0f float percent = distance / farestDistance; Log.d(TAG, "percent: " + percent); // percent , 100% -> 20% return evaluate(percent, mStickRadius, mStickRadius * 0.2f); } public Float evaluate(float fraction, Number startValue, Number endValue) { float startFloat = startValue.floatValue(); return startFloat + fraction * (endValue.floatValue() - startFloat); } @Override public boolean onTouchEvent(MotionEvent event) { float x; float y; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isOutofRange = false; isDisappear = false; x = event.getRawX(); y = event.getRawY(); updateDragCenter(x, y); break; case MotionEvent.ACTION_MOVE: x = event.getRawX(); y = event.getRawY(); updateDragCenter(x, y); // 处理断开事件 float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter); if(distance > farestDistance){ isOutofRange = true; invalidate(); } break; case MotionEvent.ACTION_UP: if(isOutofRange){ float d = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter); if(d > farestDistance){ // a. 拖拽超出范围,断开, 松手, 消失 isDisappear = true; invalidate(); }else { //b. 拖拽超出范围,断开,放回去了,恢复 updateDragCenter(mStickCenter.x, mStickCenter.y); } }else { // c. 拖拽没超出范围, 松手,弹回去 final PointF tempDragCenter = new PointF(mDragCenter.x, mDragCenter.y); ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f); mAnim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator mAnim) { // 0.0 -> 1.0f float percent = mAnim.getAnimatedFraction(); PointF p = GeometryUtil.getPointByPercent(tempDragCenter, mStickCenter, percent); updateDragCenter(p.x, p.y); } }); mAnim.setInterpolator(new OvershootInterpolator(4)); mAnim.setDuration(500); mAnim.start(); } break; default: break; } return true; } /** * 更新拖拽圆圆心坐标,并重绘界面 * @param x * @param y */ private void updateDragCenter(float x, float y) { mDragCenter.set(x, y); invalidate(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); statusBarHeight = Utils.getStatusBarHeight(this); } }
几何图形工具类
** * 几何图形工具 */ public class GeometryUtil { /** * As meaning of method name. * 获得两点之间的距离 * @param p0 * @param p1 * @return */ public static float getDistanceBetween2Points(PointF p0, PointF p1) { float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2)); return distance; } /** * Get middle point between p1 and p2. * 获得两点连线的中点 * @param p1 * @param p2 * @return */ public static PointF getMiddlePoint(PointF p1, PointF p2) { return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f); } /** * Get point between p1 and p2 by percent. * 根据百分比获取两点之间的某个点坐标 * @param p1 * @param p2 * @param percent * @return */ public static PointF getPointByPercent(PointF p1, PointF p2, float percent) { return new PointF(evaluateValue(percent, p1.x , p2.x), evaluateValue(percent, p1.y , p2.y)); } /** * 根据分度值,计算从start到end中,fraction位置的值。fraction范围为0 -> 1 * @param fraction * @param start * @param end * @return */ public static float evaluateValue(float fraction, Number start, Number end){ return start.floatValue() + (end.floatValue() - start.floatValue()) * fraction; } /** * Get the point of intersection between circle and line. * 获取 通过指定圆心,斜率为lineK的直线与圆的交点。 * * @param pMiddle The circle center point. * @param radius The circle radius. * @param lineK The slope of line which cross the pMiddle. * @return */ public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) { PointF[] points = new PointF[2]; float radian, xOffset = 0, yOffset = 0; if(lineK != null){ radian= (float) Math.atan(lineK); xOffset = (float) (Math.sin(radian) * radius); yOffset = (float) (Math.cos(radian) * radius); }else { xOffset = radius; yOffset = 0; } points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset); points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset); return points; } }
Activity
public class MainActivity extends Activity { private GooView gv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); gv=new GooView(MainActivity.this) setContentView(gv); gv.setGooViewListener(new GooViewListener(){ //重写GooViewListener接口里面的方法 }); } }
GooView 详解:
这里属于自定义View(继承于View)
extends View (GooView extends View)
重写构造方法
进行测量
重写onDraw方法
重写构造方法并初始化一个画笔(Paint对象)
public GooView(Context context) { this(context, null); } public GooView(Context context, AttributeSet attrs) { this(context, attrs , 0); } public GooView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // 做初始化操作 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.RED); }
构造如上所示的静态图坐标
//固定圆的圆上两点坐标 PointF[] mStickPoints = new PointF[]{ new PointF(250f, 250f), new PointF(250f, 350f) }; //移动圆的圆上两点坐标 PointF[] mDragPoints = new PointF[]{ new PointF(50f, 250f), new PointF(50f, 350f) }; //两圆圆心连线的中点坐标 PointF mControlPoint = new PointF(150f, 300f); //拖动圆的圆心 PointF mDragCenter = new PointF(80f, 80f); //拖动圆的半径 float mDragRadius = 14f; //固定圆的圆心 PointF mStickCenter = new PointF(150f, 150f); //静态圆的半径 float mStickRadius = 12f; private int statusBarHeight; //控制区的半径 float farestDistance = 80f;
重点
onDraw方法( 计算连接点值, 控制点, 固定圆半径)
@Override protected void onDraw(Canvas canva eecc s) { /*1. 获取固定圆半径(根据两圆圆心距离) float tempStickRadius = getTempStickRadius(); // 2. 获取直线与圆的交点 float yOffset = mStickCenter.y - mDragCenter.y; float xOffset = mStickCenter.x - mDragCenter.x; Double lineK = null; if(xOffset != 0){ lineK = (double) (yOffset / xOffset); } // 通过几何图形工具获取交点坐标 mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK); mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius, lineK); // 3. 获取控制点坐标 mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter); // 保存画布状态 canvas.save(); canvas.translate(0, -statusBarHeight); // 画出最大范围(参考用) mPaint.setStyle(Style.STROKE); canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, mPaint); mPaint.setStyle(Style.FILL); if(!isDisappear){//拖拽圆需要存在 if(!isOutofRange){//如果拖拽未超出界限 // 3. 画连接部分 Path path = new Path(); // 跳到点1 path.moveTo(mStickPoints[0].x, mStickPoints[0].y); // 画曲线1 -> 2 path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y); // 画直线2 -> 3 path.lineTo(mDragPoints[1].x, mDragPoints[1].y); // 画曲线3 -> 4 path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y); path.close(); canvas.drawPath(path, mPaint); // 画附着点(参考用) mPaint.setColor(Color.BLUE); canvas.drawCircle(mDragPoints[0].x, mDragPoints[0].y, 3f, mPaint); canvas.drawCircle(mDragPoints[1].x, mDragPoints[1].y, 3f, mPaint); canvas.drawCircle(mStickPoints[0].x, mStickPoints[0].y, 3f, mPaint); canvas.drawCircle(mStickPoints[1].x, mStickPoints[1].y, 3f, mPaint); mPaint.setColor(Color.RED); // 2. 画固定圆 canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, mPaint); } // 1. 画拖拽圆 canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, mPaint); } // 恢复上次的保存状态 canvas.restore(); }
分析:
float tempStickRadius = getTempStickRadius();
作用是根据两个圆心的距离的变化动态设置固定圆的半径,以保证在拖动过程中固定圆的大小发生变化
获取直线与圆的交点:用于构建贝塞尔曲线(即上图中的P0和P2)
float yOffset = mStickCenter.y - mDragCenter.y; float xOffset = mStickCenter.x - mDragCenter.x; Double lineK = null; if(xOffset != 0){ lineK = (double) (yOffset / xOffset); } // 通过几何图形工具获取交点坐标 mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK); mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius, lineK);
获取控制点坐标(即上图中的P1)
mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);
画出界定区域圆圈
// 保存画布状态 canvas.save(); canvas.translate(0, -statusBarHeight); // 画出最大范围(参考用)即控制界限区域的那个圆 mPaint.setStyle(Style.STROKE); canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, mPaint); mPaint.setStyle(Style.FILL);
画由1,2,3,4构成的封闭区关于类Path的使用详解
// 3. 画连接部分 Path path = new Path(); // 跳到点1 path.moveTo(mStickPoints[0].x, mStickPoints[0].y); // 画曲线1 -> 2 path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y); // 画直线2 -> 3 path.lineTo(mDragPoints[1].x, mDragPoints[1].y); // 画曲线3 -> 4 path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y); path.close(); canvas.drawPath(path, mPaint);
画固定圆和拖拽圆:
// 2. 画固定圆 canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, mPaint); // 1. 画拖拽圆 canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, mPaint); // 恢复上次的保存状态 canvas.restore();
完成上面的onDraw()函数后,将会得到一个静态效果,如下:
接下来要实现动态变化的效果
动态变化主要通过重写onTouchEvent函数,通过按下,移动,抬起三个事件来改变图形效果
1 。处理按下(MotionEvent.ACTION_DOWN)需要做的事:
根据按下的位置坐标,画出拖动点和由点1,2,3,4构成的封闭图形
case MotionEvent.ACTION_DOWN: isOutofRange = false; isDisappear = false;//标记拖动圆为超出界限区域 x = event.getRawX();//getRowX:触摸点相对于屏幕的坐标 y = event.getRawY(); updateDragCenter(x, y); //更新拖动圆(即执行onDraw函数,重绘拖动圆)
2。 处理移动(MotionEvent.ACTION_MOVE:)所需要做的事
移动过程中需要根据固定点和拖拽点的距离来改变固定点的半径,
需要判断固定点和移动点之间的距离是否大于farestDistance(即两点之间的最大限制距离,即控制界限区域的那个圆的半径)。
如果大于:断开两个圆之间的连线(此时通过getTempStickRadius()获取的固定圆的半径为0,所以固定圆将消失);
如果不大于:则动态改变拖拽圆的位置和大小,以及固定圆的大小。
case MotionEvent.ACTION_MOVE: x = event.getRawX(); y = event.getRawY(); updateDragCenter(x, y); // 处理断开事件 float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter); if(distance > farestDistance){ isOutofRange = true; invalidate(); } 在这里需要注意的是:当用拖拽出界限区域后又拖拽回到界限区域内时未做任何事情,用户可以在这里加一个else执行执行自己的代码
3 。处理手指抬起时需要做的事情
判断拖拽是否超出界限
超出界限:判断两个圆的圆心距离(在拖拽已经超出界限区域还要判断圆心距离,是因为在移动动作中,当用户移出界限区后又移回界限区时isOutofRange没有改变,但圆心距会小于farestDistance)
如果Distance>farestDistance:拖拽超出范围,断开, 松手, 消失
如果小于:拖拽超出范围,断开,放回去了,恢复
没有超出界限区: 松手,弹回去
c. 拖拽没超出范围, 松手,弹回去 final PointF tempDragCenter = new PointF(mDragCenter.x, mDragCenter.y); //值动画 ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f); mAnim.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator mAnim) { // 0.0 -> 1.0f //获取动画执行的百分比(0.0 -> 1.0f) float percent = mAnim.getAnimatedFraction(); //根据百分比获取两点之间的某个点坐标 PointF p = GeometryUtil.getPointByPercent(tempDragCenter, mStickCenter, percent); updateDragCenter(p.x, p.y); } }); mAnim.setInterpolator(new OvershootInterpolator(4)); mAnim.setDuration(500); mAnim.start(); }
辅助函数:
/** * 更新拖拽圆圆心坐标,并重绘界面 * @param x * @param y */ private void updateDragCenter(float x, float y) { mDragCenter.set(x, y); invalidate();//会调用onDraw()函数 } //view的大小发生改变时被系统回调用 @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); statusBarHeight = Utils.getStatusBarHeight(this); }
对整个自定义粘性控件的总结:
第一步:先自定义描绘控件所需要的参数常量
第二步:先通过onDraw绘画出一个想要的静态view效果图
第三步:通过重写onTouchEvent方法。分析清楚按下,移动,抬起时所需要改变的效果,在这三个动作发生时改变相应的参数并调用invalidate();方法,对view进行重绘
自定义控件暴露事件回调的方法:
1.声明一个接口:
interface GooViewListener{ // 相应动作对应的回调方法(在view的onTouchEvent的不同情况下被调用) public void fun1(); public void fun2(); .......... }
2.定义一个 GooViewListener接口变量:
public GooViewListener gvlistener;
3.为GooViewListener定义一个监听方法:
public void setGooViewListener(GooViewListener gvlistener){ this.gvlistener=gvlistener; }
相关文章推荐
- 自定义控件:QQ气泡效果粘性控件的实现
- QQ魔法表情实现原理源代码下载
- VB中利用第三方控件实现QQ垂直菜单
- C#仿QQ皮肤-实现原理系列文章导航
- C#仿QQ皮肤----基窗体FormBase与基用户控件FormBase1的实现
- C#仿QQ皮肤----基窗体FormBase与基用户控件FormBase1的实现
- QQ魔法表情实现原理(呵呵.很简单)
- C#仿QQ皮肤-CustomScrollbar 控件实现
- C#仿QQ皮肤----基窗体FormBase与基用户控件FormBase1的实现
- C#仿QQ皮肤-实现原理系列文章
- C#仿QQ皮肤-Button 控件实现
- 用DELPHI、RxRichEdit控件实现类似QQ的表情输入方法
- C# 实现完整的仿QQ截图控件
- QQ魔法表情实现原理
- C#仿QQ皮肤----基窗体FormBase与基用户控件FormBase1的实现
- Delphi版 关于QQ输入控件无法Spy到句柄的实现方案可行性研究
- QQ魔法表情实现原理
- C# 实现完整的仿QQ截图控件
- 循序渐进实现仿QQ界面(三):界面调色与控件自绘
- C#仿QQ皮肤----基窗体FormBase与基用户控件FormBase1的实现