写在前头
写消息拖拽效果的文章不少,但是大部分都把自定义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代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51private 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的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200public 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的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121public 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的调用代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public 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地址
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持靠谱客。
最后
以上就是无心戒指最近收集整理的关于Android贝塞尔曲线实现消息拖拽消失的全部内容,更多相关Android贝塞尔曲线实现消息拖拽消失内容请搜索靠谱客的其他文章。
发表评论 取消回复