自定义可拖拽显示数字BadgeView(仿QQ可拖拽控件)

自定义可拖拽显示数字BadgeView(仿QQ可拖拽控件)

样例演示

样例1
样例2

实现步骤

1.配置attrs.xml,获取配置的属性
<?xml version="1.0" encoding="utf-8"?>
<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();
2.绘制未拖动时文字及其背景
2.1测量字体宽高
    /**
     * 测量文字的宽高
     *
     * @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();
    }
2.2绘制文字及背景
    @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);
    }
2.3 测量规则
    @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;
    }
3.拖动时显示的BadgeView
3.1处理点击拖拽事件
@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;
}
3.2getScrollParent
    /**
     * 递归获取父布局中是否含可滑动的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);
    }
3.3获取DragBadgeView的缓存Bitmap
/**
 * 获取TextView的缓存bitmap
 */
private void updateCacheBitmap() {
    //把BadgeView之前的Bitmap进行回收
    mBadgeView.recycleCacheBitmap();
    //获取当前DragBadgeView的Bitmap
    setDrawingCacheEnabled(true);
    Bitmap drawingCache = getDrawingCache();
    //创建副本并赋值
    mBadgeView.cacheBitmap = Bitmap.createBitmap(drawingCache);
    setDrawingCacheEnabled(false);
}
3.4初始化BadgeView点位信息
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;
}
3.5 MOVE事件时更新BadgeView
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);
}
3.6更新拖拽点,并重回BadgeView
public void updateDragPoint(float x, float y) {
    mDragPoint.set(x, y);
    //postInvalidate内部使用Handler进行更新,考虑有可能会多线程,使用postInvalidate.(postInvalidate也可以再主线程调用,此处就是)
    BadgeView.this.postInvalidate();
}
3.7贝塞尔扯断后的回收,回调工作
//消失
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);
    }
}
3.8消失后的动画
    /**
     * 消失后的动画
     *
     * @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);
                }
            }
        });
    }
}
4绘制BadgeView,绘制二阶贝塞尔
@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'
}

xml配置

<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

Demo下载

http://download.csdn.net/detail/fendoudebb/9919991

GitHub地址

https://github.com/fendoudebb/DragBadgeView

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值