Android小红点框架(同步更新桌面图标小红点) | 秋凉一梦

自己撸的一个小红点框架UPV, 使用至今没出什么问题


UPV特点:

    • 集成简单(一个Java类)
    • 自动关联APP图标未读数、根View、子View
    • 调用简单(API清晰)
    • 稳定

UPV关系图如下:

UPV_Relationship

调用方式:

1.绑定、解绑根View(Tag请自己设定, 关联子View使用)
UPVUtils.bindRootView(rootViewA, rootViewATag)
UPVUtils.unbindRootView(rootViewA, rootViewATag)
2.绑定、解绑子View(Tag请自己设定,比如targetId)
UPVUtils.bindChildView(rootViewATag, childViewA, childViewATag)
UPVUtils.unbindChildView(rootViewATag, childViewA)
3.小红点自增、清除
UPVUtils.addChildCount(rootViewATag, childViewATag)
UPVUtils.clearChildCount(rootViewATag, childViewATag)

UPVUtils代码如下:

import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;

import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * Desc: 小红点操作工具 根View自动随子View更新
 * Create by SunHang on 2018/10/23
 * Email: sunh@eetrust.com
 */
public class UPVUtils {
    private static WeakReference<Context> context;
    //子View集合
    private static Map<String, Map<UnreadPointView, String>> viewMap = new HashMap<>();
    //根View集合
    private static Map<UnreadPointView, String> rootMap = new HashMap<>();

    private static List<String> roots = new ArrayList<>();

    public static void init(WeakReference<Context> context) {
        UPVUtils.context = context;
    }

    /*
     * 绑定子View (根View标识, 子View, 子View标识)
     */
    public static void bindChildView(String root, UnreadPointView upv, String targetId) {
        Map<UnreadPointView, String> upvMap = viewMap.get(root);
        if (upvMap == null) {
            upvMap = new HashMap<>();
            viewMap.put(root, upvMap);
        }
        upvMap.put(upv, targetId);
        //绑定成功则立即显示未读数
        refreshView(root, targetId, getSpCount(root, targetId));
    }

    /*
     * 解绑子View
     */
    public static void unbindChildView(String root, UnreadPointView upv) {
        viewMap.get(root).remove(upv);
    }

    /*
     * 刷新子View
     */
    private static void refreshView(String root, String targetId, int unreadCount) {
        Map<UnreadPointView, String> upvMap = viewMap.get(root);
        if (upvMap == null) return;
        List<UnreadPointView> views = getTargetViews(upvMap, targetId);
        if (views == null || views.isEmpty()) return;
        for (int i = 0; i < views.size(); i++) {
            views.get(i).setUnreadCount(unreadCount);
        }
    }

    /*
     * 刷新根View
     */
    private static void refreshRootView(String root) {
        List<UnreadPointView> views = getTargetViews(rootMap, root);
        if (views == null || views.isEmpty()) return;
        for (UnreadPointView view : views) {
            view.setUnreadCount(getSpCount(root, root));
        }
        setDesktopUnreadCount();
    }

    /*
     * 根据标识获取View(一个标识可能绑定多个View)
     */
    private static List<UnreadPointView> getTargetViews(Map<UnreadPointView, String> map, String targetId) {
        List<UnreadPointView> views = new ArrayList<>();
        for (UnreadPointView key : map.keySet()) {
            if (map.get(key).equals(targetId)) {
                views.add(key);
            }
        }
        return views;
    }

    /*
     * 绑定根View (根View, 根标识)
     */
    public static void bindRootView(UnreadPointView upv, String root) {
        roots.add(root);
        rootMap.put(upv, root);
        int spCount = getSpCount(root, root);
        upv.setUnreadCount(spCount);
    }

    /*
     * 解绑根View
     */
    public static void unbindRootView(UnreadPointView upv, String root) {
        roots.remove(root);
        rootMap.remove(upv);
    }

    /*
     * 获取总的未读数(方便桌面显示)
     */
    private static void setDesktopUnreadCount() {
        int total = 0;
        for (String root : roots) {
            total += getSpCount(root, root);
        }
        showDesktopUnreadCount(total);
    }

    /*
     * 根据手机型号显示桌面小红点
     */
    private static void showDesktopUnreadCount(int total) {
        SystemType systemType = RomUtils.getSystemType();
        try {

            switch (systemType) {
                case EMUI:
                    Bundle bundle = new Bundle();
                    bundle.putString("package", context.get().getPackageName());
                    bundle.putString("class", Objects.requireNonNull(Objects.requireNonNull(context.get().getPackageManager().getLaunchIntentForPackage(context.get().getPackageName())).getComponent()).getClassName());
                    bundle.putInt("badgenumber", total);
                    context.get().getContentResolver().call(Uri.parse("content://com.huawei.android.launcher.settings/badge/"), "change_badge", null, bundle);
                    break;
                case MIUI:
                    Notification.Builder builder = new Notification.Builder(context.get());
                    Notification notification = builder.build();
                    Field field = notification.getClass().getDeclaredField("extraNotification");
                    Object extraNotification = field.get(notification);
                    Method method;
                    method = extraNotification.getClass().getDeclaredMethod("setMessageCount", int.class);
                    method.invoke(extraNotification, total);
                    break;
                case VIVO:
                    Intent intent = new Intent("launcher.action.CHANGE_APPLICATION_NOTIFICATION_NUM");
                    intent.putExtra("packageName", context.get().getPackageName());
                    intent.putExtra("className", Objects.requireNonNull(Objects.requireNonNull(context.get().getPackageManager().getLaunchIntentForPackage(context.get().getPackageName())).getComponent()).getClassName());
                    intent.putExtra("notificationNum", total);
                    context.get().sendBroadcast(intent);
                    break;
                default:
                    break;
            }
        } catch (NullPointerException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /*
     * 子View未读数自增
     */
    public static void addChildCount(String root, String targetId) {
        int spCount = getSpCount(root, targetId) + 1;
        int rootCount = getSpCount(root, root) + 1;
        setSpCount(root, targetId, spCount);
        setSpCount(root, root, rootCount);
        refreshView(root, targetId, spCount);
        refreshRootView(root);
    }

    /*
     * 清除子View未读数
     */
    public static void clearChildCount(String root, String targetId) {
        int spCount = getSpCount(root, targetId);
        int rootCount = getSpCount(root, root) - spCount;
        removeSpCount(root, targetId);
        if (rootCount <= 0) {
            //根未读数<=0, 从SP中删除
            removeSpCount(root, root);
        } else {
            //>0, 设置
            setSpCount(root, root, rootCount);
        }
        refreshView(root, targetId, 0);
        refreshRootView(root);
    }

    /* 清除整个View树的未读 */
    public static void clearTreeCount(String root) {
        Map<UnreadPointView, String> upvMap = viewMap.get(root);
        if (upvMap == null) return;
        List<String> targetIds = new ArrayList<>();
        for (UnreadPointView key : upvMap.keySet()) {
            String targetId = upvMap.get(key);
            if (targetId != null) {
                targetIds.add(targetId);
            }
        }
        for (String targetId : targetIds) {
            clearChildCount(root, targetId);
        }
        removeSpCount(root, root);
        refreshRootView(root);
    }

    private static void setSpCount(String root, String targetId, Integer count) {
        if (count == null || count < 0) {
            count = 0;
        }
        SharedPreferences.Editor edit = getSp(root).edit();
        edit.putInt(targetId, count);
        edit.apply();
    }

    private static int getSpCount(String root, String targetId) {
        return getSp(root).getInt(targetId, 0);
    }

    private static void removeSpCount(String root, String targetId) {
        SharedPreferences.Editor edit = getSp(root).edit();
        edit.remove(targetId);
        edit.apply();
    }

    private static SharedPreferences getSp(String root) {
        return context.get().getSharedPreferences(root, Context.MODE_PRIVATE);
    }
}

UnreadPointView代码如下(支持拖曳消除,也可以使用自己的View, 修改对应设置未读数量、清楚未读数量方法即可)

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.AnimationDrawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.ImageView;

/**
 * Desc:
 * Create by SunHang on 2018/10/23
 * Email: sunh@eetrust.com
 */
public class UnreadPointView extends View {
    Context       context;
    WindowManager windowManager;
    int           statusBarHeight;//状态栏高度,在Window中无法测量,需要从Activity中传入

    UnreadPointView UnreadPointViewInActivity; //Activity中的红点,window中的红点维持一个Activity中红点的引用
    UnreadPointView UnreadPointViewInWindow; //Window中的红点,Acitvity中的红点需要维持一个Window中红点的引用

    Paint dragDotPaint; //红点画笔
    Paint rubberPaint;//皮筋画笔
    Paint anchorDotPaint;//錨点画笔
    Paint messageCountPaint;//未读消息数的画笔

    public static final float bezierCircleConstant = 0.552284749831f; //用于贝塞尔曲线画圆时的常量值
    float mDistance; //贝塞尔曲线画圆时的控制线长度, = bezierCircleConstant*radius

    //用六个数据点,八个控制点画红点.n = 4;
    PointF upPointFLeft, upPointFRight, downPointFLeft, downPointRight, leftPointF, rightPointF; //数据点
    //八个控制点
    PointF upLeftPointF, upRightPointF, downLeftPointF, downRightPointF, leftUpPointF, leftDownPointF, rightUpPointF, rightDownPointF; //控制点
    Path   redDotPath;//红点的贝塞尔曲线path
    Path   rubberPath;//皮筋的贝塞尔曲线path
    PointF anchorPoint;//锚点的圆心点.固定不动的点,记录红点在Activty中初始化时的位置.

    boolean isInitFromLayout = true; //是否从布局文件中初始化,true表示从布局文件中初始化(Activity中的点).false表示从代码中初始化(Window中的点)
    int     unreadCount = 0; //未读的消息数
    RectF   dragDotRectF;//红点的范围矩阵,用于判断当前的touch事件是否击中了红点.

    //从xml中读取的红点的属性
    float dragDistance;//红点的可拖拽距离,超过这个距离,红点会消失;小于这个距离,红点会复位
    float dragDotRadius;//红点的半径
    float anchorDotRadius;//锚点的半径.在拖动过程中,它的数值会不断减小.
    int dotColor;//红点,皮筋和锚点的颜色
    int textColor;//未读消息的字体颜色
    int textSize;//未读消息的字体大小
    int dotStyle;//红点的style.0,实心点;1,可拖动;2,不可拖动
    int countStyle;//未读消息数的显示风格.0,准确显示;1,超过一定数值,就显示一个大概的数
    int countLimit;//消息数量的阈值,当超过这个数,并且显示方式设置为模糊,就开始模糊显示

    float  dotRealWidth;//红点真实的宽度
    float  dotRealHeight;//红点真实的高度
    PointF dragDotCenterPoint;//红点的中心点
    PointF dragDotLeftTopPoint;//红点的左上角的点
    float  widgetCenterXInWindow, widgetCenterYInWindow;//控件的中心点在Window中的位置.
    float initAnchorRadius;//锚点初始的半径, 与未变化前的anchorDotRadius值相等


    /**
     * 用固定的属性值初始化控件
     *
     * @param context
     */
    public UnreadPointView(Context context) {
        super(context);
        initAttribute(context);
        initTools(context);
        isInitFromLayout = false;
        windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    }

    public UnreadPointView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initAttribute(context, attrs);
        initTools(context);
        isInitFromLayout = true;
        windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    }

    public UnreadPointView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttribute(context, attrs);
        initTools(context);
        isInitFromLayout = true;
        windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    }

    public UnreadPointView(Context context, WindowManager windowManager, int statusBarHeight,
                           float widgetCenterXInWindow, float widgetCenterYInWindow,
                           float dragDistance, float dragDotRadius, float anchorDotRadius,
                           int dotColor, int textColor, int textSize, int dotStyle, int unreadCount,
                           int countStyle, int countLimit) {
        super(context);
        this.context = context;
        this.windowManager = windowManager;
        this.statusBarHeight = statusBarHeight;
        this.widgetCenterXInWindow = widgetCenterXInWindow;
        this.widgetCenterYInWindow = widgetCenterYInWindow;
        this.dragDistance = dragDistance;
        this.dragDotRadius = dragDotRadius;
        this.anchorDotRadius = anchorDotRadius;
        this.dotColor = dotColor;
        this.textColor = textColor;
        this.textSize = textSize;
        this.dotStyle = dotStyle;
        this.countStyle = countStyle;
        this.unreadCount = unreadCount;
        this.countLimit = countLimit;
        initTools(context);
        isInitFromLayout = false;
    }

    //用固定的属性值初始化各个属性
    private void initAttribute(Context context) {
        dragDotRadius = Utils.dp2px(context, 20);
        anchorDotRadius = Utils.dp2px(context, 16);
        dotColor = Color.RED;
        textColor = Color.WHITE;
        textSize = Utils.sp2px(context, 24);
        dotStyle = 2;
        countStyle = 1;
        countLimit = 99;
        dragDistance = Utils.dp2px(context, 150);
    }

    //从layout文件中读取配置,初始化控件
    private void initAttribute(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.UnreadPointView);
        //红点的半径
        dragDotRadius = typedArray.getDimensionPixelOffset(R.styleable.UnreadPointView_dragDotRadius, 20);
        //锚点的半径
        anchorDotRadius = typedArray.getDimensionPixelOffset(R.styleable.UnreadPointView_anchorDotRadius, 16);
        //红点的颜色
        dotColor = typedArray.getColor(R.styleable.UnreadPointView_dotColor, Color.RED);
        //消息字体的颜色
        textColor = typedArray.getColor(R.styleable.UnreadPointView_textColor, Color.WHITE);
        //消息字体的大小
        textSize = typedArray.getDimensionPixelSize(R.styleable.UnreadPointView_textSize, 24);
        //红点的风格.0,实心点;1,QQ红点,可拖动;2,普通红点,不可拖动
        dotStyle = typedArray.getInt(R.styleable.UnreadPointView_dotStyle, 2);
        //红点可拖动的距离.也是消失距离和复位距离;
        dragDistance = typedArray.getDimensionPixelOffset(R.styleable.UnreadPointView_dragDistance, 150);
        //未读消息数的显示风格
        countStyle = typedArray.getInt(R.styleable.UnreadPointView_countStyle, 1);
        //未读消息数的阈值
        countLimit = typedArray.getInt(R.styleable.UnreadPointView_countLimit, 99);
        //未读消息数
        unreadCount = typedArray.getInt(R.styleable.UnreadPointView_unreadCount, 0);
        typedArray.recycle();
    }

    private void initTools(Context context) {
        this.context = context;

        dragDotPaint = new Paint();
        dragDotPaint.setAntiAlias(true);
        dragDotPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        dragDotPaint.setStrokeWidth(1);
        dragDotPaint.setColor(dotColor);

        if (dotStyle == 1 || dotStyle == 2) {
            messageCountPaint = new Paint();
            messageCountPaint.setColor(textColor);
            messageCountPaint.setAntiAlias(true);
            messageCountPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            messageCountPaint.setStrokeWidth(1);
            messageCountPaint.setTextSize(textSize);

            mDistance = bezierCircleConstant * dragDotRadius;

            //六个数据点
            upPointFLeft = new PointF();
            downPointFLeft = new PointF();
            upPointFRight = new PointF();
            downPointRight = new PointF();
            leftPointF = new PointF();
            rightPointF = new PointF();
            //八个控制点
            upLeftPointF = new PointF();
            upRightPointF = new PointF();
            downLeftPointF = new PointF();
            downRightPointF = new PointF();
            leftUpPointF = new PointF();
            leftDownPointF = new PointF();
            rightUpPointF = new PointF();
            rightDownPointF = new PointF();
            //绘制红点的贝塞尔曲线
            redDotPath = new Path();

            //红点的范围矩阵
            dragDotRectF = new RectF();
            //红点的中心点
            dragDotCenterPoint = new PointF();
            //红点的左上角的点
            dragDotLeftTopPoint = new PointF();
            //锚点的圆心点
            anchorPoint = new PointF(); //dotStyle==2时,不需要锚点.但是为了书写方便,还是把它放在这里.
        }

        if (dotStyle == 1) {
            rubberPaint = new Paint();
            rubberPaint.setStrokeWidth(1);
            rubberPaint.setColor(dotColor);
            rubberPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            rubberPaint.setAntiAlias(true);

            anchorDotPaint = new Paint();
            anchorDotPaint.setColor(dotColor);
            anchorDotPaint.setStrokeWidth(1);
            anchorDotPaint.setAntiAlias(true);
            anchorDotPaint.setStyle(Paint.Style.FILL_AND_STROKE);

            //绘制皮筋的贝塞尔曲线
            rubberPath = new Path();
            //初始的锚点半径
            initAnchorRadius = anchorDotRadius;
        }

        //未读消息的阈值不能小于99
        countLimit = countLimit < 99 ? 99 : countLimit;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        float width, height;

        //红点的高度固定,但是宽度是根据未读消息数而变化的
        dotRealHeight = dragDotRadius * 2;
        if (dotStyle == 0) { //实心点,没有未读消息数
            dotRealWidth = dragDotRadius * 2;
        } else { //非实心点,带消息数
            // 应该根据countStyle,countLimit和textSize动态计算红点的宽度
            //但是那样有点麻烦,留给下一个版本吧
            if (unreadCount >= 0 && unreadCount < 10) { //消息数为个位数
                dotRealWidth = dragDotRadius * 2;
            } else if (unreadCount >= 10 && unreadCount <= countLimit) { // 消息数为两位数
                dotRealWidth = 12 * dragDotRadius / 5;
            } else { //消息数为三位数及以上
                dotRealWidth = 3 * dragDotRadius;
            }
        }

        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize < dotRealWidth ? dotRealWidth : widthSize; //必须保证可以完整容纳红点
        } else {
            width = dotRealWidth;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize < dragDotRadius * 2 ? dragDotRadius * 2 : heightSize; //必须保证可以完整容纳红点
        } else {
            height = dragDotRadius * 2;
        }

        setMeasuredDimension((int) width + 2, (int) height + 2); //宽高各加两个像素,防止红点的边缘被切掉
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        if (0 != dotStyle) {
            //计算红点和锚点的中心点位置,计算红点的数据点和控制点
            computePosition();
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (0 == dotStyle) { //实心红点,没有未读消息数
            canvas.drawCircle(getWidth() / 2.0f, getHeight() / 2.0f, dragDotRadius, dragDotPaint);
        } else if (2 == dotStyle) { //带消息数,但不可拖动
            if (unreadCount > 0) {
                drawDot(canvas); //只画红点和消息
            }
        } else { //显示未读消息数
            if (unreadCount > 0 && !isDismiss) {
                if (isdragable && isInPullScale && isNotExceedPullScale) {
                    drawRubber(canvas);
                    drawAnchorDot(canvas);
                }
                drawDot(canvas); //画红点
            }
        }
    }

    //绘制红点
    private void drawDot(Canvas canvas) {
        if (unreadCount > 0 && unreadCount <= 9) {
            canvas.drawCircle(dragDotCenterPoint.x, dragDotCenterPoint.y, dragDotRadius, dragDotPaint);
        } else if (unreadCount > 9) { //用贝塞尔取现画拉伸的红点
            redDotPath.reset();
            redDotPath.moveTo(upPointFLeft.x, upPointFLeft.y);
            redDotPath.lineTo(upPointFRight.x, upPointFRight.y);
            redDotPath.cubicTo(upRightPointF.x, upRightPointF.y, rightUpPointF.x, rightUpPointF.y, rightPointF.x, rightPointF.y);
            redDotPath.cubicTo(rightDownPointF.x, rightDownPointF.y, downRightPointF.x, downRightPointF.y, downPointRight.x, downPointRight.y);
            redDotPath.lineTo(downPointFLeft.x, downPointFLeft.y);
            redDotPath.cubicTo(downLeftPointF.x, downLeftPointF.y, leftDownPointF.x, leftDownPointF.y, leftPointF.x, leftPointF.y);
            redDotPath.cubicTo(leftUpPointF.x, leftUpPointF.y, upLeftPointF.x, upLeftPointF.y, upPointFLeft.x, upPointFLeft.y);
            canvas.drawPath(redDotPath, dragDotPaint);
        }

        drawMsgCount(canvas);
    }

    //绘制红点中的消息数量文字
    private void drawMsgCount(Canvas canvas) {
        String count = "";
        if (unreadCount > 0 && unreadCount <= countLimit) {
            count = String.valueOf(unreadCount);
        } else if (unreadCount > countLimit) {
            if (0 == countStyle) { //准确显示数目
                count = String.valueOf(unreadCount);
            } else { //模糊显示
                count = String.valueOf(countLimit) + "+";
            }
        }
        if (!TextUtils.isEmpty(count)) {
            int countWidth = Utils.computeStringWidth(messageCountPaint, count);
            int countHeight = Utils.computeStringHeight(messageCountPaint, count);
            canvas.drawText(count, dragDotCenterPoint.x - countWidth / 2, dragDotCenterPoint.y + countHeight / 2, messageCountPaint);
        }
    }

    /**
     * 拖拽时,绘制一个锚点
     *
     * @param canvas
     */
    private void drawAnchorDot(Canvas canvas) {
        canvas.drawCircle(anchorPoint.x, anchorPoint.y, anchorDotRadius, anchorDotPaint);
    }

    /**
     * 拖拽时,绘制一条橡皮筋,连接红点与锚点
     *
     * @param canvas
     */
    private void drawRubber(Canvas canvas) {
        PointF[] pointFs       = MathUtils.getTangentPoint(anchorPoint.x, anchorPoint.y, anchorDotRadius, moveX, moveY, dragDotRadius);
        PointF   controlPointF = MathUtils.getMiddlePoint(anchorPoint.x, anchorPoint.y, moveX, moveY);
        //利用贝塞尔取现画出皮筋
        rubberPath.reset();
        rubberPath.moveTo(anchorPoint.x, anchorPoint.y);
        rubberPath.lineTo(pointFs[0].x, pointFs[0].y);
        rubberPath.quadTo(controlPointF.x, controlPointF.y, pointFs[1].x, pointFs[1].y);
        rubberPath.lineTo(moveX, moveY);
        rubberPath.lineTo(pointFs[2].x, pointFs[2].y);
        rubberPath.quadTo(controlPointF.x, controlPointF.y, pointFs[3].x, pointFs[3].y);
        rubberPath.lineTo(anchorPoint.x, anchorPoint.y);

        canvas.drawPath(rubberPath, rubberPaint);
    }

    float downX, downY, moveX, moveY, upX, upY;
    boolean isdragable = false;//是否可拖拽
    boolean isInPullScale = true; //是否在拉力范围内
    boolean isDismiss = false;//是否应该dismiss小红点
    boolean isNotExceedPullScale = true;//是否未曾脱离拉力范围.false表示已经至少脱离过拉力范围一次;true表示尚未脱离过拉力范围

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (1 != dotStyle) { //只有dotStyle=1时,才可以拖动
            return super.onTouchEvent(event);
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!isInitFromLayout) { //从代码中实例化View
                    downX = event.getRawX();
                    downY = event.getRawY() - getStatusBarHeight(); //校正坐标

                    //在这里,dragDotRectF还没有被赋值
                    isdragable = false;
                } else { //从xml中实例化View
                    downX = event.getX();
                    downY = event.getY();
                    if (dragDotRectF.contains(downX, downY)) { //击中了红点,允许拖动
                        isdragable = true;
                        //在这里会遇到一个坑,getLocationInWindow返回的坐标与getLocationOnScreen返回的坐标相同
                        //这会导致计算错误,引起后续一系列问题.Android坑真多,被这个问题困了很久.参考下面两个答案
                        //http://stackoverflow.com/questions/17672891/getlocationonscreen-vs-getlocationinwindow
                        //http://stackoverflow.com/questions/2638342/incorrect-coordinates-from-getlocationonscreen-getlocationinwindow
                        int[] locationInScreen = new int[2]; //控件在当前屏幕中的坐标
                        getLocationOnScreen(locationInScreen);
                        setStatusBarHeight(Utils.getStatusBarHeight(this));
                        UnreadPointViewInWindow = new UnreadPointView.Builder().setAnchorDotRadius(this.anchorDotRadius)
                                .setDotColor(this.dotColor).setDotStyle(this.dotStyle).setDragDotRadius(this.dragDotRadius)
                                .setWindowManager(this.windowManager).setContext(this.context).setCountStyle(this.countStyle)
                                .setDragDistance(this.dragDistance).setUnreadCount(this.unreadCount).setTextSize(this.textSize)
                                .setTextColor(this.textColor).setStatusBarHeight(this.statusBarHeight).setcountLimit(this.countLimit)
                                .setWidgetCenterXInWindow(locationInScreen[0] + getWidth() / 2f)
                                .setwidgetCenterYInWindow(locationInScreen[1] + getHeight() / 2f - getStatusBarHeight())
                                .create();
                        UnreadPointViewInWindow.setUnreadPointViewInActivity(this);

                        addUnreadPointViewToWindow(UnreadPointViewInWindow); //添加到window中
                        this.setVisibility(GONE);//隐藏Activity中的红点

                        //红点开始拖动时的监听
                        if (null != onDragStartListener) {
                            onDragStartListener.OnDragStart();
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (!isInitFromLayout) {
                    //拖动的时候,再允许红点拉伸
                    isdragable = true;

                    moveX = event.getRawX();
                    moveY = event.getRawY() - getStatusBarHeight();//把坐标校正成Window中坐标
                    if (MathUtils.getDistanceBetweenPoints(moveX, moveY, anchorPoint.x, anchorPoint.y) <= dragDistance) {
                        isInPullScale = true;
                        updateAnchorDotRadius(moveX, moveY);
                    } else {
                        isNotExceedPullScale = false;
                        isInPullScale = false;
                    }
                    computePosition(moveX, moveY);
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                if (!isInitFromLayout) {
                    if (isdragable && isInPullScale) { //在拉力范围内,要使红点复位
                        upX = event.getRawX();
                        upY = event.getRawY() - getStatusBarHeight();//校正坐标
                        if (isNotExceedPullScale) { //未曾脱离过拉力范围(橡皮筋还没有断),复位时,要有一个橡皮筋效果
                            animatorBackToAnchorPoint(upX, upY);
                        } else { //曾经脱离过拉力范围(橡皮筋已断),复位时,直接回到原始点
                            simpleBackToAnchorPoint(upX, upY);
                        }
                    } else if (isdragable && !isInPullScale) { //超过拉力范围,播放消失动画
                        upX = event.getRawX();
                        upY = event.getRawY() - getStatusBarHeight();//校正坐标

                        //消失
                        isDismiss = true;
                        invalidate();
                        animationDismiss(upX, upY);

                        //红点消失时的监听
                        if (null != getUnreadPointViewInActivity().getOnDotDismissListener()) {
                            getUnreadPointViewInActivity().getOnDotDismissListener().OnDotDismiss();
                        }
                    }
                }
                break;
            default:
                break;
        }
        //虽然UnreadPointViewInActivity会被隐藏,但是所有的Touch事件仍会被传递给
        //UnreadPointViewInActivity,所以需要在UnreadPointViewInActivity的onTouchEvent()函数中
        //把Touch事件传给UnreadPointViewInWindow
        if (UnreadPointViewInWindow != null) {
            UnreadPointViewInWindow.dispatchTouchEvent(event);
        }
        return true;
    }

    //拖拽过程中更新锚点半径.拖拽时,锚点的半径会逐渐变小.
    private void updateAnchorDotRadius(float moveX, float moveY) {
        float distance = MathUtils.getDistanceBetweenPoints(moveX, moveY, anchorPoint.x, anchorPoint.y);
        anchorDotRadius = initAnchorRadius - (distance / dragDistance) * (initAnchorRadius - 5);
    }

    /**
     * 获取控件的中心点在window中的坐标
     *
     * @return
     */
    public float getWidgetCenterXInWindow() {
        return widgetCenterXInWindow;
    }

    /**
     * 获取控件的中心点在window中的坐标
     *
     * @return
     */
    public float getWidgetCenterYInWindow() {
        return widgetCenterYInWindow;
    }

    /**
     * 计算红点数据点和控制点
     * 红点位于控件的中心点位置,红点的中心点位置与控件的中心点位置重合.
     */
    private void computePosition() {
        if (isInitFromLayout) {
            anchorPoint.set(getWidth() / 2.0f, getHeight() / 2.0f);//保存锚点的位置
            computePosition(getWidth() / 2.0f, getHeight() / 2.0f);
        } else {
            anchorPoint.set(getWidgetCenterXInWindow(), getWidgetCenterYInWindow());//保存锚点的位置,因为锚点的位置是固定不变的,所以不能随着的touch事件更新,故放在这里初始化
            computePosition(getWidgetCenterXInWindow(), getWidgetCenterYInWindow());
        }
    }

    /**
     * 根据中心点的位置,计算红点的数据点和控制点
     *
     * @param centerX
     * @param centerY
     */
    private void computePosition(float centerX, float centerY) {
        dragDotCenterPoint.set(centerX, centerY);//保存中心点位置
        dragDotLeftTopPoint = center2LeftTop(dragDotCenterPoint);//保存左上角的位置
        // 根据countStyle和countLimit
        if (unreadCount > 0 && unreadCount <= 9) {
            dragDotRectF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y, dragDotLeftTopPoint.x + dragDotRadius * 2, dragDotLeftTopPoint.y + dragDotRadius * 2);
        } else if (unreadCount > 9 && unreadCount <= countLimit) {
            dragDotRectF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y, dragDotLeftTopPoint.x + dragDotRadius * 12 / 5, dragDotLeftTopPoint.y + dragDotRadius * 2);
            computeRedDotBezierPoint(12 * dragDotRadius / 5, 2 * dragDotRadius);
        } else if (unreadCount > countLimit) {
            dragDotRectF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y, dragDotLeftTopPoint.x + dragDotRadius * 3, dragDotLeftTopPoint.y + dragDotRadius * 2);
            computeRedDotBezierPoint(3 * dragDotRadius, 2 * dragDotRadius);
        }
    }

    /**
     * 计算红点的数据点和控制点
     *
     * @param width  红点的实际宽度
     * @param height 红点的实际高度
     */
    private void computeRedDotBezierPoint(float width, float height) {
        //数据点
        upPointFLeft.set(dragDotLeftTopPoint.x + dragDotRadius, dragDotLeftTopPoint.y);
        leftPointF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y + dragDotRadius);
        downPointFLeft.set(dragDotLeftTopPoint.x + dragDotRadius, dragDotLeftTopPoint.y + height);

        upPointFRight.set(dragDotLeftTopPoint.x + width - dragDotRadius, dragDotLeftTopPoint.y);
        rightPointF.set(dragDotLeftTopPoint.x + width, dragDotLeftTopPoint.y + dragDotRadius);
        downPointRight.set(dragDotLeftTopPoint.x + width - dragDotRadius, dragDotLeftTopPoint.y + height);

        //控制点
        upLeftPointF.set(dragDotLeftTopPoint.x + dragDotRadius - mDistance, dragDotLeftTopPoint.y);
        upRightPointF.set(dragDotLeftTopPoint.x + width - dragDotRadius + mDistance, dragDotLeftTopPoint.y);
        downLeftPointF.set(dragDotLeftTopPoint.x + dragDotRadius - mDistance, dragDotLeftTopPoint.y + height);
        downRightPointF.set(dragDotLeftTopPoint.x + width - dragDotRadius + mDistance, dragDotLeftTopPoint.y + height);
        leftUpPointF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y + dragDotRadius - mDistance);
        leftDownPointF.set(dragDotLeftTopPoint.x, dragDotLeftTopPoint.y + dragDotRadius + mDistance);
        rightUpPointF.set(dragDotLeftTopPoint.x + width, dragDotLeftTopPoint.y + dragDotRadius - mDistance);
        rightDownPointF.set(dragDotLeftTopPoint.x + width, dragDotLeftTopPoint.y + dragDotRadius + mDistance);
    }

    /**
     * 根据中间点的坐标,计算出红点实际左上角点的坐标
     * 注意,不是控件左上角.控件是大于等于红点的
     *
     * @param centerPointF
     * @return
     */
    private PointF center2LeftTop(PointF centerPointF) {
        PointF leftTopPointF = new PointF();
        if (unreadCount >= 0 && unreadCount < 10) {
            leftTopPointF.set(centerPointF.x - dragDotRadius, centerPointF.y - dragDotRadius);
        } else if (unreadCount >= 10 && unreadCount <= countLimit) {
            leftTopPointF.set(centerPointF.x - 6 * dragDotRadius / 5, centerPointF.y - dragDotRadius);
        } else {
            // 这里需要根据countStyle的值定制
            leftTopPointF.set(centerPointF.x - 3 * dragDotRadius / 2, centerPointF.y - dragDotRadius);
        }
        return leftTopPointF;
    }

    //红点中心点坐标转换为左上角坐标
    private float centerX2StartX(float centerX) {
        float startX;
        if (unreadCount >= 0 && unreadCount < 10) {
            startX = centerX - dragDotRadius;
        } else if (unreadCount >= 10 && unreadCount <= countLimit) {
            startX = centerX - 6 * dragDotRadius / 5;
        } else {
            startX = centerX - 3 * dragDotRadius / 2;
        }
        return startX;
    }

    //红点中心点坐标转换为左上角坐标
    private float centerY2StartY(float centerY) {
        return centerY - dragDotRadius;
    }

    //小红点的消失动画
    private void animationDismiss(float upX, float upY) {
        final ImageView imageView = new ImageView(context);
        imageView.setImageResource(R.drawable.dismiss_anim);
        final AnimationDrawable    animationDrawable = (AnimationDrawable) imageView.getDrawable();
        long                       duration          = 500;
        int                        width             = imageView.getDrawable().getIntrinsicWidth();
        int                        height            = imageView.getDrawable().getIntrinsicHeight();
        WindowManager.LayoutParams layoutParams      = new WindowManager.LayoutParams();
        layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
        layoutParams.format = PixelFormat.RGBA_8888;
        layoutParams.x = (int) (upX - width / 2);
        layoutParams.y = (int) (upY - height / 2);
        layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        windowManager.addView(imageView, layoutParams);
        animationDrawable.start();
        imageView.postDelayed(new Runnable() {
            @Override
            public void run() {
                //更新未读消息数为0
                getUnreadPointViewInActivity().setUnreadCount(0);

                animationDrawable.stop();
                imageView.clearAnimation();
                windowManager.removeView(imageView);
                resetStatus();
                getUnreadPointViewInActivity().setVisibility(VISIBLE);
                getUnreadPointViewInActivity().resetStatus();
                removeUnreadPointViewToWindow();
            }
        }, duration);
    }

    //简单的复位动画,没有回弹效果.
    private void simpleBackToAnchorPoint(final float upX, final float upY) {
        ValueAnimator animatorX = ValueAnimator.ofFloat(upX, anchorPoint.x);
        animatorX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                float currentX = (float) animation.getAnimatedValue();
                float currentY = (anchorPoint.y - upY) * fraction + upY;
                moveX = currentX; //这时皮筋已断,已经不会再绘制皮筋了,所以其实可以取消对moveX和moveY的赋值操作
                moveY = currentY;
                computePosition(currentX, currentY);
                invalidate();
            }
        });
        animatorX.addListener(animatorListener);
        //不需要回弹效果,直接使用线性插值器
        animatorX.setInterpolator(new LinearInterpolator());
        animatorX.setDuration(200);
        animatorX.start();
    }

    //回到初始位置,带有回弹效果
    private void animatorBackToAnchorPoint(final float upX, final float upY) {
        ValueAnimator animatorX = ValueAnimator.ofFloat(upX, anchorPoint.x);
        animatorX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                float currentX = (float) animation.getAnimatedValue();
                float currentY = (anchorPoint.y - upY) * fraction + upY;
                moveX = currentX; //画皮筋时要用到moveX和moveY
                moveY = currentY;
                computePosition(currentX, currentY);
                invalidate();
            }
        });
        animatorX.addListener(animatorListener);
        // 这个回弹效果不够机智,将来自顶一个Interpolator优化一下
        animatorX.setInterpolator(new OvershootInterpolator(4.0f));
        //animatorX.setInterpolator(new BounceInterpolator()); //这个回弹效果不太好用
        animatorX.setDuration(500);
        animatorX.start();
    }

    //红点复位动画的监听器
    Animator.AnimatorListener animatorListener = new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {
            resetStatus();
            getUnreadPointViewInActivity().setVisibility(VISIBLE);
            getUnreadPointViewInActivity().resetStatus();

            //红点复位时的监听
            if (null != getUnreadPointViewInActivity().getOnDotResetListener()) {
                getUnreadPointViewInActivity().getOnDotResetListener().OnDotReset();
            }

            removeUnreadPointViewToWindow();
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    };

    //重置状态值
    public void resetStatus() {
        isdragable = false;
        isInPullScale = true;
        isNotExceedPullScale = true;
        isDismiss = false;
    }

    //添加红点到WindowManager
    public void addUnreadPointViewToWindow(UnreadPointView UnreadPointView) {
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.format = PixelFormat.RGBA_8888;
//        layoutParams.format = PixelFormat.TRANSLUCENT;
        layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
        layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;
        layoutParams.x = 0;
        layoutParams.y = 0;
        windowManager.addView(UnreadPointView, layoutParams);
    }

    //从Window中把红点移除
    public void removeUnreadPointViewToWindow() {
        windowManager.removeView(this);
    }

    /**
     * 保存对Activity中红点的引用
     *
     * @param UnreadPointView
     */
    public void setUnreadPointViewInActivity(UnreadPointView UnreadPointView) {
        this.UnreadPointViewInActivity = UnreadPointView;
    }

    /**
     * 获取Activity中的红点对象的引用
     *
     * @return
     */
    public UnreadPointView getUnreadPointViewInActivity() {
        return this.UnreadPointViewInActivity;
    }

    /**
     * 获取状态栏的高度
     *
     * @return
     */
    public int getStatusBarHeight() {
        return statusBarHeight;
    }

    /**
     * 设置状态栏的高度
     *
     * @param statusBarHeight
     */
    public void setStatusBarHeight(int statusBarHeight) {
        this.statusBarHeight = statusBarHeight;
    }

    /**
     * 更新未读消息的数量
     * 参考:http://blog.csdn.net/yanbober/article/details/46128379
     */
    public void setUnreadCount(int unreadCount) {
        int lastCount = this.unreadCount;
        this.unreadCount = unreadCount;
        if (0 == lastCount) {
            requestLayout();
            invalidate();
        } else if (lastCount > 0 && lastCount < 10) {
            if (unreadCount < 10) {
                invalidate();
            } else {
                requestLayout();
                invalidate();
            }
        } else if (lastCount >= 10 && lastCount <= countLimit) {
            if (unreadCount < 10) {
                requestLayout();
                invalidate();
            } else if (unreadCount >= 10 && unreadCount <= countLimit) {
                invalidate();
            } else if (unreadCount > countLimit) {
                requestLayout();
                invalidate();
            }
        } else if (lastCount > countLimit) {
            if (unreadCount > countLimit) {
                invalidate();
            } else {
                requestLayout();
                invalidate();
            }
        }
    }

    /**
     * 返回当前未读消息数量
     *
     * @return
     */
    public int getUnreadCount() {
        return unreadCount;
    }

    /**
     * 开始拖动红点的监听
     */
    public interface OnDragStartListener {
        void OnDragStart();
    }

    OnDragStartListener onDragStartListener;

    public void setOnDragStartListener(OnDragStartListener onDragStartListener) {
        this.onDragStartListener = onDragStartListener;
    }

    public OnDragStartListener getOnDragStartListener() {
        return onDragStartListener;
    }

    /**
     * 红点消失的监听
     */
    public interface OnDotDismissListener {
        void OnDotDismiss();
    }

    OnDotDismissListener onDotDismissListener;

    public void setOnDotDismissListener(OnDotDismissListener onDotDismissListener) {
        this.onDotDismissListener = onDotDismissListener;
    }

    public OnDotDismissListener getOnDotDismissListener() {
        return onDotDismissListener;
    }

    /**
     * 红点复位时的监听
     */
    public interface OnDotResetListener {
        void OnDotReset();
    }

    OnDotResetListener onDotResetListener;

    public void setOnDotResetListener(OnDotResetListener onDotResetListener) {
        this.onDotResetListener = onDotResetListener;
    }

    public OnDotResetListener getOnDotResetListener() {
        return onDotResetListener;
    }

    /**
     * 使用builder模式初始化控件
     * 从代码中初始化控件时调用
     */
    public static class Builder {
        private Context       context;
        private WindowManager windowManager;//在window中
        private int           statusBarHeight;//状态栏高度,在Window中无法测量,需要从Activity中传入
        private float         widgetCenterXInWindow;//控件的中心点在Window中的位置.
        private float         widgetCenterYInWindow;//控件的中心点在Window中的位置.

        private float dragDistance;//红点的可拖拽距离,超过这个距离,红点会消失;小于这个距离,红点会复位
        private float dragDotRadius;//红点的半径
        private float anchorDotRadius;//锚点的半径.在拖动过程中,它的数值会不断减小.
        private int dotColor;//红点,皮筋和锚点的颜色
        private int textColor;//未读消息的字体颜色
        private int textSize;//未读消息的字体大小
        private int dotStyle;//红点的style.0,实心点;1,可拖动;2,不可拖动
        private int countStyle;//未读消息数的显示风格.0,准确显示;1,超过一定数值,就显示一个大概的数
        private int countLimit;//未读消息数的阈值

        private int unreadCount;//未读消息数

        public Builder() {
        }

        public Builder setAnchorDotRadius(float anchorDotRadius) {
            this.anchorDotRadius = anchorDotRadius;
            return this;
        }

        public Builder setContext(Context context) {
            this.context = context;
            return this;
        }

        public Builder setDotColor(int dotColor) {
            this.dotColor = dotColor;
            return this;
        }

        public Builder setDotStyle(int dotStyle) {
            this.dotStyle = dotStyle;
            return this;
        }

        public Builder setDragDistance(float dragDistance) {
            this.dragDistance = dragDistance;
            return this;
        }

        public Builder setDragDotRadius(float dragDotRadius) {
            this.dragDotRadius = dragDotRadius;
            return this;
        }

        public Builder setStatusBarHeight(int statusBarHeight) {
            this.statusBarHeight = statusBarHeight;
            return this;
        }

        public Builder setWidgetCenterXInWindow(float widgetCenterXInWindow) {
            this.widgetCenterXInWindow = widgetCenterXInWindow;
            return this;
        }

        public Builder setwidgetCenterYInWindow(float widgetCenterYInWindow) {
            this.widgetCenterYInWindow = widgetCenterYInWindow;
            return this;
        }

        public Builder setTextColor(int textColor) {
            this.textColor = textColor;
            return this;
        }

        public Builder setTextSize(int textSize) {
            this.textSize = textSize;
            return this;
        }

        public Builder setcountLimit(int countLimit) {
            this.countLimit = countLimit;
            return this;
        }

        public Builder setWindowManager(WindowManager windowManager) {
            this.windowManager = windowManager;
            return this;
        }

        public Builder setUnreadCount(int unreadCount) {
            this.unreadCount = unreadCount;
            return this;
        }

        public Builder setCountStyle(int countStyle) {
            this.countStyle = countStyle;
            return this;
        }

        public UnreadPointView create() {
            return new UnreadPointView(context, windowManager, statusBarHeight,
                    widgetCenterXInWindow, widgetCenterYInWindow,
                    dragDistance, dragDotRadius, anchorDotRadius, dotColor, textColor, textSize,
                    dotStyle, unreadCount, countStyle, countLimit);
        }
    }
}

来源URL:http://www.anusha.cn/index.php/2018/12/05/android_unread_point_frame/