Android - 未读数红点View

1.DropFake

/**
 * 未读数红点View(自绘红色的圆和数字)
 * 触摸之产生DOWN/MOVE/UP事件(不允许父容器处理TouchEvent),回调给浮在上层的DropCover进行拖拽过程绘制。
 * View启动过程:Constructors -> onAttachedToWindow -> onMeasure() -> onSizeChanged() -> onLayout() -> onDraw()
 * <p>
 */
public class DropFake extends View {

  private int radius; // 圆形半径
  private float circleX; // 圆心x坐标
  private float circleY; // 圆心y坐标
  private String text; // 要显示的文本(数字)
  private boolean firstInit = true; // params init once
  private ITouchListener touchListener;

  public DropFake(Context context, AttributeSet attrs) {
    super(context, attrs);

    DropManager.getInstance().initPaint();
  }

  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (firstInit) {
      firstInit = false;
      //            radius = DropManager.CIRCLE_RADIUS; // 或者view.getWidth()/2
      radius = getWidth() / 2; // 或者view.getWidth()/2
      circleX = w / 2;
      circleY = h / 2;
    }
  }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // circle
    canvas.drawCircle(circleX, circleY, radius, DropManager.getInstance().getCirclePaint());
    // text
    if (!TextUtils.isEmpty(text)) {
      canvas.drawText(text, circleX, circleY + DropManager.getInstance().getTextYOffset(),
          DropManager.getInstance().getTextPaint());
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // 如果未初始化 DropManager,则默认任何事件都不处理
    if (!DropManager.getInstance().isEnable()) {
      return super.onTouchEvent(event);
    }
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        if (DropManager.getInstance().isTouchable()) {
          if (touchListener != null) {
            DropManager.getInstance().setTouchable(false);
            // 不允许父控件处理TouchEvent,当父控件为ListView这种本身可滑动的控件时必须要控制
            disallowInterceptTouchEvent(true);
            touchListener.onDown();
          }
          return true; // eat
        }

        return false;
      case MotionEvent.ACTION_MOVE:
        if (touchListener != null) {
          // getRaw:获取手指当前所处的相对整个屏幕的坐标
          touchListener.onMove(event.getRawX(), event.getRawY());
        }
        break;
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_CANCEL:
        if (touchListener != null) {
          // 将控制权还给父控件
          disallowInterceptTouchEvent(false);
          touchListener.onUp();
        }
        break;
      default:
        break;
    }
    return super.onTouchEvent(event);
  }

  public String getText() {
    return text;
  }

  public void setText(String text) {
    this.text = text;
    invalidate();
  }

  public void setTouchListener(ITouchListener listener) {
    touchListener = listener;
  }

  private void disallowInterceptTouchEvent(boolean disable) {
    ViewGroup parent = (ViewGroup) getParent();
    parent.requestDisallowInterceptTouchEvent(disable);

    while (true) {
      if (parent == null) {
        return;
      }

      if (parent instanceof RecyclerView || parent instanceof ListView || parent instanceof GridView ||
          parent instanceof ScrollView) {
        parent.requestDisallowInterceptTouchEvent(disable);
        return;
      }

      ViewParent vp = parent.getParent();
      if (vp instanceof ViewGroup) {
        parent = (ViewGroup) parent.getParent();
      } else {
        return; // DecorView
      }
    }
  }

  /**
   * 未读数红点检测触摸事件产生DOWN/MOVE/UP
   */
  public interface ITouchListener {

    void onDown();

    void onMove(float curX, float curY);

    void onUp();

  }

}

2.DropManager

/**
 */
public class DropManager {

  static final int TEXT_SIZE = ViewHelper.dp2px(12); // 12sp
  static final int CIRCLE_RADIUS = ViewHelper.dp2px(10); // 10dip
  // constant
  private static final String TAG = "DropManager";
  // single instance
  private static DropManager instance;
  // field
  private boolean isTouchable; // 是否响应按键事件,如果一个红点已经在响应,其它红点就不响应,同一界面始终最多只有一个红点响应触摸
  private int statusBarHeight; // 状态栏(通知栏)高度
  private DropCover dropCover; // Drop全屏动画
  private Object currentId; // 当前正在执行动画的业务节点
  private TextPaint textPaint; // 文本画笔共享
  private float textYOffset; // 文本y轴居中需要的offset
  private Paint circlePaint; // 圆形画笔共享
  private IDropListener listener; // 红点拖拽动画监听器
  private boolean enable;
  private int[] explosionResIds = new int[] {
      R.drawable.nim_explosion_one,
      R.drawable.nim_explosion_two,
      R.drawable.nim_explosion_three,
      R.drawable.nim_explosion_four,
      R.drawable.nim_explosion_five
  };

  public static synchronized DropManager getInstance() {
    if (instance == null) {
      instance = new DropManager();
    }

    return instance;
  }

  // interface
  public void init(Context context, DropCover dropCover, DropCover.IDropCompletedListener listener) {
    this.isTouchable = true;
    this.statusBarHeight = BarUtils.getStatusBarHeight();
    this.dropCover = dropCover;
    this.dropCover.addDropCompletedListener(listener);
    this.listener = null;
    this.enable = true;
  }

  public void initPaint() {
    getCirclePaint();
    getTextPaint();
  }

  public void destroy() {
    this.isTouchable = false;
    this.statusBarHeight = 0;
    if (this.dropCover != null) {
      this.dropCover.removeAllDropCompletedListeners();
      this.dropCover = null;
    }
    this.currentId = null;
    this.textPaint = null;
    this.textYOffset = 0;
    this.circlePaint = null;
    this.enable = false;
  }

  public boolean isEnable() {
    return enable;
  }

  public boolean isTouchable() {
    if (!enable) {
      return true;
    }
    return isTouchable;
  }

  public void setTouchable(boolean isTouchable) {
    this.isTouchable = isTouchable;

    if (listener != null) {
      if (!isTouchable) {
        listener.onDropBegin(); // touchable = false
      } else {
        listener.onDropEnd(); // touchable = true
      }
    }
  }

  public int getTop() {
    return statusBarHeight;
  }

  public void down(View fakeView, String text) {
    if (dropCover == null) {
      return;
    }

    dropCover.down(fakeView, text);
  }

  public void move(float curX, float curY) {
    if (dropCover == null) {
      return;
    }

    dropCover.move(curX, curY);
  }

  public void up() {
    if (dropCover == null) {
      return;
    }

    dropCover.up();
  }

  public void addDropCompletedListener(DropCover.IDropCompletedListener listener) {
    if (dropCover != null) {
      dropCover.addDropCompletedListener(listener);
    }
  }

  public void removeDropCompletedListener(DropCover.IDropCompletedListener listener) {
    if (dropCover != null) {
      dropCover.removeDropCompletedListener(listener);
    }
  }

  public Object getCurrentId() {
    return currentId;
  }

  public void setCurrentId(Object currentId) {
    this.currentId = currentId;
  }

  public Paint getCirclePaint() {
    if (circlePaint == null) {
      circlePaint = new Paint();
      circlePaint.setColor(AFrame.getContext().getResources().getColor(R.color.unread_red));
      circlePaint.setAntiAlias(true);
    }

    return circlePaint;
  }

  public TextPaint getTextPaint() {
    if (textPaint == null) {
      textPaint = new TextPaint();
      textPaint.setAntiAlias(true);
      textPaint.setColor(Color.WHITE);
      textPaint.setTextAlign(Paint.Align.CENTER);
      textPaint.setTextSize(TEXT_SIZE);
      Paint.FontMetrics textFontMetrics = textPaint.getFontMetrics();

      /*
       * drawText从baseline开始,baseline的值为0,baseline的上面为负值,baseline的下面为正值,
       * 即这里ascent为负值,descent为正值。
       * 比如ascent为-20,descent为5,那需要移动的距离就是20 - (20 + 5)/ 2
       */
      textYOffset = -textFontMetrics.ascent - (-textFontMetrics.ascent + textFontMetrics.descent) / 2;
    }

    return textPaint;
  }

  public float getTextYOffset() {
    getTextPaint();
    return textYOffset;
  }

  public int[] getExplosionResIds() {
    return explosionResIds;
  }

  public void setDropListener(IDropListener listener) {
    this.listener = listener;
  }

  public interface IDropListener {

    void onDropBegin();

    void onDropEnd();

  }

}

3.DropCover

/**
 * 悬浮在屏幕上的红点拖拽动画绘制区域
 * <p>
 */
public class DropCover extends View {

  private static final int SHAKE_ANIM_DURATION = 150; // 抖动动画执行的时间
  private static final int EXPLOSION_ANIM_FRAME_INTERVAL = 50; // 爆裂动画帧之间的间隔
  private static final int CLICK_DISTANCE_LIMIT = ViewHelper.dp2px(15); // 不超过此距离视为点击
  private static final int CLICK_DELTA_TIME_LIMIT = 10; // 超过此时长需要爆裂
  private final float MAX_RATIO = 0.8f; // 固定圆最大的缩放比例
  private final float MIN_RATIO = 0.4f; // 固定圆最小的缩放比例
  private final int DISTANCE_LIMIT = ViewHelper.dp2px(70); // 固定圆和移动圆的圆心之间的断裂距离
  private View dropFake;
  private Path path = new Path();
  private int radius; // 移动圆形半径
  private float curX; // 当前手指x坐标
  private float curY; // 当前手指y坐标
  private float circleX; // 固定圆的圆心x坐标
  private float circleY; // 固定圆的圆心y坐标
  private float ratio = 1; // 圆缩放的比例,随着手指的移动,固定的圆越来越小
  private boolean needDraw = true; // 是否需要执行onDraw方法
  private boolean hasBroken = false; // 是否已经断裂过,断裂过就不需要再画Path了
  private boolean isDistanceOverLimit = false; // 当前移动圆和固定圆的距离是否超过限值
  private boolean click = true; // 是否在点击的距离限制范围内,超过了clickDistance则不属于点击
  private long clickTime; // 记录down的时间点
  private String text; // 显示的数字
  private Bitmap[] explosionAnim; // 爆裂动画位图
  private boolean explosionAnimStart; // 爆裂动画是否开始
  private int explosionAnimNumber; // 爆裂动画帧的个数
  private int curExplosionAnimIndex; // 爆裂动画当前帧
  private int explosionAnimWidth; // 爆裂动画帧的宽度
  private int explosionAnimHeight; // 爆裂动画帧的高度
  private List<IDropCompletedListener> dropCompletedListeners; // 拖拽动作完成,回调

  /**
   * ************************* 绘制 *************************
   */
  public DropCover(Context context, AttributeSet attrs) {
    super(context, attrs);

    DropManager.getInstance().initPaint();
  }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // 绘制两个圆/Path/文本
    if (needDraw) {
      drawCore(canvas);
    }

    // 爆裂动画
    if (explosionAnimStart) {
      drawExplosionAnimation(canvas);
    }
  }

  private void drawCore(Canvas canvas) {
    if (!needDraw) {
      return;
    }

    final Paint circlePaint = DropManager.getInstance().getCirclePaint();
    // 画固定圆(如果已经断裂过了,就不需要画固定圆了)
    if (!hasBroken && !isDistanceOverLimit) {
      canvas.drawCircle(circleX, circleY, radius * ratio, circlePaint);
    }

    // 画移动圆和连线(如果已经断裂过了,就不需要再画Path了)
    if (curX != 0 && curY != 0) {
      canvas.drawCircle(curX, curY, radius, circlePaint);
      if (!hasBroken && !isDistanceOverLimit) {
        drawPath(canvas);
      }
    }

    // 数字要最后画,否则会被连线遮掩
    if (!TextUtils.isEmpty(text)) {
      final float textMove = DropManager.getInstance().getTextYOffset();
      final TextPaint textPaint = DropManager.getInstance().getTextPaint();
      if (curX != 0 && curY != 0) {
        // 移动圆里面的数字
        canvas.drawText(text, curX, curY + textMove, textPaint);
      } else {
        // 只有初始时需要绘制固定圆里面的数字
        canvas.drawText(text, circleX, circleY + textMove, textPaint);
      }
    }
  }

  /**
   * 画固定圆和移动圆之间的连线
   */
  private void drawPath(Canvas canvas) {
    path.reset();

    float distance = (float) distance(circleX, circleY, curX, curY); // 移动圆和固定圆圆心之间的距离
    float sina = (curY - circleY) / distance; // 移动圆圆心和固定圆圆心之间的连线与X轴相交形成的角度的sin值
    float cosa = (circleX - curX) / distance; // 移动圆圆心和固定圆圆心之间的连线与X轴相交形成的角度的cos值

    float AX = circleX - sina * radius * ratio;
    float AY = circleY - cosa * radius * ratio;
    float BX = circleX + sina * radius * ratio;
    float BY = circleY + cosa * radius * ratio;
    float OX = (circleX + curX) / 2;
    float OY = (circleY + curY) / 2;
    float CX = curX + sina * radius;
    float CY = curY + cosa * radius;
    float DX = curX - sina * radius;
    float DY = curY - cosa * radius;
    path.moveTo(AX, AY); // A点坐标
    path.lineTo(BX, BY); // AB连线
    path.quadTo(OX, OY, CX, CY); // 控制点为两个圆心的中间点,贝塞尔曲线,BC连线
    path.lineTo(DX, DY); // CD连线
    path.quadTo(OX, OY, AX, AY); // 控制点也是两个圆心的中间点,贝塞尔曲线,DA连线

    canvas.drawPath(path, DropManager.getInstance().getCirclePaint());
  }

  /**
   * ************************* TouchListener回调 *************************
   */
  public void down(View fakeView, String text) {
    this.needDraw = true; // 由于DropCover是公用的,每次进来时都要确保needDraw的值为true
    this.hasBroken = false; // 未断裂
    this.isDistanceOverLimit = false; // 当前移动圆和固定圆的距离是否超过限值
    this.click = true; // 点击开始

    this.dropFake = fakeView;
    int[] position = new int[2];
    dropFake.getLocationOnScreen(position);

    this.radius = DropManager.CIRCLE_RADIUS;
    // 固定圆圆心坐标,固定圆圆心坐标y,需要减去系统状态栏高度
    this.circleX = position[0] + dropFake.getWidth() / 2;
    this.circleY = position[1] - DropManager.getInstance().getTop() + dropFake.getHeight() / 2;
    // 移动圆圆心坐标
    this.curX = this.circleX;
    this.curY = this.circleY;

    this.text = text;
    this.clickTime = System.currentTimeMillis();

    // hide fake view, show current
    dropFake.setVisibility(View.INVISIBLE); // 隐藏固定范围的DropFake
    this.setVisibility(View.VISIBLE); // 当前全屏范围的DropCover可见

    invalidate();
  }

  public void move(float curX, float curY) {
    curY -= DropManager.getInstance().getTop(); // 位置校准,去掉通知栏高度

    this.curX = curX;
    this.curY = curY;

    calculateRatio((float) distance(curX, curY, circleX, circleY)); // 计算固定圆缩放的比例

    invalidate();
  }

  /**
   * 计算固定圆缩放的比例
   */
  private void calculateRatio(float distance) {
    if (isDistanceOverLimit = distance > DISTANCE_LIMIT) {
      hasBroken = true; // 已经断裂过了
    }

    // 固定圆缩放比例0.4-0.8之间
    ratio = MIN_RATIO + (MAX_RATIO - MIN_RATIO) * (1.0f * Math.max(DISTANCE_LIMIT - distance, 0)) / DISTANCE_LIMIT;
  }

  public void up() {
    boolean longClick = click && (System.currentTimeMillis() - this.clickTime > CLICK_DELTA_TIME_LIMIT); // 长按

    // 没有超出最大移动距离&&不是长按点击事件,UP时需要让移动圆回到固定圆的位置
    if (!isDistanceOverLimit && !longClick) {
      if (hasBroken) {
        // 如果已经断裂,那么直接回原点,显示FakeView
        onDropCompleted(false);
      } else {
        // 如果还未断裂,那么执行抖动动画
        shakeAnimation();
      }
      // reset
      curX = 0;
      curY = 0;
      ratio = 1;
    } else {
      // 超出最大移动距离,那么执行爆裂帧动画
      initExplosionAnimation();

      needDraw = false;
      explosionAnimStart = true;
    }

    invalidate();
  }

  public double distance(float x1, float y1, float x2, float y2) {
    double distance = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
    if (distance > CLICK_DISTANCE_LIMIT) {
      click = false; // 已经不是点击了
    }

    return distance;
  }

  /**
   * ************************* 爆炸动画(帧动画) *************************
   */
  private void initExplosionAnimation() {
    if (explosionAnim == null) {
      int[] explosionResIds = DropManager.getInstance().getExplosionResIds();
      explosionAnimNumber = explosionResIds.length;
      explosionAnim = new Bitmap[explosionAnimNumber];
      for (int i = 0; i < explosionAnimNumber; i++) {
        explosionAnim[i] = BitmapFactory.decodeResource(getResources(), explosionResIds[i]);
      }

      explosionAnimHeight = explosionAnimWidth = explosionAnim[0].getWidth(); // 每帧长宽都一致
    }
  }

  private void drawExplosionAnimation(Canvas canvas) {
    if (!explosionAnimStart) {
      return;
    }

    if (curExplosionAnimIndex < explosionAnimNumber) {
      canvas.drawBitmap(explosionAnim[curExplosionAnimIndex],
          curX - explosionAnimWidth / 2, curY - explosionAnimHeight / 2, null);
      curExplosionAnimIndex++;
      // 每隔固定时间执行
      postInvalidateDelayed(EXPLOSION_ANIM_FRAME_INTERVAL);
    } else {
      // 动画结束
      explosionAnimStart = false;
      curExplosionAnimIndex = 0;
      curX = 0;
      curY = 0;
      onDropCompleted(true); // explosive true
    }
  }

  private void recycleBitmap() {
    if (explosionAnim != null && explosionAnim.length != 0) {
      for (int i = 0; i < explosionAnim.length; i++) {
        if (explosionAnim[i] != null && !explosionAnim[i].isRecycled()) {
          explosionAnim[i].recycle();
          explosionAnim[i] = null;
        }
      }

      explosionAnim = null;
    }
  }

  /**
   * ************************* 抖动动画(View平移动画) *************************
   */
  public void shakeAnimation() {
    // 避免动画抖动的频率过大,所以除以10,另外,抖动的方向跟手指滑动的方向要相反
    Animation translateAnimation = new TranslateAnimation((circleX - curX) / 10, 0, (circleY - curY) / 10, 0);
    translateAnimation.setInterpolator(new CycleInterpolator(1));
    translateAnimation.setDuration(SHAKE_ANIM_DURATION);
    startAnimation(translateAnimation);

    translateAnimation.setAnimationListener(new Animation.AnimationListener() {
      @Override
      public void onAnimationStart(Animation animation) {
      }

      @Override
      public void onAnimationEnd(Animation animation) {
        // 抖动动画结束时,show Fake, hide current
        onDropCompleted(false);
      }

      @Override
      public void onAnimationRepeat(Animation animation) {
      }
    });
  }

  /**
   * ************************* 拖拽动作结束事件 *************************
   */
  public void addDropCompletedListener(IDropCompletedListener listener) {
    if (listener == null) {
      return;
    }

    if (dropCompletedListeners == null) {
      dropCompletedListeners = new ArrayList<>(1);
    }

    dropCompletedListeners.add(listener);
  }

  public void removeDropCompletedListener(IDropCompletedListener listener) {
    if (listener == null || dropCompletedListeners == null) {
      return;
    }

    dropCompletedListeners.remove(listener);
  }

  public void removeAllDropCompletedListeners() {
    if (dropCompletedListeners == null) {
      return;
    }

    dropCompletedListeners.clear();
  }

  private void onDropCompleted(boolean explosive) {
    dropFake.setVisibility(explosive ? View.INVISIBLE : View.VISIBLE); // show or hide fake view
    this.setVisibility(View.INVISIBLE); // hide current
    recycleBitmap(); // recycle

    // notify observer
    if (dropCompletedListeners != null) {
      for (IDropCompletedListener listener : dropCompletedListeners) {
        listener.onCompleted(DropManager.getInstance().getCurrentId(), explosive);
      }
    }

    // free
    DropManager.getInstance().setTouchable(true);
  }

  public interface IDropCompletedListener {

    void onCompleted(Object id, boolean explosive);

  }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目录介绍01.该库介绍02.效果展示03.如何使用04.注意要点05.优化问题06.部分代码逻辑01.该库介绍自定义红点控件,不用修改之前的代码,完全解耦,既可以设置红点数字控件,使用十分方便。网上看到有些案例是继承View,然后去测量和绘制红点的相关操作,此案例则不需要这样,继承TextView也可以完成设置红点功能。可以支持设置在TextView,Button,LinearLayout,RelativeLayout,TabLayout等等控件上……大概的原理是:继承TextView,然后设置LayoutParams,设置内容,设置Background等等属性,然后在通过addView添加到父控件中。02.效果展示03.如何使用如何引用implementation 'cn.yc:YCRedDotViewLib:1.0.3'如下所示//创建红点View YCRedDotView ycRedDotView = new YCRedDotView(this); //设置依附的View ycRedDotView.setTargetView(tv1); //设置红点数字 ycRedDotView.setBadgeCount(10); //设置红点位置 ycRedDotView.setRedHotViewGravity(Gravity.END); //获取小红点的数量 int count = ycRedDotView.getBadgeCount(); //如果是设置小红点,不设置数字,则可以用这个,设置属性是直径 ycRedDotView.setBadgeView(10); //设置margin ycRedDotView.setBadgeMargin(0,10,20,0);04.注意要点如果设置数字大于99,则会显示“99 ”。如果设置为0,则不可见。05.优化问题相比网上一些案例,该库不需要做测量绘制方面操作,代码十分简洁,但却也可以完成你需要的功能。有些红点View,需要你在布局中设置,要是很多地方有设置红点操作,则比较麻烦。而该案例不需要改变你之前的任何代码,只是需要按照步骤即可设置红点。设置红点view到你的控件的逻辑操作代码如下所示。如果控件父布局是帧布局,则直接添加;如果控件父布局是ViewGroup,则创建一个帧布局,然后添加红点/**  * 设置红点依附的view  * @param view                  view  */ public void setTargetView(View view){     if (getParent() != null) {         ((ViewGroup) getParent()).removeView(this);     }     if (view == null) {         return;     }     if(view.getParent() instanceof FrameLayout){         ((FrameLayout) view.getParent()).addView(this);     }else if(view.getParent() instanceof ViewGroup){         ViewGroup parentContainer = (ViewGroup) view.getParent();         int groupIndex = parentContainer.indexOfChild(view);         parentContainer.removeView(view);         FrameLayout badgeContainer = new FrameLayout(getContext());         ViewGroup.LayoutParams parentLayoutParams = view.getLayoutParams();         badgeContainer.setLayoutParams(parentLayoutParams);         view.setLayoutParams(new ViewGroup.LayoutParams(                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));         parentContainer.addView(badgeContainer, groupIndex, parentLayoutParams);         badgeContainer.addView(view);         badgeContainer.addView(this);     }else {         Log.e(getClass().getSimpleName(), "ParentView is must needed");     } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值