自定义View1——BezierBottomBar

这个是在项目中运用的自定义View的第一篇,按照字母序,第一篇首先先讲BezierBottomBar。这个控件是我从郭霖公众号之前的一篇推送上学习来的,所以有些代码是照搬的之前那篇推送,这篇文章也有很多地方直接引用了这篇推送的一些内容,下面是那篇的推送地址

https://mp.weixin.qq.com/s?__biz=MzA5MzI3NjE2MA==&mid=2650243121&idx=1&sn=a3e3368758074d509691e531a927f2c8&chksm=8863715ebf14f848f4c648575cba1d26313ad88893fb3eaf3169aa7d10c44476b779a3438409&mpshare=1&scene=23&srcid=08207tLPp42M8JTyyGQcQrJm#rd

先看效果



这个是结合了ViewPager后的效果。
这个控件总共分为三个部分,可以把这部分看做是一个VVM模式

  • 控件本体——BezierBottomBarView
  • 控制View行为的——BezierBottomBarControl
  • 定制的ViewPager

BezierBottomBarView

测量View大小

由于我们是做的是一个底边栏,所以我们要在onMeasure中设定最大高度,以防止控件占高度太高。所以在getHeight方法中,将控件最大高度设置为屏幕的1/8.

    private int getHeight(int heightMeasureSpec) {

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        DisplayMetrics dm = getResources().getDisplayMetrics();

        int height = (heightSize > dm.heightPixels) ? heightSize : dm.heightPixels;

        return (height / 8);

    }

对于单个圆的存放

在那篇推送中作者是将这个控件写成了ViewGroup+ImgView的形式,但是在我们的应用中我们想要加入手势控制动画,所以如果仍然使用作者的思路就会导致动画实现起来相对来说比较的麻烦,故而我改用纯View进行绘制。
所以我们就需要一类数据来表示单个圆的各种参数,在这里我将get和set方法省略,如果有需要可以去查看源码。

public static class BarTag {

        private int icon;
        private String tag;
        private int color;

        private float centerX;
        private float centerY;

        private float radius;

        private RectF rectF;
        private RectF dst;
        private RectF tagRectF;
    }

在这里预留了tag和图标的color方法,在里面并没有使用,如果未来有需要的话可以自行添加。这个类中主要持有的方法就是icon,radius和dst,dst表示圆的位置。

确定圆的摆放位置

在原文中,确定摆放位置是在onLayout中设置的,但是原文是将控件当做一个ViewGroup去写的,我在这里却是将其当做View去写,所以我选择在onSizeChanged中去确定圆的位置,并且存储起来,在之后的onDraw方法后用canvas去绘制外面的圆形。

在这里有一点补充:
继承与View和继承与现有控件都是下面的顺序,但是控件的大小是生成之后就固定的,不会再次改变。
onMeasure()→onSizeChanged()→onLayout()→onMeasure()→onLayout()→onDraw()

所以在onSizeChanged中我们可以这么确定一个圆的位置,interval表示两个圆之间的间隔。

        interval = (width - 2 * tabNum * radius) / (tabNum + 1);

        for (int i = 0; i < tabNum; i++) {
            float cx = interval + radius + i * (interval + 2 * radius);
            RectF rectF = new RectF(cx - radius, startY - radius, cx + radius, startY + radius);
            barTags.get(i).setRectF(rectF);
            RectF dst = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                    (int) (startY - scale * radius / g2),
                    (int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                    (int) (startY + scale * radius / g2));
            barTags.get(i).setDst(dst);
            barTags.get(i).setRadius(radius);
        }

在onSizeChanged方法中计算并且将圆的位置进行存储

对静止圆的绘制

在计算完各个圆的位置之后,我们就可以在onDraw方法中进行绘制。可以看到,如果当前圆被选中,那么就会有一个颜色填充。
所以大概就是这样

    for (int i = 0; i < tabNum; i++) {
        float cx = barTags.get(i).centerX;
        float cy = barTags.get(i).centerY;
        canvas.drawCircle(cx, cy, radius, linePaint);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), barTags.get(i).icon);
        canvas.drawBitmap(bitmap, null, barTags.get(i).dst, fillPaint);
    }

动画的绘制

(有关贝塞尔曲线及其内容请直接参考开始给出的博客,我觉得讲的不错)
我们可以仔细 的观察到,在这个动画当中,中间移动的圆分成了6个状态
这里是起始的三个状态,分别是圆,开始向左移动,向左移动一定距离,还欠缺的是准备结束向左移动,向左移动的过量和向左移动的回弹


这里只能说是三个状态

那么我们可以假设动画时间currentTime(0~1),所以我们可以将整个移动状态分为以下几个区间段:

  • 状态1,圆
    currentTime = 0
  • 状态2,即向左移动
    0 < currentTime <= 0.2
  • 状态3,即开始进入中间状态
    0.2 < currentTime <= 0.5
  • 状态4,即准备结束这段运动,是状态2的镜面对称
    0.5 < currentTime <= 0.8
  • 状态5,结束这段运动时的回弹开始
    0.8 < currentTime <= 0.9
  • 状态6,结束这段运动时候的回弹结束
    0.9 < currentTime < 1
  • 状态1,圆
    currentTime = 1

由之前那篇博客我们可以知道,使用二阶贝塞尔曲线去画一个圆,受制于p1,p2,p3,p4四个点,所以我们就可以通过改变这四个点的参数去改变这个圆的参数


这张图是原博客中的一张图,我就直接拿来用了

那么我们可以将这六个部分的代码变成这个样子

  • 状态1
        if (currentTime == 0) {
            resetP();
            canvas.drawCircle(interval + radius + (currentPos) * (interval + 2 * radius), startY, 0, clickPaint);
            fillPaint.setColor(startColor);
            canvas.translate(startX, startY);
            if (toPos > currentPos) {
                p2.setX(radius);
            } else {
                p4.setX(-radius);
            }
        }
  • 状态2
        if (currentTime > 0 && currentTime <= 0.2) {
            direction = toPos > currentPos ? true : false;
            if (animating) {
                canvas.drawCircle(interval + radius + (toPos) * (interval + 2 * radius),
                        startY,
                        radius * 1.0f * 5 * currentTime,
                        clickPaint);
            }
            canvas.translate(startX, startY);
            if (toPos > currentPos) {
                p2.setX(radius + 2 * 5 * currentTime * radius / 2);
            } else {
                p4.setX(-radius - 2 * 5 * currentTime * radius / 2);
            }
        }
  • 状态3
        if (currentTime > 0.2 && currentTime <= 0.5) {
            float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
            canvas.translate(cx, startY);
            if (toPos > currentPos) {
                p1.setX(0.5f * radius * (currentTime - 0.2f) / 0.3f);
                p2.setX(2 * radius);
                p3.setX(0.5f * radius * (currentTime - 0.2f) / 0.3f);

                p2.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
                p4.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
            } else {
                p1.setX(-0.5f * radius * (currentTime - 0.2f) / 0.3f);
                p3.setX(-0.5f * radius * (currentTime - 0.2f) / 0.3f);
                p4.setX(-2 * radius);

                p2.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
                p4.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
            }
        }
  • 状态4
        if (currentTime > 0.5 && currentTime <= 0.8) {
            float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
            canvas.translate(cx, startY);
            if (toPos > currentPos) {
                p1.setX(0.5f * radius + 0.5f * radius * (currentTime - 0.5f) / 0.3f);
                p3.setX(0.5f * radius + 0.5f * radius * (currentTime - 0.5f) / 0.3f);

                p2.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
                p4.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
            } else {
                p1.setX(-0.5f * radius - 0.5f * radius * (currentTime - 0.5f) / 0.3f);
                p3.setX(-0.5f * radius - 0.5f * radius * (currentTime - 0.5f) / 0.3f);

                p2.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
                p4.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
            }
        }
  • 状态5
       if (currentTime > 0.8 && currentTime <= 0.9) {
            p2.setMc(mc);
            p4.setMc(mc);
            float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
            canvas.translate(cx, startY);
            if (toPos > currentPos) {
                p4.setX(-radius + 1.6f * radius * (currentTime - 0.8f) / 0.1f);
            } else {
                p2.setX(radius - 1.6f * radius * (currentTime - 0.8f) / 0.1f);
            }
        }
  • 状态6
      if (currentTime > 0.9 && currentTime < 1) {
            if (toPos > currentPos) {
                p1.setX(radius);
                p3.setX(radius);
                canvas.translate(startX + distance, startY);
                p4.setX(0.6f * radius - 0.6f * radius * (currentTime - 0.9f) / 0.1f);
            } else {
                p1.setX(-radius);
                p3.setX(-radius);
                canvas.translate(startX + distance, startY);
                p2.setX(-0.6f * radius + 0.6f * radius * (currentTime - 0.9f) / 0.1f);
            }
        }

View的hide和show

show

可以看到,这个show的动画有一个稍微过一些然后再回弹的效果(虽然可能真的不太明显)并且圆是依次上升的,所以就需要让它有一个上升的次序。



我们可以看到,在sin图像中,在顶点下任取一个y值,都会有两个x值使得sin(x) = y(→_→好像讲的有点啰嗦)那么我们亦可以通过给不同的圆设置不同的初始值,来实现阶梯式上升的效果。

    void show() {
        if (!valueRunning && !hideRunning) {
            float cy = (startY + radius) / (float) Math.sin(Math.toRadians(angle));
            for (int i = 0; i < tabNum; i++) {
                hideHeight = cy + startY / (float) Math.sin(Math.toRadians(angle));
                angles[i] = i * (-10);
                dsts[i] = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                        (int) (startY - scale * radius / g2) + cy,
                        (int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                        (int) (startY + scale * radius / g2) + cy);
            }
            animState = AnimState.Show;
            handler.postDelayed(showRunnable, showTime);
        }
    }
hide

hide方法和show方法相同,也需要几个圆依次向下。所以思路也同show——赋予几个圆不同的负向初始量,然后在handle中对其进行改变

    void hide() {
        if (!valueRunning && !showRunning) {
            hideSpeed = (startY + radius + (tabNum - 1) * (radius * 2 / tabNum)) / hideTime;
            for (int i = 0; i < tabNum; i++) {
                changeHeight[i] = i * (-10);
                dsts[i] = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                        (int) (startY - scale * radius / g2),
                        (int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
                        (int) (startY + scale * radius / g2));
            }
            animState = AnimState.Hide;
            handler.postDelayed(hideRunnable, hideTime);
        }
    }

控件的单击事件

在这个控件中,我们有两个手势操作,以及单击操作。但是手势操作我们需要在整个屏幕,也就是在Activity中去操作这个全局手势,所以我们就只需要在控件的onTouchEvent中处理单击操作即可。
由于控件有show和hide两种状态,所以我们只需要让其在show的时候处理Event即可,在hide的时候我们可以选择无视。
所以在这部分就可以这么去处理TouchEvent

            if (x > interval + 2 * radius && x < (interval + 2 * radius) * tabNum) {
                if (animator != null) {
                    animator.cancel();
                }
                int toPos = (int) (x / (interval + 2 * radius));
                if (toPos != currentPos && toPos <= tabNum) {
                    startAniTo(currentPos, toPos);
                }
            } else if (x > interval && x < interval + 2 * radius) {
                if (animator != null) {
                    animator.cancel();
                }
                if (currentPos != 0)
                    startAniTo(currentPos, 0);
            }

我们可以看到,在这里就只需要判断单击的x,y值即可。

BezierBottomBarControl

由于需要在全局设置一个手势操作,并且需要在一定时间过后对BottomBar进行一个隐藏,所以需要一个ViewControl来统一的对View的状态进行操作。

设置手势操作

由于这个控件在App中仅存在于MainActivity中,所以只需要在MainActivity的onTouchEvent中将MotionEvent传入control就可以了。在Control中对其进行处理。由于我们并不需要实时的使得控件对于TouchEvent进行反馈,所以我们只需获取到Down和Up的坐标即可。

    public void setTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                if (lastX == 0) {
                    lastX = viewPager.getLastX();
                }
                if (lastY == 0) {
                    lastY = viewPager.getLastY();
                }
                float deltaY = lastY - y;
                if (deltaY > 0) {
                    if (bottomBar.getState() == BezierBottomBarView.AnimState.Hide) {
                        show();
                    }
                }
                if (deltaY < 0) {
                    if (bottomBar.getState() != BezierBottomBarView.AnimState.Hide) {
                        hide();
                    }
                }
                break;
        }
    }

show和hide

在control中起了一个定时器,当控件显示一段时间过后,就会自动调用hide方法,使得控件进行隐藏。

setViewPagerListener

由于我们的控件可以和ViewPager进行一个联动,在Control的构造方法中已经将ViewPager的实例传入,所以我们只需要设置ViewPager的Listener即可。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 流程图 1、视频编码 1、1>初始化视频编码类初始化调用:VTCompressionSessionCreate( ...
    xgou阅读 2,734评论 0 2
  • 时光荏苒,我们也经历了很多感情上的变幻莫测,但始终,最基本的友情信任还是患得患失,好像从未有过,有时我只是很失望的...
    大君22阅读 279评论 1 6
  • 姓名:母光艳 公司:宁波贞观电器 宁波盛和塾第235期,利他二组 【日精进打卡第302天】 【知-学习】 诵读《六...
    母光焱阅读 131评论 0 0