Android开发之QQ黏性控件(QQ消息未读提醒,在固定范围内,拖拽回弹,超出范围,松手消失)
自腾讯手机QQ新版推出后,想必大家都发现了,QQ的未读消息提醒,效果真的很炫酷,即在固定范围内,拖拽未读消息提醒控件,会在拖拽中心点到原中心点绘制出贝塞尔曲线,然而超过了固定范围后,松手后产生气泡消失,本人研究了下该黏性控件,已基本实现了其功能,下面贴出代码,望各路大神批评指正,灰常感谢,♪(^∇^*)
先上效果图:
核心代码如下:
MainActivity
package com.example.testvistousview; import android.app.Activity; import android.os.Bundle; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
ViscosityView
package com.example.testvistousview.customview; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.FloatEvaluator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.Typeface; import android.support.v4.view.MotionEventCompat; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.animation.CycleInterpolator; import android.view.animation.OvershootInterpolator; import com.example.testvistousview.Utils.GeometryUtils; /** * 粘性控件:消息未读提醒 */ public class ViscosityView extends View { private Paint mPaint; private PointF[] mDragPoint; private PointF[] mStickPoint; private PointF mControlPoint; private PointF mDragCenterPoint; private PointF mStickCenterPoint; private float mDragRadius; private float mStickRadius; private float xOffset; private float yOffset; private double lineK; private int statusHeight; float maxDistance = 200.0f; private boolean isOutOfRange = false; private boolean disappear = false; private Paint mTextPaint; public interface OnDragEventListener { void onDisappear(); void onReset(boolean isToReset); } public OnDragEventListener mOnDragEventListener; public OnDragEventListener getmOnDragEventListener() { return mOnDragEventListener; } public void setmOnDragEventListener(OnDragEventListener mOnDragEventListener) { this.mOnDragEventListener = mOnDragEventListener; } public ViscosityView(Context context) { this(context, null); } public ViscosityView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ViscosityView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //ANTI_ALIAS_FLAG抗锯齿 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.RED); mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(Color.WHITE); mTextPaint.setTextSize(24); mTextPaint.setTypeface(Typeface.DEFAULT_BOLD); mTextPaint.setTextAlign(Paint.Align.CENTER); mDragPoint = new PointF[2]; mStickPoint = new PointF[2]; mControlPoint = new PointF(); mDragCenterPoint = new PointF(150f, 150f); mStickCenterPoint = new PointF(220f, 250f); mStickRadius = 30f; mDragRadius = 30f; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); /** * 将画布向上平移状态栏高度的位置 */ //保存当前的的状态 canvas.save(); canvas.translate(0, -statusHeight); //计算连接部分----利用变量 //计算变量: //1.求出固定圆的半径(暂时假设为12f) float tempStickRadius = getStickRadius(); //2.求出四个附着点的坐标 xOffset = mStickCenterPoint.x - mDragCenterPoint.x; yOffset = mStickCenterPoint.y - mDragCenterPoint.y; if (xOffset != 0) { lineK = yOffset / xOffset; } mStickPoint = GeometryUtils.getIntersectionPoints(mStickCenterPoint, tempStickRadius, lineK); mDragPoint = GeometryUtils.getIntersectionPoints(mDragCenterPoint, mDragRadius, lineK); //3.求出控制点的坐标 mControlPoint = GeometryUtils.getPointByPercent(mDragCenterPoint, mStickCenterPoint, 0.618f); mPaint.setStyle(Paint.Style.STROKE); canvas.drawCircle(mStickCenterPoint.x, mStickCenterPoint.y, maxDistance, mPaint); mPaint.setStyle(Paint.Style.FILL); if (!disappear) { if (!isOutOfRange) { //绘制两圆的连接部分采用贝塞尔曲线 //绘制路径 Path path = new Path(); path.moveTo(mStickPoint[0].x, mStickPoint[0].y); path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoint[0].x, mDragPoint[0].y); path.lineTo(mDragPoint[1].x, mDragPoint[1].y); path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoint[1].x, mStickPoint[1].y); path.close(); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);//设置抗锯齿的属性 int[] colors = new int[]{ Color.RED, Color.GRAY, Color.YELLOW, Color.GREEN, Color.BLUE }; Shader shader = new LinearGradient(0, 0, 10, 10, colors, null, Shader.TileMode.REPEAT); paint.setShader(shader); canvas.drawCircle(mDragPoint[0].x, mDragPoint[0].y, 3.0f, paint); canvas.drawCircle(mDragPoint[1].x, mDragPoint[1].y, 3.0f, paint); canvas.drawCircle(mStickPoint[0].x, mStickPoint[0].y, 3.0f, paint); canvas.drawCircle(mStickPoint[1].x, mStickPoint[1].y, 3.0f, paint); //绘制贝塞尔曲线 canvas.drawPath(path, mPaint); //绘制gudingyu固定圆 canvas.drawCircle(mStickCenterPoint.x, mStickCenterPoint.y, tempStickRadius, mPaint); } //绘制拖拽圆 canvas.drawCircle(mDragCenterPoint.x, mDragCenterPoint.y, mDragRadius, mPaint); canvas.drawText("23", mDragCenterPoint.x, mDragCenterPoint.y + mDragRadius / 3.0f, mTextPaint); } //恢复到画布刚刚保存的状态 canvas.restore(); } private float getStickRadius() { float distance = GeometryUtils.getDistanceBetween2Point(mStickCenterPoint, mDragCenterPoint); distance = Math.min(distance, maxDistance); float percent = distance / maxDistance; FloatEvaluator evaluator = new FloatEvaluator(); Float evaluate = evaluator.evaluate(percent, 1.0f, 0.2f); System.out.println("percent------------------------" + percent); return evaluate * mStickRadius; } @Override public boolean onTouchEvent(MotionEvent event) { switch (MotionEventCompat.getActionMasked(event)) { case MotionEvent.ACTION_DOWN: isOutOfRange = false; disappear = false; float downX = event.getRawX(); float downY = event.getRawY(); updateDragCenter(downX, downY); break; case MotionEvent.ACTION_MOVE: float moveX = event.getRawX(); float moveY = event.getRawY(); updateDragCenter(moveX, moveY); //实时判断拖拽圆和固定圆两圆的的圆心距与最大距离的比较 if (GeometryUtils.getDistanceBetween2Point(mDragCenterPoint, mStickCenterPoint) > maxDistance) { isOutOfRange = true; invalidate(); } break; case MotionEvent.ACTION_UP: //表示已经超出范围了 if (isOutOfRange) { //实时的判断是否超出范围 if (GeometryUtils.getDistanceBetween2Point(mDragCenterPoint, mStickCenterPoint) > maxDistance) { disappear = true; if (mOnDragEventListener != null) { mOnDragEventListener.onDisappear(); } invalidate(); } else { updateDragCenter(mStickCenterPoint.x, mStickCenterPoint.y); if (mOnDragEventListener != null) { mOnDragEventListener.onReset(true); } } } else { final PointF lastPointCenter = new PointF(mDragCenterPoint.x, mDragCenterPoint.y); //updateDragCenter(mStickCenterPoint.x,mStickCenterPoint.y); ValueAnimator mValueAnimator = ValueAnimator.ofFloat(1.0f); mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float fraction = animation.getAnimatedFraction(); PointF pointF = GeometryUtils.getPointByPercent(lastPointCenter, mStickCenterPoint, fraction); updateDragCenter(pointF.x, pointF.y); } }); mValueAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (mOnDragEventListener != null) { mOnDragEventListener.onReset(true); } } }); //mValueAnimator.setInterpolator(new CycleInterpolator(4)); mValueAnimator.setInterpolator(new OvershootInterpolator()); mValueAnimator.setDuration(200); mValueAnimator.start(); } break; } return true; } /** * 更新拖拽圆的圆心坐标 * * @param x * @param y */ private void updateDragCenter(float x, float y) { mDragCenterPoint.set(x, y); invalidate(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); Rect rect = new Rect(); this.getWindowVisibleDisplayFrame(rect); statusHeight = rect.top; System.out.println("statusHeight-------------------------" + statusHeight); } }
GeometryUtils
package com.example.testvistousview.Utils; import android.animation.FloatEvaluator; import android.graphics.PointF; /** * 定义几何图形工具 */ public class GeometryUtils { /** * 计算两点之间的距离 * * @param p0 第一个点的坐标值 * @param p1 第二个点的坐标值 * @return 两点之间的距离 */ public static float getDistanceBetween2Point(PointF p0, PointF p1) { float yOffset = p0.y - p1.y; float xOffect = p0.x - p1.x; float distance = (float) Math.sqrt(Math.pow(yOffset, 2) + Math.pow(xOffect, 2)); return distance; } /** * 计算两点连线的的中心坐标 * * @param p0 * @param p1 * @return */ public static PointF getMiddleBetween2Point(PointF p0, PointF p1) { float yOffset = p0.y - p1.y; float xOffect = p0.x - p1.x; return new PointF(xOffect / 2.0f, yOffset / 2.0f); } /** * 根据百分比获得两点连线上的某个位置的坐标 * * @param p0 * @param p1 * @param percent * @return */ public static PointF getPointByPercent(PointF p0, PointF p1, float percent) { FloatEvaluator mFloatEvaluator = new FloatEvaluator(); Float xPoint = mFloatEvaluator.evaluate(percent, p0.x, p1.x); Float yPoint = mFloatEvaluator.evaluate(percent, p0.y, p1.y); return new PointF(xPoint, yPoint); } /** * 获取通过指定圆心且斜率为lineK的直线与圆的交点 * * @param pMiddle * @param radius * @param lineK * @return */ public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) { PointF[] pointFs = 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; } pointFs[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset); pointFs[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset); return pointFs; } }
代码已贴出,正所谓取之于社会,回报于社会,望各路大神批评指正,不喜勿喷,灰常感谢,♪(^∇^*)