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

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

【Android】自定义View实现可展开FloatingButton_第1张图片
按钮未展开
【Android】自定义View实现可展开FloatingButton_第2张图片
按钮展开

首先,创建一个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()&&y0&&y 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=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=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 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 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()&&y0&&y 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=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 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();
    }
}

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

你可能感兴趣的:(【Android】自定义View实现可展开FloatingButton)