【Android】自定义View实现可展开FloatingButton

之前,项目中需要一个和界面风格匹配的课展开的悬浮按钮。在尝试了多个第三方库无果后,看起来只能自己写一个了。下面开始正题。
先丢一个效果图

按钮未展开
按钮展开

首先,创建一个Myfab类,并继承自View类,然后复写初始的onMeasure方法以及构造方法

然后,分析一下这个按钮的绘制逻辑。

  • 整个按钮背景由三部分组成,上部分的半圆,中间的矩形,以及下部分的半圆。
  • 闭合的时候,中间矩形的高度为0,上下半圆贴在一起。
  • 展开的过程中,下半部分半圆不动,矩形的长随着时间增长,上方半圆的y坐标随着长方形的增长而减小(就是被长方形顶上去了)。
  • 绘制的时候判断,如果伸长的高度足够绘制下一个图标,就进行绘制。
    然后就是代码

onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width;
        int height;

        width=MeasureSpec.getSize(widthMeasureSpec);
        height=MeasureSpec.getSize(heightMeasureSpec);

        width=width>height?height:width;//取较小的
        height=height>width?width:height;

        height+=icon.size()*width;     //根据加入图标个数累加view高度

        realheight=height-width;//加入图标的总高度

        setMeasuredDimension(width,height);
    }

这里,我取了用户设置的长和宽中较小的值作为button的直径,按钮展开后的高=直径*加入的图标个数,也就是说,每一个图标所占有的区域都是一个正方形。

onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
        boolean result=false;
        switch (event.getAction())
        {
            case MotionEvent.ACTION_UP:
            {
                result=TouchMethod((int)event.getX(), (int)event.getY(),false);
                break;
            }
            case MotionEvent.ACTION_DOWN:
            {
                result=TouchMethod((int)event.getX(), (int)event.getY(),true);//
                break;
            }
        }
        if(result)
            return true; //已消费事件
        else
            return false;//未消费事件
    }

这里重写了onTouchEvent方法,以便处理view的点击事件。TouchMethod方法判断点击是否有效,如果有效,则消费点击事件,否则不消费。

TouchMethod

 private boolean TouchMethod(int x,int y ,boolean isDown)
    {
        if(y>getMeasuredHeight()-getMeasuredWidth()&&y<getMeasuredHeight()) //如果点在底部按钮上
        {
            if(!isDown)
                startAnimation();
            return true;
        }
        else if(y>0&&y<getMeasuredHeight()-getMeasuredWidth()&&isShow)//如果点在选项上,并且按钮在展开状态
        {
            if(!isDown) {
                for (int i = icon.size(); i > 0; i--) //计算并判断点在了哪个位置(view的宽度为Width,高度为icon.size*Width,相当于每个图标所占的区域都是正方形)
                {
                    if (y > (i - 1) * getMeasuredWidth() && y < i * getMeasuredWidth()) {
                        if (menuListener != null)
                            menuListener.click(icon.size() - i + 1);//调用接口
                    }
                }
            }
            return true;
        }
        else
            return false;//按钮未展开
    }

TouchMethod方法对view的点击事件进行了处理。如果按钮处于闭合状态,并且可见部分受到了点击,则展开菜单。如果不可见部分(收缩起来后上面添加的按钮部分)收到了点击,则会返回false,并由调用它的onTouchEvent方法返回未消费事件标记。如果按钮处于展开状态,并受到了点击,则会调用回调接口,并根据点击的区域传入相应的参数。

##onDraw
protected void onDraw(Canvas canvas) {
        // super.onDraw(canvas);
        int px=getMeasuredWidth()/2;

        Paint mPaint=new Paint();
        mPaint.setColor(color);       //设置画笔颜色
        mPaint.setStyle(Paint.Style.FILL);  //设置画笔模式为填充
        mPaint.setStrokeWidth(10f);//设置画笔宽度为10px
        mPaint.setAntiAlias(true);

        Path path=new Path();
        path.setFillType(Path.FillType.EVEN_ODD);
        canvas.translate(px, getMeasuredHeight()-px);//移动坐标中心

        canvas.drawArc(-px,-px,px,px,0,180,true,mPaint);//画出底部的半圆
        canvas.drawArc(-px, -px - rect, px, -rect + px, 180, 180, true, mPaint); //画出上部分的半圆
        canvas.drawRect(-px, -rect, px, 0, mPaint);//画出两个半圆中间的矩形

        Bitmap bitmap= BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_add_circle_outline_white_24dp);
        canvas.drawBitmap(bitmap,-bitmap.getWidth()/2,-bitmap.getHeight()/2,mPaint); //获取并绘制按钮没有展开时的图标

        for(int i=0;i<icon.size();i++)
        {
            if(rect>=2*px*(i+1))//2*px=getMeasuredWidth(),i+1=当前的图标个数(-y),如果上升高度足够显示下一个图标,就绘制
            {
                Bitmap bitmap1= BitmapFactory.decodeResource(getContext().getResources(),icon.get(i).intValue());
                canvas.drawBitmap(bitmap1,-bitmap.getWidth()/2,-bitmap.getHeight()/2-(i+1)*2*px,mPaint);//在相应位置绘制图标
            }
        }

        if(rect==realheight) {//完全展开
            isShow=true;
        }
        else if(rect==0) {    //完全闭合
            isShow=false;
        }
    }

首先,初始化画笔,画布等一系列东西。然后,onDraw会根据rect这个全局变量的值来进行绘制(rect的范围是0-realheight,大小的变化由自定义的Animation来控制,后面会有说明)。首先绘制的是两个半圆和半圆中间的矩形

 canvas.drawArc(-px,-px,px,px,0,180,true,mPaint);//画出底部的半圆
 canvas.drawArc(-px, -px - rect, px, -rect + px, 180, 180, true, mPaint); //画出上部分的半圆
 canvas.drawRect(-px, -rect, px, 0, mPaint);//画出两个半圆中间的矩形

之后,绘制的是按钮在没有展开时显示在上面的图标

      Bitmap bitmap= BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_add_circle_outline_white_24dp);
      canvas.drawBitmap(bitmap,-bitmap.getWidth()/2,-bitmap.getHeight()/2,mPaint); //获取并绘制按钮没有展开时的图标

再根据动态插入图标的个数,绘制剩下的图标

 for(int i=0;i<icon.size();i++)
      {
          if(rect>=2*px*(i+1))//2*px=getMeasuredWidth(),i+1=当前的图标个数(-y),如果上升高度足够显示下一个图标,就绘制
          {
              Bitmap bitmap1= BitmapFactory.decodeResource(getContext().getResources(),icon.get(i).intValue());
              canvas.drawBitmap(bitmap1,-bitmap.getWidth()/2,-bitmap.getHeight()/2-(i+1)*2*px,mPaint);//在相应位置绘制图标
          }
      }

最后,再判断是否已经展开/回缩完全,并设置相应的flag即可。
控制rect变化的动画有两个,分别控制展开和回缩

控制展开的动画

  private class ami extends Animation
    {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            rect=(int)(interpolatedTime*realheight);
            invalidate();
        }
    }

控制回缩的动画

   private class ami2 extends Animation
    {
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            rect=(int)((1-interpolatedTime)*realheight);
            invalidate();
        }
    }

以及对动画使用的控制

public void startAnimation() {
        if(!isShow) {
            ami move = new ami();
            move.setDuration(300);
            move.setInterpolator(new AccelerateDecelerateInterpolator());
            startAnimation(move);
        }
        else
        {
            ami2 move = new ami2();
            move.setDuration(300);
            move.setInterpolator(new AccelerateDecelerateInterpolator());
            startAnimation(move);
        }
    }

可以看出,想要展开/回缩button,只需要调用自定义的startAnimation()方法即可,动画持续的时间都是300毫秒。这里,我对动画设置了AccelerateDecelerateInterpolator这个插值器,以便实现开始加速和结束减速的效果,不过因为这个插值器是用的cos函数来给出插值,所以离着MD风格动画要求的精细度还差不少,并且开始和结束的加速度是一样的(MD动画要求较快的加速和缓慢的减速)...等着有空,再重新自定义个插值器吧。

以下给出整个view的完整代码

public class Myfab extends View {

    private boolean isShow=false;
    private int rect=0;
    private List<Integer> icon=new ArrayList();
    private MenuListener menuListener;

    private int color;
    private int realheight=0;

    public void setColor(int color) {
        this.color = color;
    }

    public Myfab(Context context) {
        super(context);
    }

    public Myfab(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        if (!isClickable()) {
            setClickable(true);
        }
        color=ContextCompat.getColor(context,R.color.colorPrimary);
    }

    public void collapse()
    {
        rect=0;
        invalidate();//不加动画直接缩回去
    }
    public void setIcon(List<Integer> list)
    {
        this.icon=list;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean result=false;
        switch (event.getAction())
        {
            case MotionEvent.ACTION_UP:
            {
                result=TouchMethod((int)event.getX(), (int)event.getY(),false);
                break;
            }
            case MotionEvent.ACTION_DOWN:
            {
                result=TouchMethod((int)event.getX(), (int)event.getY(),true);//
                break;
            }
        }
        if(result)
            return true; //已消费事件
        else
            return false;//未消费事件
    }

    private boolean TouchMethod(int x,int y ,boolean isDown)
    {
        if(y>getMeasuredHeight()-getMeasuredWidth()&&y<getMeasuredHeight()) //如果点在底部按钮上
        {
            if(!isDown)
                startAnimation();
            return true;
        }
        else if(y>0&&y<getMeasuredHeight()-getMeasuredWidth()&&isShow)//如果点在选项上,并且按钮在展开状态
        {
            if(!isDown) {
                for (int i = icon.size(); i > 0; i--) //计算并判断点在了哪个位置(view的宽度为Width,高度为icon.size*Width,相当于每个图标所占的区域都是正方形)
                {
                    if (y > (i - 1) * getMeasuredWidth() && y < i * getMeasuredWidth()) {
                        if (menuListener != null)
                            menuListener.click(icon.size() - i + 1);//调用接口
                    }
                }
            }
            return true;
        }
        else
            return false;//按钮未展开
    }
    @Override
    protected void onDraw(Canvas canvas) {
        // super.onDraw(canvas);
        int px=getMeasuredWidth()/2;

        Paint mPaint=new Paint();
        mPaint.setColor(color);       //设置画笔颜色
        mPaint.setStyle(Paint.Style.FILL);  //设置画笔模式为填充
        mPaint.setStrokeWidth(10f);//设置画笔宽度为10px
        mPaint.setAntiAlias(true);

        Path path=new Path();
        path.setFillType(Path.FillType.EVEN_ODD);
        canvas.translate(px, getMeasuredHeight()-px);//移动坐标中心

        canvas.drawArc(-px,-px,px,px,0,180,true,mPaint);//画出底部的半圆
        canvas.drawArc(-px, -px - rect, px, -rect + px, 180, 180, true, mPaint); //画出上部分的半圆
        canvas.drawRect(-px, -rect, px, 0, mPaint);//画出两个半圆中间的矩形

        Bitmap bitmap= BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_add_circle_outline_white_24dp);
        canvas.drawBitmap(bitmap,-bitmap.getWidth()/2,-bitmap.getHeight()/2,mPaint); //获取并绘制按钮没有展开时的图标

        for(int i=0;i<icon.size();i++)
        {
            if(rect>=2*px*(i+1))//2*px=getMeasuredWidth(),i+1=当前的图标个数(-y),如果上升高度足够显示下一个图标,就绘制
            {
                Bitmap bitmap1= BitmapFactory.decodeResource(getContext().getResources(),icon.get(i).intValue());
                canvas.drawBitmap(bitmap1,-bitmap.getWidth()/2,-bitmap.getHeight()/2-(i+1)*2*px,mPaint);//在相应位置绘制图标
            }
        }

        if(rect==realheight) {//完全展开
            isShow=true;
        }
        else if(rect==0) {    //完全闭合
            isShow=false;
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width;
        int height;

        width=MeasureSpec.getSize(widthMeasureSpec);
        height=MeasureSpec.getSize(heightMeasureSpec);

        width=width>height?height:width;//取较小的
        height=height>width?width:height;

        height+=icon.size()*width;     //根据加入图标个数累加view高度

        realheight=height-width;//加入图标的总高度

        setMeasuredDimension(width,height);
    }

    public void setMenuListener(MenuListener menuListener) {
        this.menuListener = menuListener;
    }

    private class ami extends Animation
    {

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            rect=(int)(interpolatedTime*(realheight));
            invalidate();
        }

    }
    private class ami2 extends Animation
    {

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            rect=(int)((1-interpolatedTime)*(realheight));
            invalidate();
        }

    }
    public void startAnimation() {
        if(!isShow) {
            ami move = new ami();
            move.setDuration(300);
            move.setInterpolator(new AccelerateDecelerateInterpolator());
            startAnimation(move);
        }
        else
        {
            ami2 move = new ami2();
            move.setDuration(300);
            move.setInterpolator(new AccelerateDecelerateInterpolator());
            startAnimation(move);
        }
    }
    public interface MenuListener//需实现此接口以便接受点击事件
    {
        void click(int i);
    }
}

使用范例

public class MainActivity extends AppCompatActivity implements Myfab.MenuListener {

    private Myfab fabtn;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fabtn=(Myfab) findViewById(R.id.fab);

        List<Integer> list=new ArrayList();
        list.add(R.mipmap.ic_add_circle_outline_white_24dp);
        list.add(R.mipmap.ic_add_circle_outline_white_24dp);
        list.add(R.mipmap.ic_add_circle_outline_white_24dp);
        fabtn.setIcon(list);
        fabtn.setMenuListener(this);
    }
    @Override
    public void click(int i) {

        Toast.makeText(this,String.valueOf(i),Toast.LENGTH_SHORT).show();
    }
}

希望可以对大家有所帮助。

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

推荐阅读更多精彩内容