美文网首页
【Android】自定义View实现可展开FloatingBut

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

作者: 7dollars | 来源:发表于2017-07-18 22:48 被阅读0次

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

    按钮未展开 按钮展开

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

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

    相关文章

      网友评论

          本文标题:【Android】自定义View实现可展开FloatingBut

          本文链接:https://www.haomeiwen.com/subject/rmckkxtx.html