<resources>
<declare-styleable name="DragBadgeView">
<attr name="text" format="string"/>
<attr name="textSize" format="dimension"/>
<attr name="textColor" format="color"/>
<attr name="bgColor" format="color"/>
<attr name="maxMoveRange" format="dimension"/>
<attr name="dragEnable" format="boolean"/>
declare-styleable>
resources>
TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.DragBadgeView);
mText = array.getString(R.styleable.DragBadgeView_text);
float textSize = array.getDimension(R.styleable.DragBadgeView_textSize, sp2px(10));
int bgColor = array.getColor(R.styleable.DragBadgeView_bgColor, Color.RED);
int textColor = array.getColor(R.styleable.DragBadgeView_textColor, Color.WHITE);
mMaxMoveRange = array.getDimension(R.styleable.DragBadgeView_maxMoveRange, dp2px(80));
mDragEnable = array.getBoolean(R.styleable.DragBadgeView_dragEnable, true);
array.recycle();
/**
* 测量文字的宽高
*
* @param text 需要被测量的文字
*/
private void measureText(String text) {
//使用Paint的measureText方法测量文字的宽度,再加上左右的padding值
mTextWidth = mTextPaint.measureText(text) + getPaddingLeft() + getPaddingRight();
//使用FontMetrics获取文字高度,再加上上下的padding值
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
mFontMetricsTop = fontMetrics.top;
mFontMetricsBottom = fontMetrics.bottom;
mTextHeight = Math.abs(mFontMetricsTop - mFontMetricsBottom) + getPaddingTop() +
getPaddingBottom();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float tempWidth = getWidth();
if (getWidth() < getHeight()) {//当宽度小于高度时,时宽度等于高度
tempWidth = getHeight();
}
//设置文本绘制的RectF区域
mTextRectF.set(0, 0, tempWidth, getHeight());
//绘制圆角的Rect,圆角半径为高度的一半,当宽高都是getHeight/2时,绘制的是一个圆形
canvas.drawRoundRect(mTextRectF, getHeight() / 2, getHeight() / 2, mPaint);
//居中drawText
int centerY = (int) (mTextRectF.centerY() - mFontMetricsTop / 2 - mFontMetricsBottom / 2);
String temp = mText;
if (TextUtils.isDigitsOnly(mText)) {
if (Integer.valueOf(mText) > 99) {
temp = "99+";
}
}
//初始化时设置了居中绘制文字mTextPaint.setTextAlign(Paint.Align.CENTER),drawText从mTextRectF中间为中心点
canvas.drawText(temp, mTextRectF.centerX(), centerY, mTextPaint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//取文字的宽高的最大值最为width的默认值
int width = measureDimension((int) Math.max(mTextWidth, mTextHeight), widthMeasureSpec);
int height = measureDimension((int) mTextHeight, heightMeasureSpec);
setMeasuredDimension(width, height);
}
private int measureDimension(int defaultSize, int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {//相当于我们设置为match_parent或者为一个具体的值
result = specSize;
} else if (specMode == MeasureSpec.AT_MOST) {//相当于我们设置为wrap_content
result = Math.min(defaultSize, specSize);
} else {
result = defaultSize;
}
return result;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_DOWN:
if (!mDragEnable) {//设置了不可拖动属性
return false;
}
//获取DragBadgeView所在的根View,一般为DecorView
View root = getRootView();
if (root == null || !(root instanceof ViewGroup)) {
return false;
}
ViewGroup vg = (ViewGroup) root;
//找出添加Tag的BadgeView,ListView/RecyclerView多条目时会两个Item同事Action_Down事件
View badgeView = vg.findViewWithTag(VIEW_TAG);
if (badgeView != null) {
return false;
}
//获取root(DecorView)在屏幕上的绝对坐标
root.getLocationOnScreen(mRootViewLocation);
//获取父布局是否有ListView,ScrollView等,DecorView的上一级是DecorViewImpl再上一级就不是View的实例了,getScrollParent就return null
mScrollParent = getScrollParent(this);
if (mScrollParent != null) {
mScrollParent.requestDisallowInterceptTouchEvent(true);//请求父容器不拦截DOWN事件
}
//获取DOWN事件在屏幕的绝对坐标
int location[] = new int[2];
getLocationOnScreen(location);
int downX = location[0] + (getWidth() / 2) - mRootViewLocation[0];
int downY = location[1] + (getHeight() / 2) - mRootViewLocation[1];
int radius = (getHeight()) / 2;
//DOWN事件初始化DragBadgeView的内部类BadgeView
mBadgeView = new BadgeView(getContext());
//判断是否再执行复位动画,正在执行,不传递DOWN事件
if (mBadgeView.isAnimatorRunning()) {
return false;
}
updateCacheBitmap();
//初始化BadgeView中固定圆,拖拽圆点位信息
mBadgeView.initPoints(downX, downY, event.getRawX() - mRootViewLocation[0],
event.getRawY() - mRootViewLocation[1], radius);
mBadgeView.setTag(VIEW_TAG);//给BadgeView设置Tag
View cacheView = vg.findViewWithTag(VIEW_TAG);//如果有之前的BadgeView,清除
if (cacheView != null) {
vg.removeView(cacheView);
}
//将BadgeView添加到DecorView中(DecorView是一个FragmeLayout)
((ViewGroup) root).addView(mBadgeView);
//设置DragBadgeView不可见
setVisibility(View.INVISIBLE);
//isDragging标志是区分是否在拖拽时更新了文字
isDragging = true;
break;
case MotionEvent.ACTION_MOVE:
//MOVE事件时更新BadgeView各个点位信息进行重新绘制
mBadgeView.updateView(event.getRawX() - mRootViewLocation[0],
event.getRawY() - mRootViewLocation[1]);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_DOWN://多个手指按下时
case MotionEvent.ACTION_CANCEL://BadgeView移动到屏幕左右两边边缘时点击ListView/RecyclerView条目时触发
isDragging = false;
if (mScrollParent != null) {
mScrollParent.requestDisallowInterceptTouchEvent(false);
}
if (mBadgeView == null) {
return true;
}
//UP事件时如果超过了最大范围时,消失,反之复位
if (mBadgeView.isOutOfRange) {
mBadgeView.disappear(event.getRawX() - mRootViewLocation[0],
event.getRawY() - mRootViewLocation[1]);
} else if (!mBadgeView.isResetAnimatorRunning()){
mBadgeView.reset();
}
break;
default:
break;
}
return true;
}
/**
* 递归获取父布局中是否含可滑动的ViewGroup
*
* @param v 子View
* @return null或者可滑动的ViewGroup
*/
private ViewGroup getScrollParent(View v) {
ViewParent viewParent = v.getParent();
View parent;
if (viewParent instanceof View) {
parent = (View) viewParent;
} else {
return null;
}
if (parent instanceof AbsListView || parent instanceof ScrollView || parent instanceof
ViewPager || parent instanceof ScrollingView) {
return (ViewGroup) parent;
}
return getScrollParent(parent);
}
/**
* 获取TextView的缓存bitmap
*/
private void updateCacheBitmap() {
//把BadgeView之前的Bitmap进行回收
mBadgeView.recycleCacheBitmap();
//获取当前DragBadgeView的Bitmap
setDrawingCacheEnabled(true);
Bitmap drawingCache = getDrawingCache();
//创建副本并赋值
mBadgeView.cacheBitmap = Bitmap.createBitmap(drawingCache);
setDrawingCacheEnabled(false);
}
public void initPoints(float originX, float originY, float dragX, float dragY, float r) {
mOriginPoint = new PointF(originX, originY);
mDragPoint = new PointF(dragX, dragY);
//二阶贝塞尔曲线控制点
mControlPoint = new PointF((originX + dragX) / 2.0f, (originY + dragY) / 2.0f);
mOriginRadius = r;
mDragRadius = r;
isOutOfRange = false;
isBezierBreak = false;
}
public void updateView(float x, float y) {
//获取固定点和拖拽点的距离
float distance = (float) Math.sqrt(Math.pow(mOriginPoint.y - mDragPoint.y, 2) +
Math.pow(mOriginPoint.x - mDragPoint.x, 2));
isOutOfRange = distance > mMaxMoveRange;//判断拖拽点是否超出最大范围
//固定圆,半径缩放
mOriginRadius = mDragRadius - distance / 10;
//最小半径5dp
if (mOriginRadius < dp2px(5)) {
mOriginRadius = dp2px(5);
}
//设置拖拽点,并进行重绘制
updateDragPoint(x, y);
}
public void updateDragPoint(float x, float y) {
mDragPoint.set(x, y);
//postInvalidate内部使用Handler进行更新,考虑有可能会多线程,使用postInvalidate.(postInvalidate也可以再主线程调用,此处就是)
BadgeView.this.postInvalidate();
}
//消失
public void disappear(float x, final float y) {
////DecorView中移除BadgeView
ViewGroup rootView = (ViewGroup) BadgeView.this.getParent();
rootView.removeView(BadgeView.this);
//回收Bitmap
recycleCacheBitmap();
//添加消失后的动画
addExplodeImageView(x, y, rootView);
//回调
if (mListener != null) {
mListener.onDisappear(mText);
}
}
/**
* 消失后的动画
*
* @param x BadgeView消失的x坐标
* @param y BadgeView消失的y坐标
* @param rootView DecorView
*/
private void addExplodeImageView(final float x, final float y, final ViewGroup rootView) {
final int totalDuration = 500;//动画总时长
int d = totalDuration / 5;//每帧时长
final ImageView explodeImage = new ImageView(getContext());
final AnimationDrawable explodeAnimation = new AnimationDrawable();//创建帧动画
//添加帧,图片放置在drawable-nodpi下
explodeAnimation.addFrame(ContextCompat.getDrawable(getContext(), R.drawable.pop1), d);
explodeAnimation.addFrame(ContextCompat.getDrawable(getContext(), R.drawable.pop2), d);
explodeAnimation.addFrame(ContextCompat.getDrawable(getContext(), R.drawable.pop3), d);
explodeAnimation.addFrame(ContextCompat.getDrawable(getContext(), R.drawable.pop4), d);
explodeAnimation.addFrame(ContextCompat.getDrawable(getContext(), R.drawable.pop5), d);
//设置动画只播放一次
explodeAnimation.setOneShot(true);
explodeImage.setImageDrawable(explodeAnimation);
explodeImage.setVisibility(INVISIBLE);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
//DecorView中添加ImageView
rootView.addView(explodeImage, params);
//只有当ImageView测量绘制布局完成后才能获取起大小,使用post可简单的获取
explodeImage.post(new Runnable() {
@Override
public void run() {
//设置ImageView的位置
explodeImage.setX(x - explodeImage.getWidth() / 2);
explodeImage.setY(y - explodeImage.getHeight() / 2);
explodeImage.setVisibility(VISIBLE);
explodeAnimation.start();
//当动画执行完成后将ImageView移除,并且时DragBadgeView设置为不可见
Handler handler = explodeImage.getHandler();
if (handler != null) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
explodeImage.setVisibility(GONE);
//动画结束后DecorView中移除ImageView控件
rootView.removeView(explodeImage);
DragBadgeView.this.setVisibility(INVISIBLE);
}
}, totalDuration);
}
}
});
}
}
@Override
protected void onDraw(Canvas canvas) {
if (!isOutOfRange && !isBezierBreak) {
//重置Path
mPath.reset();
float dx = mDragPoint.x - mOriginPoint.x;
float dy = mDragPoint.y - mOriginPoint.y;
//两圆交点偏移量
float oDx = mOriginRadius;
float oDy = 0;
float dDx = mDragRadius;
float dDy = 0;
if (dx != 0) {
double a = Math.atan(dy / dx);//a:角度
oDx = (float) (Math.sin(a) * mOriginRadius);
oDy = (float) (Math.cos(a) * mOriginRadius);
dDx = (float) (Math.sin(a) * mDragRadius);
dDy = (float) (Math.cos(a) * mDragRadius);
}
//贝塞尔曲线控制点
mControlPoint.set((mOriginPoint.x + mDragPoint.x) / 2.0f,
(mOriginPoint.y + mDragPoint.y) / 2.0f);
//移动到第一个二阶贝塞尔曲线开始的点
mPath.moveTo(mOriginPoint.x + oDx, mOriginPoint.y - oDy);
mPath.quadTo(mControlPoint.x, mControlPoint.y,
mDragPoint.x + dDx, mDragPoint.y - dDy);
//连接到第二个二阶贝塞尔曲线开始的点
mPath.lineTo(mDragPoint.x - dDx, mDragPoint.y + dDy);
mPath.quadTo(mControlPoint.x, mControlPoint.y, mOriginPoint.x - oDx,
mOriginPoint.y + oDy);
mPath.close();
canvas.drawPath(mPath, mPaint);
//画固定圆
canvas.drawCircle(mOriginPoint.x, mOriginPoint.y, mOriginRadius, mPaint);
/*// 画出贝塞尔曲线可显示的范围
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(mOriginPoint.x, mOriginPoint.y, mMaxMoveRange, mPaint);
mPaint.setStyle(Paint.Style.FILL);*/
} else {
isBezierBreak = true;
}
//拖拽的图像
canvas.drawBitmap(cacheBitmap, mDragPoint.x - cacheBitmap.getWidth() / 2,
mDragPoint.y - cacheBitmap.getHeight() / 2, mPaint);
}
dependencies {
compile 'com.fendoudebb.view:dragbadgeview:1.0.2'
}
<com.fendoudebb.view.DragBadgeView
android:id="@+id/drag_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="2dp"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:paddingTop="2dp"
app:dragEnable="false"/>
mDragBadgeView.setOnDragBadgeViewListener(new DragBadgeView.OnDragBadgeViewListener() {
@Override
public void onDisappear(String text) {
Toast.makeText(getApplicationContext(), text + "条信息隐藏!", Toast.LENGTH_SHORT).show();
}
});
app:text="测试"
mDragBadgeView.setText("测试");
app:textSize="12sp"
mDragBadgeView.setTextSize(dp2sp(15));
app:bgColor="#f0f"
mDragBadgeView.setBgColor(Color.BLUE);
app:dragEnable="false"
mDragBadgeView.setDragEnable(true);
启舰的自定义控件: http://blog.csdn.net/harvic880925/article/details/51615221
仿QQ 拖动小红点原理及其实现: http://blog.csdn.net/u011748648/article/details/48132349
http://download.csdn.net/detail/fendoudebb/9919991
https://github.com/fendoudebb/DragBadgeView