时间:2021-01-23 10:13:12 | 栏目: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地址