[Android]仿新版QQ的tab下面拖拽标记为已读的效果
以下内容为原创,欢迎转载,转载请注明
来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4182929.html
可拖拽的红点,(仿新版QQ,tab下面拖拽标记为已读的效果),拖拽一定的距离可以消失回调。
GitHub:DraggableFlagView(https://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 }