贝塞尔曲线之聊天未读数气泡-进化

之前写了一个基础思路,实现了基础的功能,现在就需要让他可以露脸,提升颜值,提升作用,基础效果回顾:

base.gif

进化~
evolve.gif

基本效果都实现了~ 代码 略有粗糙~ 进化版在基础上优化了

  • 气泡的显示效果
  • 动态设置属性,并增加了几个属性
  • 添加到依附控件以及添加到屏幕,可以在整个屏幕内拖动

首先看怎么实现文字多的时候使用椭圆背景,

    /**
     * 测量文本占用大小
     */
    private void measureText() {
        mBadgeTextRect.left = 0;
        mBadgeTextRect.top = 0;
        if (TextUtils.isEmpty(mTextStr)) {
            mBadgeTextRect.right = 0;
            mBadgeTextRect.bottom = 0;
        } else {
            mBadgeTextPaint.setTextSize(mTextSize);
            mBadgeTextRect.right = mBadgeTextPaint.measureText(mTextStr);
            mBadgeTextFontMetrics = mBadgeTextPaint.getFontMetrics();
            mBadgeTextRect.bottom = mBadgeTextFontMetrics.descent - mBadgeTextFontMetrics.ascent;
        }
    }

    public void DrawBubMoveable(Canvas canvas, PointF center, float radius) {
        if (mTextStr.isEmpty() || mTextStr.length() == 1) {
            //数字为一位的时候
            mBadgeBackgroundRect.left = center.x - (int) radius;
            mBadgeBackgroundRect.top = center.y - (int) radius;
            mBadgeBackgroundRect.right = center.x + (int) radius;
            mBadgeBackgroundRect.bottom = center.y + (int) radius;
            canvas.drawCircle(center.x, center.y, radius, mBubblePaint);
        } else {
            mBadgeBackgroundRect.left = center.x - (mBadgeTextRect.width() / 2f + mBadgePadding);
            mBadgeBackgroundRect.top = center.y - (mBadgeTextRect.height() / 2f + mBadgePadding * 0.5f);
            mBadgeBackgroundRect.right = center.x + (mBadgeTextRect.width() / 2f + mBadgePadding);
            mBadgeBackgroundRect.bottom = center.y + (mBadgeTextRect.height() / 2f + mBadgePadding * 0.5f);

            radius = mBadgeBackgroundRect.height() / 2f;
            mBubMoveableRadius = radius;
            canvas.drawRoundRect(mBadgeBackgroundRect, radius, radius, mBubblePaint);
        }


        if (!mTextStr.isEmpty()) {
            canvas.drawText(mTextStr, center.x,
                    (mBadgeBackgroundRect.bottom + mBadgeBackgroundRect.top
                            - mBadgeTextFontMetrics.bottom - mBadgeTextFontMetrics.top) / 2f,
                    mBadgeTextPaint);
            //基线中点
            /*canvas.drawCircle(center.x,
                    (mBadgeBackgroundRect.bottom + mBadgeBackgroundRect.top) / 2 + (
                            mBadgeTextFontMetrics.bottom - mBadgeTextFontMetrics.top) / 2f - mBadgeTextFontMetrics.bottom, 10.0f,
                    mBadgeTextPaint);*/
        }
    }

首先需要计算文本大小,知道文本的左上右下角,Paint.FontMetrics 可以获得文字的 top,ascent,desent, bottom, leading这几个属性

text.png

baseline以下是正值,以上是负值 descent - ascent 就是文字的高度了,基线的计算
baseline = center +(FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom

更多相关知识移步HenCoder Android 开发进阶:自定义 View 1-3 drawText() 文字的绘制

这里在文字长度大于2个的时候,计算一块区域,画椭圆就好~ 在计算椭圆半径的时候把mBubMoveableRadius,动圆的半径也赋值了,因为在画连接线的时候使用到了半径,值如果太大会超出气泡背景。其余页面优化就是在设置值后的,重新赋值,或者重新计算位置的逻辑。

                new DragBubbleView(this)
                .bindTarget(btn)
                .setBadgeNumber(999)//设置数字
                .setBadgeText("一二一")//设置文字
                .setBadgeTextColor(getResources().getColor(R.color.colorAccent))//文字颜色
                .setBadgeBackgroundColor(getResources().getColor(R.color.colorAccent))//背景颜色
                .setBadgeTextSize(12, true)//设置文字大小
                .setBadgePadding(5,true)//设置文字Padding
                .setBadgeGravity(Gravity.BOTTOM|Gravity.START)//设置Gravity
                .setGravityOffset(10,10,true)//设置偏移
                .setBadgeBackgroundSize(12)//设置气泡半径  - - 别写太大,会有意想不到的效果
                .setExactMode(true)//设置是否是精确值
                .setOnDragStateChangedListener(new Badge.OnDragStateChangedListener() {
                    @Override
                    public void onDragStateChanged(int dragState, Badge badge, View targetView) {
                        switch (dragState) {
                            case BUBBLE_STATE_DISMISS:
                                badge.setBadgeNumber(badge.getBadgeNumber() + 1);
                                break;
                        }
                    }
                });//设置监听才可以拖拽

嗯~ 属性设置最后就成了这样
在列表中使用的时候,为了防止复用导致的 重复显示,也可以使用xml初始化,然后对VIew进行显示隐藏,有人会问new出来的不能显示隐藏吗,亲测不行= =,原因还在寻找,如果有人遇到过同样的问题,还请指点一二~

    <dragbubbleevolve.beyond.com.dragbubbleevolve.DragBubbleView
        android:id="@+id/drag"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

    <!--
    app:bubble_color="#ff0000"
    app:bubble_radius="12dp"
    app:bubble_text="92"
    app:bubble_textColor="#ffffff"
    app:bubble_textSize="12sp"
    -->

                DragBubbleView dragBubbleView = helper.getView(R.id.drag);
                if (item) {
                    dragBubbleView.setVisibility(View.VISIBLE);
                    dragBubbleView
                            .bindTarget(textView)
                            .setBadgeNumber(100+helper.getLayoutPosition())//设置数字
                            .setBadgeTextSize(10, true)//设置文字大小
                            .setBadgeBackgroundSize(10)
                            .setExactMode(true)//设置是否是精确值
                            .setOnDragStateChangedListener(new Badge.OnDragStateChangedListener() {
                                @Override
                                public void onDragStateChanged(int dragState, Badge badge, View targetView) {
                                    switch (dragState) {
                                        case BUBBLE_STATE_DISMISS:
                                            getData().set(helper.getLayoutPosition(),false);
                                            break;
                                    }
                                }
                            });
                } else {
                    dragBubbleView.setVisibility(View.GONE);
                }

属性部分源码(链接在最下面)体现了,注意重置各个属性值就行,得动态属性嘛~ 剩下的就是怎么添加在控件上,怎么将气泡移出本身的范围~ 这应该是这个自定义View的难点了吧~

    @Override
    public Badge bindTarget(final View targetView) {
        if (targetView == null) {
            throw new IllegalStateException("targetView can not be null");
        }
        if (getParent() != null) {
            ((ViewGroup) getParent()).removeView(this);
        }

        ViewParent targetParent = targetView.getParent();
        if (targetParent != null && targetParent instanceof ViewGroup) {
            mTargetView = targetView;
            if (targetParent instanceof BadgeContainer) {
                ((BadgeContainer) targetParent).addView(this);
            } else {
                ViewGroup targetContainer = (ViewGroup) targetParent;
                int index = targetContainer.indexOfChild(targetView);
                ViewGroup.LayoutParams targetParams = targetView.getLayoutParams();
                targetContainer.removeView(targetView);
                final BadgeContainer badgeContainer = new BadgeContainer(getContext());
                if (targetContainer instanceof RelativeLayout) {
                    badgeContainer.setId(targetView.getId());
                }
                targetContainer.addView(badgeContainer, index, targetParams);
                badgeContainer.addView(targetView);
                badgeContainer.addView(this);
            }
        } else {
            throw new IllegalStateException("targetView must have a parent");
        }
        return this;
    }

先看这段,我相信很多类型的控件这段代码是经常见到的,首先BadgeContainer是一个自定义ViewGroup,他会添加把依附控件和自定义View添加到自身,并通过子View计算所占大小,并通过计算除的宽高来测量我们的自定义View。

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            View targetView = null, badgeView = null;
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                if (!(child instanceof DragBubbleView)) {
                    //拿到依附控件
                    targetView = child;
                } else {
                    badgeView = child;
                }
            }
            if (targetView == null) {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            } else {
                targetView.measure(widthMeasureSpec, heightMeasureSpec);
                if (badgeView != null) {
                    badgeView.measure(MeasureSpec.makeMeasureSpec(targetView.getMeasuredWidth(), MeasureSpec.EXACTLY),
                            MeasureSpec.makeMeasureSpec(targetView.getMeasuredHeight(), MeasureSpec.EXACTLY));
                }
                setMeasuredDimension(targetView.getMeasuredWidth(), targetView.getMeasuredHeight());
            }
        }

然后拿到依附控件的父控件,把依附控件从父控件中删除,并把BadgeContainer 添加到父控件中,并把依附控件和自定义View添加到BadgeContainer 中。这样就把我们的自定义View添加在了控件上,但是,现在我们自定义VIew的操作范围,只是依附控件的大小,除非依附控件是全屏的,否者还是没有什么效果,所以我们就要把我们的自定义View再添加到更外层的父控件。
不知道大家认不认识android.R.id.content,这是DecorView下一个FrameLayout的id,我这里直接添加在了id为android.R.id.content的view上。我正在写关于View绘制流程的文章,基本都是源码,所以还得多复查几遍。

    private void findActivityRoot(View view) {
        if (view.getParent() != null) {
            findActivityRoot((View) view.getParent());
        } else if (view instanceof FrameLayout && view.getId()==android.R.id.content) {
            mActivityRoot = (ViewGroup) view;
        }
    }

然后在手势滑动的时候判断是添加在控件上还是添加在外层View上

    protected void screenFromWindow(boolean screen) {
        if (getParent() != null) {
            ((ViewGroup) getParent()).removeView(this);
        }

        if (screen) {
            //添加到最外层父类
            mActivityRoot.addView(this, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                    FrameLayout.LayoutParams.MATCH_PARENT));
        } else {
            //添加控件上
            bindTarget(mTargetView);
        }
    }

我在手指落下,可以构成连接状态的时候,将自定义View添加在了外层View上,在回弹动画,和爆炸动画结束后,重新添加在了依附控件上,如果不重新添加在依附控件上,会发现,屏幕所有 的点击事件都被 我们的自定义View拦截进行处理~
这是在处理我们自定义View展示大小是应该注意的,还应该注意,手势处理时使用getRawY用的绝对坐标,之前基础中用的都是相对坐标,计算动圆圆心的时机,重置那些属性以及时机,还有就是很多处理逻辑。。。

最后感谢进化内容参考的项目

BadgeView

没有这个项目,可能我还得好久才能写完进化部分的内容,特别感谢~
写这个项目从最初的画圆,学习基线,写字,到现在的应该说可以在项目中使用(暂时在本机上没有什么明显的bug,性能方面还不太清楚),途中遇到了一位书友~ 一起解决讨论了很多问题,受益很大

这篇文章是边学习边写Demo,然后写的文章,可想而知我还是个很小的菜鸟,如果其中有错误还请指出,我会尽快修改文章,并改正自己的理解,谢谢。

最后附上源码

我朋友和我说,在没看源码之前,根本想不到这种思路,但是看了源码时候,感觉又都会~ 个人理解,其实我们现在都只是一个学习积累的过程,这个效果用到了很多知识点,比如 path,贝塞尔曲线,手势,动画什么的,分成知识点去学习它,然后去组合它们,感觉也是很收益。

你们的点赞,是我们学习分享的最大动力~ 每个人都一样,更何况我们这些菜鸟呢?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351