[Android]仿新版QQ的tab下面拖拽标记为已读的效果


可拖拽的红点,(仿新版QQ,tab下面拖拽标记为已读的效果),拖拽一定的距离可以消失回调。

  

 

GitHub:DraggableFlagViewhttps://github.com/wangjiegulu/DraggableFlagView

实现原理:

当根据touch事件的移动,不断调用onDraw()方法进行刷新绘制。

*注意:这里原来的小红点称为红点A;根据手指移动绘制的小红点称为红点B。

touch事件移动的时候需要处理的逻辑:

1. 红点A的半径根据滑动的距离会不断地变小。

2. 红点B会紧随手指的位置移动。

3. 在红点A和红点B之间需要用贝塞尔曲线绘制连接区域。

4. 如果红点A和红点B之间的间距达到了设置的最大的距离,则表示,这次的拖拽会有效,一旦放手红点就会消失。

5. 如果达到了第4中情况,则红点A和中间连接的贝塞尔曲线不会被绘制。

6. 如果红点A和红点B之间的距离没有达到设置的最大的距离,则放手后,红点B消失,红点A从原来变小的半径使用反弹动画变换到原来最初的状态

一些工具类需要依赖 AndroidBucket(https://github.com/wangjiegulu/AndroidBucket),nineoldandroid

使用方式:


<com.wangjie.draggableflagview.DraggableFlagView
       
xmlns:dfv=”http://schemas.android.com/apk/res/com.wangjie.draggableflagview
android:id
=”@+id/main_dfv”
android:layout_width
=”20dp”
android:layout_height
=”20dp”
android:layout_alignParentBottom
=”true”
android:layout_margin
=”15dp”
dfv:color
=”#FF3B30”
/>

 


 1 public class MainActivity extends Activity implements DraggableFlagView.OnDraggableFlagViewListener, View.OnClickListener {
2
3 @Override
4 public void onCreate(Bundle savedInstanceState) {
5 super.onCreate(savedInstanceState);
6 setContentView(R.layout.main);
7 findViewById(R.id.main_btn).setOnClickListener(this);
8
9 DraggableFlagView draggableFlagView = (DraggableFlagView) findViewById(R.id.main_dfv);
10 draggableFlagView.setOnDraggableFlagViewListener(this);
11 draggableFlagView.setText(“7”);
12 }
13
14 @Override
15 public void onFlagDismiss(DraggableFlagView view) {
16 Toast.makeText(this, “onFlagDismiss”, Toast.LENGTH_SHORT).show();
17 }
18
19 @Override
20 public void onClick(View v) {
21 switch (v.getId()) {
22 case R.id.main_btn:
23 Toast.makeText(this, “hello world”, Toast.LENGTH_SHORT).show();
24 break;
25 }
26 }
27 }

DraggableFlagView代码:

 


  1 /
2 Author: wangjie
3 Email: tiantian.china.2@gmail.com
4 Date: 12/23/14.
5 /
6 public class DraggableFlagView extends View {
7 private static final String TAG = DraggableFlagView.class.getSimpleName();
8
9 public static interface OnDraggableFlagViewListener {
10 /
11 拖拽销毁圆点后的回调
12
13 @param view
14 /
15 void onFlagDismiss(DraggableFlagView view);
16 }
17
18 private OnDraggableFlagViewListener onDraggableFlagViewListener;
19
20 public void setOnDraggableFlagViewListener(OnDraggableFlagViewListener onDraggableFlagViewListener) {
21 this.onDraggableFlagViewListener = onDraggableFlagViewListener;
22 }
23
24 public DraggableFlagView(Context context) {
25 super(context);
26 init(context);
27 }
28
29 private int patientColor = Color.RED;
30
31 public DraggableFlagView(Context context, AttributeSet attrs) {
32 super(context, attrs);
33 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DraggableFlagView);
34 int indexCount = a.getIndexCount();
35 for (int i = 0; i < indexCount; i++) {
36 int attrIndex = a.getIndex(i);
37 if (attrIndex == R.styleable.DraggableFlagView_color) {
38 patientColor = a.getColor(attrIndex, Color.RED);
39 }
40 }
41 a.recycle();
42 init(context);
43 }
44
45 public DraggableFlagView(Context context, AttributeSet attrs, int defStyle) {
46 super(context, attrs, defStyle);
47 init(context);
48 }
49
50 private Context context;
51 private int originRadius; // 初始的圆的半径
52 private int originWidth;
53 private int originHeight;
54
55 private int maxMoveLength; // 最大的移动拉长距离
56 private boolean isArrivedMaxMoved; // 达到了最大的拉长距离(松手可以触发事件)
57
58 private int curRadius; // 当前点的半径
59 private int touchedPointRadius; // touch的圆的半径
60 private Point startPoint = new Point();
61 private Point endPoint = new Point();
62
63 private Paint paint; // 绘制圆形图形
64 private Paint textPaint; // 绘制圆形图形
65 private Paint.FontMetrics textFontMetrics;
66
67 private int[] location;
68
69 private boolean isTouched; // 是否是触摸状态
70
71 private Triangle triangle = new Triangle();
72
73 private String text = “”; // 正常状态下显示的文字
74
75 private void init(Context context) {
76 this.context = context;
77
78 setBackgroundColor(Color.TRANSPARENT);
79
80 // 设置绘制flag的paint
81 paint = new Paint();
82 paint.setColor(patientColor);
83 paint.setAntiAlias(true);
84
85 // 设置绘制文字的paint
86 textPaint = new Paint();
87 textPaint.setAntiAlias(true);
88 textPaint.setColor(Color.WHITE);
89 textPaint.setTextSize(ABTextUtil.sp2px(context, 12));
90 textPaint.setTextAlign(Paint.Align.CENTER);
91 textFontMetrics = paint.getFontMetrics();
92
93 }
94
95 RelativeLayout.LayoutParams originLp; // 实际的layoutparams
96 RelativeLayout.LayoutParams newLp; // 触摸时候的LayoutParams
97
98 private boolean isFirst = true;
99
100 @Override
101 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
102 super.onSizeChanged(w, h, oldw, oldh);
103 // Logger.d(TAG, String.format(“onSizeChanged, w: %s, h: %s, oldw: %s, oldh: %s”, w, h, oldw, oldh));
104 if (isFirst && w > 0 && h > 0) {
105 isFirst = false;
106
107 originWidth = w;
108 originHeight = h;
109
110 originRadius = Math.min(originWidth, originHeight) / 2;
111 curRadius = originRadius;
112 touchedPointRadius = originRadius;
113
114 maxMoveLength = ABAppUtil.getDeviceHeight(context) / 6;
115
116 refreshStartPoint();
117
118 ViewGroup.LayoutParams lp = this.getLayoutParams();
119 if (RelativeLayout.LayoutParams.class.isAssignableFrom(lp.getClass())) {
120 originLp = (RelativeLayout.LayoutParams) lp;
121 }
122 newLp = new RelativeLayout.LayoutParams(lp.width, lp.height);
123 }
124
125 }
126
127 @Override
128 public void setLayoutParams(ViewGroup.LayoutParams params) {
129 super.setLayoutParams(params);
130 refreshStartPoint();
131 }
132
133 /
134 修改layoutParams后,需要重新设置startPoint
135 /
136 private void refreshStartPoint() {
137 location = new int[2];
138 this.getLocationInWindow(location);
139 // Logger.d(TAG, “location on screen: “ + Arrays.toString(location));
140 // startPoint.set(location[0], location[1] + h);
141 try {
142 location[1] = location[1] - ABAppUtil.getTopBarHeight((Activity) context);
143 } catch (Exception ex) {
144 }
145
146 startPoint.set(location[0], location[1] + getMeasuredHeight());
147 // Logger.d(TAG, “startPoint: “ + startPoint);
148 }
149
150 Path path = new Path();
151
152 @Override
153 protected void onDraw(Canvas canvas) {
154 super.onDraw(canvas);
155 canvas.drawColor(Color.TRANSPARENT);
156
157 int startCircleX = 0, startCircleY = 0;
158 if (isTouched) { // 触摸状态
159
160 startCircleX = startPoint.x + curRadius;
161 startCircleY = startPoint.y - curRadius;
162 // 绘制原来的圆形(触摸移动的时候半径会不断变化)
163 canvas.drawCircle(startCircleX, startCircleY, curRadius, paint);
164 // 绘制手指跟踪的圆形
165 int endCircleX = endPoint.x;
166 int endCircleY = endPoint.y;
167 canvas.drawCircle(endCircleX, endCircleY, originRadius, paint);
168
169 if (!isArrivedMaxMoved) { // 没有达到拉伸最大值
170 path.reset();
171 double sin = triangle.deltaY / triangle.hypotenuse;
172 double cos = triangle.deltaX / triangle.hypotenuse;
173
174 // A点
175 path.moveTo(
176 (float) (startCircleX - curRadius sin),
177 (float) (startCircleY - curRadius
cos)
178 );
179 // B点
180 path.lineTo(
181 (float) (startCircleX + curRadius sin),
182 (float) (startCircleY + curRadius
cos)
183 );
184 // C点
185 path.quadTo(
186 (startCircleX + endCircleX) / 2, (startCircleY + endCircleY) / 2,
187 (float) (endCircleX + originRadius sin), (float) (endCircleY + originRadius cos)
188 );
189 // D点
190 path.lineTo(
191 (float) (endCircleX - originRadius sin),
192 (float) (endCircleY - originRadius
cos)
193 );
194 // A点
195 path.quadTo(
196 (startCircleX + endCircleX) / 2, (startCircleY + endCircleY) / 2,
197 (float) (startCircleX - curRadius sin), (float) (startCircleY - curRadius cos)
198 );
199 canvas.drawPath(path, paint);
200 }
201
202
203 } else { // 非触摸状态
204 if (curRadius > 0) {
205 startCircleX = curRadius;
206 startCircleY = originHeight - curRadius;
207 canvas.drawCircle(startCircleX, startCircleY, curRadius, paint);
208 if (curRadius == originRadius) { // 只有在恢复正常的情况下才显示文字
209 // 绘制文字
210 float textH = textFontMetrics.bottom - textFontMetrics.top;
211 canvas.drawText(text, startCircleX, startCircleY + textH / 2, textPaint);
212 // canvas.drawText(text, startCircleX, startCircleY, textPaint);
213 }
214 }
215
216 }
217
218 // Logger.d(TAG, “circleX: “ + startCircleX + “, circleY: “ + startCircleY + “, curRadius: “ + curRadius);
219
220
221 }
222
223 float downX = Float.MAX_VALUE;
224 float downY = Float.MAX_VALUE;
225
226 @Override
227 public boolean onTouchEvent(MotionEvent event) {
228 super.onTouchEvent(event);
229 // Logger.d(TAG, “onTouchEvent: “ + event);
230 switch (event.getAction()) {
231 case MotionEvent.ACTION_DOWN:
232 isTouched = true;
233 this.setLayoutParams(newLp);
234 endPoint.x = (int) downX;
235 endPoint.y = (int) downY;
236
237 changeViewHeight(this, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
238 postInvalidate();
239
240 downX = event.getX() + location[0];
241 downY = event.getY() + location[1];
242 // Logger.d(TAG, String.format(“downX: %f, downY: %f”, downX, downY));
243
244 break;
245 case MotionEvent.ACTION_MOVE:
246 // 计算直角边和斜边(用于计算绘制两圆之间的填充去)
247 triangle.deltaX = event.getX() - downX;
248 triangle.deltaY = -1 (event.getY() - downY); // y轴方向相反,所有需要取反
249 double distance = Math.sqrt(triangle.deltaX
triangle.deltaX + triangle.deltaY triangle.deltaY);
250 triangle.hypotenuse = distance;
251 // Logger.d(TAG, “triangle: “ + triangle);
252 refreshCurRadiusByMoveDistance((int) distance);
253
254 endPoint.x = (int) event.getX();
255 endPoint.y = (int) event.getY();
256
257 postInvalidate();
258
259 break;
260 case MotionEvent.ACTION_UP:
261 isTouched = false;
262 this.setLayoutParams(originLp);
263
264 if (isArrivedMaxMoved) { // 触发事件
265 changeViewHeight(this, originWidth, originHeight);
266 postInvalidate();
267 if (null != onDraggableFlagViewListener) {
268 onDraggableFlagViewListener.onFlagDismiss(this);
269 }
270 Logger.d(TAG, “触发事件…”);
271 resetAfterDismiss();
272 } else { // 还原
273 changeViewHeight(this, originWidth, originHeight);
274 startRollBackAnimation(500/
ms*/);
275 }
276
277 downX = Float.MAX_VALUE;
278 downY = Float.MAX_VALUE;
279 break;
280 }
281
282 return true;
283 }
284
285 /
286 触发事件之后重置
287 /
288 private void resetAfterDismiss() {
289 this.setVisibility(GONE);
290 text = “”;
291 isArrivedMaxMoved = false;
292 curRadius = originRadius;
293 postInvalidate();
294 }
295
296 /
297 根据移动的距离来刷新原来的圆半径大小
298
299 @param distance
300 /
301 private void refreshCurRadiusByMoveDistance(int distance) {
302 if (distance > maxMoveLength) {
303 isArrivedMaxMoved = true;
304 curRadius = 0;
305 } else {
306 isArrivedMaxMoved = false;
307 float calcRadius = (1 - 1f distance / maxMoveLength) originRadius;
308 float maxRadius = ABTextUtil.dip2px(context, 2);
309 curRadius = (int) Math.max(calcRadius, maxRadius);
310 // Logger.d(TAG, “[refreshCurRadiusByMoveDistance]curRadius: “ + curRadius + “, calcRadius: “ + calcRadius + “, maxRadius: “ + maxRadius);
311 }
312
313 }
314
315
316 /
317 改变某控件的高度
318
319 @param view
320 @param height
321 /
322 private void changeViewHeight(View view, int width, int height) {
323 ViewGroup.LayoutParams lp = view.getLayoutParams();
324 if (null == lp) {
325 lp = originLp;
326 }
327 lp.width = width;
328 lp.height = height;
329 view.setLayoutParams(lp);
330 }
331
332 /**
333 回滚状态动画
334 /
335 private ValueAnimator rollBackAnim;
336
337 private void startRollBackAnimation(long duration) {
338 if (null == rollBackAnim) {
339 rollBackAnim = ValueAnimator.ofFloat(curRadius, originRadius);
340 rollBackAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
341 @Override
342 public void onAnimationUpdate(ValueAnimator animation) {
343 float value = (float) animation.getAnimatedValue();
344 curRadius = (int) value;
345 postInvalidate();
346 }
347 });
348 rollBackAnim.setInterpolator(new BounceInterpolator()); // 反弹效果
349 rollBackAnim.addListener(new AnimatorListenerAdapter() {
350 @Override
351 public void onAnimationEnd(Animator animation) {
352 super.onAnimationEnd(animation);
353 DraggableFlagView.this.clearAnimation();
354 }
355 });
356 }
357 rollBackAnim.setDuration(duration);
358 rollBackAnim.start();
359 }
360
361
362 /**
363 计算四个坐标的三角边关系
364 */
365 class Triangle {
366 double deltaX;
367 double deltaY;
368 double hypotenuse;
369
370 @Override
371 public String toString() {
372 return “Triangle{“ +
373 “deltaX=” + deltaX +
374 “, deltaY=” + deltaY +
375 “, hypotenuse=” + hypotenuse +
376 ‘}’;
377 }
378 }
379
380 public String getText() {
381 return text;
382 }
383
384 public void setText(String text) {
385 this.text = text;
386 postInvalidate();
387 }
388 }

 



来源博客:Wang Jie's Blog
本文链接:https://blog.wangjiegulu.com/2014/12/24/Android-仿新版QQ的tab下面拖拽标记为已读的效果/
版权声明:本博客所有文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处。